@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,66 @@
1
+ export class TickSystem {
2
+ constructor(tickRate = 128) {
3
+ this.tickRate = tickRate
4
+ this.tickDuration = 1000 / tickRate
5
+ this.currentTick = 0
6
+ this.lastTickTime = 0
7
+ this.callbacks = []
8
+ this.running = false
9
+ this._reloadLocked = false
10
+ this._reloadResolve = null
11
+ this._tickInProgress = false
12
+ }
13
+
14
+ onTick(callback) {
15
+ this.callbacks.push(callback)
16
+ }
17
+
18
+ start() {
19
+ if (this.running) return
20
+ this.running = true
21
+ this.lastTickTime = Date.now()
22
+ this.loop()
23
+ }
24
+
25
+ loop() {
26
+ if (!this.running) return
27
+ const now = Date.now()
28
+ const elapsed = now - this.lastTickTime
29
+ if (elapsed >= this.tickDuration && !this._reloadLocked) {
30
+ this._tickInProgress = true
31
+ this.currentTick++
32
+ this.lastTickTime = now
33
+ for (const callback of this.callbacks) {
34
+ callback(this.currentTick, this.tickDuration / 1000)
35
+ }
36
+ this._tickInProgress = false
37
+ if (this._reloadResolve) {
38
+ this._reloadResolve()
39
+ this._reloadResolve = null
40
+ }
41
+ }
42
+ setImmediate(() => this.loop())
43
+ }
44
+
45
+ pauseForReload() {
46
+ this._reloadLocked = true
47
+ if (!this._tickInProgress) return Promise.resolve()
48
+ return new Promise(resolve => { this._reloadResolve = resolve })
49
+ }
50
+
51
+ resumeAfterReload() {
52
+ this._reloadLocked = false
53
+ }
54
+
55
+ stop() {
56
+ this.running = false
57
+ }
58
+
59
+ getTick() {
60
+ return this.currentTick
61
+ }
62
+
63
+ getTickDuration() {
64
+ return this.tickDuration / 1000
65
+ }
66
+ }
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ export function extractMeshFromGLB(filepath, meshIndex = 0) {
4
+ const buf = readFileSync(filepath)
5
+ if (buf.toString('ascii', 0, 4) !== 'glTF') throw new Error('Not a GLB file')
6
+ const jsonLen = buf.readUInt32LE(12)
7
+ const json = JSON.parse(buf.toString('utf-8', 20, 20 + jsonLen))
8
+ const binOffset = 20 + jsonLen + 8
9
+ const mesh = json.meshes[meshIndex]
10
+ if (!mesh) throw new Error(`Mesh index ${meshIndex} not found`)
11
+ const prim = mesh.primitives[0]
12
+ const posAcc = json.accessors[prim.attributes.POSITION]
13
+ const posView = json.bufferViews[posAcc.bufferView]
14
+ const posOff = binOffset + (posView.byteOffset || 0) + (posAcc.byteOffset || 0)
15
+ const vertices = new Float32Array(buf.buffer.slice(buf.byteOffset + posOff, buf.byteOffset + posOff + posAcc.count * 12))
16
+ let indices = null
17
+ if (prim.indices !== undefined) {
18
+ const idxAcc = json.accessors[prim.indices]
19
+ const idxView = json.bufferViews[idxAcc.bufferView]
20
+ const idxOff = binOffset + (idxView.byteOffset || 0) + (idxAcc.byteOffset || 0)
21
+ if (idxAcc.componentType === 5123) {
22
+ const raw = new Uint16Array(buf.buffer.slice(buf.byteOffset + idxOff, buf.byteOffset + idxOff + idxAcc.count * 2))
23
+ indices = new Uint32Array(raw)
24
+ } else {
25
+ indices = new Uint32Array(buf.buffer.slice(buf.byteOffset + idxOff, buf.byteOffset + idxOff + idxAcc.count * 4))
26
+ }
27
+ }
28
+ return { vertices, indices, vertexCount: posAcc.count, triangleCount: indices ? indices.length / 3 : 0, name: mesh.name }
29
+ }
@@ -0,0 +1,195 @@
1
+ import initJolt from 'jolt-physics/wasm-compat'
2
+ import { extractMeshFromGLB } from './GLBLoader.js'
3
+ const LAYER_STATIC = 0, LAYER_DYNAMIC = 1, NUM_LAYERS = 2
4
+ let joltInstance = null
5
+ async function getJolt() { if (!joltInstance) joltInstance = await initJolt(); return joltInstance }
6
+ export class PhysicsWorld {
7
+ constructor(config = {}) {
8
+ this.gravity = config.gravity || [0, -9.81, 0]
9
+ this.Jolt = null; this.jolt = null; this.physicsSystem = null; this.bodyInterface = null
10
+ this.bodies = new Map(); this.bodyMeta = new Map()
11
+ this._objFilter = null; this._ovbp = null
12
+ }
13
+ async init() {
14
+ const J = await getJolt()
15
+ this.Jolt = J
16
+ const settings = new J.JoltSettings()
17
+ const objFilter = new J.ObjectLayerPairFilterTable(NUM_LAYERS)
18
+ objFilter.EnableCollision(LAYER_STATIC, LAYER_DYNAMIC); objFilter.EnableCollision(LAYER_DYNAMIC, LAYER_DYNAMIC)
19
+ const bpI = new J.BroadPhaseLayerInterfaceTable(NUM_LAYERS, 2)
20
+ bpI.MapObjectToBroadPhaseLayer(LAYER_STATIC, new J.BroadPhaseLayer(0))
21
+ bpI.MapObjectToBroadPhaseLayer(LAYER_DYNAMIC, new J.BroadPhaseLayer(1))
22
+ const ovbp = new J.ObjectVsBroadPhaseLayerFilterTable(bpI, 2, objFilter, NUM_LAYERS)
23
+ settings.mObjectLayerPairFilter = objFilter; settings.mBroadPhaseLayerInterface = bpI
24
+ settings.mObjectVsBroadPhaseLayerFilter = ovbp
25
+ this._objFilter = objFilter; this._ovbp = ovbp
26
+ this.jolt = new J.JoltInterface(settings); J.destroy(settings)
27
+ this.physicsSystem = this.jolt.GetPhysicsSystem(); this.bodyInterface = this.physicsSystem.GetBodyInterface()
28
+ const [gx, gy, gz] = this.gravity
29
+ this.physicsSystem.SetGravity(new J.Vec3(gx, gy, gz))
30
+ return this
31
+ }
32
+ _addBody(shape, position, motionType, layer, opts = {}) {
33
+ const J = this.Jolt
34
+ const pos = new J.RVec3(position[0], position[1], position[2])
35
+ const rot = opts.rotation ? new J.Quat(...opts.rotation) : new J.Quat(0, 0, 0, 1)
36
+ const cs = new J.BodyCreationSettings(shape, pos, rot, motionType, layer)
37
+ if (opts.mass) { cs.mMassPropertiesOverride.mMass = opts.mass; cs.mOverrideMassProperties = J.EOverrideMassProperties_CalculateInertia }
38
+ if (opts.friction !== undefined) cs.mFriction = opts.friction
39
+ if (opts.restitution !== undefined) cs.mRestitution = opts.restitution
40
+ const activate = motionType === J.EMotionType_Static ? J.EActivation_DontActivate : J.EActivation_Activate
41
+ const body = this.bodyInterface.CreateBody(cs); this.bodyInterface.AddBody(body.GetID(), activate)
42
+ J.destroy(cs)
43
+ const id = body.GetID().GetIndexAndSequenceNumber()
44
+ this.bodies.set(id, body); this.bodyMeta.set(id, opts.meta || {})
45
+ return id
46
+ }
47
+ addStaticBox(halfExtents, position, rotation) {
48
+ const J = this.Jolt
49
+ const shape = new J.BoxShape(new J.Vec3(halfExtents[0], halfExtents[1], halfExtents[2]), 0.05, null)
50
+ return this._addBody(shape, position, J.EMotionType_Static, LAYER_STATIC, { rotation, meta: { type: 'static', shape: 'box' } })
51
+ }
52
+ addBody(shapeType, params, position, motionType, opts = {}) {
53
+ const J = this.Jolt
54
+ let shape, layer
55
+ if (shapeType === 'box') shape = new J.BoxShape(new J.Vec3(params[0], params[1], params[2]), 0.05, null)
56
+ else if (shapeType === 'sphere') shape = new J.SphereShape(params)
57
+ else if (shapeType === 'capsule') shape = new J.CapsuleShape(params[1], params[0])
58
+ else return null
59
+ const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
60
+ layer = motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC
61
+ return this._addBody(shape, position, mt, layer, { ...opts, meta: { type: motionType, shape: shapeType } })
62
+ }
63
+ addStaticTrimesh(glbPath, meshIndex = 0) {
64
+ const J = this.Jolt
65
+ const mesh = extractMeshFromGLB(glbPath, meshIndex)
66
+ const triangles = new J.TriangleList(); triangles.resize(mesh.triangleCount)
67
+ for (let t = 0; t < mesh.triangleCount; t++) {
68
+ const tri = triangles.at(t)
69
+ for (let v = 0; v < 3; v++) {
70
+ const idx = mesh.indices[t * 3 + v]
71
+ tri.set_mV(v, new J.Float3(mesh.vertices[idx * 3], mesh.vertices[idx * 3 + 1], mesh.vertices[idx * 3 + 2]))
72
+ }
73
+ }
74
+ const shape = new J.MeshShapeSettings(triangles).Create().Get()
75
+ return this._addBody(shape, [0, 0, 0], J.EMotionType_Static, LAYER_STATIC, { meta: { type: 'static', shape: 'trimesh', mesh: mesh.name, triangles: mesh.triangleCount } })
76
+ }
77
+ addPlayerCharacter(radius, halfHeight, position, mass) {
78
+ const J = this.Jolt
79
+ const cvs = new J.CharacterVirtualSettings()
80
+ cvs.mMass = mass || 80
81
+ cvs.mMaxSlopeAngle = 0.7854
82
+ cvs.mShape = new J.CapsuleShape(halfHeight, radius)
83
+ cvs.mBackFaceMode = J.EBackFaceMode_CollideWithBackFaces
84
+ cvs.mCharacterPadding = 0.02
85
+ cvs.mPenetrationRecoverySpeed = 1.0
86
+ cvs.mPredictiveContactDistance = 0.1
87
+ cvs.mSupportingVolume = new J.Plane(J.Vec3.prototype.sAxisY(), -radius)
88
+ const pos = new J.RVec3(position[0], position[1], position[2])
89
+ const ch = new J.CharacterVirtual(cvs, pos, J.Quat.prototype.sIdentity(), this.physicsSystem)
90
+ J.destroy(cvs)
91
+ if (!this._charFilters) {
92
+ this._charFilters = {
93
+ bp: new J.DefaultBroadPhaseLayerFilter(this.jolt.GetObjectVsBroadPhaseLayerFilter(), LAYER_DYNAMIC),
94
+ ol: new J.DefaultObjectLayerFilter(this.jolt.GetObjectLayerPairFilter(), LAYER_DYNAMIC),
95
+ body: new J.BodyFilter(),
96
+ shape: new J.ShapeFilter()
97
+ }
98
+ this._charUpdateSettings = new J.ExtendedUpdateSettings()
99
+ this._charUpdateSettings.mStickToFloorStepDown = new J.Vec3(0, -0.5, 0)
100
+ this._charUpdateSettings.mWalkStairsStepUp = new J.Vec3(0, 0.4, 0)
101
+ this._charGravity = new J.Vec3(this.gravity[0], this.gravity[1], this.gravity[2])
102
+ }
103
+ const id = this._nextCharId = (this._nextCharId || 0) + 1
104
+ if (!this.characters) this.characters = new Map()
105
+ this.characters.set(id, ch)
106
+ return id
107
+ }
108
+ updateCharacter(charId, dt) {
109
+ const ch = this.characters?.get(charId)
110
+ if (!ch) return
111
+ const f = this._charFilters
112
+ ch.ExtendedUpdate(dt, this._charGravity, this._charUpdateSettings, f.bp, f.ol, f.body, f.shape, this.jolt.GetTempAllocator())
113
+ }
114
+ getCharacterPosition(charId) {
115
+ const ch = this.characters?.get(charId); if (!ch) return [0, 0, 0]
116
+ const p = ch.GetPosition(); return [p.GetX(), p.GetY(), p.GetZ()]
117
+ }
118
+ getCharacterVelocity(charId) {
119
+ const ch = this.characters?.get(charId); if (!ch) return [0, 0, 0]
120
+ const v = ch.GetLinearVelocity(); return [v.GetX(), v.GetY(), v.GetZ()]
121
+ }
122
+ setCharacterVelocity(charId, velocity) {
123
+ const ch = this.characters?.get(charId); if (!ch) return
124
+ ch.SetLinearVelocity(new this.Jolt.Vec3(velocity[0], velocity[1], velocity[2]))
125
+ }
126
+ setCharacterPosition(charId, position) {
127
+ const ch = this.characters?.get(charId); if (!ch) return
128
+ ch.SetPosition(new this.Jolt.RVec3(position[0], position[1], position[2]))
129
+ }
130
+ getCharacterGroundState(charId) {
131
+ const ch = this.characters?.get(charId); if (!ch) return false
132
+ return ch.GetGroundState() === this.Jolt.EGroundState_OnGround
133
+ }
134
+ removeCharacter(charId) {
135
+ if (!this.characters) return
136
+ this.characters.delete(charId)
137
+ }
138
+ _getBody(bodyId) { return this.bodies.get(bodyId) }
139
+ getBodyPosition(bodyId) {
140
+ const b = this._getBody(bodyId); if (!b) return [0, 0, 0]
141
+ const p = this.bodyInterface.GetPosition(b.GetID()); return [p.GetX(), p.GetY(), p.GetZ()]
142
+ }
143
+ getBodyRotation(bodyId) {
144
+ const b = this._getBody(bodyId); if (!b) return [0, 0, 0, 1]
145
+ const r = this.bodyInterface.GetRotation(b.GetID()); return [r.GetX(), r.GetY(), r.GetZ(), r.GetW()]
146
+ }
147
+ getBodyVelocity(bodyId) {
148
+ const b = this._getBody(bodyId); if (!b) return [0, 0, 0]
149
+ const v = this.bodyInterface.GetLinearVelocity(b.GetID()); return [v.GetX(), v.GetY(), v.GetZ()]
150
+ }
151
+ setBodyPosition(bodyId, position) {
152
+ const b = this._getBody(bodyId); if (!b) return
153
+ this.bodyInterface.SetPosition(b.GetID(), new this.Jolt.RVec3(position[0], position[1], position[2]), this.Jolt.EActivation_Activate)
154
+ }
155
+ setBodyVelocity(bodyId, velocity) {
156
+ const b = this._getBody(bodyId); if (!b) return
157
+ this.bodyInterface.SetLinearVelocity(b.GetID(), new this.Jolt.Vec3(velocity[0], velocity[1], velocity[2]))
158
+ }
159
+ addForce(bodyId, force) {
160
+ const b = this._getBody(bodyId); if (!b) return
161
+ this.bodyInterface.AddForce(b.GetID(), new this.Jolt.Vec3(force[0], force[1], force[2]))
162
+ }
163
+ addImpulse(bodyId, impulse) {
164
+ const b = this._getBody(bodyId); if (!b) return
165
+ this.bodyInterface.AddImpulse(b.GetID(), new this.Jolt.Vec3(impulse[0], impulse[1], impulse[2]))
166
+ }
167
+ step(deltaTime) { if (!this.jolt) return; this.jolt.Step(deltaTime, deltaTime > 1 / 55 ? 2 : 1) }
168
+ removeBody(bodyId) {
169
+ const b = this._getBody(bodyId); if (!b) return
170
+ this.bodyInterface.RemoveBody(b.GetID()); this.bodyInterface.DestroyBody(b.GetID())
171
+ this.bodies.delete(bodyId); this.bodyMeta.delete(bodyId)
172
+ }
173
+ raycast(origin, direction, maxDistance = 1000, excludeBodyId = null) {
174
+ if (!this.physicsSystem) return { hit: false, distance: maxDistance, body: null, position: null }
175
+ const J = this.Jolt
176
+ const len = Math.hypot(direction[0], direction[1], direction[2])
177
+ const dir = len > 0 ? [direction[0] / len, direction[1] / len, direction[2] / len] : direction
178
+ const ray = new J.RRayCast(new J.RVec3(origin[0], origin[1], origin[2]), new J.Vec3(dir[0] * maxDistance, dir[1] * maxDistance, dir[2] * maxDistance))
179
+ const rs = new J.RayCastSettings(), col = new J.CastRayClosestHitCollisionCollector()
180
+ const bp = new J.DefaultBroadPhaseLayerFilter(this._ovbp, LAYER_DYNAMIC)
181
+ const ol = new J.DefaultObjectLayerFilter(this._objFilter, LAYER_DYNAMIC)
182
+ const eb = excludeBodyId != null ? this._getBody(excludeBodyId) : null
183
+ const bf = eb ? new J.IgnoreSingleBodyFilter(eb.GetID()) : new J.BodyFilter()
184
+ const sf = new J.ShapeFilter()
185
+ this.physicsSystem.GetNarrowPhaseQuery().CastRay(ray, rs, col, bp, ol, bf, sf)
186
+ let result
187
+ if (col.HadHit()) {
188
+ const dist = col.get_mHit().mFraction * maxDistance
189
+ result = { hit: true, distance: dist, body: null, position: [origin[0] + dir[0] * dist, origin[1] + dir[1] * dist, origin[2] + dir[2] * dist] }
190
+ } else { result = { hit: false, distance: maxDistance, body: null, position: null } }
191
+ J.destroy(ray); J.destroy(rs); J.destroy(col); J.destroy(bp); J.destroy(ol); J.destroy(bf); J.destroy(sf)
192
+ return result
193
+ }
194
+ destroy() { for (const [id] of this.bodies) this.removeBody(id); if (this.jolt) { this.Jolt.destroy(this.jolt); this.jolt = null } }
195
+ }
@@ -0,0 +1,60 @@
1
+ import { pack, unpack } from './msgpack.js'
2
+
3
+ function toUint8(input) {
4
+ if (input instanceof Uint8Array) return input
5
+ if (input instanceof ArrayBuffer) return new Uint8Array(input)
6
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
7
+ return new Uint8Array(input)
8
+ }
9
+
10
+ export class Codec {
11
+ constructor() {
12
+ this.bytesSent = 0
13
+ this.bytesReceived = 0
14
+ this.messagesSent = 0
15
+ this.messagesReceived = 0
16
+ this.sendSequence = 0
17
+ }
18
+
19
+ encode(type, payload) {
20
+ const seq = this.sendSequence++ & 0xFFFF
21
+ const body = pack(payload)
22
+ const bodyBytes = toUint8(body)
23
+ const frame = new Uint8Array(3 + bodyBytes.length)
24
+ frame[0] = type
25
+ frame[1] = (seq >> 8) & 0xFF
26
+ frame[2] = seq & 0xFF
27
+ frame.set(bodyBytes, 3)
28
+ this.bytesSent += frame.length
29
+ this.messagesSent++
30
+ return frame
31
+ }
32
+
33
+ decode(buffer) {
34
+ const buf = toUint8(buffer)
35
+ if (buf.length < 3) return null
36
+ const type = buf[0]
37
+ const seq = (buf[1] << 8) | buf[2]
38
+ const payload = buf.length > 3 ? unpack(buf.slice(3)) : null
39
+ this.bytesReceived += buf.length
40
+ this.messagesReceived++
41
+ return { type, seq, payload }
42
+ }
43
+
44
+ getStats() {
45
+ return {
46
+ bytesSent: this.bytesSent,
47
+ bytesReceived: this.bytesReceived,
48
+ messagesSent: this.messagesSent,
49
+ messagesReceived: this.messagesReceived,
50
+ sendSequence: this.sendSequence
51
+ }
52
+ }
53
+
54
+ resetStats() {
55
+ this.bytesSent = 0
56
+ this.bytesReceived = 0
57
+ this.messagesSent = 0
58
+ this.messagesReceived = 0
59
+ }
60
+ }
@@ -0,0 +1,60 @@
1
+ export class EventEmitter {
2
+ constructor() {
3
+ this.listeners = new Map()
4
+ }
5
+
6
+ on(event, fn) {
7
+ if (!this.listeners.has(event)) {
8
+ this.listeners.set(event, [])
9
+ }
10
+ this.listeners.get(event).push(fn)
11
+ return this
12
+ }
13
+
14
+ off(event, fn) {
15
+ if (!this.listeners.has(event)) return this
16
+ const arr = this.listeners.get(event)
17
+ const idx = arr.indexOf(fn)
18
+ if (idx >= 0) arr.splice(idx, 1)
19
+ return this
20
+ }
21
+
22
+ once(event, fn) {
23
+ const wrapper = (...args) => {
24
+ fn(...args)
25
+ this.off(event, wrapper)
26
+ }
27
+ this.on(event, wrapper)
28
+ return this
29
+ }
30
+
31
+ emit(event, ...args) {
32
+ if (!this.listeners.has(event)) return false
33
+ this.listeners.get(event).forEach(fn => {
34
+ try {
35
+ fn(...args)
36
+ } catch (err) {
37
+ console.error(`Error in listener for event '${event}':`, err)
38
+ }
39
+ })
40
+ return true
41
+ }
42
+
43
+ removeListener(event, fn) {
44
+ return this.off(event, fn)
45
+ }
46
+
47
+ removeAllListeners(event) {
48
+ if (event) {
49
+ this.listeners.delete(event)
50
+ } else {
51
+ this.listeners.clear()
52
+ }
53
+ return this
54
+ }
55
+
56
+ listenerCount(event) {
57
+ if (!this.listeners.has(event)) return 0
58
+ return this.listeners.get(event).length
59
+ }
60
+ }
@@ -0,0 +1,73 @@
1
+ export const MSG = {
2
+ HANDSHAKE: 0x01,
3
+ HANDSHAKE_ACK: 0x02,
4
+ HEARTBEAT: 0x03,
5
+ HEARTBEAT_ACK: 0x04,
6
+
7
+ SNAPSHOT: 0x10,
8
+ INPUT: 0x11,
9
+ STATE_CORRECTION: 0x12,
10
+ DELTA_UPDATE: 0x13,
11
+
12
+ PLAYER_JOIN: 0x20,
13
+ PLAYER_LEAVE: 0x21,
14
+ PLAYER_INPUT: 0x22,
15
+
16
+ ENTITY_SPAWN: 0x30,
17
+ ENTITY_DESTROY: 0x31,
18
+ ENTITY_UPDATE: 0x32,
19
+ APP_EVENT: 0x33,
20
+
21
+ CLIENT_LOG: 0x40,
22
+ CLIENT_ERROR: 0x41,
23
+ CLIENT_WARN: 0x42,
24
+ CLIENT_PERF: 0x43,
25
+ CLIENT_STATE: 0x44,
26
+
27
+ SERVER_LOG: 0x50,
28
+ DEBUG_SNAPSHOT: 0x51,
29
+ INSPECT_ENTITY: 0x52,
30
+ INSPECT_RESPONSE: 0x53,
31
+
32
+ RECONNECT: 0x60,
33
+ RECONNECT_ACK: 0x61,
34
+ STATE_RECOVERY: 0x62,
35
+ DISCONNECT_REASON: 0x63,
36
+
37
+ HOT_RELOAD: 0x70,
38
+ WORLD_DEF: 0x71,
39
+ APP_MODULE: 0x72,
40
+ ASSET_UPDATE: 0x73,
41
+ BUS_EVENT: 0x74
42
+ }
43
+
44
+ const nameMap = new Map()
45
+ for (const [name, id] of Object.entries(MSG)) nameMap.set(id, name)
46
+
47
+ export function msgName(id) {
48
+ return nameMap.get(id) || `UNKNOWN(0x${id.toString(16)})`
49
+ }
50
+
51
+ export const UNRELIABLE_MSGS = new Set([
52
+ 0x03, 0x04, 0x10, 0x11, 0x12, 0x13, 0x22, 0x43, 0x44
53
+ ])
54
+
55
+ export function isUnreliable(type) {
56
+ return UNRELIABLE_MSGS.has(type)
57
+ }
58
+
59
+ export const DISCONNECT_REASONS = {
60
+ NORMAL: 0,
61
+ TIMEOUT: 1,
62
+ KICKED: 2,
63
+ SERVER_SHUTDOWN: 3,
64
+ INVALID_SESSION: 4,
65
+ RATE_LIMITED: 5
66
+ }
67
+
68
+ export const CONNECTION_QUALITY = {
69
+ EXCELLENT: 'excellent',
70
+ GOOD: 'good',
71
+ POOR: 'poor',
72
+ CRITICAL: 'critical'
73
+ }
@@ -0,0 +1,71 @@
1
+ export class SequenceTracker {
2
+ constructor(windowSize = 256) {
3
+ this.windowSize = windowSize
4
+ this.nextExpected = 0
5
+ this.received = new Set()
6
+ this.outOfOrder = []
7
+ this.totalReceived = 0
8
+ this.totalExpected = 0
9
+ this.gaps = 0
10
+ }
11
+
12
+ track(seq) {
13
+ this.totalReceived++
14
+ if (seq === this.nextExpected) {
15
+ this.nextExpected = (this.nextExpected + 1) & 0xFFFF
16
+ this._drainOutOfOrder()
17
+ return { inOrder: true, gap: 0 }
18
+ }
19
+ const gap = (seq - this.nextExpected + 0x10000) & 0xFFFF
20
+ if (gap > this.windowSize) {
21
+ this.nextExpected = (seq + 1) & 0xFFFF
22
+ this.outOfOrder = []
23
+ this.received.clear()
24
+ return { inOrder: true, gap: 0, reset: true }
25
+ }
26
+ this.gaps++
27
+ this.outOfOrder.push(seq)
28
+ this.outOfOrder.sort((a, b) => {
29
+ const da = (a - this.nextExpected + 0x10000) & 0xFFFF
30
+ const db = (b - this.nextExpected + 0x10000) & 0xFFFF
31
+ return da - db
32
+ })
33
+ this.received.add(seq)
34
+ return { inOrder: false, gap }
35
+ }
36
+
37
+ _drainOutOfOrder() {
38
+ while (this.outOfOrder.length > 0 && this.outOfOrder[0] === this.nextExpected) {
39
+ this.outOfOrder.shift()
40
+ this.received.delete(this.nextExpected)
41
+ this.nextExpected = (this.nextExpected + 1) & 0xFFFF
42
+ }
43
+ }
44
+
45
+ getPacketLoss() {
46
+ if (this.totalReceived === 0) return 0
47
+ const expected = this.nextExpected
48
+ if (expected === 0) return 0
49
+ const lost = expected - this.totalReceived
50
+ return Math.max(0, lost / expected)
51
+ }
52
+
53
+ getStats() {
54
+ return {
55
+ nextExpected: this.nextExpected,
56
+ totalReceived: this.totalReceived,
57
+ outOfOrderCount: this.outOfOrder.length,
58
+ gapEvents: this.gaps,
59
+ packetLoss: this.getPacketLoss()
60
+ }
61
+ }
62
+
63
+ reset() {
64
+ this.nextExpected = 0
65
+ this.received.clear()
66
+ this.outOfOrder = []
67
+ this.totalReceived = 0
68
+ this.totalExpected = 0
69
+ this.gaps = 0
70
+ }
71
+ }
@@ -0,0 +1,119 @@
1
+ const encoder = new TextEncoder()
2
+ const decoder = new TextDecoder()
3
+
4
+ export function pack(value) {
5
+ const chunks = []
6
+ function write(value) {
7
+ if (value === null || value === undefined) {
8
+ chunks.push(0xc0)
9
+ } else if (value === false) {
10
+ chunks.push(0xc2)
11
+ } else if (value === true) {
12
+ chunks.push(0xc3)
13
+ } else if (typeof value === 'number') {
14
+ if (Number.isInteger(value)) {
15
+ if (value >= 0) {
16
+ if (value < 128) chunks.push(value)
17
+ else if (value < 256) chunks.push(0xcc, value)
18
+ else if (value < 65536) chunks.push(0xcd, value >> 8, value & 0xff)
19
+ else if (value < 4294967296) chunks.push(0xce, (value >> 24) & 0xff, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff)
20
+ else writeFloat64(value)
21
+ } else {
22
+ if (value >= -32) chunks.push(value & 0xff)
23
+ else if (value >= -128) chunks.push(0xd0, value & 0xff)
24
+ else if (value >= -32768) chunks.push(0xd1, (value >> 8) & 0xff, value & 0xff)
25
+ else if (value >= -2147483648) chunks.push(0xd2, (value >> 24) & 0xff, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff)
26
+ else writeFloat64(value)
27
+ }
28
+ } else {
29
+ writeFloat64(value)
30
+ }
31
+ } else if (typeof value === 'string') {
32
+ const bytes = encoder.encode(value)
33
+ const len = bytes.length
34
+ if (len < 32) chunks.push(0xa0 | len)
35
+ else if (len < 256) chunks.push(0xd9, len)
36
+ else if (len < 65536) chunks.push(0xda, len >> 8, len & 0xff)
37
+ else chunks.push(0xdb, (len >> 24) & 0xff, (len >> 16) & 0xff, (len >> 8) & 0xff, len & 0xff)
38
+ for (let i = 0; i < len; i++) chunks.push(bytes[i])
39
+ } else if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
40
+ const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : value
41
+ const len = bytes.length
42
+ if (len < 256) chunks.push(0xc4, len)
43
+ else if (len < 65536) chunks.push(0xc5, len >> 8, len & 0xff)
44
+ else chunks.push(0xc6, (len >> 24) & 0xff, (len >> 16) & 0xff, (len >> 8) & 0xff, len & 0xff)
45
+ for (let i = 0; i < len; i++) chunks.push(bytes[i])
46
+ } else if (Array.isArray(value)) {
47
+ const len = value.length
48
+ if (len < 16) chunks.push(0x90 | len)
49
+ else if (len < 65536) chunks.push(0xdc, len >> 8, len & 0xff)
50
+ else chunks.push(0xdd, (len >> 24) & 0xff, (len >> 16) & 0xff, (len >> 8) & 0xff, len & 0xff)
51
+ for (let i = 0; i < len; i++) write(value[i])
52
+ } else if (typeof value === 'object') {
53
+ const keys = Object.keys(value)
54
+ const len = keys.length
55
+ if (len < 16) chunks.push(0x80 | len)
56
+ else if (len < 65536) chunks.push(0xde, len >> 8, len & 0xff)
57
+ else chunks.push(0xdf, (len >> 24) & 0xff, (len >> 16) & 0xff, (len >> 8) & 0xff, len & 0xff)
58
+ for (const key of keys) { write(key); write(value[key]) }
59
+ }
60
+ }
61
+ function writeFloat64(value) {
62
+ chunks.push(0xcb)
63
+ const buf = new ArrayBuffer(8)
64
+ new DataView(buf).setFloat64(0, value, false)
65
+ const bytes = new Uint8Array(buf)
66
+ for (let i = 0; i < 8; i++) chunks.push(bytes[i])
67
+ }
68
+ write(value)
69
+ return new Uint8Array(chunks)
70
+ }
71
+
72
+ export function unpack(buffer) {
73
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
74
+ let offset = 0
75
+ function read() {
76
+ const byte = bytes[offset++]
77
+ if (byte < 0x80) return byte
78
+ if ((byte & 0xf0) === 0x80) return readMap(byte & 0x0f)
79
+ if ((byte & 0xf0) === 0x90) return readArray(byte & 0x0f)
80
+ if ((byte & 0xe0) === 0xa0) return readString(byte & 0x1f)
81
+ if (byte >= 0xe0) return byte - 256
82
+ switch (byte) {
83
+ case 0xc0: return null
84
+ case 0xc2: return false
85
+ case 0xc3: return true
86
+ case 0xc4: return readBin(bytes[offset++])
87
+ case 0xc5: return readBin(readUint16())
88
+ case 0xc6: return readBin(readUint32())
89
+ case 0xca: return readFloat32()
90
+ case 0xcb: return readFloat64()
91
+ case 0xcc: return bytes[offset++]
92
+ case 0xcd: return readUint16()
93
+ case 0xce: return readUint32()
94
+ case 0xd0: return readInt8()
95
+ case 0xd1: return readInt16()
96
+ case 0xd2: return readInt32()
97
+ case 0xd9: return readString(bytes[offset++])
98
+ case 0xda: return readString(readUint16())
99
+ case 0xdb: return readString(readUint32())
100
+ case 0xdc: return readArray(readUint16())
101
+ case 0xdd: return readArray(readUint32())
102
+ case 0xde: return readMap(readUint16())
103
+ case 0xdf: return readMap(readUint32())
104
+ default: throw new Error(`Unknown msgpack type: 0x${byte.toString(16)}`)
105
+ }
106
+ }
107
+ function readUint16() { const v = (bytes[offset] << 8) | bytes[offset + 1]; offset += 2; return v }
108
+ function readUint32() { const v = (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; offset += 4; return v >>> 0 }
109
+ function readInt8() { const v = bytes[offset++]; return v > 127 ? v - 256 : v }
110
+ function readInt16() { const v = (bytes[offset] << 8) | bytes[offset + 1]; offset += 2; return v > 32767 ? v - 65536 : v }
111
+ function readInt32() { const v = (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; offset += 4; return v }
112
+ function readFloat32() { const buf = new ArrayBuffer(4); const view = new Uint8Array(buf); for (let i = 0; i < 4; i++) view[i] = bytes[offset++]; return new DataView(buf).getFloat32(0, false) }
113
+ function readFloat64() { const buf = new ArrayBuffer(8); const view = new Uint8Array(buf); for (let i = 0; i < 8; i++) view[i] = bytes[offset++]; return new DataView(buf).getFloat64(0, false) }
114
+ function readString(len) { const slice = bytes.subarray(offset, offset + len); offset += len; return decoder.decode(slice) }
115
+ function readBin(len) { const slice = bytes.slice(offset, offset + len); offset += len; return slice }
116
+ function readArray(len) { const arr = new Array(len); for (let i = 0; i < len; i++) arr[i] = read(); return arr }
117
+ function readMap(len) { const obj = {}; for (let i = 0; i < len; i++) { const key = read(); obj[key] = read() } return obj }
118
+ return read()
119
+ }