@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,160 @@
1
+ import { readdir, readFile, watch, access } from 'node:fs/promises'
2
+ import { join, basename, extname, resolve } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ const BLOCKED_PATTERNS = [
6
+ 'process.exit', 'child_process', 'require(', '__proto__',
7
+ 'Object.prototype', 'globalThis', 'eval(', 'import('
8
+ ]
9
+
10
+ export class AppLoader {
11
+ constructor(runtime, config = {}) {
12
+ this._runtime = runtime
13
+ this._dir = config.dir || './apps'
14
+ this._watchers = new Map()
15
+ this._loaded = new Map()
16
+ this._onReloadCallback = null
17
+ }
18
+
19
+ async _resolvePath(name) {
20
+ const flat = join(this._dir, `${name}.js`)
21
+ try { await access(flat); return flat } catch {}
22
+ const folder = join(this._dir, name, 'index.js')
23
+ try { await access(folder); return folder } catch {}
24
+ return null
25
+ }
26
+
27
+ async loadAll() {
28
+ const entries = await readdir(this._dir, { withFileTypes: true }).catch(() => [])
29
+ const results = []
30
+ for (const entry of entries) {
31
+ let name = null
32
+ if (entry.isFile() && entry.name.endsWith('.js')) {
33
+ name = basename(entry.name, extname(entry.name))
34
+ } else if (entry.isDirectory()) {
35
+ try { await access(join(this._dir, entry.name, 'index.js')); name = entry.name } catch {}
36
+ }
37
+ if (name) {
38
+ const loaded = await this.loadApp(name)
39
+ if (loaded) results.push(name)
40
+ }
41
+ }
42
+ return results
43
+ }
44
+
45
+ async loadApp(name) {
46
+ const filePath = await this._resolvePath(name)
47
+ if (!filePath) return null
48
+ try {
49
+ const source = await readFile(filePath, 'utf-8')
50
+ if (!this._validate(source, name)) return null
51
+ const appDef = await this._evaluate(source, filePath)
52
+ if (!appDef) return null
53
+ this._runtime.registerApp(name, appDef)
54
+ this._loaded.set(name, { filePath, source, clientCode: source })
55
+ return appDef
56
+ } catch (e) {
57
+ console.error(`[AppLoader] failed to load "${name}": ${e.message}\n file: ${filePath}\n stack: ${e.stack?.split('\n').slice(1, 3).join('\n ') || 'none'}`)
58
+ return null
59
+ }
60
+ }
61
+
62
+ _validate(source, name) {
63
+ for (const pattern of BLOCKED_PATTERNS) {
64
+ if (source.includes(pattern)) {
65
+ console.error(`[AppLoader] blocked pattern "${pattern}" in ${name}`)
66
+ return false
67
+ }
68
+ }
69
+ return true
70
+ }
71
+
72
+ async _evaluate(source, filePath) {
73
+ try {
74
+ const absPath = resolve(filePath)
75
+ const url = pathToFileURL(absPath).href + `?t=${Date.now()}`
76
+ const mod = await import(url)
77
+ return mod.default || mod
78
+ } catch (e) {
79
+ console.error(`[AppLoader] syntax/eval error in "${filePath}": ${e.message}\n ${e.stack?.split('\n').slice(1, 3).join('\n ') || ''}`)
80
+ return null
81
+ }
82
+ }
83
+
84
+ async watchAll() {
85
+ try {
86
+ const ac = new AbortController()
87
+ const watcher = watch(this._dir, { recursive: true, signal: ac.signal })
88
+ this._watchers.set('__dir__', ac)
89
+ ;(async () => {
90
+ try {
91
+ for await (const event of watcher) {
92
+ if (!event.filename || !event.filename.endsWith('.js')) continue
93
+ const parts = event.filename.replace(/\\/g, '/').split('/')
94
+ const name = parts.length > 1
95
+ ? parts[0]
96
+ : basename(event.filename, extname(event.filename))
97
+ await this._onFileChange(name)
98
+ }
99
+ } catch (e) {
100
+ if (e.name !== 'AbortError') {
101
+ console.error(`[AppLoader] watch error:`, e.message)
102
+ }
103
+ }
104
+ })()
105
+ } catch (e) {
106
+ console.error(`[AppLoader] watchAll error:`, e.message)
107
+ }
108
+ }
109
+
110
+ async _onFileChange(name) {
111
+ console.log(`[AppLoader] reloading ${name}`)
112
+ const appDef = await this.loadApp(name)
113
+ if (appDef) {
114
+ const cb = this._onReloadCallback ? (n, d) => {
115
+ this._onReloadCallback(n, this._loaded.get(n)?.clientCode)
116
+ } : null
117
+ this._runtime.queueReload(name, appDef, cb)
118
+ console.log(`[AppLoader] queued hot reload ${name}`)
119
+ }
120
+ }
121
+
122
+ stopWatching() {
123
+ for (const ac of this._watchers.values()) {
124
+ ac.abort()
125
+ }
126
+ this._watchers.clear()
127
+ }
128
+
129
+ getLoaded() {
130
+ return Array.from(this._loaded.keys())
131
+ }
132
+
133
+ getClientModules() {
134
+ const modules = {}
135
+ for (const [name, data] of this._loaded) {
136
+ if (data.clientCode) modules[name] = data.clientCode
137
+ }
138
+ return modules
139
+ }
140
+
141
+ getClientModule(name) {
142
+ return this._loaded.get(name)?.clientCode || null
143
+ }
144
+
145
+ async loadFromString(name, source) {
146
+ if (!this._validate(source, name)) return null
147
+ try {
148
+ const fn = new Function('exports', source + '\nreturn exports;')
149
+ const exports = {}
150
+ const result = fn(exports)
151
+ const appDef = result.default || result
152
+ this._runtime.registerApp(name, appDef)
153
+ this._loaded.set(name, { source, filePath: null })
154
+ return appDef
155
+ } catch (e) {
156
+ console.error(`[AppLoader] string eval error:`, e.message)
157
+ return null
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,192 @@
1
+ import { AppContext } from './AppContext.js'
2
+ import { HotReloadQueue } from './HotReloadQueue.js'
3
+ import { EventBus } from './EventBus.js'
4
+ import { mulQuat, rotVec } from '../math.js'
5
+ import { MSG } from '../protocol/MessageTypes.js'
6
+
7
+ export class AppRuntime {
8
+ constructor(c = {}) {
9
+ this.entities = new Map(); this.apps = new Map(); this.contexts = new Map()
10
+ this.gravity = c.gravity || [0, -9.81, 0]
11
+ this.currentTick = 0; this.deltaTime = 0; this.elapsed = 0
12
+ this._playerManager = c.playerManager || null; this._physics = c.physics || null; this._physicsIntegration = c.physicsIntegration || null
13
+ this._connections = c.connections || null; this._stageLoader = c.stageLoader || null
14
+ this._nextEntityId = 1; this._appDefs = new Map(); this._timers = new Map()
15
+ this._hotReload = new HotReloadQueue(this)
16
+ this._eventBus = c.eventBus || new EventBus()
17
+ this._eventLog = c.eventLog || null
18
+ this._storage = c.storage || null
19
+ this._eventBus.on('*', (event) => {
20
+ if (event.channel.startsWith('system.')) return
21
+ this._log('bus_event', { channel: event.channel, data: event.data }, event.meta)
22
+ })
23
+ this._eventBus.on('system.handover', (event) => {
24
+ const { targetEntityId, stateData } = event.data || {}
25
+ if (targetEntityId) this.fireEvent(targetEntityId, 'onHandover', event.meta.sourceEntity, stateData)
26
+ })
27
+ }
28
+
29
+ registerApp(name, appDef) { this._appDefs.set(name, appDef) }
30
+
31
+ spawnEntity(id, config = {}) {
32
+ const entityId = id || `entity_${this._nextEntityId++}`
33
+ const entity = {
34
+ id: entityId, model: config.model || null,
35
+ position: config.position ? [...config.position] : [0, 0, 0],
36
+ rotation: config.rotation || [0, 0, 0, 1],
37
+ scale: config.scale ? [...config.scale] : [1, 1, 1],
38
+ velocity: [0, 0, 0], mass: 1, bodyType: 'static', collider: null,
39
+ parent: null, children: new Set(),
40
+ _appState: null, _appName: config.app || null, _config: config.config || null, custom: null
41
+ }
42
+ this.entities.set(entityId, entity)
43
+ this._log('entity_spawn', { id: entityId, config }, { sourceEntity: entityId })
44
+ if (config.parent) {
45
+ const p = this.entities.get(config.parent)
46
+ if (p) { entity.parent = config.parent; p.children.add(entityId) }
47
+ }
48
+ if (config.autoTrimesh && entity.model && this._physics) {
49
+ entity.collider = { type: 'trimesh', model: entity.model }
50
+ entity._physicsBodyId = this._physics.addStaticTrimesh(entity.model, 0)
51
+ }
52
+ if (config.app) this._attachApp(entityId, config.app)
53
+ this._spatialInsert(entity)
54
+ return entity
55
+ }
56
+
57
+ _attachApp(entityId, appName) {
58
+ const entity = this.entities.get(entityId), appDef = this._appDefs.get(appName)
59
+ if (!entity || !appDef) return
60
+ const ctx = new AppContext(entity, this)
61
+ this.contexts.set(entityId, ctx); this.apps.set(entityId, appDef)
62
+ this._safeCall(appDef.server || appDef, 'setup', [ctx], `setup(${appName})`)
63
+ }
64
+
65
+ attachApp(entityId, appName) { this._attachApp(entityId, appName) }
66
+ spawnWithApp(id, cfg = {}, app) { return this.spawnEntity(id, { ...cfg, app }) }
67
+ attachAppToEntity(eid, app, cfg = {}) { const e = this.getEntity(eid); if (!e) return false; e._config = cfg; this._attachApp(eid, app); return true }
68
+ reattachAppToEntity(eid, app) { this.detachApp(eid); this._attachApp(eid, app) }
69
+ getEntityWithApp(eid) { const e = this.entities.get(eid); return { entity: e, appName: e?._appName, hasApp: !!e?._appName } }
70
+
71
+ detachApp(entityId) {
72
+ const appDef = this.apps.get(entityId), ctx = this.contexts.get(entityId)
73
+ if (appDef && ctx) this._safeCall(appDef.server || appDef, 'teardown', [ctx], 'teardown')
74
+ this._eventBus.destroyScope(entityId)
75
+ this.clearTimers(entityId); this.apps.delete(entityId); this.contexts.delete(entityId)
76
+ }
77
+
78
+ destroyEntity(entityId) {
79
+ const entity = this.entities.get(entityId); if (!entity) return
80
+ this._log('entity_destroy', { id: entityId }, { sourceEntity: entityId })
81
+ for (const childId of [...entity.children]) this.destroyEntity(childId)
82
+ if (entity.parent) { const p = this.entities.get(entity.parent); if (p) p.children.delete(entityId) }
83
+ this._eventBus.destroyScope(entityId)
84
+ this.detachApp(entityId); this._spatialRemove(entityId); this.entities.delete(entityId)
85
+ }
86
+
87
+ reparent(entityId, newParentId) {
88
+ const e = this.entities.get(entityId); if (!e) return
89
+ if (e.parent) { const old = this.entities.get(e.parent); if (old) old.children.delete(entityId) }
90
+ e.parent = null
91
+ if (newParentId) { const np = this.entities.get(newParentId); if (np) { e.parent = newParentId; np.children.add(entityId) } }
92
+ }
93
+
94
+ getWorldTransform(entityId) {
95
+ const e = this.entities.get(entityId); if (!e) return null
96
+ const local = { position: [...e.position], rotation: [...e.rotation], scale: [...e.scale] }
97
+ if (!e.parent) return local
98
+ const pt = this.getWorldTransform(e.parent); if (!pt) return local
99
+ const sp = [e.position[0]*pt.scale[0], e.position[1]*pt.scale[1], e.position[2]*pt.scale[2]]
100
+ const rp = rotVec(sp, pt.rotation)
101
+ return { position: [pt.position[0]+rp[0], pt.position[1]+rp[1], pt.position[2]+rp[2]], rotation: mulQuat(pt.rotation, e.rotation), scale: [pt.scale[0]*e.scale[0], pt.scale[1]*e.scale[1], pt.scale[2]*e.scale[2]] }
102
+ }
103
+
104
+ tick(tickNum, dt) {
105
+ this.currentTick = tickNum; this.deltaTime = dt; this.elapsed += dt
106
+ for (const [entityId, appDef] of this.apps) {
107
+ const ctx = this.contexts.get(entityId); if (!ctx) continue
108
+ this._safeCall(appDef.server || appDef, 'update', [ctx, dt], `update(${entityId})`)
109
+ }
110
+ this._tickTimers(dt); this._spatialSync(); this._tickCollisions()
111
+ }
112
+
113
+ _encodeEntity(id, e) {
114
+ const r = Array.isArray(e.rotation) ? [...e.rotation] : [e.rotation.x || 0, e.rotation.y || 0, e.rotation.z || 0, e.rotation.w || 1]
115
+ return { id, model: e.model, position: [...e.position], rotation: r, scale: [...e.scale], bodyType: e.bodyType, custom: e.custom || null, parent: e.parent || null }
116
+ }
117
+
118
+ getSnapshot() {
119
+ const entities = []
120
+ for (const [id, e] of this.entities) entities.push(this._encodeEntity(id, e))
121
+ return { tick: this.currentTick, timestamp: Date.now(), entities }
122
+ }
123
+
124
+ getSnapshotForPlayer(playerPosition, radius) {
125
+ const relevant = new Set(this.relevantEntities(playerPosition, radius))
126
+ const entities = []
127
+ for (const [id, e] of this.entities) { if (relevant.has(id)) entities.push(this._encodeEntity(id, e)) }
128
+ return { tick: this.currentTick, timestamp: Date.now(), entities }
129
+ }
130
+
131
+ queryEntities(f) { return f ? Array.from(this.entities.values()).filter(f) : Array.from(this.entities.values()) }
132
+ getEntity(id) { return this.entities.get(id) || null }
133
+ fireEvent(eid, en, ...a) { const ad = this.apps.get(eid), c = this.contexts.get(eid); if (!ad || !c) return; this._log('app_event', { entityId: eid, event: en, args: a }, { sourceEntity: eid }); const s = ad.server || ad; if (s[en]) this._safeCall(s, en, [c, ...a], `${en}(${eid})`) }
134
+ fireInteract(eid, p) { this.fireEvent(eid, 'onInteract', p) }
135
+ fireMessage(eid, m) { this.fireEvent(eid, 'onMessage', m) }
136
+ addTimer(e, d, fn, r) { if (!this._timers.has(e)) this._timers.set(e, []); this._timers.get(e).push({ remaining: d, fn, repeat: r, interval: d }) }
137
+ clearTimers(eid) { this._timers.delete(eid) }
138
+
139
+ _tickTimers(dt) {
140
+ for (const [eid, timers] of this._timers) {
141
+ const keep = []
142
+ for (const t of timers) {
143
+ t.remaining -= dt
144
+ if (t.remaining <= 0) { try { t.fn() } catch (e) { console.error(`[AppRuntime] timer(${eid}):`, e.message) }; if (t.repeat) { t.remaining = t.interval; keep.push(t) } }
145
+ else keep.push(t)
146
+ }
147
+ if (keep.length) this._timers.set(eid, keep); else this._timers.delete(eid)
148
+ }
149
+ }
150
+
151
+ _tickCollisions() {
152
+ const c = Array.from(this.entities.values()).filter(e => e.collider && this.apps.has(e.id))
153
+ for (let i = 0; i < c.length; i++) for (let j = i + 1; j < c.length; j++) {
154
+ const a = c[i], b = c[j], d = Math.hypot(b.position[0]-a.position[0], b.position[1]-a.position[1], b.position[2]-a.position[2])
155
+ if (d < this._colR(a.collider) + this._colR(b.collider)) {
156
+ this.fireEvent(a.id, 'onCollision', { id: b.id, position: b.position, velocity: b.velocity })
157
+ this.fireEvent(b.id, 'onCollision', { id: a.id, position: a.position, velocity: a.velocity })
158
+ }
159
+ }
160
+ }
161
+
162
+ _colR(c) { return !c ? 0 : c.type === 'sphere' ? (c.radius||1) : c.type === 'capsule' ? Math.max(c.radius||0.5,(c.height||1)/2) : c.type === 'box' ? Math.max(...(c.size||c.halfExtents||[1,1,1])) : 1 }
163
+ setPlayerManager(pm) { this._playerManager = pm }
164
+ setStageLoader(sl) { this._stageLoader = sl }
165
+ getPlayers() { return this._playerManager ? this._playerManager.getConnectedPlayers() : [] }
166
+
167
+ getNearestPlayer(pos, r) {
168
+ let n = null, md = r * r
169
+ for (const p of this.getPlayers()) { const pp = p.state?.position; if (!pp) continue; const d = (pp[0]-pos[0])**2+(pp[1]-pos[1])**2+(pp[2]-pos[2])**2; if (d < md) { md = d; n = p } }
170
+ return n
171
+ }
172
+
173
+ broadcastToPlayers(m) { if (this._connections) this._connections.broadcast(MSG.APP_EVENT, m); else if (this._playerManager) this._playerManager.broadcast(m) }
174
+ sendToPlayer(id, m) { if (this._connections) this._connections.send(id, MSG.APP_EVENT, m); else if (this._playerManager) this._playerManager.sendToPlayer(id, m) }
175
+ setPlayerPosition(id, p) { this._physicsIntegration?.setPlayerPosition(id, p); if (this._playerManager) { const pl = this._playerManager.getPlayer(id); if (pl) pl.state.position = [...p] } }
176
+
177
+ queueReload(n, d, cb) { this._hotReload.enqueue(n, d, cb) }
178
+ _drainReloadQueue() { this._hotReload.drain() }
179
+ hotReload(n, d) { this._hotReload._execute(n, d) }
180
+
181
+ _spatialInsert(entity) {
182
+ if (!this._stageLoader) return; const stage = this._stageLoader.getActiveStage()
183
+ if (stage && !stage.hasEntity(entity.id)) { stage.entityIds.add(entity.id); stage.spatial.insert(entity.id, entity.position); if (entity.bodyType === 'static') stage._staticIds.add(entity.id) }
184
+ }
185
+ _spatialRemove(entityId) { if (!this._stageLoader) return; const stage = this._stageLoader.getActiveStage(); if (stage) { stage.spatial.remove(entityId); stage._staticIds.delete(entityId); stage.entityIds.delete(entityId) } }
186
+ _spatialSync() { if (this._stageLoader) this._stageLoader.syncAllPositions() }
187
+ nearbyEntities(position, radius) { if (!this._stageLoader) return Array.from(this.entities.keys()); return this._stageLoader.getNearbyEntities(position, radius) }
188
+ relevantEntities(position, radius) { if (!this._stageLoader) return Array.from(this.entities.keys()); return this._stageLoader.getRelevantEntities(position, radius) }
189
+
190
+ _log(type, data, meta = {}) { if (this._eventLog) this._eventLog.record(type, data, { ...meta, tick: this.currentTick }) }
191
+ _safeCall(o, m, a, l) { if (!o?.[m]) return; try { o[m](...a) } catch (e) { console.error(`[AppRuntime] ${l}: ${e.message}\n ${e.stack?.split('\n').slice(1, 3).join('\n ') || ''}`) } }
192
+ }
@@ -0,0 +1,62 @@
1
+ export class EventBus {
2
+ constructor() {
3
+ this._handlers = new Map()
4
+ this._scoped = new Map()
5
+ }
6
+
7
+ on(channel, handler) {
8
+ if (!this._handlers.has(channel)) this._handlers.set(channel, new Set())
9
+ this._handlers.get(channel).add(handler)
10
+ return () => this.off(channel, handler)
11
+ }
12
+
13
+ off(channel, handler) {
14
+ const set = this._handlers.get(channel)
15
+ if (set) { set.delete(handler); if (set.size === 0) this._handlers.delete(channel) }
16
+ }
17
+
18
+ once(channel, handler) {
19
+ const wrapper = (...args) => { this.off(channel, wrapper); handler(...args) }
20
+ wrapper._original = handler
21
+ this.on(channel, wrapper)
22
+ }
23
+
24
+ emit(channel, data, meta = {}) {
25
+ const event = { channel, data, meta: { ...meta, timestamp: Date.now() } }
26
+ const exact = this._handlers.get(channel)
27
+ if (exact) for (const fn of exact) { try { fn(event) } catch (e) { console.error(`[EventBus] ${channel}:`, e.message) } }
28
+ for (const [pattern, handlers] of this._handlers) {
29
+ if (pattern === channel || !pattern.endsWith('*')) continue
30
+ const prefix = pattern.slice(0, -1)
31
+ if (channel.startsWith(prefix)) {
32
+ for (const fn of handlers) { try { fn(event) } catch (e) { console.error(`[EventBus] ${pattern}:`, e.message) } }
33
+ }
34
+ }
35
+ return event
36
+ }
37
+
38
+ scope(entityId) {
39
+ const unsubs = []
40
+ const scoped = {
41
+ on: (ch, fn) => { const u = this.on(ch, fn); unsubs.push(u); return u },
42
+ off: (ch, fn) => this.off(ch, fn),
43
+ once: (ch, fn) => { const wrapper = (...a) => { this.off(ch, wrapper); fn(...a) }; wrapper._original = fn; const u = this.on(ch, wrapper); unsubs.push(u); return u },
44
+ emit: (ch, data, meta = {}) => this.emit(ch, data, { ...meta, sourceEntity: entityId }),
45
+ handover: (targetEntityId, stateData) => this.emit('system.handover', { targetEntityId, stateData }, { sourceEntity: entityId }),
46
+ destroy: () => { for (const u of unsubs) u(); unsubs.length = 0 }
47
+ }
48
+ this._scoped.set(entityId, scoped)
49
+ return scoped
50
+ }
51
+
52
+ destroyScope(entityId) {
53
+ const scoped = this._scoped.get(entityId)
54
+ if (scoped) { scoped.destroy(); this._scoped.delete(entityId) }
55
+ }
56
+
57
+ clear() {
58
+ for (const scoped of this._scoped.values()) scoped.destroy()
59
+ this._scoped.clear()
60
+ this._handlers.clear()
61
+ }
62
+ }
@@ -0,0 +1,63 @@
1
+ import { AppContext } from './AppContext.js'
2
+
3
+ export class HotReloadQueue {
4
+ constructor(runtime) {
5
+ this._runtime = runtime
6
+ this._queue = []
7
+ this._inProgress = false
8
+ }
9
+
10
+ enqueue(name, def, callback) {
11
+ this._queue.push({ name, def, callback })
12
+ }
13
+
14
+ drain() {
15
+ if (this._inProgress || this._queue.length === 0) return
16
+ this._inProgress = true
17
+ try {
18
+ while (this._queue.length > 0) {
19
+ const { name, def, callback } = this._queue.shift()
20
+ try {
21
+ this._execute(name, def)
22
+ this._resetHeartbeats()
23
+ if (callback) {
24
+ try { callback(name, def) } catch (e) {
25
+ console.error(`[HotReloadQueue] callback error:`, e.message)
26
+ }
27
+ }
28
+ } catch (e) {
29
+ console.error(`[HotReloadQueue] hotReload(${name}) error:`, e.message)
30
+ }
31
+ }
32
+ } finally {
33
+ this._inProgress = false
34
+ }
35
+ }
36
+
37
+ _execute(name, def) {
38
+ const rt = this._runtime
39
+ rt._appDefs.set(name, def)
40
+ for (const [eid, ent] of rt.entities) {
41
+ if (ent._appName !== name) continue
42
+ const old = rt.apps.get(eid), oldCtx = rt.contexts.get(eid)
43
+ if (old && oldCtx) rt._safeCall(old.server || old, 'teardown', [oldCtx], 'teardown')
44
+ rt.clearTimers(eid)
45
+ const ctx = new AppContext(ent, rt)
46
+ rt.contexts.set(eid, ctx)
47
+ rt.apps.set(eid, def)
48
+ rt._safeCall(def.server || def, 'setup', [ctx], `hotReload(${name})`)
49
+ }
50
+ }
51
+
52
+ _resetHeartbeats() {
53
+ const conn = this._runtime._connections
54
+ if (!conn) return
55
+ for (const client of conn.clients.values()) {
56
+ client.lastHeartbeat = Date.now()
57
+ }
58
+ }
59
+
60
+ get pending() {
61
+ return this._queue.length
62
+ }
63
+ }
@@ -0,0 +1,85 @@
1
+ export class InputHandler {
2
+ constructor(config = {}) {
3
+ this.keys = new Map()
4
+ this.mouseX = 0
5
+ this.mouseY = 0
6
+ this.mouseDown = false
7
+ this.callbacks = []
8
+ this.enabled = true
9
+
10
+ if (config.enableKeyboard !== false) {
11
+ this.setupKeyboardListeners()
12
+ }
13
+
14
+ if (config.enableMouse !== false) {
15
+ this.setupMouseListeners()
16
+ }
17
+ }
18
+
19
+ setupKeyboardListeners() {
20
+ if (typeof window === 'undefined') return
21
+
22
+ window.addEventListener('keydown', (e) => {
23
+ this.keys.set(e.key.toLowerCase(), true)
24
+ })
25
+
26
+ window.addEventListener('keyup', (e) => {
27
+ this.keys.set(e.key.toLowerCase(), false)
28
+ })
29
+ }
30
+
31
+ setupMouseListeners() {
32
+ if (typeof window === 'undefined') return
33
+
34
+ document.addEventListener('mousemove', (e) => {
35
+ this.mouseX = e.clientX
36
+ this.mouseY = e.clientY
37
+ })
38
+
39
+ document.addEventListener('mousedown', (e) => {
40
+ this.mouseDown = true
41
+ })
42
+
43
+ document.addEventListener('mouseup', (e) => {
44
+ this.mouseDown = false
45
+ })
46
+ }
47
+
48
+ getInput() {
49
+ if (!this.enabled) {
50
+ return {
51
+ forward: false,
52
+ backward: false,
53
+ left: false,
54
+ right: false,
55
+ jump: false,
56
+ shoot: this.mouseDown,
57
+ mouseX: this.mouseX,
58
+ mouseY: this.mouseY
59
+ }
60
+ }
61
+
62
+ return {
63
+ forward: this.keys.get('w') || this.keys.get('arrowup') || false,
64
+ backward: this.keys.get('s') || this.keys.get('arrowdown') || false,
65
+ left: this.keys.get('a') || this.keys.get('arrowleft') || false,
66
+ right: this.keys.get('d') || this.keys.get('arrowright') || false,
67
+ jump: this.keys.get(' ') || false,
68
+ shoot: this.mouseDown,
69
+ mouseX: this.mouseX,
70
+ mouseY: this.mouseY
71
+ }
72
+ }
73
+
74
+ onInput(callback) {
75
+ this.callbacks.push(callback)
76
+ }
77
+
78
+ enable() {
79
+ this.enabled = true
80
+ }
81
+
82
+ disable() {
83
+ this.enabled = false
84
+ }
85
+ }