@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,52 @@
1
+ export class Inspector {
2
+ constructor() {
3
+ this.clients = new Map()
4
+ }
5
+
6
+ handleMessage(clientId, msg) {
7
+ if (!msg || msg.type < 100) return false
8
+ const msgType = msg.type
9
+ if (msgType >= 100 && msgType <= 199) {
10
+ this._handleDebugMessage(clientId, msg)
11
+ return true
12
+ }
13
+ return false
14
+ }
15
+
16
+ _handleDebugMessage(clientId, msg) {
17
+ const client = this.clients.get(clientId) || {
18
+ id: clientId,
19
+ debugMessages: []
20
+ }
21
+ client.lastDebugMessage = Date.now()
22
+ client.debugMessages = client.debugMessages || []
23
+ client.debugMessages.push({
24
+ type: msg.type,
25
+ timestamp: Date.now(),
26
+ payload: msg.payload
27
+ })
28
+ if (client.debugMessages.length > 1000) {
29
+ client.debugMessages.shift()
30
+ }
31
+ this.clients.set(clientId, client)
32
+ }
33
+
34
+ removeClient(clientId) {
35
+ this.clients.delete(clientId)
36
+ }
37
+
38
+ getAllClients(connections) {
39
+ const result = []
40
+ for (const [clientId, client] of this.clients.entries()) {
41
+ const conn = connections.getClient(clientId)
42
+ if (conn) {
43
+ result.push({
44
+ id: clientId,
45
+ debugMessages: client.debugMessages.length,
46
+ lastMessage: client.lastDebugMessage
47
+ })
48
+ }
49
+ }
50
+ return result
51
+ }
52
+ }
@@ -0,0 +1,42 @@
1
+ export class StateInspector {
2
+ constructor() {
3
+ this.snapshots = []
4
+ this.delays = []
5
+ this.corrections = []
6
+ this.maxSnapshots = 100
7
+ }
8
+
9
+ recordSnapshot(snap) {
10
+ this.snapshots.push({ ...snap, timestamp: Date.now() })
11
+ if (this.snapshots.length > this.maxSnapshots) {
12
+ this.snapshots.shift()
13
+ }
14
+ }
15
+
16
+ recordSnapshotDelay(delay) {
17
+ this.delays.push(delay)
18
+ if (this.delays.length > this.maxSnapshots) {
19
+ this.delays.shift()
20
+ }
21
+ }
22
+
23
+ recordCorrection(playerId, oldState, newState, deviationMs) {
24
+ this.corrections.push({ playerId, newState, deviationMs, timestamp: Date.now() })
25
+ if (this.corrections.length > this.maxSnapshots) {
26
+ this.corrections.shift()
27
+ }
28
+ }
29
+
30
+ getStats() {
31
+ const avgDelay = this.delays.length > 0
32
+ ? this.delays.reduce((a, b) => a + b, 0) / this.delays.length
33
+ : 0
34
+ return {
35
+ snapshots: this.snapshots.length,
36
+ delays: this.delays.length,
37
+ avgDelay,
38
+ corrections: this.corrections.length,
39
+ lastSnapshot: this.snapshots[this.snapshots.length - 1] || null
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,8 @@
1
+ export { InputHandler } from './client/InputHandler.js'
2
+ export { PhysicsNetworkClient } from './client/PhysicsNetworkClient.js'
3
+ export { PredictionEngine } from './client/PredictionEngine.js'
4
+ export { ReconciliationEngine } from './client/ReconciliationEngine.js'
5
+
6
+ export { MSG, msgName, DISCONNECT_REASONS, CONNECTION_QUALITY, UNRELIABLE_MSGS, isUnreliable } from './protocol/MessageTypes.js'
7
+ export { Codec } from './protocol/Codec.js'
8
+ export { SequenceTracker } from './protocol/SequenceTracker.js'
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './index.server.js'
@@ -0,0 +1,27 @@
1
+ export { PhysicsWorld } from './physics/World.js'
2
+
3
+ export { AppRuntime } from './apps/AppRuntime.js'
4
+ export { EventBus } from './apps/EventBus.js'
5
+ export { StageLoader } from './stage/StageLoader.js'
6
+ export { Stage } from './stage/Stage.js'
7
+ export { SpatialIndex } from './spatial/Octree.js'
8
+
9
+ export { TickSystem } from './netcode/TickSystem.js'
10
+ export { NetworkState } from './netcode/NetworkState.js'
11
+ export { SnapshotEncoder } from './netcode/SnapshotEncoder.js'
12
+ export { PlayerManager } from './netcode/PlayerManager.js'
13
+ export { LagCompensator } from './netcode/LagCompensator.js'
14
+ export { PhysicsIntegration } from './netcode/PhysicsIntegration.js'
15
+ export { EventLog } from './netcode/EventLog.js'
16
+
17
+ export { StorageAdapter } from './storage/StorageAdapter.js'
18
+ export { FSAdapter } from './storage/FSAdapter.js'
19
+
20
+ export { MSG, msgName, DISCONNECT_REASONS, CONNECTION_QUALITY, UNRELIABLE_MSGS, isUnreliable } from './protocol/MessageTypes.js'
21
+ export { Codec } from './protocol/Codec.js'
22
+ export { SequenceTracker } from './protocol/SequenceTracker.js'
23
+
24
+ export { TransportWrapper } from './transport/TransportWrapper.js'
25
+ export { WebSocketTransport } from './transport/WebSocketTransport.js'
26
+ export { WebTransportTransport } from './transport/WebTransportTransport.js'
27
+ export { WebTransportServer, WEBTRANSPORT_AVAILABLE } from './transport/WebTransportServer.js'
package/src/math.js ADDED
@@ -0,0 +1,21 @@
1
+ export function mulQuat(a, b) {
2
+ return [
3
+ a[3]*b[0] + a[0]*b[3] + a[1]*b[2] - a[2]*b[1],
4
+ a[3]*b[1] - a[0]*b[2] + a[1]*b[3] + a[2]*b[0],
5
+ a[3]*b[2] + a[0]*b[1] - a[1]*b[0] + a[2]*b[3],
6
+ a[3]*b[3] - a[0]*b[0] - a[1]*b[1] - a[2]*b[2]
7
+ ]
8
+ }
9
+
10
+ export function rotVec(v, q) {
11
+ const [qx, qy, qz, qw] = q
12
+ const ix = qw*v[0] + qy*v[2] - qz*v[1]
13
+ const iy = qw*v[1] + qz*v[0] - qx*v[2]
14
+ const iz = qw*v[2] + qx*v[1] - qy*v[0]
15
+ const iw = -qx*v[0] - qy*v[1] - qz*v[2]
16
+ return [
17
+ ix*qw + iw*-qx + iy*-qz - iz*-qy,
18
+ iy*qw + iw*-qy + iz*-qx - ix*-qz,
19
+ iz*qw + iw*-qz + ix*-qy - iy*-qx
20
+ ]
21
+ }
@@ -0,0 +1,74 @@
1
+ export class EventLog {
2
+ constructor(config = {}) {
3
+ this._log = []
4
+ this._maxSize = config.maxSize || 100000
5
+ this._nextId = 1
6
+ this._recording = true
7
+ }
8
+
9
+ record(type, data, meta = {}) {
10
+ if (!this._recording) return null
11
+ const event = {
12
+ id: this._nextId++,
13
+ tick: meta.tick || 0,
14
+ timestamp: Date.now(),
15
+ type,
16
+ data,
17
+ meta: { actor: meta.actor || null, reason: meta.reason || null, context: meta.context || null, sourceApp: meta.sourceApp || null, sourceEntity: meta.sourceEntity || null, causalEventId: meta.causalEventId || null, ...meta }
18
+ }
19
+ this._log.push(event)
20
+ if (this._log.length > this._maxSize) this._log.shift()
21
+ return event
22
+ }
23
+
24
+ query(filter = {}) {
25
+ return this._log.filter(e => {
26
+ if (filter.type && e.type !== filter.type) return false
27
+ if (filter.tick !== undefined && e.tick !== filter.tick) return false
28
+ if (filter.tickRange && (e.tick < filter.tickRange[0] || e.tick > filter.tickRange[1])) return false
29
+ if (filter.actor && e.meta.actor !== filter.actor) return false
30
+ if (filter.entity && e.meta.sourceEntity !== filter.entity) return false
31
+ if (filter.app && e.meta.sourceApp !== filter.app) return false
32
+ return true
33
+ })
34
+ }
35
+
36
+ getRange(startTick, endTick) {
37
+ return this._log.filter(e => e.tick >= startTick && e.tick <= endTick)
38
+ }
39
+
40
+ get size() { return this._log.length }
41
+ get lastTick() { return this._log.length > 0 ? this._log[this._log.length - 1].tick : 0 }
42
+
43
+ pause() { this._recording = false }
44
+ resume() { this._recording = true }
45
+ clear() { this._log = []; this._nextId = 1 }
46
+
47
+ serialize() { return JSON.stringify(this._log) }
48
+
49
+ static deserialize(json) {
50
+ const log = new EventLog()
51
+ log._log = JSON.parse(json)
52
+ log._nextId = log._log.length > 0 ? log._log[log._log.length - 1].id + 1 : 1
53
+ return log
54
+ }
55
+
56
+ replay(runtime, options = {}) {
57
+ const speed = options.speed || 1
58
+ const startTick = options.startTick || 0
59
+ const endTick = options.endTick || Infinity
60
+ const events = this._log.filter(e => e.tick >= startTick && e.tick <= endTick)
61
+ const result = { eventsReplayed: 0, errors: [] }
62
+ for (const event of events) {
63
+ try {
64
+ switch (event.type) {
65
+ case 'entity_spawn': runtime.spawnEntity(event.data.id, event.data.config); break
66
+ case 'entity_destroy': runtime.destroyEntity(event.data.id); break
67
+ case 'bus_event': runtime._eventBus?.emit(event.data.channel, event.data.data, event.meta); break
68
+ }
69
+ result.eventsReplayed++
70
+ } catch (e) { result.errors.push({ eventId: event.id, error: e.message }) }
71
+ }
72
+ return result
73
+ }
74
+ }
@@ -0,0 +1,78 @@
1
+ export class LagCompensator {
2
+ constructor(historyWindow = 500) {
3
+ this.historyWindow = historyWindow
4
+ this.playerHistory = new Map()
5
+ }
6
+
7
+ recordPlayerPosition(playerId, position, rotation, velocity, tick) {
8
+ if (!this.playerHistory.has(playerId)) {
9
+ this.playerHistory.set(playerId, [])
10
+ }
11
+
12
+ const history = this.playerHistory.get(playerId)
13
+ history.push({
14
+ tick,
15
+ timestamp: Date.now(),
16
+ position: [...position],
17
+ rotation: [...rotation],
18
+ velocity: [...velocity]
19
+ })
20
+
21
+ const cutoff = Date.now() - this.historyWindow
22
+ while (history.length > 0 && history[0].timestamp < cutoff) {
23
+ history.shift()
24
+ }
25
+ }
26
+
27
+ getPlayerStateAtTime(playerId, millisAgo) {
28
+ const history = this.playerHistory.get(playerId)
29
+ if (!history || history.length === 0) return null
30
+
31
+ const targetTime = Date.now() - millisAgo
32
+ let best = null
33
+
34
+ for (const state of history) {
35
+ if (state.timestamp <= targetTime) {
36
+ best = state
37
+ } else {
38
+ break
39
+ }
40
+ }
41
+
42
+ return best
43
+ }
44
+
45
+ validateShot(shooterId, targetId, latencyMs) {
46
+ const targetState = this.getPlayerStateAtTime(targetId, latencyMs)
47
+ if (!targetState) return { valid: false, reason: 'no_history' }
48
+
49
+ const speed = Math.sqrt(targetState.velocity[0]**2 + targetState.velocity[1]**2 + targetState.velocity[2]**2)
50
+
51
+ if (speed > 30) {
52
+ return { valid: true, reason: 'fast_moving_target', state: targetState }
53
+ }
54
+
55
+ return { valid: true, reason: 'valid_shot', state: targetState }
56
+ }
57
+
58
+ detectTeleport(playerId, newPosition, threshold = 50) {
59
+ const history = this.playerHistory.get(playerId)
60
+ if (!history || history.length < 2) return false
61
+
62
+ const lastPos = history[history.length - 1].position
63
+ const dist = Math.sqrt((newPosition[0] - lastPos[0])**2 + (newPosition[1] - lastPos[1])**2 + (newPosition[2] - lastPos[2])**2)
64
+
65
+ return dist > threshold
66
+ }
67
+
68
+ clearPlayerHistory(playerId) {
69
+ this.playerHistory.delete(playerId)
70
+ }
71
+
72
+ getStats() {
73
+ return {
74
+ trackedPlayers: this.playerHistory.size,
75
+ totalSamples: Array.from(this.playerHistory.values()).reduce((sum, h) => sum + h.length, 0)
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,66 @@
1
+ export class NetworkState {
2
+ constructor() {
3
+ this.players = new Map()
4
+ this.tick = 0
5
+ this.timestamp = 0
6
+ }
7
+
8
+ addPlayer(playerId, initialState = {}) {
9
+ this.players.set(playerId, {
10
+ id: playerId,
11
+ position: initialState.position || [0, 0, 0],
12
+ rotation: initialState.rotation || [0, 0, 0, 1],
13
+ velocity: initialState.velocity || [0, 0, 0],
14
+ angularVelocity: initialState.angularVelocity || [0, 0, 0],
15
+ onGround: initialState.onGround !== undefined ? initialState.onGround : true,
16
+ health: initialState.health || 100,
17
+ inputSequence: 0,
18
+ lastUpdate: Date.now()
19
+ })
20
+ }
21
+
22
+ removePlayer(playerId) {
23
+ this.players.delete(playerId)
24
+ }
25
+
26
+ getPlayer(playerId) {
27
+ return this.players.get(playerId)
28
+ }
29
+
30
+ updatePlayer(playerId, state) {
31
+ const player = this.players.get(playerId)
32
+ if (player) {
33
+ Object.assign(player, state)
34
+ player.lastUpdate = Date.now()
35
+ }
36
+ }
37
+
38
+ getAllPlayers() {
39
+ return Array.from(this.players.values())
40
+ }
41
+
42
+ getSnapshot() {
43
+ return {
44
+ tick: this.tick,
45
+ timestamp: this.timestamp,
46
+ players: this.getAllPlayers().map(p => ({
47
+ id: p.id,
48
+ position: p.position,
49
+ rotation: p.rotation,
50
+ velocity: p.velocity,
51
+ onGround: p.onGround,
52
+ health: p.health,
53
+ inputSequence: p.inputSequence
54
+ }))
55
+ }
56
+ }
57
+
58
+ setTick(tick, timestamp = Date.now()) {
59
+ this.tick = tick
60
+ this.timestamp = timestamp
61
+ }
62
+
63
+ clear() {
64
+ this.players.clear()
65
+ }
66
+ }
@@ -0,0 +1,132 @@
1
+ export class PhysicsIntegration {
2
+ constructor(config = {}) {
3
+ this.physicsWorld = config.physicsWorld || null
4
+ this.config = {
5
+ gravity: config.gravity || [0, -9.81, 0],
6
+ capsuleRadius: config.capsuleRadius || 0.4,
7
+ capsuleHalfHeight: config.capsuleHalfHeight || 0.9,
8
+ playerMass: config.playerMass || 160,
9
+ ...config
10
+ }
11
+ this.playerBodies = new Map()
12
+ }
13
+
14
+ setPhysicsWorld(world) {
15
+ this.physicsWorld = world
16
+ }
17
+
18
+ addPlayerCollider(playerId, radius = 0.4) {
19
+ if (!this.physicsWorld) {
20
+ this.playerBodies.set(playerId, { id: playerId, charId: null, onGround: true })
21
+ return
22
+ }
23
+ const charId = this.physicsWorld.addPlayerCharacter(
24
+ radius,
25
+ this.config.capsuleHalfHeight,
26
+ [0, 5, 0],
27
+ this.config.playerMass
28
+ )
29
+ this.playerBodies.set(playerId, { id: playerId, charId, onGround: true })
30
+ }
31
+
32
+ removePlayerCollider(playerId) {
33
+ const data = this.playerBodies.get(playerId)
34
+ if (data?.charId && this.physicsWorld) {
35
+ this.physicsWorld.removeCharacter(data.charId)
36
+ }
37
+ this.playerBodies.delete(playerId)
38
+ }
39
+
40
+ updatePlayerPhysics(playerId, state, deltaTime) {
41
+ const data = this.playerBodies.get(playerId)
42
+ if (!data || !data.charId || !this.physicsWorld) {
43
+ return this._fallbackPhysics(playerId, state, deltaTime)
44
+ }
45
+ const charId = data.charId
46
+ const currentVel = this.physicsWorld.getCharacterVelocity(charId)
47
+ const onGround = this.physicsWorld.getCharacterGroundState(charId)
48
+ let vy
49
+ if (onGround) {
50
+ vy = state.velocity[1] > 0 ? state.velocity[1] : 0
51
+ } else {
52
+ vy = currentVel[1] + this.config.gravity[1] * deltaTime
53
+ }
54
+ this.physicsWorld.setCharacterVelocity(charId, [state.velocity[0], vy, state.velocity[2]])
55
+ this.physicsWorld.updateCharacter(charId, deltaTime)
56
+ const pos = this.physicsWorld.getCharacterPosition(charId)
57
+ const vel = this.physicsWorld.getCharacterVelocity(charId)
58
+ data.onGround = this.physicsWorld.getCharacterGroundState(charId)
59
+ state.position = pos
60
+ state.velocity = vel
61
+ state.onGround = data.onGround
62
+ return state
63
+ }
64
+
65
+ _fallbackPhysics(playerId, state, deltaTime) {
66
+ state.velocity[1] += this.config.gravity[1] * deltaTime
67
+ state.position[0] += state.velocity[0] * deltaTime
68
+ state.position[1] += state.velocity[1] * deltaTime
69
+ state.position[2] += state.velocity[2] * deltaTime
70
+ if (state.position[1] <= 0) {
71
+ state.position[1] = 0
72
+ state.velocity[1] = 0
73
+ state.onGround = true
74
+ } else {
75
+ state.onGround = false
76
+ }
77
+ return state
78
+ }
79
+
80
+ setPlayerPosition(playerId, position) {
81
+ const data = this.playerBodies.get(playerId)
82
+ if (data?.charId && this.physicsWorld) {
83
+ this.physicsWorld.setCharacterPosition(data.charId, position)
84
+ }
85
+ }
86
+
87
+ getPlayerPosition(playerId) {
88
+ const data = this.playerBodies.get(playerId)
89
+ if (data?.charId && this.physicsWorld) {
90
+ return this.physicsWorld.getCharacterPosition(data.charId)
91
+ }
92
+ return [0, 0, 0]
93
+ }
94
+
95
+ checkCollisionWithOthers(playerId, allPlayers) {
96
+ const data = this.playerBodies.get(playerId)
97
+ if (!data) return []
98
+ const pos = data.charId && this.physicsWorld
99
+ ? this.physicsWorld.getCharacterPosition(data.charId)
100
+ : [0, 0, 0]
101
+ const collisions = []
102
+ for (const other of allPlayers) {
103
+ if (other.id === playerId) continue
104
+ const otherPos = other.state?.position || other.position
105
+ if (!otherPos) continue
106
+ const dx = otherPos[0] - pos[0]
107
+ const dy = otherPos[1] - pos[1]
108
+ const dz = otherPos[2] - pos[2]
109
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz)
110
+ const minDist = this.config.capsuleRadius * 2
111
+ if (distance < minDist && distance > 0) {
112
+ collisions.push({ playerId: other.id, distance, normal: [dx / distance, dy / distance, dz / distance] })
113
+ }
114
+ }
115
+ return collisions
116
+ }
117
+
118
+ raycast(origin, direction, maxDistance) {
119
+ if (!this.physicsWorld) return { hit: false, distance: maxDistance }
120
+ return this.physicsWorld.raycast(origin, direction, maxDistance)
121
+ }
122
+
123
+ validateMovement(playerId, newPosition, oldPosition) {
124
+ const distance = Math.hypot(
125
+ newPosition[0] - oldPosition[0],
126
+ newPosition[1] - oldPosition[1],
127
+ newPosition[2] - oldPosition[2]
128
+ )
129
+ if (distance > 2.0) return { valid: false, reason: 'move_too_far', distance }
130
+ return { valid: true }
131
+ }
132
+ }
@@ -0,0 +1,109 @@
1
+ export class PlayerManager {
2
+ constructor() {
3
+ this.players = new Map()
4
+ this.nextPlayerId = 1
5
+ this.inputBuffers = new Map()
6
+ }
7
+
8
+ addPlayer(socket, initialState = {}) {
9
+ const playerId = this.nextPlayerId++
10
+ const pos = initialState.position || [0, 0, 0]
11
+ const player = {
12
+ id: playerId,
13
+ socket,
14
+ state: {
15
+ position: [...pos],
16
+ rotation: initialState.rotation || [0, 0, 0, 1],
17
+ velocity: initialState.velocity || [0, 0, 0],
18
+ angularVelocity: initialState.angularVelocity || [0, 0, 0],
19
+ onGround: true,
20
+ health: 100
21
+ },
22
+ inputSequence: 0,
23
+ lastInputTime: 0,
24
+ connected: true,
25
+ joinTime: Date.now()
26
+ }
27
+ this.players.set(playerId, player)
28
+ this.inputBuffers.set(playerId, [])
29
+ return playerId
30
+ }
31
+
32
+ removePlayer(playerId) {
33
+ this.players.delete(playerId)
34
+ this.inputBuffers.delete(playerId)
35
+ }
36
+
37
+ getPlayer(playerId) {
38
+ return this.players.get(playerId)
39
+ }
40
+
41
+ getAllPlayers() {
42
+ return Array.from(this.players.values())
43
+ }
44
+
45
+ getConnectedPlayers() {
46
+ return this.getAllPlayers().filter(p => p.connected)
47
+ }
48
+
49
+ getPlayerCount() {
50
+ return this.players.size
51
+ }
52
+
53
+ updatePlayerState(playerId, state) {
54
+ const player = this.players.get(playerId)
55
+ if (player) Object.assign(player.state, state)
56
+ }
57
+
58
+ addInput(playerId, input) {
59
+ const player = this.players.get(playerId)
60
+ if (!player) return
61
+ player.inputSequence++
62
+ player.lastInputTime = Date.now()
63
+ const inputs = this.inputBuffers.get(playerId)
64
+ if (inputs) {
65
+ inputs.push({ sequence: player.inputSequence, data: input, timestamp: Date.now() })
66
+ if (inputs.length > 128) inputs.shift()
67
+ }
68
+ }
69
+
70
+ getInputs(playerId) {
71
+ return this.inputBuffers.get(playerId) || []
72
+ }
73
+
74
+ clearInputs(playerId) {
75
+ const inputs = this.inputBuffers.get(playerId)
76
+ if (inputs) inputs.length = 0
77
+ }
78
+
79
+ broadcast(message) {
80
+ const json = JSON.stringify(message)
81
+ for (const player of this.getConnectedPlayers()) {
82
+ if (player.socket && player.socket.send) {
83
+ try { player.socket.send(json) } catch (e) {}
84
+ }
85
+ }
86
+ }
87
+
88
+ broadcastBinary(buffer) {
89
+ for (const player of this.getConnectedPlayers()) {
90
+ if (player.socket && player.socket.send) {
91
+ try { player.socket.send(buffer) } catch (e) {}
92
+ }
93
+ }
94
+ }
95
+
96
+ sendToPlayer(playerId, message) {
97
+ const player = this.players.get(playerId)
98
+ if (player && player.socket && player.socket.send) {
99
+ try { player.socket.send(JSON.stringify(message)) } catch (e) {}
100
+ }
101
+ }
102
+
103
+ sendBinaryToPlayer(playerId, buffer) {
104
+ const player = this.players.get(playerId)
105
+ if (player && player.socket && player.socket.send) {
106
+ try { player.socket.send(buffer) } catch (e) {}
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,49 @@
1
+ function quantize(v, precision) {
2
+ return Math.round(v * precision) / precision
3
+ }
4
+
5
+ export class SnapshotEncoder {
6
+ static encode(snapshot) {
7
+ const players = (snapshot.players || []).map(p => [
8
+ p.id,
9
+ quantize(p.position[0], 100), quantize(p.position[1], 100), quantize(p.position[2], 100),
10
+ quantize(p.rotation[0], 10000), quantize(p.rotation[1], 10000), quantize(p.rotation[2], 10000), quantize(p.rotation[3], 10000),
11
+ quantize(p.velocity[0], 100), quantize(p.velocity[1], 100), quantize(p.velocity[2], 100),
12
+ p.onGround ? 1 : 0,
13
+ Math.round(p.health || 0),
14
+ p.inputSequence || 0
15
+ ])
16
+ const entities = (snapshot.entities || []).map(e => [
17
+ e.id,
18
+ e.model || '',
19
+ quantize(e.position[0], 100), quantize(e.position[1], 100), quantize(e.position[2], 100),
20
+ quantize(e.rotation[0], 10000), quantize(e.rotation[1], 10000), quantize(e.rotation[2], 10000), quantize(e.rotation[3], 10000),
21
+ e.bodyType || 'static',
22
+ e.custom || null
23
+ ])
24
+ return { tick: snapshot.tick || 0, timestamp: snapshot.timestamp || 0, players, entities }
25
+ }
26
+
27
+ static decode(data) {
28
+ if (data.players && Array.isArray(data.players)) {
29
+ const players = data.players.map(p => {
30
+ if (Array.isArray(p)) return {
31
+ id: p[0], position: [p[1], p[2], p[3]],
32
+ rotation: [p[4], p[5], p[6], p[7]],
33
+ velocity: [p[8], p[9], p[10]],
34
+ onGround: p[11] === 1, health: p[12], inputSequence: p[13]
35
+ }
36
+ return p
37
+ })
38
+ const entities = (data.entities || []).map(e => {
39
+ if (Array.isArray(e)) return {
40
+ id: e[0], model: e[1], position: [e[2], e[3], e[4]],
41
+ rotation: [e[5], e[6], e[7], e[8]], bodyType: e[9], custom: e[10]
42
+ }
43
+ return e
44
+ })
45
+ return { tick: data.tick, timestamp: data.timestamp, players, entities }
46
+ }
47
+ return data
48
+ }
49
+ }