spoint 0.1.608 → 0.1.609
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/client/app.js +3 -1
- package/client/core/PlayerLOD.js +36 -18
- package/package.json +1 -1
- package/src/apps/AppRuntime.js +28 -4
- package/src/client/BaseClient.js +21 -0
- package/src/physics/GLBLoader.js +2 -0
- package/src/physics/World.js +1 -0
- package/src/sdk/FreddieBridge.js +1 -0
package/client/app.js
CHANGED
|
@@ -643,7 +643,9 @@ if (deviceInfo.isMobile) { mobileControls = new MobileControls({ joystickRadius:
|
|
|
643
643
|
installQualityPresets()
|
|
644
644
|
QualityPresets.autoApplyPersisted({ renderer, deviceInfo })
|
|
645
645
|
const cam = createCameraController(camera, scene)
|
|
646
|
-
|
|
646
|
+
let _savedCam = null
|
|
647
|
+
try { _savedCam = JSON.parse(sessionStorage.getItem('cam') || 'null') } catch (e) { console.warn('[boot] discarding malformed sessionStorage.cam:', e?.message || e) }
|
|
648
|
+
cam.restore(_savedCam); sessionStorage.removeItem('cam')
|
|
647
649
|
let xrSystem = null
|
|
648
650
|
// Floating-origin: rebases the whole render-space scene graph (camera + every top-level scene
|
|
649
651
|
// child) toward (0,0,0) whenever the camera drifts REBASE_THRESHOLD_M from the last rebase point, so
|
package/client/core/PlayerLOD.js
CHANGED
|
@@ -52,7 +52,16 @@ export const TIER_FULL = 0
|
|
|
52
52
|
export const TIER_REDUCED = 1
|
|
53
53
|
export const TIER_DOT = 2
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
let _idxScratch = new Int32Array(0)
|
|
56
|
+
let _d2Scratch = new Float64Array(0)
|
|
57
|
+
const _full = new Set(), _reduced = new Set(), _dot = new Set()
|
|
58
|
+
const _order = []
|
|
59
|
+
|
|
60
|
+
function ensureScratchCapacity(n) {
|
|
61
|
+
if (_idxScratch.length >= n) return
|
|
62
|
+
_idxScratch = new Int32Array(n)
|
|
63
|
+
_d2Scratch = new Float64Array(n)
|
|
64
|
+
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* Classifies every remote player id in `entries` (array of {id, x, y, z}) into FULL/REDUCED/DOT tiers
|
|
@@ -60,26 +69,31 @@ const _dScratch = []
|
|
|
60
69
|
* THREE scene access, no side effects -- so it is directly unit-exercisable against real player position
|
|
61
70
|
* arrays (see the live witness in the task's verification pass). Returns { full: Set<id>, reduced:
|
|
62
71
|
* Set<id>, dot: Set<id> } plus `order` (the same ids sorted nearest-first, useful for a caller wanting a
|
|
63
|
-
* stable "top N" without re-sorting).
|
|
72
|
+
* stable "top N" without re-sorting). The returned Sets/array are module-level scratch, reused every call.
|
|
64
73
|
*/
|
|
65
74
|
export function classifyPlayerTiers(entries, viewerPos, fullCount = PLAYER_LOD_FULL_COUNT, reducedD2 = PLAYER_LOD_REDUCED_D2) {
|
|
66
75
|
const vx = viewerPos.x, vy = viewerPos.y, vz = viewerPos.z
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
const n = entries.length
|
|
77
|
+
ensureScratchCapacity(n)
|
|
78
|
+
for (let i = 0; i < n; i++) {
|
|
69
79
|
const e = entries[i]
|
|
70
80
|
const dx = e.x - vx, dy = e.y - vy, dz = e.z - vz
|
|
71
|
-
|
|
81
|
+
_idxScratch[i] = i
|
|
82
|
+
_d2Scratch[i] = dx * dx + dy * dy + dz * dz
|
|
72
83
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
const idxView = _idxScratch.subarray(0, n)
|
|
85
|
+
idxView.sort((a, b) => _d2Scratch[a] - _d2Scratch[b])
|
|
86
|
+
_full.clear(); _reduced.clear(); _dot.clear(); _order.length = 0
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
const srcIdx = idxView[i]
|
|
89
|
+
const id = entries[srcIdx].id
|
|
90
|
+
const d2 = _d2Scratch[srcIdx]
|
|
91
|
+
_order.push(id)
|
|
92
|
+
if (i < fullCount) _full.add(id)
|
|
93
|
+
else if (d2 < reducedD2) _reduced.add(id)
|
|
94
|
+
else _dot.add(id)
|
|
81
95
|
}
|
|
82
|
-
return { full, reduced, dot, order }
|
|
96
|
+
return { full: _full, reduced: _reduced, dot: _dot, order: _order }
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
/** Single-entity tier classification (no sort/allocation) -- for a caller that already knows a player's
|
|
@@ -202,6 +216,8 @@ export function createCrowdDotRenderer(scene, opts = {}) {
|
|
|
202
216
|
// owns that per-frame loop (tickPlayerAnimators) and is the correct place to apply the verdict, keeping
|
|
203
217
|
// this module a pure classifier + the crowd-dot renderer, not a second render-loop owner.
|
|
204
218
|
|
|
219
|
+
const _dotFallbackScratch = []
|
|
220
|
+
|
|
205
221
|
export function installPlayerLOD(scene, opts = {}) {
|
|
206
222
|
const dots = createCrowdDotRenderer(scene, opts)
|
|
207
223
|
let lastTiers = { full: new Set(), reduced: new Set(), dot: new Set(), order: [] }
|
|
@@ -225,10 +241,12 @@ export function installPlayerLOD(scene, opts = {}) {
|
|
|
225
241
|
if (serverDots && serverDots.length) {
|
|
226
242
|
dots.update(serverDots, viewerPos.y, groundHeightFn)
|
|
227
243
|
} else if (tiers.dot.size > 0) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
244
|
+
_dotFallbackScratch.length = 0
|
|
245
|
+
for (let i = 0; i < remoteEntries.length; i++) {
|
|
246
|
+
const e = remoteEntries[i]
|
|
247
|
+
if (tiers.dot.has(e.id)) _dotFallbackScratch.push(e)
|
|
248
|
+
}
|
|
249
|
+
dots.update(_dotFallbackScratch, viewerPos.y, groundHeightFn)
|
|
232
250
|
} else {
|
|
233
251
|
dots.update(null)
|
|
234
252
|
}
|
package/package.json
CHANGED
package/src/apps/AppRuntime.js
CHANGED
|
@@ -4,8 +4,18 @@ 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
|
|
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 } } catch {}
|
|
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
|
+
}
|
|
9
19
|
import { SpatialIndex } from '../spatial/Octree.js'
|
|
10
20
|
import { vecOK } from '../shared/vecGuard.js'
|
|
11
21
|
import { weaponNameToCode } from '../shared/WeaponCodes.js'
|
|
@@ -142,8 +152,22 @@ export class AppRuntime {
|
|
|
142
152
|
if (!p) return p
|
|
143
153
|
// non-Node (Worker, no fs): must return an origin-absolute path or fetch() resolves against the worker script's own URL, not the page origin
|
|
144
154
|
if (!_resolve) { const rel = p.startsWith('./') ? p.slice(1) : p; return rel.startsWith('/') ? rel : '/' + rel }
|
|
145
|
-
const
|
|
146
|
-
|
|
155
|
+
const cwdRoot = _resolve(process.cwd())
|
|
156
|
+
const local = _resolve(p)
|
|
157
|
+
if (_existsSync(local)) {
|
|
158
|
+
const contained = containedAssetPath(local, cwdRoot)
|
|
159
|
+
if (!contained) { console.warn(`[AppRuntime] resolveAssetPath rejected '${p}' -- resolves outside the server root`); return null }
|
|
160
|
+
return contained
|
|
161
|
+
}
|
|
162
|
+
if (this._sdkRoot) {
|
|
163
|
+
const sdk = _resolve(this._sdkRoot, p)
|
|
164
|
+
if (_existsSync(sdk)) {
|
|
165
|
+
const contained = containedAssetPath(sdk, _resolve(this._sdkRoot))
|
|
166
|
+
if (!contained) { console.warn(`[AppRuntime] resolveAssetPath rejected '${p}' -- resolves outside the SDK root`); return null }
|
|
167
|
+
console.debug(`[SDK-DEFAULT] using bundled asset: ${p}`)
|
|
168
|
+
return contained
|
|
169
|
+
}
|
|
170
|
+
}
|
|
147
171
|
return local
|
|
148
172
|
}
|
|
149
173
|
|
package/src/client/BaseClient.js
CHANGED
|
@@ -69,6 +69,7 @@ export class BaseClient {
|
|
|
69
69
|
try {
|
|
70
70
|
msg = unpack(bytes)
|
|
71
71
|
} catch (e) { console.error('[client] wire decode failed (corrupt message dropped):', e?.message || e); this.callbacks.onMessageError('decode', e); return }
|
|
72
|
+
if (msg.type === MSG.NOSTR_AUTH_CHALLENGE) { this._handleNostrAuthChallenge(msg.payload || {}); return }
|
|
72
73
|
try {
|
|
73
74
|
const result = this._msgHandler.handleMessage(msg.type, msg.payload || {}, this._snapProc)
|
|
74
75
|
this._handleSessionTokens(msg.type, result)
|
|
@@ -77,6 +78,26 @@ export class BaseClient {
|
|
|
77
78
|
} catch (e) { console.error('[client] message handler failed (type ' + msg?.type + '):', e?.message || e); this.callbacks.onMessageError('handler', e, msg?.type) }
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
async _handleNostrAuthChallenge(payload) {
|
|
82
|
+
if (payload.error) { console.error('[client] nostr auth failed:', payload.error); this.callbacks.onMessageError('nostrAuth', new Error(payload.error)); return }
|
|
83
|
+
const challenge = payload.challenge
|
|
84
|
+
if (!challenge) return
|
|
85
|
+
try {
|
|
86
|
+
const NostrTools = await import('nostr-tools')
|
|
87
|
+
const storage = typeof localStorage !== 'undefined' ? localStorage : null
|
|
88
|
+
const skHex = storage?.getItem('zn_sk')
|
|
89
|
+
let sk = skHex ? Uint8Array.from(skHex.match(/.{2}/g).map(b => parseInt(b, 16))) : null
|
|
90
|
+
if (!sk) {
|
|
91
|
+
sk = NostrTools.generateSecretKey()
|
|
92
|
+
storage?.setItem('zn_sk', Array.from(sk).map(b => b.toString(16).padStart(2, '0')).join(''))
|
|
93
|
+
storage?.setItem('zn_pk', NostrTools.getPublicKey(sk))
|
|
94
|
+
}
|
|
95
|
+
const pubkey = NostrTools.getPublicKey(sk)
|
|
96
|
+
const event = NostrTools.finalizeEvent({ kind: 27235, created_at: Math.floor(Date.now() / 1000), tags: [], content: challenge }, sk)
|
|
97
|
+
this.send(MSG.NOSTR_AUTH_RESPONSE, { pubkey, sig: event.sig, id: event.id, created_at: event.created_at, kind: event.kind, tags: event.tags })
|
|
98
|
+
} catch (e) { console.error('[client] nostr auth challenge response failed:', e?.message || e); this.callbacks.onMessageError('nostrAuth', e) }
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
_handleSessionTokens(type, result) {}
|
|
81
102
|
|
|
82
103
|
_onSnapshot(data, msgType) {
|
package/src/physics/GLBLoader.js
CHANGED
|
@@ -8,6 +8,7 @@ let _readFileSync = null
|
|
|
8
8
|
try { if (typeof process !== 'undefined' && process.versions?.node) { const m = await import('node:fs'); _readFileSync = m.readFileSync } } catch {}
|
|
9
9
|
|
|
10
10
|
function readGLBSync(filepath) {
|
|
11
|
+
if (!filepath) throw new Error('GLBLoader: no filepath given (resolveAssetPath rejected or returned an empty path)')
|
|
11
12
|
if (!_readFileSync) throw new Error('readFileSync not available — use URL-based async methods in browser')
|
|
12
13
|
const buf = _readFileSync(filepath)
|
|
13
14
|
if (buf.toString('ascii', 0, 4) !== 'glTF') throw new Error('Not a GLB file')
|
|
@@ -17,6 +18,7 @@ function readGLBSync(filepath) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async function readGLBAsync(pathOrUrl) {
|
|
21
|
+
if (!pathOrUrl) throw new Error('GLBLoader: no path given (resolveAssetPath rejected or returned an empty path)')
|
|
20
22
|
if (_readFileSync && !pathOrUrl.startsWith('http') && !pathOrUrl.startsWith('/')) {
|
|
21
23
|
return readGLBSync(pathOrUrl)
|
|
22
24
|
}
|
package/src/physics/World.js
CHANGED
|
@@ -243,6 +243,7 @@ export class PhysicsWorld {
|
|
|
243
243
|
// In-flight dedupe (_trimeshInflight) additionally prevents two concurrent placements of the same
|
|
244
244
|
// model+scale from racing two independent cook operations before either populates the cache.
|
|
245
245
|
async addStaticTrimeshAsync(glbPath, meshIndex = 0, position = [0, 0, 0], scale = [1, 1, 1], rotation = [0, 0, 0, 1]) {
|
|
246
|
+
if (!glbPath) throw new Error('addStaticTrimeshAsync: no glbPath (resolveAssetPath rejected or returned an empty path)')
|
|
246
247
|
const J = this.Jolt
|
|
247
248
|
const key = `${glbPath}|${scale[0]},${scale[1]},${scale[2]}`
|
|
248
249
|
let shape = this._trimeshCache.get(key)
|
package/src/sdk/FreddieBridge.js
CHANGED
|
@@ -115,6 +115,7 @@ export function validateMessage(msg) {
|
|
|
115
115
|
if (typeof p.entityId !== 'string' || !p.entityId) errors.push('payload.entityId is required')
|
|
116
116
|
if (!PRIMITIVES.includes(p.primitive)) errors.push(`payload.primitive must be one of: ${PRIMITIVES.join(', ')}`)
|
|
117
117
|
if (p.primitive === 'model' && typeof p.model !== 'string') errors.push('payload.model is required when primitive is "model"')
|
|
118
|
+
if (p.primitive === 'model' && typeof p.model === 'string' && (!p.model || p.model.split(/[\\/]/).includes('..') || /^[a-zA-Z]:[\\/]/.test(p.model) || p.model.startsWith('/') || p.model.startsWith('\\'))) errors.push('payload.model must be a non-empty relative path with no ".." segments and no drive-absolute or root-absolute prefix')
|
|
118
119
|
if (p.position && (!Array.isArray(p.position) || p.position.length !== 3)) errors.push('payload.position must be [x,y,z]')
|
|
119
120
|
if (p.scale && (!Array.isArray(p.scale) || p.scale.length !== 3)) errors.push('payload.scale must be [sx,sy,sz]')
|
|
120
121
|
}
|