spoint 0.1.665 → 0.1.666
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.
- package/package.json +1 -1
- package/src/apps/AppRuntime.js +1 -64
- package/src/apps/AppRuntimeState.js +75 -0
package/package.json
CHANGED
package/src/apps/AppRuntime.js
CHANGED
|
@@ -4,18 +4,6 @@ import { EventBus } from './EventBus.js'
|
|
|
4
4
|
import { createEcsEntityMap } from './EcsEntityMap.js'
|
|
5
5
|
import { mulQuat, rotVec } from '../math.js'
|
|
6
6
|
import { MSG } from '../protocol/MessageTypes.js'
|
|
7
|
-
let _existsSync = null, _resolve = null, _realpathSync = null, _sep = '/'
|
|
8
|
-
try { if (typeof process !== 'undefined' && process.versions?.node) { const fs = await import('node:fs'); const path = await import('node:path'); _existsSync = fs.existsSync; _resolve = path.resolve; _realpathSync = fs.realpathSync; _sep = path.sep } } catch {}
|
|
9
|
-
|
|
10
|
-
function containedAssetPath(filePath, rootDir) {
|
|
11
|
-
if (!_realpathSync || !rootDir) return null
|
|
12
|
-
let rootReal
|
|
13
|
-
try { rootReal = _realpathSync(rootDir) } catch { return null }
|
|
14
|
-
const prefix = rootReal.endsWith(_sep) ? rootReal : rootReal + _sep
|
|
15
|
-
let real
|
|
16
|
-
try { real = _realpathSync(filePath) } catch { return null }
|
|
17
|
-
return (real === rootReal || real.startsWith(prefix)) ? real : null
|
|
18
|
-
}
|
|
19
7
|
import { SpatialIndex } from '../spatial/Octree.js'
|
|
20
8
|
import { vecOK } from '../shared/vecGuard.js'
|
|
21
9
|
import { weaponNameToCode } from '../shared/WeaponCodes.js'
|
|
@@ -24,58 +12,7 @@ import { mixinTick } from './AppRuntimeTick.js'
|
|
|
24
12
|
import { installCustomVersion } from './CustomVersion.js'
|
|
25
13
|
import { resolveCCD } from './AppPhysics.js'
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
// several shipped apps (apps/tps-game/index.js's buffs/invuln/respawning/ammo/reloading/lastEmoteAt/
|
|
29
|
-
// fallTimers/killStreaks/powerups/playerStats, all constructed as `new Map()` in setup()) store real
|
|
30
|
-
// Map/Set instances in it. A naive `JSON.parse(JSON.stringify(appState))` -- the discipline
|
|
31
|
-
// snapshotGameState/restoreGameState already used for entity.custom, which genuinely IS meant to be
|
|
32
|
-
// plain-JSON/editor-facing -- silently downgrades any Map to `{}` (JSON.stringify(new Map()) is "{}",
|
|
33
|
-
// there is no Map wire type) and any Set to `{}` too. Both restoreGameState (in-process rollback +
|
|
34
|
-
// hot-reload, see rollback-entity-gamestate-snapshot) and WorldPersistence.js's on-disk restart-survival
|
|
35
|
-
// save/restore round-trip appState through this exact path, so EVERY app whose ctx.state holds a Map
|
|
36
|
-
// silently loses it on the next resimulate/restore -- e.g. `for (const [pid,buff] of ctx.state.buffs)`
|
|
37
|
-
// throwing "ctx.state.buffs is not iterable" the next time update() runs after a restore, since
|
|
38
|
-
// ctx.state.buffs is now a plain object, not a Map. Fixed at the root (not per-app defensive guards,
|
|
39
|
-
// which only patch the ONE field a given app happened to notice broke) via a tagged Map/Set-preserving
|
|
40
|
-
// pair, used ONLY for appState (custom stays plain JSON -- editor/wire-format contract, must not
|
|
41
|
-
// silently start carrying non-JSON-safe tags).
|
|
42
|
-
//
|
|
43
|
-
// IMPORTANT split, found live via the WorldPersistence.js on-disk path: snapshotGameState's OWN output
|
|
44
|
-
// must stay a plain-JSON-safe TAGGED shape (never a live Map/Set instance), not a fully round-tripped-
|
|
45
|
-
// back-to-Map value -- WorldPersistence.buildWorldSnapshot takes snapshotGameState's entities verbatim
|
|
46
|
-
// and hands them straight to storage.set (FSAdapter.set does its own unguarded JSON.stringify(value)
|
|
47
|
-
// with no replacer of its own), so a live Map sitting in that structure gets silently flattened to `{}`
|
|
48
|
-
// a SECOND time on the way to disk, exactly reproducing the original bug one level downstream. Tagging
|
|
49
|
-
// at snapshot time (tagAppState) and reviving only at actual restore time (untagAppState, called from
|
|
50
|
-
// restoreGameState) keeps the snapshot object plain-JSON-safe everywhere in between, correct for both
|
|
51
|
-
// the in-process rollback consumer (never itself re-serializes the snapshot) and the on-disk consumer
|
|
52
|
-
// (storage.set's plain JSON.stringify is now a no-op on an already-plain-JSON tree).
|
|
53
|
-
function tagAppState(state) {
|
|
54
|
-
if (!state) return null
|
|
55
|
-
const replacer = (key, value) => {
|
|
56
|
-
if (value instanceof Map) return { __type: 'Map', entries: [...value.entries()] }
|
|
57
|
-
if (value instanceof Set) return { __type: 'Set', values: [...value.values()] }
|
|
58
|
-
return value
|
|
59
|
-
}
|
|
60
|
-
return JSON.parse(JSON.stringify(state, replacer))
|
|
61
|
-
}
|
|
62
|
-
// Reviving on the way in tolerates EITHER a tagged plain-JSON shape (the normal case: freshly tagged by
|
|
63
|
-
// tagAppState above, or read back off disk after a real storage.get/JSON.parse round trip) OR a
|
|
64
|
-
// snapshot taken BEFORE this fix existed (on-disk, from an old process -- has plain `{}` where a Map
|
|
65
|
-
// used to be, no tag at all) -- reviving that degrades to a plain object, not a fabricated Map, which is
|
|
66
|
-
// the correct honest-degrade behavior for old data rather than a hard requirement on snapshot shape.
|
|
67
|
-
function untagAppState(state) {
|
|
68
|
-
if (!state) return null
|
|
69
|
-
const reviver = (key, value) => {
|
|
70
|
-
if (value && typeof value === 'object' && value.__type === 'Map' && Array.isArray(value.entries)) return new Map(value.entries)
|
|
71
|
-
if (value && typeof value === 'object' && value.__type === 'Set' && Array.isArray(value.values)) return new Set(value.values)
|
|
72
|
-
return value
|
|
73
|
-
}
|
|
74
|
-
// state may already be a plain-JSON tree (tagAppState's own output, or a fresh JSON.parse off disk) --
|
|
75
|
-
// stringify-then-parse-with-reviver is the simplest way to apply the reviver uniformly to every nested
|
|
76
|
-
// level without hand-walking the tree twice for the two different possible input shapes.
|
|
77
|
-
return JSON.parse(JSON.stringify(state), reviver)
|
|
78
|
-
}
|
|
15
|
+
import { containedAssetPath, tagAppState, untagAppState, _existsSync, _resolve } from './AppRuntimeState.js'
|
|
79
16
|
|
|
80
17
|
export class AppRuntime {
|
|
81
18
|
constructor(c = {}) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Stateless entity/app-state helpers for AppRuntime.js: sandboxed asset-path containment check and the
|
|
2
|
+
// Map/Set-preserving tag/untag pair for ctx.state (app-owned internal state that may hold live Map/Set
|
|
3
|
+
// instances -- see tagAppState's own comment for why the naive JSON round-trip snapshotGameState/
|
|
4
|
+
// restoreGameState uses for entity.custom silently drops them). Split out as AppRuntime.js's only
|
|
5
|
+
// pure-function block -- everything else in that file is an AppRuntime instance method reading `this.*`.
|
|
6
|
+
// Node fs/path handles are resolved here too (containedAssetPath needs realpathSync/sep); AppRuntime.js
|
|
7
|
+
// imports the SAME resolved handles for its own resolveAssetPath rather than re-resolving them.
|
|
8
|
+
|
|
9
|
+
let _existsSync = null, _resolve = null, _realpathSync = null, _sep = '/'
|
|
10
|
+
try { if (typeof process !== 'undefined' && process.versions?.node) { const fs = await import('node:fs'); const path = await import('node:path'); _existsSync = fs.existsSync; _resolve = path.resolve; _realpathSync = fs.realpathSync; _sep = path.sep } } catch {}
|
|
11
|
+
|
|
12
|
+
function containedAssetPath(filePath, rootDir) {
|
|
13
|
+
if (!_realpathSync || !rootDir) return null
|
|
14
|
+
let rootReal
|
|
15
|
+
try { rootReal = _realpathSync(rootDir) } catch { return null }
|
|
16
|
+
const prefix = rootReal.endsWith(_sep) ? rootReal : rootReal + _sep
|
|
17
|
+
let real
|
|
18
|
+
try { real = _realpathSync(filePath) } catch { return null }
|
|
19
|
+
return (real === rootReal || real.startsWith(prefix)) ? real : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// tps-game-ctx-state-buffs-not-iterable: ctx.state (entity._appState) is app-owned internal state, and
|
|
23
|
+
// several shipped apps (apps/tps-game/index.js's buffs/invuln/respawning/ammo/reloading/lastEmoteAt/
|
|
24
|
+
// fallTimers/killStreaks/powerups/playerStats, all constructed as `new Map()` in setup()) store real
|
|
25
|
+
// Map/Set instances in it. A naive `JSON.parse(JSON.stringify(appState))` -- the discipline
|
|
26
|
+
// snapshotGameState/restoreGameState already used for entity.custom, which genuinely IS meant to be
|
|
27
|
+
// plain-JSON/editor-facing -- silently downgrades any Map to `{}` (JSON.stringify(new Map()) is "{}",
|
|
28
|
+
// there is no Map wire type) and any Set to `{}` too. Both restoreGameState (in-process rollback +
|
|
29
|
+
// hot-reload, see rollback-entity-gamestate-snapshot) and WorldPersistence.js's on-disk restart-survival
|
|
30
|
+
// save/restore round-trip appState through this exact path, so EVERY app whose ctx.state holds a Map
|
|
31
|
+
// silently loses it on the next resimulate/restore -- e.g. `for (const [pid,buff] of ctx.state.buffs)`
|
|
32
|
+
// throwing "ctx.state.buffs is not iterable" the next time update() runs after a restore, since
|
|
33
|
+
// ctx.state.buffs is now a plain object, not a Map. Fixed at the root (not per-app defensive guards,
|
|
34
|
+
// which only patch the ONE field a given app happened to notice broke) via a tagged Map/Set-preserving
|
|
35
|
+
// pair, used ONLY for appState (custom stays plain JSON -- editor/wire-format contract, must not
|
|
36
|
+
// silently start carrying non-JSON-safe tags).
|
|
37
|
+
//
|
|
38
|
+
// IMPORTANT split, found live via the WorldPersistence.js on-disk path: snapshotGameState's OWN output
|
|
39
|
+
// must stay a plain-JSON-safe TAGGED shape (never a live Map/Set instance), not a fully round-tripped-
|
|
40
|
+
// back-to-Map value -- WorldPersistence.buildWorldSnapshot takes snapshotGameState's entities verbatim
|
|
41
|
+
// and hands them straight to storage.set (FSAdapter.set does its own unguarded JSON.stringify(value)
|
|
42
|
+
// with no replacer of its own), so a live Map sitting in that structure gets silently flattened to `{}`
|
|
43
|
+
// a SECOND time on the way to disk, exactly reproducing the original bug one level downstream. Tagging
|
|
44
|
+
// at snapshot time (tagAppState) and reviving only at actual restore time (untagAppState, called from
|
|
45
|
+
// restoreGameState) keeps the snapshot object plain-JSON-safe everywhere in between, correct for both
|
|
46
|
+
// the in-process rollback consumer (never itself re-serializes the snapshot) and the on-disk consumer
|
|
47
|
+
// (storage.set's plain JSON.stringify is now a no-op on an already-plain-JSON tree).
|
|
48
|
+
function tagAppState(state) {
|
|
49
|
+
if (!state) return null
|
|
50
|
+
const replacer = (key, value) => {
|
|
51
|
+
if (value instanceof Map) return { __type: 'Map', entries: [...value.entries()] }
|
|
52
|
+
if (value instanceof Set) return { __type: 'Set', values: [...value.values()] }
|
|
53
|
+
return value
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(JSON.stringify(state, replacer))
|
|
56
|
+
}
|
|
57
|
+
// Reviving on the way in tolerates EITHER a tagged plain-JSON shape (the normal case: freshly tagged by
|
|
58
|
+
// tagAppState above, or read back off disk after a real storage.get/JSON.parse round trip) OR a
|
|
59
|
+
// snapshot taken BEFORE this fix existed (on-disk, from an old process -- has plain `{}` where a Map
|
|
60
|
+
// used to be, no tag at all) -- reviving that degrades to a plain object, not a fabricated Map, which is
|
|
61
|
+
// the correct honest-degrade behavior for old data rather than a hard requirement on snapshot shape.
|
|
62
|
+
function untagAppState(state) {
|
|
63
|
+
if (!state) return null
|
|
64
|
+
const reviver = (key, value) => {
|
|
65
|
+
if (value && typeof value === 'object' && value.__type === 'Map' && Array.isArray(value.entries)) return new Map(value.entries)
|
|
66
|
+
if (value && typeof value === 'object' && value.__type === 'Set' && Array.isArray(value.values)) return new Set(value.values)
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
69
|
+
// state may already be a plain-JSON tree (tagAppState's own output, or a fresh JSON.parse off disk) --
|
|
70
|
+
// stringify-then-parse-with-reviver is the simplest way to apply the reviver uniformly to every nested
|
|
71
|
+
// level without hand-walking the tree twice for the two different possible input shapes.
|
|
72
|
+
return JSON.parse(JSON.stringify(state), reviver)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { containedAssetPath, tagAppState, untagAppState, _existsSync, _resolve, _realpathSync, _sep }
|