@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.
- package/LICENSE +674 -0
- package/README.md +250 -0
- package/apps/environment/index.js +17 -0
- package/apps/interactive-door/index.js +33 -0
- package/apps/patrol-npc/index.js +37 -0
- package/apps/physics-crate/index.js +23 -0
- package/apps/power-crate/index.js +169 -0
- package/apps/tps-game/index.js +168 -0
- package/apps/tps-game/schwust.glb +0 -0
- package/apps/world/index.js +22 -0
- package/client/app.js +181 -0
- package/client/camera.js +116 -0
- package/client/index.html +24 -0
- package/client/style.css +11 -0
- package/package.json +52 -0
- package/server.js +3 -0
- package/src/apps/AppContext.js +172 -0
- package/src/apps/AppLoader.js +160 -0
- package/src/apps/AppRuntime.js +192 -0
- package/src/apps/EventBus.js +62 -0
- package/src/apps/HotReloadQueue.js +63 -0
- package/src/client/InputHandler.js +85 -0
- package/src/client/PhysicsNetworkClient.js +171 -0
- package/src/client/PredictionEngine.js +123 -0
- package/src/client/ReconciliationEngine.js +54 -0
- package/src/connection/ConnectionManager.js +133 -0
- package/src/connection/QualityMonitor.js +46 -0
- package/src/connection/SessionStore.js +67 -0
- package/src/debug/CliDebugger.js +93 -0
- package/src/debug/Inspector.js +52 -0
- package/src/debug/StateInspector.js +42 -0
- package/src/index.client.js +8 -0
- package/src/index.js +1 -0
- package/src/index.server.js +27 -0
- package/src/math.js +21 -0
- package/src/netcode/EventLog.js +74 -0
- package/src/netcode/LagCompensator.js +78 -0
- package/src/netcode/NetworkState.js +66 -0
- package/src/netcode/PhysicsIntegration.js +132 -0
- package/src/netcode/PlayerManager.js +109 -0
- package/src/netcode/SnapshotEncoder.js +49 -0
- package/src/netcode/TickSystem.js +66 -0
- package/src/physics/GLBLoader.js +29 -0
- package/src/physics/World.js +195 -0
- package/src/protocol/Codec.js +60 -0
- package/src/protocol/EventEmitter.js +60 -0
- package/src/protocol/MessageTypes.js +73 -0
- package/src/protocol/SequenceTracker.js +71 -0
- package/src/protocol/msgpack.js +119 -0
- package/src/sdk/ClientMessageHandler.js +80 -0
- package/src/sdk/ReloadHandlers.js +68 -0
- package/src/sdk/ReloadManager.js +126 -0
- package/src/sdk/ServerAPI.js +133 -0
- package/src/sdk/ServerHandlers.js +76 -0
- package/src/sdk/StaticHandler.js +32 -0
- package/src/sdk/TickHandler.js +84 -0
- package/src/sdk/client.js +122 -0
- package/src/sdk/server.js +184 -0
- package/src/shared/movement.js +69 -0
- package/src/spatial/Octree.js +91 -0
- package/src/stage/Stage.js +90 -0
- package/src/stage/StageLoader.js +95 -0
- package/src/storage/FSAdapter.js +56 -0
- package/src/storage/StorageAdapter.js +7 -0
- package/src/transport/TransportWrapper.js +25 -0
- package/src/transport/WebSocketTransport.js +55 -0
- package/src/transport/WebTransportServer.js +83 -0
- package/src/transport/WebTransportTransport.js +94 -0
- package/world/kaira.glb +0 -0
- 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
|
+
}
|