com.robotsquid.squidhub 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ // A tiny ordered JSON document model.
2
+ //
3
+ // ⚠️ It exists because the manifest editor must PRESERVE KEYS IT DOES NOT
4
+ // MODEL. A manifest may carry `skus`, `achievements` and `rewardTable` — none
5
+ // of which this package understands — and round-tripping through a typed model
6
+ // (Unity's JsonUtility included) drops every field the type does not declare.
7
+ // The publisher would discover that when their catalogue emptied, one release
8
+ // after the edit that did it.
9
+ //
10
+ // ⚠️ Member ORDER is preserved, and numbers keep their original text. A dialog
11
+ // that rewrites `"saveSchemaVersion": 1` as `1.0`, or reshuffles a hand-authored
12
+ // file, produces a diff nobody can review — and this file is committed.
13
+ //
14
+ // ⚠️ Pure System.* — no UnityEditor, no UnityEngine. Unit-tested under plain
15
+ // `dotnet` in tools/test, the same rule SquidHubHtml and SquidHubBuildScan
16
+ // follow, and for the same reason: this is where the real logic lives.
17
+ using System;
18
+ using System.Collections.Generic;
19
+ using System.Globalization;
20
+ using System.Text;
21
+
22
+ namespace SquidHub.Editor
23
+ {
24
+ internal enum JsonKind { Null, Bool, Number, String, Array, Object }
25
+
26
+ internal sealed class JsonValue
27
+ {
28
+ public JsonKind Kind = JsonKind.Null;
29
+ public bool Bool;
30
+ /// <summary>The number exactly as it was written, so a round trip is byte-identical.</summary>
31
+ public string NumberText;
32
+ public string String;
33
+ public List<JsonValue> Items;
34
+ /// <summary>Ordered, because a rewritten manifest is a reviewable diff only if it keeps its shape.</summary>
35
+ public List<KeyValuePair<string, JsonValue>> Members;
36
+
37
+ public static JsonValue NewObject() =>
38
+ new JsonValue { Kind = JsonKind.Object, Members = new List<KeyValuePair<string, JsonValue>>() };
39
+
40
+ public static JsonValue NewArray() =>
41
+ new JsonValue { Kind = JsonKind.Array, Items = new List<JsonValue>() };
42
+
43
+ public static JsonValue Str(string value) =>
44
+ new JsonValue { Kind = JsonKind.String, String = value ?? "" };
45
+
46
+ public static JsonValue Of(bool value) => new JsonValue { Kind = JsonKind.Bool, Bool = value };
47
+
48
+ public static JsonValue Of(long value) => new JsonValue
49
+ {
50
+ Kind = JsonKind.Number,
51
+ NumberText = value.ToString(CultureInfo.InvariantCulture),
52
+ };
53
+
54
+ public static JsonValue Null() => new JsonValue { Kind = JsonKind.Null };
55
+
56
+ public JsonValue Get(string name)
57
+ {
58
+ if (Kind != JsonKind.Object) return null;
59
+ foreach (var member in Members)
60
+ if (member.Key == name) return member.Value;
61
+ return null;
62
+ }
63
+
64
+ /// <summary>Replaces in place when present — which is what keeps the key in its original position.</summary>
65
+ public void Set(string name, JsonValue value)
66
+ {
67
+ if (Kind != JsonKind.Object) throw new InvalidOperationException("not an object");
68
+ for (var i = 0; i < Members.Count; i++)
69
+ {
70
+ if (Members[i].Key != name) continue;
71
+ Members[i] = new KeyValuePair<string, JsonValue>(name, value);
72
+ return;
73
+ }
74
+ Members.Add(new KeyValuePair<string, JsonValue>(name, value));
75
+ }
76
+
77
+ public void Remove(string name)
78
+ {
79
+ if (Kind != JsonKind.Object) return;
80
+ for (var i = 0; i < Members.Count; i++)
81
+ {
82
+ if (Members[i].Key != name) continue;
83
+ Members.RemoveAt(i);
84
+ return;
85
+ }
86
+ }
87
+
88
+ /// <summary>The string, or <paramref name="fallback"/> for anything that is not one.</summary>
89
+ public string AsString(string fallback = "") => Kind == JsonKind.String ? String : fallback;
90
+
91
+ public bool AsBool(bool fallback = false) => Kind == JsonKind.Bool ? Bool : fallback;
92
+
93
+ /// <summary>
94
+ /// ⚠️ Returns the fallback for a non-integer, matching the server: it
95
+ /// refuses anything that is not a positive integer for
96
+ /// `saveSchemaVersion` rather than coercing it, because a NaN in the
97
+ /// column that decides whether a rollback is safe eats saves.
98
+ /// </summary>
99
+ public int AsInt(int fallback)
100
+ {
101
+ if (Kind != JsonKind.Number) return fallback;
102
+ return int.TryParse(NumberText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
103
+ ? parsed
104
+ : fallback;
105
+ }
106
+
107
+ public List<string> AsStringList()
108
+ {
109
+ var result = new List<string>();
110
+ if (Kind != JsonKind.Array) return result;
111
+ foreach (var item in Items)
112
+ if (item.Kind == JsonKind.String) result.Add(item.String);
113
+ return result;
114
+ }
115
+
116
+ /// <summary>
117
+ /// A deep copy.
118
+ ///
119
+ /// ⚠️ Serialising must not MUTATE. `ToJson(forBuild: true)` strips keys
120
+ /// that describe a different set of bytes, and doing that in place also
121
+ /// stripped them from the manifest still open in the editor — so writing
122
+ /// a build folder silently edited the file the publisher had not saved.
123
+ /// A test caught it; the fix is that nothing serialises the live tree.
124
+ /// </summary>
125
+ public JsonValue Clone()
126
+ {
127
+ var copy = new JsonValue
128
+ {
129
+ Kind = Kind,
130
+ Bool = Bool,
131
+ NumberText = NumberText,
132
+ String = String,
133
+ };
134
+ if (Items != null)
135
+ {
136
+ copy.Items = new List<JsonValue>(Items.Count);
137
+ foreach (var item in Items) copy.Items.Add(item.Clone());
138
+ }
139
+ if (Members != null)
140
+ {
141
+ copy.Members = new List<KeyValuePair<string, JsonValue>>(Members.Count);
142
+ foreach (var member in Members)
143
+ copy.Members.Add(new KeyValuePair<string, JsonValue>(member.Key, member.Value.Clone()));
144
+ }
145
+ return copy;
146
+ }
147
+
148
+ public static JsonValue ArrayOf(IEnumerable<string> values)
149
+ {
150
+ var array = NewArray();
151
+ foreach (var value in values) array.Items.Add(Str(value));
152
+ return array;
153
+ }
154
+ }
155
+
156
+ internal static class SquidHubJson
157
+ {
158
+ /// <summary>Parses JSON text. Throws <see cref="FormatException"/> on anything malformed.</summary>
159
+ public static JsonValue Parse(string text)
160
+ {
161
+ if (text == null) throw new FormatException("no JSON to read");
162
+ var index = 0;
163
+ var value = ParseValue(text, ref index);
164
+ SkipWhitespace(text, ref index);
165
+ if (index != text.Length) throw new FormatException("trailing content after the JSON value");
166
+ return value;
167
+ }
168
+
169
+ /// <summary>Two-space indented, newline-terminated — the shape the other manifests in the repo are written in.</summary>
170
+ public static string Write(JsonValue value)
171
+ {
172
+ var sb = new StringBuilder();
173
+ WriteValue(sb, value, 0);
174
+ return sb.Append('\n').ToString();
175
+ }
176
+
177
+ private static void WriteValue(StringBuilder sb, JsonValue value, int depth)
178
+ {
179
+ switch (value.Kind)
180
+ {
181
+ case JsonKind.Null: sb.Append("null"); break;
182
+ case JsonKind.Bool: sb.Append(value.Bool ? "true" : "false"); break;
183
+ case JsonKind.Number: sb.Append(value.NumberText); break;
184
+ case JsonKind.String: sb.Append(SquidHubBuildScan.JsonString(value.String)); break;
185
+
186
+ case JsonKind.Array:
187
+ if (value.Items.Count == 0) { sb.Append("[]"); break; }
188
+ sb.Append("[\n");
189
+ for (var i = 0; i < value.Items.Count; i++)
190
+ {
191
+ Indent(sb, depth + 1);
192
+ WriteValue(sb, value.Items[i], depth + 1);
193
+ if (i < value.Items.Count - 1) sb.Append(',');
194
+ sb.Append('\n');
195
+ }
196
+ Indent(sb, depth);
197
+ sb.Append(']');
198
+ break;
199
+
200
+ case JsonKind.Object:
201
+ if (value.Members.Count == 0) { sb.Append("{}"); break; }
202
+ sb.Append("{\n");
203
+ for (var i = 0; i < value.Members.Count; i++)
204
+ {
205
+ Indent(sb, depth + 1);
206
+ sb.Append(SquidHubBuildScan.JsonString(value.Members[i].Key)).Append(": ");
207
+ WriteValue(sb, value.Members[i].Value, depth + 1);
208
+ if (i < value.Members.Count - 1) sb.Append(',');
209
+ sb.Append('\n');
210
+ }
211
+ Indent(sb, depth);
212
+ sb.Append('}');
213
+ break;
214
+ }
215
+ }
216
+
217
+ private static void Indent(StringBuilder sb, int depth) => sb.Append(' ', depth * 2);
218
+
219
+ private static JsonValue ParseValue(string text, ref int i)
220
+ {
221
+ SkipWhitespace(text, ref i);
222
+ if (i >= text.Length) throw new FormatException("unexpected end of JSON");
223
+
224
+ switch (text[i])
225
+ {
226
+ case '{': return ParseObject(text, ref i);
227
+ case '[': return ParseArray(text, ref i);
228
+ case '"': return JsonValue.Str(ParseString(text, ref i));
229
+ case 't': Expect(text, ref i, "true"); return JsonValue.Of(true);
230
+ case 'f': Expect(text, ref i, "false"); return JsonValue.Of(false);
231
+ case 'n': Expect(text, ref i, "null"); return JsonValue.Null();
232
+ default: return ParseNumber(text, ref i);
233
+ }
234
+ }
235
+
236
+ private static JsonValue ParseObject(string text, ref int i)
237
+ {
238
+ var result = JsonValue.NewObject();
239
+ i++; // '{'
240
+ SkipWhitespace(text, ref i);
241
+ if (i < text.Length && text[i] == '}') { i++; return result; }
242
+
243
+ while (true)
244
+ {
245
+ SkipWhitespace(text, ref i);
246
+ if (i >= text.Length || text[i] != '"') throw new FormatException("expected a key at " + i);
247
+ var key = ParseString(text, ref i);
248
+
249
+ SkipWhitespace(text, ref i);
250
+ if (i >= text.Length || text[i] != ':') throw new FormatException("expected ':' at " + i);
251
+ i++;
252
+
253
+ // ⚠️ Added, never Set: a duplicate key in the SOURCE file is
254
+ // preserved as written rather than quietly collapsed. We did not
255
+ // write it and cannot know which one the author meant.
256
+ result.Members.Add(new KeyValuePair<string, JsonValue>(key, ParseValue(text, ref i)));
257
+
258
+ SkipWhitespace(text, ref i);
259
+ if (i >= text.Length) throw new FormatException("unterminated object");
260
+ if (text[i] == ',') { i++; continue; }
261
+ if (text[i] == '}') { i++; return result; }
262
+ throw new FormatException("expected ',' or '}' at " + i);
263
+ }
264
+ }
265
+
266
+ private static JsonValue ParseArray(string text, ref int i)
267
+ {
268
+ var result = JsonValue.NewArray();
269
+ i++; // '['
270
+ SkipWhitespace(text, ref i);
271
+ if (i < text.Length && text[i] == ']') { i++; return result; }
272
+
273
+ while (true)
274
+ {
275
+ result.Items.Add(ParseValue(text, ref i));
276
+ SkipWhitespace(text, ref i);
277
+ if (i >= text.Length) throw new FormatException("unterminated array");
278
+ if (text[i] == ',') { i++; continue; }
279
+ if (text[i] == ']') { i++; return result; }
280
+ throw new FormatException("expected ',' or ']' at " + i);
281
+ }
282
+ }
283
+
284
+ private static string ParseString(string text, ref int i)
285
+ {
286
+ i++; // opening quote
287
+ var sb = new StringBuilder();
288
+ while (true)
289
+ {
290
+ if (i >= text.Length) throw new FormatException("unterminated string");
291
+ var c = text[i++];
292
+ if (c == '"') return sb.ToString();
293
+ if (c != '\\') { sb.Append(c); continue; }
294
+
295
+ if (i >= text.Length) throw new FormatException("unterminated escape");
296
+ var escape = text[i++];
297
+ switch (escape)
298
+ {
299
+ case '"': sb.Append('"'); break;
300
+ case '\\': sb.Append('\\'); break;
301
+ case '/': sb.Append('/'); break;
302
+ case 'b': sb.Append('\b'); break;
303
+ case 'f': sb.Append('\f'); break;
304
+ case 'n': sb.Append('\n'); break;
305
+ case 'r': sb.Append('\r'); break;
306
+ case 't': sb.Append('\t'); break;
307
+ case 'u':
308
+ if (i + 4 > text.Length) throw new FormatException("truncated \\u escape");
309
+ sb.Append((char)Convert.ToInt32(text.Substring(i, 4), 16));
310
+ i += 4;
311
+ break;
312
+ default: throw new FormatException("unknown escape \\" + escape);
313
+ }
314
+ }
315
+ }
316
+
317
+ private static JsonValue ParseNumber(string text, ref int i)
318
+ {
319
+ var start = i;
320
+ if (i < text.Length && (text[i] == '-' || text[i] == '+')) i++;
321
+ while (i < text.Length && (char.IsDigit(text[i]) || text[i] == '.' ||
322
+ text[i] == 'e' || text[i] == 'E' ||
323
+ text[i] == '-' || text[i] == '+')) i++;
324
+
325
+ var raw = text.Substring(start, i - start);
326
+ if (raw.Length == 0 ||
327
+ !double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out _))
328
+ {
329
+ throw new FormatException("not a number at " + start + ": '" + raw + "'");
330
+ }
331
+ return new JsonValue { Kind = JsonKind.Number, NumberText = raw };
332
+ }
333
+
334
+ private static void Expect(string text, ref int i, string literal)
335
+ {
336
+ if (i + literal.Length > text.Length || text.Substring(i, literal.Length) != literal)
337
+ throw new FormatException("expected '" + literal + "' at " + i);
338
+ i += literal.Length;
339
+ }
340
+
341
+ private static void SkipWhitespace(string text, ref int i)
342
+ {
343
+ while (i < text.Length && (text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r')) i++;
344
+ }
345
+ }
346
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 33456e1dcf046fa1f5728895b7fb30d5
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: