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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.665",
3
+ "version": "0.1.666",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -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
- // tps-game-ctx-state-buffs-not-iterable: ctx.state (entity._appState) is app-owned internal state, and
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 }