@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,171 @@
1
+ import { PredictionEngine } from './PredictionEngine.js'
2
+ import { pack, unpack } from '../protocol/msgpack.js'
3
+ import { MSG } from '../protocol/MessageTypes.js'
4
+
5
+ export class PhysicsNetworkClient {
6
+ constructor(config = {}) {
7
+ this.config = { url: config.url || 'ws://localhost:8080/ws', tickRate: config.tickRate || 128, predictionEnabled: config.predictionEnabled !== false, debug: config.debug || false, ...config }
8
+ this.ws = null
9
+ this.playerId = null
10
+ this.connected = false
11
+ this._predEngine = null
12
+ this._playerStates = new Map()
13
+ this._entityStates = new Map()
14
+ this.lastSnapshotTick = 0
15
+ this.currentTick = 0
16
+ this.state = { players: [], entities: [] }
17
+ this.heartbeatTimer = null
18
+ this.callbacks = {
19
+ onConnect: config.onConnect || (() => {}),
20
+ onDisconnect: config.onDisconnect || (() => {}),
21
+ onPlayerJoined: config.onPlayerJoined || (() => {}),
22
+ onPlayerLeft: config.onPlayerLeft || (() => {}),
23
+ onEntityAdded: config.onEntityAdded || (() => {}),
24
+ onEntityRemoved: config.onEntityRemoved || (() => {}),
25
+ onSnapshot: config.onSnapshot || (() => {}),
26
+ onRender: config.onRender || (() => {}),
27
+ onStateUpdate: config.onStateUpdate || (() => {}),
28
+ onWorldDef: config.onWorldDef || (() => {}),
29
+ onAppModule: config.onAppModule || (() => {}),
30
+ onAssetUpdate: config.onAssetUpdate || (() => {}),
31
+ onAppEvent: config.onAppEvent || (() => {})
32
+ }
33
+ }
34
+
35
+ async connect() {
36
+ return new Promise((resolve, reject) => {
37
+ try {
38
+ this.ws = new WebSocket(this.config.url)
39
+ this.ws.binaryType = 'arraybuffer'
40
+ this.ws.onopen = () => this._onOpen(resolve)
41
+ this.ws.onmessage = (event) => this.onMessage(event.data)
42
+ this.ws.onclose = () => this._onClose()
43
+ this.ws.onerror = (error) => this._onError(error, reject)
44
+ } catch (error) { reject(error) }
45
+ })
46
+ }
47
+
48
+ _onOpen(resolve) {
49
+ this.connected = true
50
+ this._startHeartbeat()
51
+ this.callbacks.onConnect()
52
+ resolve()
53
+ }
54
+
55
+ _onClose() {
56
+ this.connected = false
57
+ this._stopHeartbeat()
58
+ this.callbacks.onDisconnect()
59
+ }
60
+
61
+ _onError(error, reject) { reject(error) }
62
+
63
+ sendInput(input) {
64
+ if (!this._isOpen()) return
65
+ if (this.config.predictionEnabled && this._predEngine) this._predEngine.addInput(input)
66
+ this.ws.send(pack({ type: MSG.INPUT, payload: { input } }))
67
+ }
68
+
69
+ sendFire(data) {
70
+ if (!this._isOpen()) return
71
+ this.ws.send(pack({ type: MSG.APP_EVENT, payload: { type: 'fire', shooterId: this.playerId, ...data } }))
72
+ }
73
+
74
+ _isOpen() { return this.ws && this.ws.readyState === WebSocket.OPEN }
75
+
76
+ onMessage(data) {
77
+ try {
78
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data
79
+ const msg = unpack(bytes)
80
+ this._handleMessage(msg.type, msg.payload || {})
81
+ } catch (e) { console.error('[client] parse error:', e) }
82
+ }
83
+
84
+ _handleMessage(type, payload) {
85
+ if (type === MSG.HANDSHAKE_ACK) {
86
+ this.playerId = payload.playerId
87
+ this.currentTick = payload.tick
88
+ this._predEngine = new PredictionEngine(this.config.tickRate)
89
+ this._predEngine.init(this.playerId)
90
+ } else if (type === MSG.SNAPSHOT || type === MSG.STATE_CORRECTION) {
91
+ this._onSnapshot(payload)
92
+ } else if (type === MSG.PLAYER_LEAVE) {
93
+ this._playerStates.delete(payload.playerId)
94
+ this.callbacks.onPlayerLeft(payload.playerId)
95
+ } else if (type === MSG.WORLD_DEF) {
96
+ if (payload.movement && this._predEngine) this._predEngine.setMovement(payload.movement)
97
+ if (payload.gravity && this._predEngine) this._predEngine.setGravity(payload.gravity)
98
+ this.callbacks.onWorldDef?.(payload)
99
+ } else if (type === MSG.APP_EVENT) {
100
+ this.callbacks.onAppEvent?.(payload)
101
+ } else if (type === MSG.HOT_RELOAD || type === MSG.APP_MODULE || type === MSG.ASSET_UPDATE) {
102
+ const cb = { [MSG.HOT_RELOAD]: 'onHotReload', [MSG.APP_MODULE]: 'onAppModule', [MSG.ASSET_UPDATE]: 'onAssetUpdate' }[type]
103
+ this.callbacks[cb]?.(payload)
104
+ }
105
+ }
106
+
107
+ _onSnapshot(data) {
108
+ this.lastSnapshotTick = this.currentTick = data.tick || 0
109
+ for (const p of data.players || []) {
110
+ const { playerId, state } = this._parsePlayer(p)
111
+ if (!this._playerStates.has(playerId)) this.callbacks.onPlayerJoined(playerId, state)
112
+ this._playerStates.set(playerId, state)
113
+ if (playerId === this.playerId && this.config.predictionEnabled && this._predEngine) {
114
+ this._predEngine.onServerSnapshot({ players: [state] }, this.currentTick)
115
+ }
116
+ }
117
+ for (const e of data.entities || []) {
118
+ const { entityId, state } = this._parseEntity(e)
119
+ if (!this._entityStates.has(entityId)) this.callbacks.onEntityAdded(entityId, state)
120
+ this._entityStates.set(entityId, state)
121
+ }
122
+ this.state.players = Array.from(this._playerStates.values())
123
+ this.state.entities = Array.from(this._entityStates.values())
124
+ this.callbacks.onSnapshot(data)
125
+ this.callbacks.onStateUpdate(this.state)
126
+ this._render()
127
+ }
128
+
129
+ _parsePlayer(p) {
130
+ if (Array.isArray(p)) return { playerId: p[0], state: { id: p[0], position: [p[1], p[2], p[3]], rotation: [p[4], p[5], p[6], p[7]], velocity: [p[8], p[9], p[10]], onGround: p[11] === 1, health: p[12], inputSequence: p[13] } }
131
+ return { playerId: p.id || p.i, state: { id: p.id || p.i, position: p.position || [0, 0, 0], rotation: p.rotation || [0, 0, 0, 1], velocity: p.velocity || [0, 0, 0], onGround: p.onGround ?? false, health: p.health ?? 100 } }
132
+ }
133
+
134
+ _parseEntity(e) {
135
+ if (Array.isArray(e)) return { entityId: e[0], state: { id: e[0], model: e[1], position: [e[2], e[3], e[4]], rotation: [e[5], e[6], e[7], e[8]], bodyType: e[9], custom: e[10] } }
136
+ return { entityId: e.id, state: { id: e.id, model: e.model, position: e.position || [0, 0, 0], rotation: e.rotation || [0, 0, 0, 1], bodyType: e.bodyType || 'static', custom: e.custom || null } }
137
+ }
138
+
139
+ _render() {
140
+ const displayStates = new Map()
141
+ for (const [playerId, serverState] of this._playerStates) {
142
+ displayStates.set(playerId, playerId === this.playerId && this.config.predictionEnabled && this._predEngine ? this._predEngine.getDisplayState(this.currentTick, 0) : serverState)
143
+ }
144
+ this.callbacks.onRender(displayStates)
145
+ }
146
+
147
+ getLocalState() {
148
+ return this.config.predictionEnabled && this._predEngine ? this._predEngine.localState : this._playerStates.get(this.playerId)
149
+ }
150
+
151
+ getRemoteState(playerId) { return this._playerStates.get(playerId) }
152
+ getAllStates() { return new Map(this._playerStates) }
153
+ getEntity(entityId) { return this._entityStates.get(entityId) }
154
+ getAllEntities() { return new Map(this._entityStates) }
155
+
156
+ disconnect() {
157
+ this._stopHeartbeat()
158
+ if (this.ws) this.ws.close()
159
+ }
160
+
161
+ _startHeartbeat() {
162
+ this._stopHeartbeat()
163
+ this.heartbeatTimer = setInterval(() => {
164
+ if (this._isOpen()) this.ws.send(pack({ type: MSG.HEARTBEAT, payload: {} }))
165
+ }, 1000)
166
+ }
167
+
168
+ _stopHeartbeat() {
169
+ if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null }
170
+ }
171
+ }
@@ -0,0 +1,123 @@
1
+ import { ReconciliationEngine } from './ReconciliationEngine.js'
2
+ import { applyMovement, DEFAULT_MOVEMENT } from '../shared/movement.js'
3
+
4
+ export class PredictionEngine {
5
+ constructor(tickRate = 128) {
6
+ this.tickRate = tickRate
7
+ this.tickDuration = 1000 / tickRate
8
+ this.localPlayerId = null
9
+ this.localState = null
10
+ this.lastServerState = null
11
+ this.inputHistory = []
12
+ this.reconciliationEngine = new ReconciliationEngine()
13
+ this.movement = { ...DEFAULT_MOVEMENT }
14
+ this.gravityY = -9.81
15
+ }
16
+
17
+ setMovement(m) { Object.assign(this.movement, m) }
18
+
19
+ setGravity(g) { if (g && g[1] != null) this.gravityY = g[1] }
20
+
21
+ init(playerId, initialState = {}) {
22
+ this.localPlayerId = playerId
23
+ this.localState = {
24
+ id: playerId,
25
+ position: initialState.position || [0, 0, 0],
26
+ rotation: initialState.rotation || [0, 0, 0, 1],
27
+ velocity: initialState.velocity || [0, 0, 0],
28
+ onGround: true,
29
+ health: initialState.health || 100
30
+ }
31
+ this.lastServerState = JSON.parse(JSON.stringify(this.localState))
32
+ }
33
+
34
+ addInput(input) {
35
+ this.inputHistory.push({
36
+ sequence: this.inputHistory.length,
37
+ data: input,
38
+ timestamp: Date.now()
39
+ })
40
+ if (this.inputHistory.length > 128) {
41
+ this.inputHistory.shift()
42
+ }
43
+ this.predict(input)
44
+ }
45
+
46
+ predict(input) {
47
+ const dt = this.tickDuration / 1000
48
+ const state = this.localState
49
+ applyMovement(state, input, this.movement, dt)
50
+ state.velocity[1] += this.gravityY * dt
51
+ state.position[0] += state.velocity[0] * dt
52
+ state.position[1] += state.velocity[1] * dt
53
+ state.position[2] += state.velocity[2] * dt
54
+ if (state.position[1] < 0) {
55
+ state.position[1] = 0
56
+ state.velocity[1] = 0
57
+ state.onGround = true
58
+ }
59
+ }
60
+
61
+ interpolate(factor) {
62
+ if (!this.lastServerState || !this.localState) return this.localState
63
+ return {
64
+ id: this.localState.id,
65
+ position: [
66
+ this.lastServerState.position[0] + (this.localState.position[0] - this.lastServerState.position[0]) * factor,
67
+ this.lastServerState.position[1] + (this.localState.position[1] - this.lastServerState.position[1]) * factor,
68
+ this.lastServerState.position[2] + (this.localState.position[2] - this.lastServerState.position[2]) * factor
69
+ ],
70
+ rotation: this.localState.rotation,
71
+ velocity: this.localState.velocity,
72
+ health: this.localState.health,
73
+ onGround: this.localState.onGround
74
+ }
75
+ }
76
+
77
+ extrapolate(ticksAhead = 1) {
78
+ const extrapolated = JSON.parse(JSON.stringify(this.localState))
79
+ const dt = (this.tickDuration / 1000) * ticksAhead
80
+ extrapolated.position[0] += this.localState.velocity[0] * dt
81
+ extrapolated.position[1] += this.localState.velocity[1] * dt
82
+ extrapolated.position[2] += this.localState.velocity[2] * dt
83
+ return extrapolated
84
+ }
85
+
86
+ onServerSnapshot(snapshot, tick) {
87
+ for (const serverPlayer of snapshot.players) {
88
+ if (serverPlayer.id === this.localPlayerId) {
89
+ this.lastServerState = JSON.parse(JSON.stringify(serverPlayer))
90
+ const reconciliation = this.reconciliationEngine.reconcile(
91
+ this.lastServerState, this.localState, tick
92
+ )
93
+ if (reconciliation.needsCorrection) {
94
+ this.reconciliationEngine.applyCorrection(this.localState, reconciliation.correction)
95
+ this.resimulate()
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ resimulate() {
102
+ const baseState = JSON.parse(JSON.stringify(this.lastServerState))
103
+ this.localState = baseState
104
+ for (const input of this.inputHistory) {
105
+ this.predict(input.data)
106
+ }
107
+ }
108
+
109
+ getDisplayState(tick, ticksSinceLastSnapshot) {
110
+ const alpha = (ticksSinceLastSnapshot % 1) / 1
111
+ return this.interpolate(alpha)
112
+ }
113
+
114
+ getInputHistory() { return this.inputHistory }
115
+
116
+ calculateDivergence() {
117
+ if (!this.lastServerState || !this.localState) return 0
118
+ const dx = this.localState.position[0] - this.lastServerState.position[0]
119
+ const dy = this.localState.position[1] - this.lastServerState.position[1]
120
+ const dz = this.localState.position[2] - this.lastServerState.position[2]
121
+ return Math.sqrt(dx * dx + dy * dy + dz * dz)
122
+ }
123
+ }
@@ -0,0 +1,54 @@
1
+ export class ReconciliationEngine {
2
+ constructor(config = {}) {
3
+ this.correctionThreshold = config.correctionThreshold || 0.01
4
+ this.correctionSpeed = config.correctionSpeed || 0.5
5
+ this.lastReconcileTime = 0
6
+ this.reconcileInterval = config.reconcileInterval || 100
7
+ }
8
+
9
+ reconcile(serverState, localState, tick) {
10
+ const now = Date.now()
11
+ if (now - this.lastReconcileTime < this.reconcileInterval) {
12
+ return { needsCorrection: false, correction: null }
13
+ }
14
+
15
+ this.lastReconcileTime = now
16
+
17
+ const divergence = this.calculateDivergence(serverState, localState)
18
+
19
+ if (divergence < this.correctionThreshold) {
20
+ return { needsCorrection: false, divergence }
21
+ }
22
+
23
+ const correction = this.generateCorrection(serverState, localState)
24
+ return { needsCorrection: true, correction, divergence }
25
+ }
26
+
27
+ calculateDivergence(serverState, localState) {
28
+ if (!serverState || !localState) return 0
29
+
30
+ const dx = serverState.position[0] - localState.position[0]
31
+ const dy = serverState.position[1] - localState.position[1]
32
+ const dz = serverState.position[2] - localState.position[2]
33
+
34
+ return Math.sqrt(dx * dx + dy * dy + dz * dz)
35
+ }
36
+
37
+ generateCorrection(serverState, localState) {
38
+ return {
39
+ position: [
40
+ serverState.position[0] * this.correctionSpeed + localState.position[0] * (1 - this.correctionSpeed),
41
+ serverState.position[1] * this.correctionSpeed + localState.position[1] * (1 - this.correctionSpeed),
42
+ serverState.position[2] * this.correctionSpeed + localState.position[2] * (1 - this.correctionSpeed)
43
+ ],
44
+ velocity: [...serverState.velocity],
45
+ onGround: serverState.onGround
46
+ }
47
+ }
48
+
49
+ applyCorrection(localState, correction) {
50
+ localState.position = correction.position
51
+ localState.velocity = correction.velocity
52
+ localState.onGround = correction.onGround
53
+ }
54
+ }
@@ -0,0 +1,133 @@
1
+ import { pack, unpack } from '../protocol/msgpack.js'
2
+ import { isUnreliable } from '../protocol/MessageTypes.js'
3
+ import { EventEmitter } from '../protocol/EventEmitter.js'
4
+
5
+ export class ConnectionManager extends EventEmitter {
6
+ constructor(options = {}) {
7
+ super()
8
+ this.clients = new Map()
9
+ this.heartbeatInterval = options.heartbeatInterval || 1000
10
+ this.heartbeatTimeout = options.heartbeatTimeout || 3000
11
+ this.timers = new Map()
12
+ }
13
+
14
+ addClient(clientId, transport) {
15
+ const client = {
16
+ id: clientId,
17
+ transport,
18
+ lastHeartbeat: Date.now(),
19
+ sessionToken: null,
20
+ transportType: transport.type || 'websocket'
21
+ }
22
+
23
+ transport.on('message', (data) => {
24
+ try {
25
+ client.lastHeartbeat = Date.now()
26
+ const msg = unpack(data)
27
+ this.emit('message', clientId, msg)
28
+ } catch (err) {
29
+ console.error(`[connection] decode error for ${clientId}:`, err.message)
30
+ }
31
+ })
32
+
33
+ transport.on('close', () => {
34
+ this.removeClient(clientId)
35
+ this.emit('disconnect', clientId, 'closed')
36
+ })
37
+
38
+ transport.on('error', (err) => {
39
+ console.error(`[connection] transport error for ${clientId}:`, err.message)
40
+ this.removeClient(clientId)
41
+ this.emit('disconnect', clientId, 'error')
42
+ })
43
+
44
+ this.clients.set(clientId, client)
45
+ this._setupHeartbeat(clientId)
46
+ return client
47
+ }
48
+
49
+ _setupHeartbeat(clientId) {
50
+ const check = () => {
51
+ const client = this.clients.get(clientId)
52
+ if (!client) return
53
+ const age = Date.now() - client.lastHeartbeat
54
+ if (age > this.heartbeatTimeout) {
55
+ this.removeClient(clientId)
56
+ this.emit('disconnect', clientId, 'timeout')
57
+ return
58
+ }
59
+ const timer = setTimeout(check, this.heartbeatInterval)
60
+ this.timers.set(`hb-${clientId}`, timer)
61
+ }
62
+ const timer = setTimeout(check, this.heartbeatInterval)
63
+ this.timers.set(`hb-${clientId}`, timer)
64
+ }
65
+
66
+ removeClient(clientId) {
67
+ const client = this.clients.get(clientId)
68
+ if (!client) return
69
+ if (client.transport && client.transport.isOpen) {
70
+ client.transport.close()
71
+ }
72
+ this.clients.delete(clientId)
73
+ const timer = this.timers.get(`hb-${clientId}`)
74
+ if (timer) clearTimeout(timer)
75
+ this.timers.delete(`hb-${clientId}`)
76
+ }
77
+
78
+ getClient(clientId) {
79
+ return this.clients.get(clientId)
80
+ }
81
+
82
+ send(clientId, type, payload = {}) {
83
+ const client = this.clients.get(clientId)
84
+ if (!client || !client.transport.isOpen) return false
85
+ try {
86
+ const data = pack({ type, payload })
87
+ if (isUnreliable(type)) return client.transport.sendUnreliable(data)
88
+ return client.transport.send(data)
89
+ } catch (err) {
90
+ console.error(`[connection] send error to ${clientId}:`, err.message)
91
+ return false
92
+ }
93
+ }
94
+
95
+ broadcast(type, payload = {}) {
96
+ const data = pack({ type, payload })
97
+ const unreliable = isUnreliable(type)
98
+ let count = 0
99
+ for (const client of this.clients.values()) {
100
+ if (!client.transport.isOpen) continue
101
+ try {
102
+ if (unreliable) client.transport.sendUnreliable(data)
103
+ else client.transport.send(data)
104
+ count++
105
+ } catch (err) {
106
+ console.error(`[connection] broadcast error to ${client.id}:`, err.message)
107
+ }
108
+ }
109
+ return count
110
+ }
111
+
112
+ getAllStats() {
113
+ return {
114
+ activeConnections: this.clients.size,
115
+ clients: Array.from(this.clients.entries()).map(([id, c]) => ({
116
+ id,
117
+ transport: c.transportType,
118
+ sessionToken: c.sessionToken ? '***' : null
119
+ }))
120
+ }
121
+ }
122
+
123
+ destroy() {
124
+ for (const clientId of this.clients.keys()) {
125
+ this.removeClient(clientId)
126
+ }
127
+ for (const timer of this.timers.values()) {
128
+ clearTimeout(timer)
129
+ }
130
+ this.clients.clear()
131
+ this.timers.clear()
132
+ }
133
+ }
@@ -0,0 +1,46 @@
1
+ export class QualityMonitor {
2
+ constructor() {
3
+ this.bytesIn = 0
4
+ this.bytesOut = 0
5
+ this.heartbeatsSent = 0
6
+ this.heartbeatsReceived = 0
7
+ this.rttSamples = []
8
+ this.startTime = Date.now()
9
+ }
10
+
11
+ recordBytesIn(count) {
12
+ this.bytesIn += count
13
+ }
14
+
15
+ recordBytesOut(count) {
16
+ this.bytesOut += count
17
+ }
18
+
19
+ recordHeartbeatSent() {
20
+ this.heartbeatsSent++
21
+ }
22
+
23
+ recordHeartbeatReceived() {
24
+ this.heartbeatsReceived++
25
+ }
26
+
27
+ recordRtt(rtt) {
28
+ this.rttSamples.push(rtt)
29
+ if (this.rttSamples.length > 100) this.rttSamples.shift()
30
+ }
31
+
32
+ getStats() {
33
+ const elapsed = Date.now() - this.startTime
34
+ const avgRtt = this.rttSamples.length > 0
35
+ ? this.rttSamples.reduce((a, b) => a + b, 0) / this.rttSamples.length
36
+ : 0
37
+ return {
38
+ bytesIn: this.bytesIn,
39
+ bytesOut: this.bytesOut,
40
+ heartbeatsSent: this.heartbeatsSent,
41
+ heartbeatsReceived: this.heartbeatsReceived,
42
+ avgRtt,
43
+ uptime: elapsed
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,67 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ export class SessionStore {
4
+ constructor(options = {}) {
5
+ this.ttl = options.ttl || 30000
6
+ this.sessions = new Map()
7
+ this.timers = new Map()
8
+ }
9
+
10
+ create(playerId, state) {
11
+ const token = randomBytes(16).toString('hex')
12
+ const session = {
13
+ token,
14
+ playerId,
15
+ state: state ? { ...state } : {},
16
+ createdAt: Date.now()
17
+ }
18
+ this.sessions.set(token, session)
19
+ this._setupExpire(token)
20
+ return token
21
+ }
22
+
23
+ update(token, data) {
24
+ const session = this.sessions.get(token)
25
+ if (!session) return false
26
+ if (data.state) Object.assign(session.state, data.state)
27
+ session.lastUpdated = Date.now()
28
+ return true
29
+ }
30
+
31
+ get(token) {
32
+ const session = this.sessions.get(token)
33
+ if (!session) return null
34
+ if (Date.now() - session.createdAt > this.ttl) {
35
+ this.destroy(token)
36
+ return null
37
+ }
38
+ return session
39
+ }
40
+
41
+ _setupExpire(token) {
42
+ const timer = setTimeout(() => {
43
+ this.sessions.delete(token)
44
+ this.timers.delete(token)
45
+ }, this.ttl)
46
+ this.timers.set(token, timer)
47
+ }
48
+
49
+ destroy(token) {
50
+ const timer = this.timers.get(token)
51
+ if (timer) clearTimeout(timer)
52
+ this.timers.delete(token)
53
+ this.sessions.delete(token)
54
+ }
55
+
56
+ getActiveCount() {
57
+ return this.sessions.size
58
+ }
59
+
60
+ destroyAll() {
61
+ for (const timer of this.timers.values()) {
62
+ clearTimeout(timer)
63
+ }
64
+ this.timers.clear()
65
+ this.sessions.clear()
66
+ }
67
+ }
@@ -0,0 +1,93 @@
1
+ export class CliDebugger {
2
+ constructor(prefix = '[spawnpoint]') {
3
+ this.prefix = prefix
4
+ this.startTime = Date.now()
5
+ this.history = []
6
+ }
7
+
8
+ _timestamp() {
9
+ const elapsed = Date.now() - this.startTime
10
+ const s = (elapsed / 1000).toFixed(2)
11
+ return `${s}s`.padStart(8)
12
+ }
13
+
14
+ _formatVec3(v) {
15
+ if (!v) return '(null)'
16
+ return `(${v[0].toFixed(2)}, ${v[1].toFixed(2)}, ${v[2].toFixed(2)})`
17
+ }
18
+
19
+ spawn(entity, position) {
20
+ const msg = `${this.prefix} SPAWN ${String(entity).padEnd(15)} @ ${this._formatVec3(position)}`
21
+ console.log(`${this._timestamp()} ${msg}`)
22
+ this.history.push(msg)
23
+ }
24
+
25
+ collision(a, b, position) {
26
+ const msg = `${this.prefix} COLLISION ${String(a).padEnd(8)} <-> ${String(b).padEnd(8)} @ ${this._formatVec3(position)}`
27
+ console.log(`${this._timestamp()} ${msg}`)
28
+ this.history.push(msg)
29
+ }
30
+
31
+ hit(shooter, target, damage) {
32
+ const msg = `${this.prefix} HIT ${String(shooter).padEnd(10)} -> ${String(target).padEnd(10)} [${damage}hp]`
33
+ console.log(`${this._timestamp()} ${msg}`)
34
+ this.history.push(msg)
35
+ }
36
+
37
+ death(entity, damage) {
38
+ const msg = `${this.prefix} DEATH ${String(entity).padEnd(15)} from ${damage}hp`
39
+ console.log(`${this._timestamp()} ${msg}`)
40
+ this.history.push(msg)
41
+ }
42
+
43
+ respawn(entity, position) {
44
+ const msg = `${this.prefix} RESPAWN ${String(entity).padEnd(10)} @ ${this._formatVec3(position)}`
45
+ console.log(`${this._timestamp()} ${msg}`)
46
+ this.history.push(msg)
47
+ }
48
+
49
+ state(entity, key, value) {
50
+ const v = typeof value === 'object' ? JSON.stringify(value) : String(value)
51
+ const msg = `${this.prefix} STATE ${String(entity).padEnd(15)} ${key}=${v}`
52
+ console.log(`${this._timestamp()} ${msg}`)
53
+ }
54
+
55
+ perf(label, ms) {
56
+ const status = ms < 10 ? '✓' : ms < 20 ? '⚠' : '✗'
57
+ const msg = `${this.prefix} ${status} ${label.padEnd(20)} ${ms.toFixed(1)}ms`
58
+ console.log(`${this._timestamp()} ${msg}`)
59
+ }
60
+
61
+ physics(body, pos, vel, health) {
62
+ const speed = Math.sqrt(vel[0]*vel[0] + vel[1]*vel[1] + vel[2]*vel[2]).toFixed(1)
63
+ const msg = `${this.prefix} PHY ${String(body).padEnd(10)} pos=${this._formatVec3(pos)} vel=${speed}m/s hp=${health}`
64
+ console.log(`${this._timestamp()} ${msg}`)
65
+ }
66
+
67
+ error(category, message) {
68
+ const msg = `${this.prefix} ERROR [${category}] ${message}`
69
+ console.error(`${this._timestamp()} ${msg}`)
70
+ this.history.push(msg)
71
+ }
72
+
73
+ section(title) {
74
+ console.log(`\n${this.prefix} ${'='.repeat(50)}`)
75
+ console.log(`${this.prefix} ${title}`)
76
+ console.log(`${this.prefix} ${'='.repeat(50)}\n`)
77
+ }
78
+
79
+ summary(stats = {}) {
80
+ console.log(`\n${this.prefix} SESSION SUMMARY`)
81
+ for (const [key, value] of Object.entries(stats)) {
82
+ console.log(`${this.prefix} ${key}: ${value}`)
83
+ }
84
+ }
85
+
86
+ log(message) {
87
+ const msg = `${this.prefix} ${message}`
88
+ console.log(`${this._timestamp()} ${msg}`)
89
+ this.history.push(msg)
90
+ }
91
+ }
92
+
93
+ export const cliDebugger = new CliDebugger()