@lanmower/entrypoint 0.16.0

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.
Files changed (70) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +250 -0
  3. package/apps/environment/index.js +17 -0
  4. package/apps/interactive-door/index.js +33 -0
  5. package/apps/patrol-npc/index.js +37 -0
  6. package/apps/physics-crate/index.js +23 -0
  7. package/apps/power-crate/index.js +169 -0
  8. package/apps/tps-game/index.js +168 -0
  9. package/apps/tps-game/schwust.glb +0 -0
  10. package/apps/world/index.js +22 -0
  11. package/client/app.js +181 -0
  12. package/client/camera.js +116 -0
  13. package/client/index.html +24 -0
  14. package/client/style.css +11 -0
  15. package/package.json +52 -0
  16. package/server.js +3 -0
  17. package/src/apps/AppContext.js +172 -0
  18. package/src/apps/AppLoader.js +160 -0
  19. package/src/apps/AppRuntime.js +192 -0
  20. package/src/apps/EventBus.js +62 -0
  21. package/src/apps/HotReloadQueue.js +63 -0
  22. package/src/client/InputHandler.js +85 -0
  23. package/src/client/PhysicsNetworkClient.js +171 -0
  24. package/src/client/PredictionEngine.js +123 -0
  25. package/src/client/ReconciliationEngine.js +54 -0
  26. package/src/connection/ConnectionManager.js +133 -0
  27. package/src/connection/QualityMonitor.js +46 -0
  28. package/src/connection/SessionStore.js +67 -0
  29. package/src/debug/CliDebugger.js +93 -0
  30. package/src/debug/Inspector.js +52 -0
  31. package/src/debug/StateInspector.js +42 -0
  32. package/src/index.client.js +8 -0
  33. package/src/index.js +1 -0
  34. package/src/index.server.js +27 -0
  35. package/src/math.js +21 -0
  36. package/src/netcode/EventLog.js +74 -0
  37. package/src/netcode/LagCompensator.js +78 -0
  38. package/src/netcode/NetworkState.js +66 -0
  39. package/src/netcode/PhysicsIntegration.js +132 -0
  40. package/src/netcode/PlayerManager.js +109 -0
  41. package/src/netcode/SnapshotEncoder.js +49 -0
  42. package/src/netcode/TickSystem.js +66 -0
  43. package/src/physics/GLBLoader.js +29 -0
  44. package/src/physics/World.js +195 -0
  45. package/src/protocol/Codec.js +60 -0
  46. package/src/protocol/EventEmitter.js +60 -0
  47. package/src/protocol/MessageTypes.js +73 -0
  48. package/src/protocol/SequenceTracker.js +71 -0
  49. package/src/protocol/msgpack.js +119 -0
  50. package/src/sdk/ClientMessageHandler.js +80 -0
  51. package/src/sdk/ReloadHandlers.js +68 -0
  52. package/src/sdk/ReloadManager.js +126 -0
  53. package/src/sdk/ServerAPI.js +133 -0
  54. package/src/sdk/ServerHandlers.js +76 -0
  55. package/src/sdk/StaticHandler.js +32 -0
  56. package/src/sdk/TickHandler.js +84 -0
  57. package/src/sdk/client.js +122 -0
  58. package/src/sdk/server.js +184 -0
  59. package/src/shared/movement.js +69 -0
  60. package/src/spatial/Octree.js +91 -0
  61. package/src/stage/Stage.js +90 -0
  62. package/src/stage/StageLoader.js +95 -0
  63. package/src/storage/FSAdapter.js +56 -0
  64. package/src/storage/StorageAdapter.js +7 -0
  65. package/src/transport/TransportWrapper.js +25 -0
  66. package/src/transport/WebSocketTransport.js +55 -0
  67. package/src/transport/WebTransportServer.js +83 -0
  68. package/src/transport/WebTransportTransport.js +94 -0
  69. package/world/kaira.glb +0 -0
  70. package/world/schwust.glb +0 -0
@@ -0,0 +1,122 @@
1
+ import { MSG } from '../protocol/MessageTypes.js'
2
+ import { pack } from '../protocol/msgpack.js'
3
+ import { QualityMonitor } from '../connection/QualityMonitor.js'
4
+ import { StateInspector } from '../debug/StateInspector.js'
5
+ import { createMessageRouter } from './ClientMessageHandler.js'
6
+ import { EventEmitter } from '../protocol/EventEmitter.js'
7
+
8
+ export function createClient(config = {}) {
9
+ const serverUrl = config.serverUrl || 'ws://localhost:8080'
10
+ const emitter = new EventEmitter()
11
+ const quality = new QualityMonitor()
12
+ const stateInspector = new StateInspector()
13
+
14
+ const state = {
15
+ ws: null, transportType: 'websocket',
16
+ playerId: null, sessionToken: null, tick: 0,
17
+ entities: new Map(), players: new Map(),
18
+ connected: false, reconnecting: false, reconnectAttempts: 0,
19
+ reconnectTimer: null, heartbeatTimer: null
20
+ }
21
+
22
+ function _sendRaw(data) {
23
+ if (state.ws && state.ws.readyState === 1) { state.ws.send(data); return true }
24
+ return false
25
+ }
26
+
27
+ function send(type, payload) {
28
+ const frame = pack({ type, payload })
29
+ quality.recordBytesOut(frame.length)
30
+ return _sendRaw(frame)
31
+ }
32
+
33
+ const onMessage = createMessageRouter({
34
+ emitter, quality, stateInspector, send,
35
+ getState: (k) => state[k],
36
+ setState: (k, v) => { state[k] = v }
37
+ })
38
+
39
+ function _startHeartbeats() {
40
+ state.heartbeatTimer = setInterval(() => {
41
+ quality.recordHeartbeatSent()
42
+ send(MSG.HEARTBEAT, { ts: Date.now() })
43
+ }, 1000)
44
+ }
45
+
46
+ function _attemptReconnect() {
47
+ if (state.reconnectAttempts >= 10) { emitter.emit('reconnectFailed'); return }
48
+ state.reconnecting = true
49
+ const delay = Math.min(100 * Math.pow(2, state.reconnectAttempts), 5000)
50
+ state.reconnectAttempts++
51
+ state.reconnectTimer = setTimeout(() => {
52
+ _connect().then(() => {
53
+ if (state.sessionToken) send(MSG.RECONNECT, { sessionToken: state.sessionToken })
54
+ }).catch(() => _attemptReconnect())
55
+ }, delay)
56
+ }
57
+
58
+ function _connect() {
59
+ return new Promise((resolve, reject) => {
60
+ try {
61
+ const WS = config.WebSocket || (typeof WebSocket !== 'undefined' ? WebSocket : null)
62
+ if (!WS) return reject(new Error('No WebSocket available'))
63
+ state.ws = new WS(serverUrl)
64
+ state.transportType = 'websocket'
65
+ if (state.ws.binaryType !== undefined) state.ws.binaryType = 'arraybuffer'
66
+ const onOpen = () => { state.connected = true; _startHeartbeats(); resolve() }
67
+ const onClose = () => {
68
+ state.connected = false
69
+ if (state.heartbeatTimer) clearInterval(state.heartbeatTimer)
70
+ emitter.emit('disconnect')
71
+ if (!state.reconnecting && state.sessionToken) _attemptReconnect()
72
+ }
73
+ const onErr = (e) => reject(e)
74
+ if (state.ws.on) {
75
+ state.ws.on('open', onOpen); state.ws.on('message', onMessage)
76
+ state.ws.on('close', onClose); state.ws.on('error', onErr)
77
+ } else {
78
+ state.ws.onopen = onOpen; state.ws.onmessage = (e) => onMessage(e.data)
79
+ state.ws.onclose = onClose; state.ws.onerror = onErr
80
+ }
81
+ } catch (e) { reject(e) }
82
+ })
83
+ }
84
+
85
+ const api = {
86
+ get playerId() { return state.playerId },
87
+ get tick() { return state.tick },
88
+ get entities() { return state.entities },
89
+ get players() { return state.players },
90
+ get isConnected() { return state.connected },
91
+ get sessionToken() { return state.sessionToken },
92
+ get transportType() { return state.transportType },
93
+ quality, stateInspector,
94
+ on: emitter.on.bind(emitter),
95
+ off: emitter.off.bind(emitter),
96
+ connect() { return _connect() },
97
+ connectNode(WS) { config.WebSocket = WS; return _connect() },
98
+ sendInput(input) { send(MSG.INPUT, { input }) },
99
+ interact(entityId) { send(MSG.APP_EVENT, { entityId }) },
100
+ disconnect() {
101
+ state.reconnecting = true
102
+ if (state.reconnectTimer) clearTimeout(state.reconnectTimer)
103
+ if (state.heartbeatTimer) clearInterval(state.heartbeatTimer)
104
+ state.connected = false
105
+ if (state.ws) state.ws.close()
106
+ },
107
+ getStats() {
108
+ return {
109
+ quality: quality.getStats(), stateSync: stateInspector.getStats(),
110
+ transport: state.transportType
111
+ }
112
+ },
113
+ getEntity(id) { return state.entities.get(id) || null },
114
+ getPlayer(id) { return state.players.get(id) || null }
115
+ }
116
+
117
+ if (typeof globalThis !== 'undefined') {
118
+ if (!globalThis.__DEBUG__) globalThis.__DEBUG__ = {}
119
+ globalThis.__DEBUG__.client = api
120
+ }
121
+ return api
122
+ }
@@ -0,0 +1,184 @@
1
+ import { join, dirname, resolve } from 'node:path'
2
+ import { fileURLToPath, pathToFileURL } from 'node:url'
3
+ import { MSG } from '../protocol/MessageTypes.js'
4
+ import { ConnectionManager } from '../connection/ConnectionManager.js'
5
+ import { SessionStore } from '../connection/SessionStore.js'
6
+ import { Inspector } from '../debug/Inspector.js'
7
+ import { TickSystem } from '../netcode/TickSystem.js'
8
+ import { PlayerManager } from '../netcode/PlayerManager.js'
9
+ import { NetworkState } from '../netcode/NetworkState.js'
10
+ import { LagCompensator } from '../netcode/LagCompensator.js'
11
+ import { PhysicsIntegration } from '../netcode/PhysicsIntegration.js'
12
+ import { PhysicsWorld } from '../physics/World.js'
13
+ import { AppRuntime } from '../apps/AppRuntime.js'
14
+ import { AppLoader } from '../apps/AppLoader.js'
15
+ import { StageLoader } from '../stage/StageLoader.js'
16
+ import { createTickHandler } from './TickHandler.js'
17
+ import { EventEmitter } from '../protocol/EventEmitter.js'
18
+ import { EventBus } from '../apps/EventBus.js'
19
+ import { EventLog } from '../netcode/EventLog.js'
20
+ import { FSAdapter } from '../storage/FSAdapter.js'
21
+ import { ReloadManager } from './ReloadManager.js'
22
+ import { createReloadHandlers } from './ReloadHandlers.js'
23
+ import { createServerAPI } from './ServerAPI.js'
24
+ import { createConnectionHandlers } from './ServerHandlers.js'
25
+
26
+ export async function boot(overrides = {}) {
27
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..')
28
+ const worldPath = resolve(ROOT, 'apps/world/index.js')
29
+ const worldUrl = pathToFileURL(worldPath).href + `?t=${Date.now()}`
30
+ const worldMod = await import(worldUrl)
31
+ const worldDef = worldMod.default || worldMod
32
+ const config = {
33
+ port: parseInt(process.env.PORT || String(worldDef.port || 8080), 10),
34
+ tickRate: worldDef.tickRate || 128,
35
+ appsDir: join(ROOT, 'apps'),
36
+ gravity: worldDef.gravity,
37
+ movement: worldDef.movement,
38
+ staticDirs: [
39
+ { prefix: '/src/', dir: join(ROOT, 'src') },
40
+ { prefix: '/world/', dir: join(ROOT, 'world') },
41
+ { prefix: '/node_modules/', dir: join(ROOT, 'node_modules') },
42
+ { prefix: '/', dir: join(ROOT, 'client') }
43
+ ],
44
+ ...overrides
45
+ }
46
+ const server = await createServer(config)
47
+ await server.loadWorld(worldDef)
48
+ server.on('playerJoin', ({ id }) => console.log())
49
+ server.on('playerLeave', ({ id }) => console.log())
50
+ const info = await server.start()
51
+ console.log()
52
+ return server
53
+ }
54
+
55
+ export async function createServer(config = {}) {
56
+ const port = config.port || 8080
57
+ const tickRate = config.tickRate || 128
58
+ const appsDir = config.appsDir || './apps'
59
+ const gravity = config.gravity || [0, -9.81, 0]
60
+ const movement = config.movement || {}
61
+ const staticDirs = config.staticDirs || []
62
+
63
+ const storageDir = config.storageDir || './data'
64
+ const physics = new PhysicsWorld({ gravity })
65
+ await physics.init()
66
+
67
+ const emitter = new EventEmitter()
68
+ const eventBus = new EventBus()
69
+ const eventLog = new EventLog()
70
+ const storage = new FSAdapter(storageDir)
71
+ const tickSystem = new TickSystem(tickRate)
72
+ const playerManager = new PlayerManager()
73
+ const networkState = new NetworkState()
74
+ const lagCompensator = new LagCompensator()
75
+ const physicsIntegration = new PhysicsIntegration({ gravity, physicsWorld: physics })
76
+ const connections = new ConnectionManager({
77
+ heartbeatInterval: config.heartbeatInterval || 1000,
78
+ heartbeatTimeout: config.heartbeatTimeout || 3000
79
+ })
80
+ const sessions = new SessionStore({ ttl: config.sessionTTL || 30000 })
81
+ const inspector = new Inspector()
82
+ const reloadManager = new ReloadManager()
83
+
84
+ const appRuntime = new AppRuntime({ gravity, playerManager, physics, physicsIntegration, connections, eventBus, eventLog, storage })
85
+ appRuntime.setPlayerManager(playerManager)
86
+ const appLoader = new AppLoader(appRuntime, { dir: appsDir })
87
+ const stageLoader = new StageLoader(appRuntime)
88
+ appRuntime.setStageLoader(stageLoader)
89
+
90
+ appLoader._onReloadCallback = (name, code) => {
91
+ connections.broadcast(MSG.APP_MODULE, { app: name, code })
92
+ }
93
+
94
+ const ctx = {
95
+ config,
96
+ port,
97
+ tickRate,
98
+ appsDir,
99
+ gravity,
100
+ movement,
101
+ staticDirs,
102
+ physics,
103
+ emitter,
104
+ tickSystem,
105
+ playerManager,
106
+ networkState,
107
+ lagCompensator,
108
+ physicsIntegration,
109
+ connections,
110
+ sessions,
111
+ inspector,
112
+ reloadManager,
113
+ eventBus,
114
+ eventLog,
115
+ storage,
116
+ appRuntime,
117
+ appLoader,
118
+ stageLoader,
119
+ currentWorldDef: null,
120
+ worldSpawnPoint: [0, 5, 0],
121
+ snapshotSeq: 0,
122
+ httpServer: null,
123
+ wss: null,
124
+ wtServer: null,
125
+ handlerState: { fn: null },
126
+ onTick: (tick, dt) => { if (ctx.handlerState.fn) ctx.handlerState.fn(tick, dt) },
127
+ setTickHandler: (fn) => { ctx.handlerState.fn = fn }
128
+ }
129
+
130
+ const reloadHandlers = createReloadHandlers({
131
+ networkState,
132
+ playerManager,
133
+ physicsIntegration,
134
+ lagCompensator,
135
+ physics,
136
+ appRuntime,
137
+ connections
138
+ })
139
+ ctx.reloadHandlers = reloadHandlers
140
+
141
+ ctx.setTickHandler(createTickHandler({
142
+ networkState,
143
+ playerManager,
144
+ physicsIntegration,
145
+ lagCompensator,
146
+ physics,
147
+ appRuntime,
148
+ connections,
149
+ movement,
150
+ stageLoader,
151
+ eventLog
152
+ }))
153
+
154
+ const { onClientConnect } = createConnectionHandlers(ctx)
155
+ ctx.onClientConnect = onClientConnect
156
+
157
+ ctx.setupSDKWatchers = () => {
158
+ const reloadTick = async () => { ctx.setTickHandler(await reloadHandlers.reloadTickHandler()) }
159
+ const w = [
160
+ ['tick-handler', './src/sdk/TickHandler.js', reloadTick],
161
+ ['physics-integration', './src/netcode/PhysicsIntegration.js', reloadHandlers.reloadPhysicsIntegration],
162
+ ['lag-compensator', './src/netcode/LagCompensator.js', reloadHandlers.reloadLagCompensator],
163
+ ['player-manager', './src/netcode/PlayerManager.js', reloadHandlers.reloadPlayerManager],
164
+ ['network-state', './src/netcode/NetworkState.js', reloadHandlers.reloadNetworkState]
165
+ ]
166
+ for (const [id, path, reload] of w) reloadManager.addWatcher(id, path, reload)
167
+ const clientReload = () => { connections.broadcast(MSG.HOT_RELOAD, { timestamp: Date.now() }) }
168
+ const clientFiles = [
169
+ ['client-app', './client/app.js'],
170
+ ['client-camera', './client/camera.js'],
171
+ ['client-input', './src/client/InputHandler.js'],
172
+ ['client-network', './src/client/PhysicsNetworkClient.js'],
173
+ ['client-prediction', './src/client/PredictionEngine.js'],
174
+ ['client-reconciliation', './src/client/ReconciliationEngine.js'],
175
+ ['client-index', './src/index.client.js']
176
+ ]
177
+ for (const [id, path] of clientFiles) reloadManager.addWatcher(id, path, clientReload)
178
+ }
179
+
180
+ const api = createServerAPI(ctx)
181
+ if (typeof globalThis.__DEBUG__ === 'undefined') globalThis.__DEBUG__ = {}
182
+ globalThis.__DEBUG__.server = api
183
+ return api
184
+ }
@@ -0,0 +1,69 @@
1
+ export function applyMovement(state, input, movement, dt) {
2
+ const { maxSpeed, groundAccel, airAccel, friction, stopSpeed, jumpImpulse } = movement
3
+ let vx = state.velocity[0], vz = state.velocity[2]
4
+ let wishX = 0, wishZ = 0, wishSpeed = 0, jumped = false
5
+
6
+ if (input) {
7
+ let fx = 0, fz = 0
8
+ if (input.forward) fz += 1
9
+ if (input.backward) fz -= 1
10
+ if (input.left) fx -= 1
11
+ if (input.right) fx += 1
12
+ const flen = Math.sqrt(fx * fx + fz * fz)
13
+ if (flen > 0) { fx /= flen; fz /= flen }
14
+ const yaw = input.yaw || 0
15
+ const cy = Math.cos(yaw), sy = Math.sin(yaw)
16
+ wishX = fz * sy - fx * cy
17
+ wishZ = fx * sy + fz * cy
18
+ wishSpeed = flen > 0 ? maxSpeed : 0
19
+ if (input.jump && state.onGround) {
20
+ state.velocity[1] = jumpImpulse
21
+ state.onGround = false
22
+ jumped = true
23
+ }
24
+ }
25
+
26
+ if (state.onGround && !jumped) {
27
+ const speed = Math.sqrt(vx * vx + vz * vz)
28
+ if (speed > 0.1) {
29
+ const control = speed < stopSpeed ? stopSpeed : speed
30
+ const drop = control * friction * dt
31
+ let newSpeed = speed - drop
32
+ if (newSpeed < 0) newSpeed = 0
33
+ const scale = newSpeed / speed
34
+ vx *= scale; vz *= scale
35
+ } else { vx = 0; vz = 0 }
36
+ if (wishSpeed > 0) {
37
+ const cur = vx * wishX + vz * wishZ
38
+ let add = wishSpeed - cur
39
+ if (add > 0) {
40
+ let as = groundAccel * wishSpeed * dt
41
+ if (as > add) as = add
42
+ vx += as * wishX; vz += as * wishZ
43
+ }
44
+ }
45
+ } else {
46
+ if (wishSpeed > 0) {
47
+ const cur = vx * wishX + vz * wishZ
48
+ let add = wishSpeed - cur
49
+ if (add > 0) {
50
+ let as = airAccel * wishSpeed * dt
51
+ if (as > add) as = add
52
+ vx += as * wishX; vz += as * wishZ
53
+ }
54
+ }
55
+ }
56
+
57
+ state.velocity[0] = vx
58
+ state.velocity[2] = vz
59
+ return { wishX, wishZ, wishSpeed, jumped }
60
+ }
61
+
62
+ export const DEFAULT_MOVEMENT = {
63
+ maxSpeed: 8.0,
64
+ groundAccel: 10.0,
65
+ airAccel: 1.0,
66
+ friction: 6.0,
67
+ stopSpeed: 2.0,
68
+ jumpImpulse: 4.5
69
+ }
@@ -0,0 +1,91 @@
1
+ import { octree } from 'd3-octree'
2
+
3
+ export class SpatialIndex {
4
+ constructor(config = {}) {
5
+ this._tree = octree()
6
+ this._entities = new Map()
7
+ this._relevanceRadius = config.relevanceRadius || 200
8
+ }
9
+
10
+ insert(id, position) {
11
+ this.remove(id)
12
+ const point = [position[0], position[1], position[2]]
13
+ point._entityId = id
14
+ this._entities.set(id, point)
15
+ this._tree.add(point)
16
+ }
17
+
18
+ remove(id) {
19
+ const existing = this._entities.get(id)
20
+ if (!existing) return
21
+ this._tree.remove(existing)
22
+ this._entities.delete(id)
23
+ }
24
+
25
+ update(id, position) {
26
+ this.insert(id, position)
27
+ }
28
+
29
+ has(id) {
30
+ return this._entities.has(id)
31
+ }
32
+
33
+ getPosition(id) {
34
+ const p = this._entities.get(id)
35
+ return p ? [p[0], p[1], p[2]] : null
36
+ }
37
+
38
+ nearby(position, radius) {
39
+ const cx = position[0], cy = position[1], cz = position[2]
40
+ const r2 = radius * radius
41
+ const results = []
42
+ this._tree.visit((node, x0, y0, z0, x1, y1, z1) => {
43
+ if (!node.length) {
44
+ let d = node
45
+ do {
46
+ const p = d.data
47
+ const dx = p[0] - cx, dy = p[1] - cy, dz = p[2] - cz
48
+ if (dx * dx + dy * dy + dz * dz <= r2) {
49
+ results.push(p._entityId)
50
+ }
51
+ } while (d = d.next)
52
+ }
53
+ const nx = Math.max(x0, Math.min(cx, x1))
54
+ const ny = Math.max(y0, Math.min(cy, y1))
55
+ const nz = Math.max(z0, Math.min(cz, z1))
56
+ const ddx = nx - cx, ddy = ny - cy, ddz = nz - cz
57
+ return ddx * ddx + ddy * ddy + ddz * ddz > r2
58
+ })
59
+ return results
60
+ }
61
+
62
+ nearest(position, radius) {
63
+ const p = this._tree.find(position[0], position[1], position[2], radius)
64
+ return p ? p._entityId : null
65
+ }
66
+
67
+ get size() {
68
+ return this._entities.size
69
+ }
70
+
71
+ clear() {
72
+ this._tree = octree()
73
+ this._entities.clear()
74
+ }
75
+
76
+ rebuild() {
77
+ const entries = Array.from(this._entities.entries())
78
+ this._tree = octree()
79
+ for (const [, point] of entries) {
80
+ this._tree.add(point)
81
+ }
82
+ }
83
+
84
+ get relevanceRadius() {
85
+ return this._relevanceRadius
86
+ }
87
+
88
+ set relevanceRadius(v) {
89
+ this._relevanceRadius = v
90
+ }
91
+ }
@@ -0,0 +1,90 @@
1
+ import { SpatialIndex } from '../spatial/Octree.js'
2
+
3
+ export class Stage {
4
+ constructor(name, config = {}) {
5
+ this.name = name
6
+ this.entityIds = new Set()
7
+ this.spatial = new SpatialIndex({ relevanceRadius: config.relevanceRadius || 200 })
8
+ this.gravity = config.gravity || null
9
+ this.spawnPoint = config.spawnPoint || null
10
+ this.playerModel = config.playerModel || null
11
+ this._runtime = null
12
+ this._staticIds = new Set()
13
+ }
14
+
15
+ bind(runtime) {
16
+ this._runtime = runtime
17
+ }
18
+
19
+ addEntity(id, config = {}) {
20
+ if (!this._runtime) return null
21
+ const entity = this._runtime.spawnEntity(id, config)
22
+ this.entityIds.add(entity.id)
23
+ const pos = entity.position || [0, 0, 0]
24
+ this.spatial.insert(entity.id, pos)
25
+ if (entity.bodyType === 'static' || config.autoTrimesh) {
26
+ this._staticIds.add(entity.id)
27
+ }
28
+ return entity
29
+ }
30
+
31
+ removeEntity(id) {
32
+ if (!this._runtime) return
33
+ this.spatial.remove(id)
34
+ this._staticIds.delete(id)
35
+ this.entityIds.delete(id)
36
+ this._runtime.destroyEntity(id)
37
+ }
38
+
39
+ updateEntityPosition(id, position) {
40
+ if (!this.entityIds.has(id)) return
41
+ this.spatial.update(id, position)
42
+ }
43
+
44
+ getNearbyEntities(position, radius) {
45
+ return this.spatial.nearby(position, radius || this.spatial.relevanceRadius)
46
+ }
47
+
48
+ getRelevantEntities(position, radius) {
49
+ const nearby = this.spatial.nearby(position, radius || this.spatial.relevanceRadius)
50
+ const set = new Set(nearby)
51
+ for (const sid of this._staticIds) set.add(sid)
52
+ return Array.from(set)
53
+ }
54
+
55
+ getStaticIds() {
56
+ return Array.from(this._staticIds)
57
+ }
58
+
59
+ hasEntity(id) {
60
+ return this.entityIds.has(id)
61
+ }
62
+
63
+ get entityCount() {
64
+ return this.entityIds.size
65
+ }
66
+
67
+ clear() {
68
+ if (!this._runtime) return
69
+ for (const id of [...this.entityIds]) {
70
+ this._runtime.destroyEntity(id)
71
+ }
72
+ this.entityIds.clear()
73
+ this._staticIds.clear()
74
+ this.spatial.clear()
75
+ }
76
+
77
+ syncPositions() {
78
+ if (!this._runtime) return
79
+ for (const id of this.entityIds) {
80
+ const e = this._runtime.getEntity(id)
81
+ if (e && e.bodyType !== 'static') {
82
+ this.spatial.update(id, e.position)
83
+ }
84
+ }
85
+ }
86
+
87
+ getAllEntityIds() {
88
+ return Array.from(this.entityIds)
89
+ }
90
+ }
@@ -0,0 +1,95 @@
1
+ import { Stage } from './Stage.js'
2
+
3
+ export class StageLoader {
4
+ constructor(runtime) {
5
+ this._runtime = runtime
6
+ this._stages = new Map()
7
+ this._activeStage = null
8
+ }
9
+
10
+ loadFromDefinition(name, worldDef) {
11
+ const stage = new Stage(name, {
12
+ relevanceRadius: worldDef.relevanceRadius || 200,
13
+ gravity: worldDef.gravity,
14
+ spawnPoint: worldDef.spawnPoint,
15
+ playerModel: worldDef.playerModel
16
+ })
17
+ stage.bind(this._runtime)
18
+
19
+ if (worldDef.gravity) {
20
+ this._runtime.gravity = [...worldDef.gravity]
21
+ }
22
+
23
+ for (const entDef of worldDef.entities || []) {
24
+ const cfg = {
25
+ model: entDef.model,
26
+ position: entDef.position || [0, 0, 0],
27
+ rotation: entDef.rotation,
28
+ scale: entDef.scale,
29
+ app: entDef.app,
30
+ config: entDef.config || null
31
+ }
32
+ if (entDef.model && !entDef.app) {
33
+ cfg.autoTrimesh = true
34
+ }
35
+ stage.addEntity(entDef.id || null, cfg)
36
+ }
37
+
38
+ this._stages.set(name, stage)
39
+ if (!this._activeStage) this._activeStage = stage
40
+ return stage
41
+ }
42
+
43
+ getStage(name) {
44
+ return this._stages.get(name) || null
45
+ }
46
+
47
+ getActiveStage() {
48
+ return this._activeStage
49
+ }
50
+
51
+ setActiveStage(name) {
52
+ const stage = this._stages.get(name)
53
+ if (stage) this._activeStage = stage
54
+ return stage
55
+ }
56
+
57
+ removeStage(name) {
58
+ const stage = this._stages.get(name)
59
+ if (!stage) return
60
+ stage.clear()
61
+ this._stages.delete(name)
62
+ if (this._activeStage === stage) {
63
+ this._activeStage = this._stages.values().next().value || null
64
+ }
65
+ }
66
+
67
+ swapStage(oldName, newName, newDef) {
68
+ this.removeStage(oldName)
69
+ return this.loadFromDefinition(newName, newDef)
70
+ }
71
+
72
+ getAllStageNames() {
73
+ return Array.from(this._stages.keys())
74
+ }
75
+
76
+ get stageCount() {
77
+ return this._stages.size
78
+ }
79
+
80
+ syncAllPositions() {
81
+ for (const stage of this._stages.values()) {
82
+ stage.syncPositions()
83
+ }
84
+ }
85
+
86
+ getNearbyEntities(position, radius) {
87
+ if (!this._activeStage) return []
88
+ return this._activeStage.getNearbyEntities(position, radius)
89
+ }
90
+
91
+ getRelevantEntities(position, radius) {
92
+ if (!this._activeStage) return []
93
+ return this._activeStage.getRelevantEntities(position, radius)
94
+ }
95
+ }
@@ -0,0 +1,56 @@
1
+ import { readFile, writeFile, unlink, readdir, mkdir, rename, access } from 'node:fs/promises'
2
+ import { join, dirname } from 'node:path'
3
+ import { StorageAdapter } from './StorageAdapter.js'
4
+
5
+ export class FSAdapter extends StorageAdapter {
6
+ constructor(baseDir = './data') {
7
+ super()
8
+ this._dir = baseDir
9
+ this._ready = false
10
+ }
11
+
12
+ async _ensureDir() {
13
+ if (this._ready) return
14
+ await mkdir(this._dir, { recursive: true }).catch(() => {})
15
+ this._ready = true
16
+ }
17
+
18
+ _path(key) {
19
+ const safe = key.replace(/[^a-zA-Z0-9._-]/g, '_')
20
+ return join(this._dir, `${safe}.json`)
21
+ }
22
+
23
+ async get(key) {
24
+ try {
25
+ const data = await readFile(this._path(key), 'utf-8')
26
+ return JSON.parse(data)
27
+ } catch { return undefined }
28
+ }
29
+
30
+ async set(key, value) {
31
+ await this._ensureDir()
32
+ const fp = this._path(key)
33
+ const tmp = fp + '.tmp'
34
+ await writeFile(tmp, JSON.stringify(value), 'utf-8')
35
+ await rename(tmp, fp)
36
+ }
37
+
38
+ async delete(key) {
39
+ try { await unlink(this._path(key)) } catch {}
40
+ }
41
+
42
+ async list(prefix = '') {
43
+ await this._ensureDir()
44
+ try {
45
+ const files = await readdir(this._dir)
46
+ const safe = prefix.replace(/[^a-zA-Z0-9._-]/g, '_')
47
+ return files
48
+ .filter(f => f.endsWith('.json') && f.startsWith(safe))
49
+ .map(f => f.slice(0, -5))
50
+ } catch { return [] }
51
+ }
52
+
53
+ async has(key) {
54
+ try { await access(this._path(key)); return true } catch { return false }
55
+ }
56
+ }