isaacscript-common 6.10.0 → 6.11.0

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,388 @@
1
+ --
2
+ -- json.lua
3
+ --
4
+ -- Copyright (c) 2020 rxi
5
+ --
6
+ -- Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ -- this software and associated documentation files (the "Software"), to deal in
8
+ -- the Software without restriction, including without limitation the rights to
9
+ -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10
+ -- of the Software, and to permit persons to whom the Software is furnished to do
11
+ -- so, subject to the following conditions:
12
+ --
13
+ -- The above copyright notice and this permission notice shall be included in all
14
+ -- copies or substantial portions of the Software.
15
+ --
16
+ -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ -- SOFTWARE.
23
+ --
24
+
25
+ local json = { _version = "0.1.2" }
26
+
27
+ -------------------------------------------------------------------------------
28
+ -- Encode
29
+ -------------------------------------------------------------------------------
30
+
31
+ local encode
32
+
33
+ local escape_char_map = {
34
+ [ "\\" ] = "\\",
35
+ [ "\"" ] = "\"",
36
+ [ "\b" ] = "b",
37
+ [ "\f" ] = "f",
38
+ [ "\n" ] = "n",
39
+ [ "\r" ] = "r",
40
+ [ "\t" ] = "t",
41
+ }
42
+
43
+ local escape_char_map_inv = { [ "/" ] = "/" }
44
+ for k, v in pairs(escape_char_map) do
45
+ escape_char_map_inv[v] = k
46
+ end
47
+
48
+
49
+ local function escape_char(c)
50
+ return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
51
+ end
52
+
53
+
54
+ local function encode_nil(val)
55
+ return "null"
56
+ end
57
+
58
+
59
+ local function encode_table(val, stack)
60
+ local res = {}
61
+ stack = stack or {}
62
+
63
+ -- Circular reference?
64
+ if stack[val] then error("circular reference") end
65
+
66
+ stack[val] = true
67
+
68
+ if rawget(val, 1) ~= nil or next(val) == nil then
69
+ -- Treat as array -- check keys are valid and it is not sparse
70
+ local n = 0
71
+ for k in pairs(val) do
72
+ if type(k) ~= "number" then
73
+ error("invalid table: mixed or invalid key types")
74
+ end
75
+ n = n + 1
76
+ end
77
+ if n ~= #val then
78
+ error("invalid table: sparse array")
79
+ end
80
+ -- Encode
81
+ for i, v in ipairs(val) do
82
+ table.insert(res, encode(v, stack))
83
+ end
84
+ stack[val] = nil
85
+ return "[" .. table.concat(res, ",") .. "]"
86
+
87
+ else
88
+ -- Treat as an object
89
+ for k, v in pairs(val) do
90
+ if type(k) ~= "string" then
91
+ error("invalid table: mixed or invalid key types")
92
+ end
93
+ table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
94
+ end
95
+ stack[val] = nil
96
+ return "{" .. table.concat(res, ",") .. "}"
97
+ end
98
+ end
99
+
100
+
101
+ local function encode_string(val)
102
+ return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
103
+ end
104
+
105
+
106
+ local function encode_number(val)
107
+ -- Check for NaN, -inf and inf
108
+ if val ~= val or val <= -math.huge or val >= math.huge then
109
+ error("unexpected number value '" .. tostring(val) .. "'")
110
+ end
111
+ return string.format("%.14g", val)
112
+ end
113
+
114
+
115
+ local type_func_map = {
116
+ [ "nil" ] = encode_nil,
117
+ [ "table" ] = encode_table,
118
+ [ "string" ] = encode_string,
119
+ [ "number" ] = encode_number,
120
+ [ "boolean" ] = tostring,
121
+ }
122
+
123
+
124
+ encode = function(val, stack)
125
+ local t = type(val)
126
+ local f = type_func_map[t]
127
+ if f then
128
+ return f(val, stack)
129
+ end
130
+ error("unexpected type '" .. t .. "'")
131
+ end
132
+
133
+
134
+ function json.encode(val)
135
+ return ( encode(val) )
136
+ end
137
+
138
+
139
+ -------------------------------------------------------------------------------
140
+ -- Decode
141
+ -------------------------------------------------------------------------------
142
+
143
+ local parse
144
+
145
+ local function create_set(...)
146
+ local res = {}
147
+ for i = 1, select("#", ...) do
148
+ res[ select(i, ...) ] = true
149
+ end
150
+ return res
151
+ end
152
+
153
+ local space_chars = create_set(" ", "\t", "\r", "\n")
154
+ local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
155
+ local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
156
+ local literals = create_set("true", "false", "null")
157
+
158
+ local literal_map = {
159
+ [ "true" ] = true,
160
+ [ "false" ] = false,
161
+ [ "null" ] = nil,
162
+ }
163
+
164
+
165
+ local function next_char(str, idx, set, negate)
166
+ for i = idx, #str do
167
+ if set[str:sub(i, i)] ~= negate then
168
+ return i
169
+ end
170
+ end
171
+ return #str + 1
172
+ end
173
+
174
+
175
+ local function decode_error(str, idx, msg)
176
+ local line_count = 1
177
+ local col_count = 1
178
+ for i = 1, idx - 1 do
179
+ col_count = col_count + 1
180
+ if str:sub(i, i) == "\n" then
181
+ line_count = line_count + 1
182
+ col_count = 1
183
+ end
184
+ end
185
+ error( string.format("%s at line %d col %d", msg, line_count, col_count) )
186
+ end
187
+
188
+
189
+ local function codepoint_to_utf8(n)
190
+ -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
191
+ local f = math.floor
192
+ if n <= 0x7f then
193
+ return string.char(n)
194
+ elseif n <= 0x7ff then
195
+ return string.char(f(n / 64) + 192, n % 64 + 128)
196
+ elseif n <= 0xffff then
197
+ return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
198
+ elseif n <= 0x10ffff then
199
+ return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
200
+ f(n % 4096 / 64) + 128, n % 64 + 128)
201
+ end
202
+ error( string.format("invalid unicode codepoint '%x'", n) )
203
+ end
204
+
205
+
206
+ local function parse_unicode_escape(s)
207
+ local n1 = tonumber( s:sub(1, 4), 16 )
208
+ local n2 = tonumber( s:sub(7, 10), 16 )
209
+ -- Surrogate pair?
210
+ if n2 then
211
+ return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
212
+ else
213
+ return codepoint_to_utf8(n1)
214
+ end
215
+ end
216
+
217
+
218
+ local function parse_string(str, i)
219
+ local res = ""
220
+ local j = i + 1
221
+ local k = j
222
+
223
+ while j <= #str do
224
+ local x = str:byte(j)
225
+
226
+ if x < 32 then
227
+ decode_error(str, j, "control character in string")
228
+
229
+ elseif x == 92 then -- `\`: Escape
230
+ res = res .. str:sub(k, j - 1)
231
+ j = j + 1
232
+ local c = str:sub(j, j)
233
+ if c == "u" then
234
+ local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
235
+ or str:match("^%x%x%x%x", j + 1)
236
+ or decode_error(str, j - 1, "invalid unicode escape in string")
237
+ res = res .. parse_unicode_escape(hex)
238
+ j = j + #hex
239
+ else
240
+ if not escape_chars[c] then
241
+ decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
242
+ end
243
+ res = res .. escape_char_map_inv[c]
244
+ end
245
+ k = j + 1
246
+
247
+ elseif x == 34 then -- `"`: End of string
248
+ res = res .. str:sub(k, j - 1)
249
+ return res, j + 1
250
+ end
251
+
252
+ j = j + 1
253
+ end
254
+
255
+ decode_error(str, i, "expected closing quote for string")
256
+ end
257
+
258
+
259
+ local function parse_number(str, i)
260
+ local x = next_char(str, i, delim_chars)
261
+ local s = str:sub(i, x - 1)
262
+ local n = tonumber(s)
263
+ if not n then
264
+ decode_error(str, i, "invalid number '" .. s .. "'")
265
+ end
266
+ return n, x
267
+ end
268
+
269
+
270
+ local function parse_literal(str, i)
271
+ local x = next_char(str, i, delim_chars)
272
+ local word = str:sub(i, x - 1)
273
+ if not literals[word] then
274
+ decode_error(str, i, "invalid literal '" .. word .. "'")
275
+ end
276
+ return literal_map[word], x
277
+ end
278
+
279
+
280
+ local function parse_array(str, i)
281
+ local res = {}
282
+ local n = 1
283
+ i = i + 1
284
+ while 1 do
285
+ local x
286
+ i = next_char(str, i, space_chars, true)
287
+ -- Empty / end of array?
288
+ if str:sub(i, i) == "]" then
289
+ i = i + 1
290
+ break
291
+ end
292
+ -- Read token
293
+ x, i = parse(str, i)
294
+ res[n] = x
295
+ n = n + 1
296
+ -- Next token
297
+ i = next_char(str, i, space_chars, true)
298
+ local chr = str:sub(i, i)
299
+ i = i + 1
300
+ if chr == "]" then break end
301
+ if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
302
+ end
303
+ return res, i
304
+ end
305
+
306
+
307
+ local function parse_object(str, i)
308
+ local res = {}
309
+ i = i + 1
310
+ while 1 do
311
+ local key, val
312
+ i = next_char(str, i, space_chars, true)
313
+ -- Empty / end of object?
314
+ if str:sub(i, i) == "}" then
315
+ i = i + 1
316
+ break
317
+ end
318
+ -- Read key
319
+ if str:sub(i, i) ~= '"' then
320
+ decode_error(str, i, "expected string for key")
321
+ end
322
+ key, i = parse(str, i)
323
+ -- Read ':' delimiter
324
+ i = next_char(str, i, space_chars, true)
325
+ if str:sub(i, i) ~= ":" then
326
+ decode_error(str, i, "expected ':' after key")
327
+ end
328
+ i = next_char(str, i + 1, space_chars, true)
329
+ -- Read value
330
+ val, i = parse(str, i)
331
+ -- Set
332
+ res[key] = val
333
+ -- Next token
334
+ i = next_char(str, i, space_chars, true)
335
+ local chr = str:sub(i, i)
336
+ i = i + 1
337
+ if chr == "}" then break end
338
+ if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
339
+ end
340
+ return res, i
341
+ end
342
+
343
+
344
+ local char_func_map = {
345
+ [ '"' ] = parse_string,
346
+ [ "0" ] = parse_number,
347
+ [ "1" ] = parse_number,
348
+ [ "2" ] = parse_number,
349
+ [ "3" ] = parse_number,
350
+ [ "4" ] = parse_number,
351
+ [ "5" ] = parse_number,
352
+ [ "6" ] = parse_number,
353
+ [ "7" ] = parse_number,
354
+ [ "8" ] = parse_number,
355
+ [ "9" ] = parse_number,
356
+ [ "-" ] = parse_number,
357
+ [ "t" ] = parse_literal,
358
+ [ "f" ] = parse_literal,
359
+ [ "n" ] = parse_literal,
360
+ [ "[" ] = parse_array,
361
+ [ "{" ] = parse_object,
362
+ }
363
+
364
+
365
+ parse = function(str, idx)
366
+ local chr = str:sub(idx, idx)
367
+ local f = char_func_map[chr]
368
+ if f then
369
+ return f(str, idx)
370
+ end
371
+ decode_error(str, idx, "unexpected character '" .. chr .. "'")
372
+ end
373
+
374
+
375
+ function json.decode(str)
376
+ if type(str) ~= "string" then
377
+ error("expected argument of type string, got " .. type(str))
378
+ end
379
+ local res, idx = parse(str, next_char(str, 1, space_chars, true))
380
+ idx = next_char(str, idx, space_chars, true)
381
+ if idx <= #str then
382
+ decode_error(str, idx, "trailing garbage")
383
+ end
384
+ return res
385
+ end
386
+
387
+
388
+ return json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isaacscript-common",
3
- "version": "6.10.0",
3
+ "version": "6.11.0",
4
4
  "description": "Helper functions and features for IsaacScript mods.",
5
5
  "keywords": [
6
6
  "isaac",
@@ -8,7 +8,7 @@ import { TSTLClass } from "../types/private/TSTLClass";
8
8
  import { isArray } from "./array";
9
9
  import { getEnumValues } from "./enums";
10
10
  import { getIsaacAPIClassName } from "./isaacAPIClass";
11
- import { log, logTable } from "./log";
11
+ import { log } from "./log";
12
12
  import {
13
13
  copyIsaacAPIClass,
14
14
  deserializeIsaacAPIClass,
@@ -209,6 +209,11 @@ function deepCopyDefaultMap(
209
209
  traversalDescription: string,
210
210
  insideMap: boolean,
211
211
  ) {
212
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
213
+ if (SAVE_DATA_MANAGER_DEBUG) {
214
+ log("deepCopy is copying a DefaultMap.");
215
+ }
216
+
212
217
  const constructorArg = isDefaultMap(defaultMap)
213
218
  ? defaultMap.getConstructorArg()
214
219
  : undefined; // The undefined case is handled explicitly in the "getNewDefaultMap" function.
@@ -291,7 +296,7 @@ function getNewDefaultMap(
291
296
  serializationType: SerializationType,
292
297
  traversalDescription: string,
293
298
  constructorArg: unknown,
294
- ) {
299
+ ): DefaultMap<AnyNotNil, unknown> | LuaMap<AnyNotNil, unknown> {
295
300
  switch (serializationType) {
296
301
  case SerializationType.NONE: {
297
302
  // eslint-disable-next-line isaacscript/no-invalid-default-map
@@ -336,6 +341,11 @@ function deepCopyMap(
336
341
  traversalDescription: string,
337
342
  insideMap: boolean,
338
343
  ) {
344
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
345
+ if (SAVE_DATA_MANAGER_DEBUG) {
346
+ log("deepCopy is copying a Map.");
347
+ }
348
+
339
349
  let newMap: Map<AnyNotNil, unknown> | LuaMap<AnyNotNil, unknown>;
340
350
  if (serializationType === SerializationType.SERIALIZE) {
341
351
  // Since we are serializing, the new object will be a Lua table.
@@ -384,6 +394,11 @@ function deepCopySet(
384
394
  traversalDescription: string,
385
395
  insideMap: boolean,
386
396
  ) {
397
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
398
+ if (SAVE_DATA_MANAGER_DEBUG) {
399
+ log("deepCopy is copying a Set.");
400
+ }
401
+
387
402
  let newSet: Set<AnyNotNil> | LuaMap<AnyNotNil, string>;
388
403
  if (serializationType === SerializationType.SERIALIZE) {
389
404
  // For serialization purposes, we represent a `Set` as a table with keys that match the
@@ -405,7 +420,7 @@ function deepCopySet(
405
420
  // Differentiating between the two types looks superfluous but is necessary for TSTL to produce
406
421
  // the proper set method call.
407
422
  if (isTSTLSet(newSet)) {
408
- // We should never be serializing an object of type Set.
423
+ // We should never be serializing an object of type `Set`.
409
424
  error(
410
425
  "The deep copy function cannot convert number keys to strings for a Set.",
411
426
  );
@@ -433,6 +448,11 @@ function deepCopyTSTLClass(
433
448
  traversalDescription: string,
434
449
  insideMap: boolean,
435
450
  ) {
451
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
452
+ if (SAVE_DATA_MANAGER_DEBUG) {
453
+ log("deepCopy is copying a TSTL class.");
454
+ }
455
+
436
456
  let newClass: TSTLClass | LuaMap<AnyNotNil, unknown>;
437
457
  if (serializationType === SerializationType.SERIALIZE) {
438
458
  // Since we are serializing, the new object will be a Lua table.
@@ -467,6 +487,11 @@ function deepCopyArray(
467
487
  traversalDescription: string,
468
488
  insideMap: boolean,
469
489
  ) {
490
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
491
+ if (SAVE_DATA_MANAGER_DEBUG) {
492
+ log("deepCopy is copying an array.");
493
+ }
494
+
470
495
  const newArray: unknown[] = [];
471
496
 
472
497
  for (const value of array) {
@@ -488,6 +513,11 @@ function deepCopyNormalLuaTable(
488
513
  traversalDescription: string,
489
514
  insideMap: boolean,
490
515
  ) {
516
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
517
+ if (SAVE_DATA_MANAGER_DEBUG) {
518
+ log("deepCopy is copying a normal Lua table.");
519
+ }
520
+
491
521
  const newTable = new LuaMap<AnyNotNil, unknown>();
492
522
  const { entries, convertedNumberKeysToStrings } = getCopiedEntries(
493
523
  luaMap,
@@ -535,10 +565,17 @@ function getCopiedEntries(
535
565
 
536
566
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
537
567
  if (SAVE_DATA_MANAGER_DEBUG) {
538
- logTable(entries);
539
568
  entries.sort(twoDimensionalSort);
540
569
  }
541
570
 
571
+ // During serialization, we brand some Lua tables with a special identifier to signify that it has
572
+ // keys that should be deserialized to numbers.
573
+ const convertStringKeysToNumbers =
574
+ serializationType === SerializationType.DESERIALIZE &&
575
+ entries.some(
576
+ ([key]) => key === (SerializationBrand.OBJECT_WITH_NUMBER_KEYS as string),
577
+ );
578
+
542
579
  const hasNumberKeys = entries.some(([key]) => isNumber(key));
543
580
  const convertNumberKeysToStrings =
544
581
  serializationType === SerializationType.SERIALIZE && hasNumberKeys;
@@ -560,7 +597,16 @@ function getCopiedEntries(
560
597
  insideMap,
561
598
  );
562
599
 
563
- const keyToUse = convertNumberKeysToStrings ? tostring(key) : key;
600
+ let keyToUse = key;
601
+ if (convertStringKeysToNumbers) {
602
+ const numberKey = tonumber(key);
603
+ if (numberKey !== undefined) {
604
+ keyToUse = numberKey;
605
+ }
606
+ }
607
+ if (convertNumberKeysToStrings) {
608
+ keyToUse = tostring(key);
609
+ }
564
610
  copiedEntries.push([keyToUse, newValue]);
565
611
  }
566
612
 
@@ -600,6 +646,11 @@ function deepCopyUserdata(
600
646
  serializationType: SerializationType,
601
647
  traversalDescription: string,
602
648
  ) {
649
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
650
+ if (SAVE_DATA_MANAGER_DEBUG) {
651
+ log("deepCopy is copying userdata.");
652
+ }
653
+
603
654
  const classType = getIsaacAPIClassName(value);
604
655
  if (classType === undefined) {
605
656
  error(