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
|
@@ -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 }
|