@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,80 @@
1
+ import { MSG } from '../protocol/MessageTypes.js'
2
+ import { SnapshotEncoder } from '../netcode/SnapshotEncoder.js'
3
+ import { unpack } from '../protocol/msgpack.js'
4
+
5
+ export function createMessageRouter(deps) {
6
+ const { emitter, quality, stateInspector, send, getState, setState } = deps
7
+
8
+ function handleSnapshot(data) {
9
+ try {
10
+ const decoded = SnapshotEncoder.decode(data)
11
+ if (decoded.tick) {
12
+ stateInspector.recordSnapshotDelay(Date.now() - (decoded.timestamp || 0))
13
+ setState('tick', decoded.tick)
14
+ }
15
+ for (const p of decoded.players || []) getState('players').set(p.id, p)
16
+ for (const e of decoded.entities || []) getState('entities').set(e.id, e)
17
+ emitter.emit('snapshot', decoded)
18
+ } catch (e) {
19
+ emitter.emit('error', e)
20
+ }
21
+ }
22
+
23
+ return function onMessage(raw) {
24
+ const buf = raw instanceof ArrayBuffer ? new Uint8Array(raw) : (raw instanceof Uint8Array ? raw : new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength))
25
+ let msg
26
+ try { msg = unpack(buf) } catch (e) { return }
27
+ if (!msg || typeof msg !== 'object') return
28
+ const byteLen = buf.length || buf.byteLength || 0
29
+ quality.recordBytesIn(byteLen)
30
+
31
+ if (msg.type === MSG.HEARTBEAT) {
32
+ send(MSG.HEARTBEAT_ACK, { ts: msg.payload?.ts })
33
+ return
34
+ }
35
+ if (msg.type === MSG.HEARTBEAT_ACK && msg.payload?.ts) {
36
+ quality.recordRtt(Date.now() - msg.payload.ts)
37
+ quality.recordHeartbeatReceived()
38
+ return
39
+ }
40
+ if (msg.type === MSG.HANDSHAKE_ACK) {
41
+ setState('playerId', msg.payload.playerId)
42
+ setState('sessionToken', msg.payload.sessionToken)
43
+ setState('tick', msg.payload.tick)
44
+ emitter.emit('connect', { playerId: msg.payload.playerId, tick: msg.payload.tick })
45
+ return
46
+ }
47
+ if (msg.type === MSG.SNAPSHOT) { handleSnapshot(msg.payload); return }
48
+ if (msg.type === MSG.PLAYER_LEAVE) {
49
+ getState('players').delete(msg.payload.playerId)
50
+ emitter.emit('playerLeave', msg.payload.playerId)
51
+ return
52
+ }
53
+ if (msg.type === MSG.RECONNECT_ACK) {
54
+ setState('playerId', msg.payload.playerId)
55
+ setState('sessionToken', msg.payload.sessionToken)
56
+ setState('tick', msg.payload.tick)
57
+ setState('reconnecting', false)
58
+ setState('reconnectAttempts', 0)
59
+ emitter.emit('reconnect', msg.payload)
60
+ return
61
+ }
62
+ if (msg.type === MSG.STATE_RECOVERY) {
63
+ const decoded = SnapshotEncoder.decode(msg.payload.snapshot)
64
+ for (const p of decoded.players || []) getState('players').set(p.id, p)
65
+ for (const e of decoded.entities || []) getState('entities').set(e.id, e)
66
+ emitter.emit('stateRecovery', decoded)
67
+ return
68
+ }
69
+ if (msg.type === MSG.STATE_CORRECTION) {
70
+ stateInspector.recordCorrection(getState('playerId'), null, msg.payload, 0)
71
+ emitter.emit('correction', msg.payload)
72
+ return
73
+ }
74
+ if (msg.type === MSG.DISCONNECT_REASON) {
75
+ emitter.emit('disconnectReason', msg.payload)
76
+ return
77
+ }
78
+ emitter.emit('message', msg)
79
+ }
80
+ }
@@ -0,0 +1,68 @@
1
+ function swapInstance(target, NewClass, constructArgs, stateKeys) {
2
+ const oldProto = Object.getPrototypeOf(target)
3
+ const oldOwnDescriptors = Object.getOwnPropertyDescriptors(target)
4
+ try {
5
+ const fresh = new NewClass(...constructArgs)
6
+ Object.setPrototypeOf(target, NewClass.prototype)
7
+ const freshDescriptors = Object.getOwnPropertyDescriptors(fresh)
8
+ for (const key of Object.keys(freshDescriptors)) {
9
+ if (!stateKeys.includes(key)) {
10
+ Object.defineProperty(target, key, freshDescriptors[key])
11
+ }
12
+ }
13
+ for (const key of Object.keys(oldOwnDescriptors)) {
14
+ if (!(key in freshDescriptors) && !stateKeys.includes(key)) {
15
+ delete target[key]
16
+ }
17
+ }
18
+ } catch (e) {
19
+ Object.setPrototypeOf(target, oldProto)
20
+ for (const [key, desc] of Object.entries(oldOwnDescriptors)) {
21
+ Object.defineProperty(target, key, desc)
22
+ }
23
+ for (const key of Object.keys(target)) {
24
+ if (!(key in oldOwnDescriptors)) delete target[key]
25
+ }
26
+ throw e
27
+ }
28
+ }
29
+
30
+ export function createReloadHandlers(deps) {
31
+ const {
32
+ networkState, playerManager, physicsIntegration,
33
+ lagCompensator, physics, appRuntime, connections
34
+ } = deps
35
+
36
+ const reloadTickHandler = async () => {
37
+ const { createTickHandler: refreshHandler } = await import('./TickHandler.js?' + Date.now())
38
+ return refreshHandler(deps)
39
+ }
40
+
41
+ const reloadPhysicsIntegration = async () => {
42
+ const { PhysicsIntegration: New } = await import('../netcode/PhysicsIntegration.js?' + Date.now())
43
+ swapInstance(physicsIntegration, New, [{ ...physicsIntegration.config, physicsWorld: physics }], ['playerBodies'])
44
+ }
45
+
46
+ const reloadLagCompensator = async () => {
47
+ const { LagCompensator: New } = await import('../netcode/LagCompensator.js?' + Date.now())
48
+ swapInstance(lagCompensator, New, [lagCompensator.historyWindow], ['playerHistory'])
49
+ }
50
+
51
+ const reloadPlayerManager = async () => {
52
+ const { PlayerManager: New } = await import('../netcode/PlayerManager.js?' + Date.now())
53
+ swapInstance(playerManager, New, [], ['players', 'inputBuffers', 'nextPlayerId'])
54
+ }
55
+
56
+ const reloadNetworkState = async () => {
57
+ const { NetworkState: New } = await import('../netcode/NetworkState.js?' + Date.now())
58
+ swapInstance(networkState, New, [], ['players', 'tick', 'timestamp'])
59
+ }
60
+
61
+ return {
62
+ reloadTickHandler,
63
+ reloadPhysicsIntegration,
64
+ reloadLagCompensator,
65
+ reloadPlayerManager,
66
+ reloadNetworkState
67
+ }
68
+ }
@@ -0,0 +1,126 @@
1
+ import { watch } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+
4
+ export class ReloadManager {
5
+ constructor() {
6
+ this._watchers = new Map()
7
+ this._moduleCache = new Map()
8
+ this._reloadState = new Map()
9
+ this._debounceTimers = new Map()
10
+ this._failureCounters = new Map()
11
+ this._validators = new Map()
12
+ this._MAX_FAILURES = 3
13
+ this._MAX_BACKOFF = 400
14
+ }
15
+
16
+ addWatcher(moduleId, filePath, onReload, validator) {
17
+ const absPath = resolve(filePath)
18
+ if (this._watchers.has(moduleId)) return
19
+ this._reloadState.set(moduleId, { inProgress: false, lastSuccess: null, failureCount: 0 })
20
+ this._failureCounters.set(moduleId, 0)
21
+ if (validator) this._validators.set(moduleId, validator)
22
+ const startWatch = async () => {
23
+ try {
24
+ const ac = new AbortController()
25
+ this._watchers.set(moduleId, ac)
26
+ const watcher = watch(absPath, { signal: ac.signal })
27
+ ;(async () => {
28
+ try {
29
+ for await (const event of watcher) {
30
+ this._debounce(moduleId, () => this._handleReload(moduleId, onReload))
31
+ }
32
+ } catch (e) {
33
+ if (e.name !== 'AbortError') {
34
+ console.error(`[ReloadManager] watch error for ${moduleId}:`, e.message)
35
+ }
36
+ }
37
+ })()
38
+ } catch (e) {
39
+ console.error(`[ReloadManager] failed to start watcher for ${moduleId}:`, e.message)
40
+ }
41
+ }
42
+ startWatch()
43
+ }
44
+
45
+ _debounce(moduleId, fn) {
46
+ if (this._debounceTimers.has(moduleId)) {
47
+ clearTimeout(this._debounceTimers.get(moduleId))
48
+ }
49
+ this._failureCounters.set(moduleId, 0)
50
+ const timer = setTimeout(() => {
51
+ fn()
52
+ this._debounceTimers.delete(moduleId)
53
+ }, 100)
54
+ this._debounceTimers.set(moduleId, timer)
55
+ }
56
+
57
+ async _handleReload(moduleId, onReload) {
58
+ const state = this._reloadState.get(moduleId)
59
+ if (!state) return
60
+ if (state.inProgress) return
61
+ state.inProgress = true
62
+ const failureCount = this._failureCounters.get(moduleId) || 0
63
+ if (failureCount >= this._MAX_FAILURES) {
64
+ console.error(`[ReloadManager] ${moduleId} exceeded max failures, stopping auto-reload`)
65
+ state.inProgress = false
66
+ return
67
+ }
68
+ const validator = this._validators.get(moduleId)
69
+ if (validator) {
70
+ try {
71
+ const valid = await validator()
72
+ if (!valid) {
73
+ console.warn(`[ReloadManager] ${moduleId} failed pre-reload validation, skipping`)
74
+ state.inProgress = false
75
+ return
76
+ }
77
+ } catch (e) {
78
+ console.warn(`[ReloadManager] ${moduleId} validation error, skipping:`, e.message)
79
+ state.inProgress = false
80
+ return
81
+ }
82
+ }
83
+ try {
84
+ await onReload()
85
+ this._failureCounters.set(moduleId, 0)
86
+ state.lastSuccess = Date.now()
87
+ console.log(`[ReloadManager] successfully reloaded ${moduleId}`)
88
+ } catch (e) {
89
+ const newFailureCount = failureCount + 1
90
+ this._failureCounters.set(moduleId, newFailureCount)
91
+ const backoff = Math.min(100 * Math.pow(2, failureCount - 1), this._MAX_BACKOFF)
92
+ console.error(`[ReloadManager] reload failed for ${moduleId} (${newFailureCount}/${this._MAX_FAILURES}):`, e.message)
93
+ if (newFailureCount < this._MAX_FAILURES) {
94
+ console.log(`[ReloadManager] retrying ${moduleId} in ${backoff}ms`)
95
+ await new Promise(resolve => setTimeout(resolve, backoff))
96
+ state.inProgress = false
97
+ await this._handleReload(moduleId, onReload)
98
+ return
99
+ } else {
100
+ console.error(`[ReloadManager] ${moduleId} gave up after ${newFailureCount} failures`)
101
+ }
102
+ }
103
+ state.inProgress = false
104
+ }
105
+
106
+ stopWatcher(moduleId) {
107
+ const ac = this._watchers.get(moduleId)
108
+ if (ac) ac.abort()
109
+ this._watchers.delete(moduleId)
110
+ this._debounceTimers.delete(moduleId)
111
+ }
112
+
113
+ stopAllWatchers() {
114
+ for (const ac of this._watchers.values()) ac.abort()
115
+ this._watchers.clear()
116
+ for (const timer of this._debounceTimers.values()) clearTimeout(timer)
117
+ this._debounceTimers.clear()
118
+ }
119
+
120
+ cacheModule(moduleId, module) { this._moduleCache.set(moduleId, module) }
121
+ getModule(moduleId) { return this._moduleCache.get(moduleId) }
122
+ getState(moduleId) { return this._reloadState.get(moduleId) }
123
+ getStats() {
124
+ return { watchers: this._watchers.size, modules: this._moduleCache.size, failures: Object.fromEntries(this._failureCounters) }
125
+ }
126
+ }
@@ -0,0 +1,133 @@
1
+ import { createServer as createHttpServer } from 'node:http'
2
+ import { WebSocketServer as WSServer } from 'ws'
3
+ import { MSG } from '../protocol/MessageTypes.js'
4
+ import { SnapshotEncoder } from '../netcode/SnapshotEncoder.js'
5
+ import { createStaticHandler } from './StaticHandler.js'
6
+ import { WebSocketTransport } from '../transport/WebSocketTransport.js'
7
+ import { WebTransportServer } from '../transport/WebTransportServer.js'
8
+
9
+ export function createServerAPI(ctx) {
10
+ const { config, port, tickRate, staticDirs, appLoader, appRuntime, physics, stageLoader } = ctx
11
+ const { tickSystem, playerManager, networkState, lagCompensator, connections, sessions, inspector, emitter, reloadManager, eventBus, eventLog, storage } = ctx
12
+
13
+ return {
14
+ physics,
15
+ runtime: appRuntime,
16
+ loader: appLoader,
17
+ tickSystem,
18
+ playerManager,
19
+ networkState,
20
+ lagCompensator,
21
+ connections,
22
+ sessions,
23
+ inspector,
24
+ emitter,
25
+ reloadManager,
26
+ eventBus,
27
+ eventLog,
28
+ storage,
29
+ on: emitter.on.bind(emitter),
30
+ off: emitter.off.bind(emitter),
31
+
32
+ stageLoader,
33
+
34
+ async loadWorld(worldDef) {
35
+ ctx.currentWorldDef = worldDef
36
+ if (worldDef.spawnPoint) ctx.worldSpawnPoint = [...worldDef.spawnPoint]
37
+ await appLoader.loadAll()
38
+ const stage = stageLoader.loadFromDefinition('main', worldDef)
39
+ return { entities: new Map(), apps: new Map(), count: stage.entityCount }
40
+ },
41
+
42
+ async start() {
43
+ await appLoader.loadAll()
44
+ return new Promise((resolve, reject) => {
45
+ if (staticDirs.length > 0) {
46
+ ctx.httpServer = createHttpServer(createStaticHandler(staticDirs))
47
+ ctx.wss = new WSServer({ server: ctx.httpServer, path: '/ws' })
48
+ ctx.httpServer.on('error', reject)
49
+ ctx.httpServer.listen(port, () => {
50
+ attachWSHandlers(ctx)
51
+ resolve({ port: ctx.port, tickRate: ctx.tickRate })
52
+ })
53
+ } else {
54
+ ctx.wss = new WSServer({ port }, () => {
55
+ attachWSHandlers(ctx)
56
+ resolve({ port: ctx.port, tickRate: ctx.tickRate })
57
+ })
58
+ ctx.wss.on('error', reject)
59
+ }
60
+ })
61
+ },
62
+
63
+ stop() {
64
+ tickSystem.stop()
65
+ appLoader.stopWatching()
66
+ reloadManager.stopAllWatchers()
67
+ connections.destroy()
68
+ sessions.destroyAll()
69
+ if (ctx.wtServer) ctx.wtServer.stop()
70
+ if (ctx.wss) ctx.wss.close()
71
+ if (ctx.httpServer) ctx.httpServer.close()
72
+ physics.destroy()
73
+ },
74
+
75
+ send(id, type, p) {
76
+ return connections.send(id, type, p)
77
+ },
78
+
79
+ broadcast(type, p) {
80
+ connections.broadcast(type, p)
81
+ },
82
+
83
+ getPlayerCount() {
84
+ return playerManager.getPlayerCount()
85
+ },
86
+
87
+ getEntityCount() {
88
+ return appRuntime.entities.size
89
+ },
90
+
91
+ getSnapshot() {
92
+ return appRuntime.getSnapshot()
93
+ },
94
+
95
+ reloadTickHandler: async () => {
96
+ ctx.setTickHandler(await ctx.reloadHandlers.reloadTickHandler())
97
+ },
98
+
99
+ getReloadStats() {
100
+ return reloadManager.getStats()
101
+ },
102
+
103
+ getAllStats() {
104
+ return {
105
+ connections: connections.getAllStats(),
106
+ inspector: inspector.getAllClients(connections),
107
+ sessions: sessions.getActiveCount(),
108
+ tick: tickSystem.currentTick,
109
+ players: playerManager.getPlayerCount()
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ function attachWSHandlers(ctx) {
116
+ ctx.wss.on('connection', (socket) => {
117
+ ctx.onClientConnect(new WebSocketTransport(socket))
118
+ })
119
+ if (ctx.config.webTransport) {
120
+ const wtp = ctx.config.webTransport.port || 4433
121
+ ctx.wtServer = new WebTransportServer({
122
+ port: wtp,
123
+ cert: ctx.config.webTransport.cert,
124
+ key: ctx.config.webTransport.key
125
+ })
126
+ ctx.wtServer.on('session', ctx.onClientConnect)
127
+ if (ctx.wtServer.start()) console.log()
128
+ }
129
+ ctx.tickSystem.onTick(ctx.onTick)
130
+ ctx.tickSystem.start()
131
+ ctx.appLoader.watchAll()
132
+ ctx.setupSDKWatchers()
133
+ }
@@ -0,0 +1,76 @@
1
+ import { MSG, DISCONNECT_REASONS } from '../protocol/MessageTypes.js'
2
+ import { SnapshotEncoder } from '../netcode/SnapshotEncoder.js'
3
+
4
+ export function createConnectionHandlers(ctx) {
5
+ const { tickSystem, playerManager, networkState, lagCompensator, physicsIntegration, connections, sessions, appLoader, appRuntime, emitter, inspector } = ctx
6
+
7
+ function onClientConnect(transport) {
8
+ const sp = [...ctx.worldSpawnPoint]
9
+ const playerId = playerManager.addPlayer(transport, { position: sp })
10
+ networkState.addPlayer(playerId, { position: sp })
11
+ physicsIntegration.addPlayerCollider(playerId, 0.4)
12
+ physicsIntegration.setPlayerPosition(playerId, sp)
13
+ const playerState = playerManager.getPlayer(playerId).state
14
+ lagCompensator.recordPlayerPosition(playerId, playerState.position, playerState.rotation, playerState.velocity, tickSystem.currentTick)
15
+ const client = connections.addClient(playerId, transport)
16
+ client.sessionToken = sessions.create(playerId, playerManager.getPlayer(playerId).state)
17
+ connections.send(playerId, MSG.HANDSHAKE_ACK, { playerId, tick: tickSystem.currentTick, sessionToken: client.sessionToken, tickRate: ctx.tickRate })
18
+ if (ctx.currentWorldDef) {
19
+ connections.send(playerId, MSG.WORLD_DEF, ctx.currentWorldDef)
20
+ }
21
+ const clientModules = appLoader.getClientModules()
22
+ for (const [appName, code] of Object.entries(clientModules)) {
23
+ connections.send(playerId, MSG.APP_MODULE, { app: appName, code })
24
+ }
25
+ const snap = appRuntime.getSnapshot()
26
+ connections.send(playerId, MSG.SNAPSHOT, { seq: ++ctx.snapshotSeq, ...SnapshotEncoder.encode(snap) })
27
+ appRuntime.fireMessage('game', { type: 'player_join', playerId })
28
+ emitter.emit('playerJoin', { id: playerId })
29
+ }
30
+
31
+ connections.on('message', (clientId, msg) => {
32
+ if (inspector.handleMessage(clientId, msg)) return
33
+ if (msg.type === MSG.INPUT || msg.type === MSG.PLAYER_INPUT) {
34
+ playerManager.addInput(clientId, msg.payload?.input || msg.payload)
35
+ return
36
+ }
37
+ if (msg.type === MSG.APP_EVENT) {
38
+ if (msg.payload?.entityId) appRuntime.fireInteract(msg.payload.entityId, { id: clientId })
39
+ if (msg.payload?.type === 'fire') {
40
+ const shooter = playerManager.getPlayer(clientId)
41
+ const pos = shooter?.state?.position || [0, 0, 0]
42
+ const origin = [pos[0], pos[1] + 0.9, pos[2]]
43
+ appRuntime.fireMessage('game', { ...msg.payload, shooterId: clientId, origin })
44
+ }
45
+ return
46
+ }
47
+ if (msg.type === MSG.RECONNECT) {
48
+ const session = sessions.get(msg.payload?.sessionToken)
49
+ if (!session) {
50
+ connections.send(clientId, MSG.DISCONNECT_REASON, { code: DISCONNECT_REASONS.INVALID_SESSION })
51
+ return
52
+ }
53
+ const snap = networkState.getSnapshot()
54
+ const ents = appRuntime.getSnapshot()
55
+ connections.send(clientId, MSG.RECONNECT_ACK, { playerId: session.playerId, tick: tickSystem.currentTick, sessionToken: msg.payload.sessionToken })
56
+ connections.send(clientId, MSG.STATE_RECOVERY, { snapshot: SnapshotEncoder.encode({ tick: snap.tick, timestamp: snap.timestamp, players: snap.players, entities: ents.entities }), tick: tickSystem.currentTick })
57
+ return
58
+ }
59
+ emitter.emit('message', clientId, msg)
60
+ })
61
+
62
+ connections.on('disconnect', (clientId, reason) => {
63
+ const client = connections.getClient(clientId)
64
+ if (client?.sessionToken) { const p = playerManager.getPlayer(clientId); if (p) sessions.update(client.sessionToken, { state: p.state }) }
65
+ appRuntime.fireMessage('game', { type: 'player_leave', playerId: clientId })
66
+ physicsIntegration.removePlayerCollider(clientId)
67
+ lagCompensator.clearPlayerHistory(clientId)
68
+ inspector.removeClient(clientId)
69
+ playerManager.removePlayer(clientId)
70
+ networkState.removePlayer(clientId)
71
+ connections.broadcast(MSG.PLAYER_LEAVE, { playerId: clientId })
72
+ emitter.emit('playerLeave', { id: clientId, reason })
73
+ })
74
+
75
+ return { onClientConnect }
76
+ }
@@ -0,0 +1,32 @@
1
+ import { readFileSync, existsSync } from 'node:fs'
2
+ import { join, extname } from 'node:path'
3
+
4
+ const MIME_TYPES = {
5
+ '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
6
+ '.json': 'application/json', '.glb': 'model/gltf-binary', '.gltf': 'model/gltf+json',
7
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp',
8
+ '.svg': 'image/svg+xml', '.wasm': 'application/wasm'
9
+ }
10
+
11
+ export function createStaticHandler(dirs) {
12
+ return (req, res) => {
13
+ const url = req.url.split('?')[0]
14
+ for (const { prefix, dir } of dirs) {
15
+ if (!url.startsWith(prefix)) continue
16
+ const relative = url === prefix ? '/index.html' : url.slice(prefix.length)
17
+ const fp = join(dir, relative)
18
+ if (existsSync(fp)) {
19
+ const ext = extname(fp)
20
+ const headers = { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' }
21
+ if (ext === '.js' || ext === '.html' || ext === '.css') {
22
+ headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
23
+ }
24
+ res.writeHead(200, headers)
25
+ res.end(readFileSync(fp))
26
+ return
27
+ }
28
+ }
29
+ res.writeHead(404)
30
+ res.end('not found')
31
+ }
32
+ }
@@ -0,0 +1,84 @@
1
+ import { MSG } from '../protocol/MessageTypes.js'
2
+ import { SnapshotEncoder } from '../netcode/SnapshotEncoder.js'
3
+ import { applyMovement, DEFAULT_MOVEMENT } from '../shared/movement.js'
4
+
5
+ export function createTickHandler(deps) {
6
+ const {
7
+ networkState, playerManager, physicsIntegration,
8
+ lagCompensator, physics, appRuntime, connections,
9
+ movement: m = {}, stageLoader, eventLog
10
+ } = deps
11
+ const movement = { ...DEFAULT_MOVEMENT, ...m }
12
+ const collisionRestitution = m.collisionRestitution || 0.2
13
+ const collisionDamping = m.collisionDamping || 0.25
14
+ let snapshotSeq = 0
15
+
16
+ return function onTick(tick, dt) {
17
+ networkState.setTick(tick, Date.now())
18
+ for (const player of playerManager.getConnectedPlayers()) {
19
+ const inputs = playerManager.getInputs(player.id)
20
+ const st = player.state
21
+ let inp = null
22
+
23
+ if (inputs.length > 0) {
24
+ inp = inputs[inputs.length - 1].data
25
+ if (inp) {
26
+ const yaw = inp.yaw || 0
27
+ st.rotation = [0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)]
28
+ }
29
+ playerManager.clearInputs(player.id)
30
+ }
31
+
32
+ applyMovement(st, inp, movement, dt)
33
+ const updated = physicsIntegration.updatePlayerPhysics(player.id, st, dt)
34
+ st.position = updated.position
35
+ st.velocity = updated.velocity
36
+ st.onGround = updated.onGround
37
+ lagCompensator.recordPlayerPosition(player.id, st.position, st.rotation, st.velocity, tick)
38
+ networkState.updatePlayer(player.id, {
39
+ position: st.position, rotation: st.rotation,
40
+ velocity: st.velocity, onGround: st.onGround,
41
+ health: st.health, inputSequence: player.inputSequence
42
+ })
43
+ }
44
+ const players = playerManager.getConnectedPlayers()
45
+ for (const player of players) {
46
+ const collisions = physicsIntegration.checkCollisionWithOthers(player.id, players)
47
+ for (const collision of collisions) {
48
+ const other = playerManager.getPlayer(collision.playerId)
49
+ if (!other) continue
50
+ const dx = collision.normal[0], dy = collision.normal[1], dz = collision.normal[2]
51
+ const relVx = other.state.velocity[0] - player.state.velocity[0]
52
+ const relVz = other.state.velocity[2] - player.state.velocity[2]
53
+ const relDotNorm = relVx * dx + relVz * dz
54
+ if (relDotNorm >= 0) continue
55
+ const impulse = (1 + collisionRestitution) * relDotNorm * 0.5
56
+ player.state.velocity[0] -= impulse * dx * collisionDamping
57
+ player.state.velocity[2] -= impulse * dz * collisionDamping
58
+ other.state.velocity[0] += impulse * dx * collisionDamping
59
+ other.state.velocity[2] += impulse * dz * collisionDamping
60
+ }
61
+ }
62
+ physics.step(dt)
63
+ appRuntime.tick(tick, dt)
64
+ const playerSnap = networkState.getSnapshot()
65
+ snapshotSeq++
66
+ if (stageLoader && stageLoader.getActiveStage()) {
67
+ for (const player of players) {
68
+ const pos = player.state.position
69
+ const entitySnap = appRuntime.getSnapshotForPlayer(pos, stageLoader.getActiveStage().spatial.relevanceRadius)
70
+ const combined = { tick: playerSnap.tick, timestamp: playerSnap.timestamp, players: playerSnap.players, entities: entitySnap.entities }
71
+ connections.send(player.id, MSG.SNAPSHOT, { seq: snapshotSeq, ...SnapshotEncoder.encode(combined) })
72
+ }
73
+ } else {
74
+ const entitySnap = appRuntime.getSnapshot()
75
+ const combined = { tick: playerSnap.tick, timestamp: playerSnap.timestamp, players: playerSnap.players, entities: entitySnap.entities }
76
+ connections.broadcast(MSG.SNAPSHOT, { seq: snapshotSeq, ...SnapshotEncoder.encode(combined) })
77
+ }
78
+ try {
79
+ appRuntime._drainReloadQueue()
80
+ } catch (e) {
81
+ console.error('[TickHandler] reload queue error:', e.message)
82
+ }
83
+ }
84
+ }