spoint 0.1.666 → 0.1.667

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.666",
3
+ "version": "0.1.667",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -4,6 +4,7 @@ import { BIOME_PRESETS } from '../terrain/BiomeOverride.js'
4
4
  import { createGrassDecal } from '../terrain/GrassDecal.js'
5
5
  import { createEditOpLog } from './EditOpLog.js'
6
6
  import { createAgentEditServer } from './AgentEditServer.js'
7
+ import { TEXT_EXTS, isTextFile, sanitizeFsError, WORLD_CONFIG_KEYS, serializeEntity, serializeWorld, serializeWorldSource } from './EditorHandlersSerialize.js'
7
8
 
8
9
  const isNode = typeof process !== 'undefined' && process.versions?.node
9
10
  let _fs = null, _path = null, _bakeMinimapIfMissing = null
@@ -46,25 +47,8 @@ const statSync = _fs?.statSync, mkdirSync = _fs?.mkdirSync
46
47
  const realpathSync = _fs?.realpathSync
47
48
  const unlinkSync = _fs?.unlinkSync, renameSync = _fs?.renameSync, rmSync = _fs?.rmSync
48
49
 
49
- // Extensions read/written as text by the fs-browse panel; anything else is reported binary (size-only, no decode).
50
- const TEXT_EXTS = new Set(['.js', '.mjs', '.json', '.md', '.txt', '.css', '.html', '.yml', '.yaml', '.svg'])
51
- function isTextFile(name) {
52
- const i = name.lastIndexOf('.')
53
- return i >= 0 && TEXT_EXTS.has(name.slice(i).toLowerCase())
54
- }
55
50
  const resolvePath = _path?.resolve || (() => ''), joinPath = _path?.join || (() => ''), dirnamePath = _path?.dirname || (() => ''), pathSep = _path?.sep || '/'
56
51
 
57
- // Node's raw fs error messages embed the server's ABSOLUTE filesystem path (e.g. "ENOENT: ...,
58
- // mkdir 'C:\dev\spoint\apps\...'" or a null-byte TypeError quoting the full resolved path) -- sending
59
- // that verbatim to an editor client leaks server directory layout to whatever authored the request.
60
- // Found live via an adversarial VERIFY-phase sweep (null-byte path, overlong-path ENOENT) against the
61
- // real server: both errors round-tripped with the absolute apps-root path intact. Strip it down to the
62
- // operation-relevant leaf (Node error CODE + the client-relative path already known from payload.path)
63
- // so the client still gets an actionable reason without the server's real directory structure.
64
- function sanitizeFsError(e, clientRelativePath) {
65
- const code = e && e.code ? e.code : (e && e.name) || 'ERROR'
66
- return `${code}: operation failed on '${clientRelativePath}'`
67
- }
68
52
 
69
53
  // realpath-based containment check: a symlink inside rootDir could otherwise point outside it and let an editor client read/write arbitrary server files
70
54
  function containedReal(filePath, rootDir) {
@@ -117,43 +101,6 @@ function containedRealCreateParent(filePath, rootDir) {
117
101
  return joinPath(ancestorReal, ...missingSuffix, filePath.slice(dir.length + pathSep.length))
118
102
  }
119
103
 
120
- const WORLD_CONFIG_KEYS = ['port', 'tickRate', 'entityTickRate', 'gravity', 'relevanceRadius', 'physicsRadius', 'physicsBodyBudget', 'movement', 'player', 'scene', 'camera', 'animation', 'input', 'spawnPoint', 'spawnPoints', 'playerModel', 'trustedApps']
121
-
122
- function serializeEntity(e) {
123
- const out = { id: e.id }
124
- if (e.model) out.model = e.model
125
- out.position = [e.position[0], e.position[1], e.position[2]]
126
- const r = e.rotation
127
- if (r && !(r[0] === 0 && r[1] === 0 && r[2] === 0 && r[3] === 1)) out.rotation = [r[0], r[1], r[2], r[3]]
128
- const s = e.scale
129
- if (s && !(s[0] === 1 && s[1] === 1 && s[2] === 1)) out.scale = [s[0], s[1], s[2]]
130
- if (e._appName) out.app = e._appName
131
- if (e.bodyType && e.bodyType !== 'static') out.bodyType = e.bodyType
132
- if (e._config) out.config = e._config
133
- if (e.custom) out.custom = e.custom
134
- if (e.parent) out.parent = e.parent
135
- return out
136
- }
137
-
138
- function serializeWorld(appRuntime, sourceWorldDef) {
139
- const def = {}
140
- const src = sourceWorldDef || {}
141
- for (const k of WORLD_CONFIG_KEYS) if (src[k] !== undefined) def[k] = src[k]
142
- const entities = []
143
- for (const e of appRuntime.entities.values()) {
144
- // Keep any entity carrying authored state: an app, a model, custom props (incl. a primitive's mesh/editorProp
145
- // edits), a saved app config, OR a hierarchy parent (a reparented empty anchor). The old filter dropped an
146
- // entity that had only a _config or only a parent, silently losing that authoring on save.
147
- if (!e._appName && !e.model && !e.custom && !e._config && !e.parent) continue
148
- entities.push(serializeEntity(e))
149
- }
150
- def.entities = entities
151
- return def
152
- }
153
-
154
- function serializeWorldSource(def) {
155
- return JSON.stringify(def, null, 2)
156
- }
157
104
 
158
105
  // In-memory prefab registry, best-effort mirrored to data/prefabs.json when a real filesystem is available
159
106
  // (Node server), inert (memory-only, cleared on restart) under the Worker/singleplayer runtime -- mirrors
@@ -0,0 +1,62 @@
1
+ // Pure entity/world serialization + text-file-extension helpers for EditorHandlers.js's
2
+ // createEditorHandlers: no fs/path dependency, no closure state -- split out as the one genuinely
3
+ // self-contained block in this file (containedReal/containedRealCreateParent/prefab-persistence all
4
+ // depend on the Node-vs-Worker fs handle dance at this file's own top, and stay there).
5
+
6
+ const TEXT_EXTS = new Set(['.js', '.mjs', '.json', '.md', '.txt', '.css', '.html', '.yml', '.yaml', '.svg'])
7
+ function isTextFile(name) {
8
+ const i = name.lastIndexOf('.')
9
+ return i >= 0 && TEXT_EXTS.has(name.slice(i).toLowerCase())
10
+ }
11
+
12
+ // Node's raw fs error messages embed the server's ABSOLUTE filesystem path (e.g. "ENOENT: ...,
13
+ // mkdir 'C:\dev\spoint\apps\...'" or a null-byte TypeError quoting the full resolved path) -- sending
14
+ // that verbatim to an editor client leaks server directory layout to whatever authored the request.
15
+ // Found live via an adversarial VERIFY-phase sweep (null-byte path, overlong-path ENOENT) against the
16
+ // real server: both errors round-tripped with the absolute apps-root path intact. Strip it down to the
17
+ // operation-relevant leaf (Node error CODE + the client-relative path already known from payload.path)
18
+ // so the client still gets an actionable reason without the server's real directory structure.
19
+ function sanitizeFsError(e, clientRelativePath) {
20
+ const code = e && e.code ? e.code : (e && e.name) || 'ERROR'
21
+ return `${code}: operation failed on '${clientRelativePath}'`
22
+ }
23
+
24
+ const WORLD_CONFIG_KEYS = ['port', 'tickRate', 'entityTickRate', 'gravity', 'relevanceRadius', 'physicsRadius', 'physicsBodyBudget', 'movement', 'player', 'scene', 'camera', 'animation', 'input', 'spawnPoint', 'spawnPoints', 'playerModel', 'trustedApps']
25
+
26
+ function serializeEntity(e) {
27
+ const out = { id: e.id }
28
+ if (e.model) out.model = e.model
29
+ out.position = [e.position[0], e.position[1], e.position[2]]
30
+ const r = e.rotation
31
+ if (r && !(r[0] === 0 && r[1] === 0 && r[2] === 0 && r[3] === 1)) out.rotation = [r[0], r[1], r[2], r[3]]
32
+ const s = e.scale
33
+ if (s && !(s[0] === 1 && s[1] === 1 && s[2] === 1)) out.scale = [s[0], s[1], s[2]]
34
+ if (e._appName) out.app = e._appName
35
+ if (e.bodyType && e.bodyType !== 'static') out.bodyType = e.bodyType
36
+ if (e._config) out.config = e._config
37
+ if (e.custom) out.custom = e.custom
38
+ if (e.parent) out.parent = e.parent
39
+ return out
40
+ }
41
+
42
+ function serializeWorld(appRuntime, sourceWorldDef) {
43
+ const def = {}
44
+ const src = sourceWorldDef || {}
45
+ for (const k of WORLD_CONFIG_KEYS) if (src[k] !== undefined) def[k] = src[k]
46
+ const entities = []
47
+ for (const e of appRuntime.entities.values()) {
48
+ // Keep any entity carrying authored state: an app, a model, custom props (incl. a primitive's mesh/editorProp
49
+ // edits), a saved app config, OR a hierarchy parent (a reparented empty anchor). The old filter dropped an
50
+ // entity that had only a _config or only a parent, silently losing that authoring on save.
51
+ if (!e._appName && !e.model && !e.custom && !e._config && !e.parent) continue
52
+ entities.push(serializeEntity(e))
53
+ }
54
+ def.entities = entities
55
+ return def
56
+ }
57
+
58
+ function serializeWorldSource(def) {
59
+ return JSON.stringify(def, null, 2)
60
+ }
61
+
62
+ export { TEXT_EXTS, isTextFile, sanitizeFsError, WORLD_CONFIG_KEYS, serializeEntity, serializeWorld, serializeWorldSource }