@lanmower/entrypoint 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +250 -0
- package/apps/environment/index.js +17 -0
- package/apps/interactive-door/index.js +33 -0
- package/apps/patrol-npc/index.js +37 -0
- package/apps/physics-crate/index.js +23 -0
- package/apps/power-crate/index.js +169 -0
- package/apps/tps-game/index.js +168 -0
- package/apps/tps-game/schwust.glb +0 -0
- package/apps/world/index.js +22 -0
- package/client/app.js +181 -0
- package/client/camera.js +116 -0
- package/client/index.html +24 -0
- package/client/style.css +11 -0
- package/package.json +52 -0
- package/server.js +3 -0
- package/src/apps/AppContext.js +172 -0
- package/src/apps/AppLoader.js +160 -0
- package/src/apps/AppRuntime.js +192 -0
- package/src/apps/EventBus.js +62 -0
- package/src/apps/HotReloadQueue.js +63 -0
- package/src/client/InputHandler.js +85 -0
- package/src/client/PhysicsNetworkClient.js +171 -0
- package/src/client/PredictionEngine.js +123 -0
- package/src/client/ReconciliationEngine.js +54 -0
- package/src/connection/ConnectionManager.js +133 -0
- package/src/connection/QualityMonitor.js +46 -0
- package/src/connection/SessionStore.js +67 -0
- package/src/debug/CliDebugger.js +93 -0
- package/src/debug/Inspector.js +52 -0
- package/src/debug/StateInspector.js +42 -0
- package/src/index.client.js +8 -0
- package/src/index.js +1 -0
- package/src/index.server.js +27 -0
- package/src/math.js +21 -0
- package/src/netcode/EventLog.js +74 -0
- package/src/netcode/LagCompensator.js +78 -0
- package/src/netcode/NetworkState.js +66 -0
- package/src/netcode/PhysicsIntegration.js +132 -0
- package/src/netcode/PlayerManager.js +109 -0
- package/src/netcode/SnapshotEncoder.js +49 -0
- package/src/netcode/TickSystem.js +66 -0
- package/src/physics/GLBLoader.js +29 -0
- package/src/physics/World.js +195 -0
- package/src/protocol/Codec.js +60 -0
- package/src/protocol/EventEmitter.js +60 -0
- package/src/protocol/MessageTypes.js +73 -0
- package/src/protocol/SequenceTracker.js +71 -0
- package/src/protocol/msgpack.js +119 -0
- package/src/sdk/ClientMessageHandler.js +80 -0
- package/src/sdk/ReloadHandlers.js +68 -0
- package/src/sdk/ReloadManager.js +126 -0
- package/src/sdk/ServerAPI.js +133 -0
- package/src/sdk/ServerHandlers.js +76 -0
- package/src/sdk/StaticHandler.js +32 -0
- package/src/sdk/TickHandler.js +84 -0
- package/src/sdk/client.js +122 -0
- package/src/sdk/server.js +184 -0
- package/src/shared/movement.js +69 -0
- package/src/spatial/Octree.js +91 -0
- package/src/stage/Stage.js +90 -0
- package/src/stage/StageLoader.js +95 -0
- package/src/storage/FSAdapter.js +56 -0
- package/src/storage/StorageAdapter.js +7 -0
- package/src/transport/TransportWrapper.js +25 -0
- package/src/transport/WebSocketTransport.js +55 -0
- package/src/transport/WebTransportServer.js +83 -0
- package/src/transport/WebTransportTransport.js +94 -0
- package/world/kaira.glb +0 -0
- package/world/schwust.glb +0 -0
|
@@ -0,0 +1,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
|
+
}
|