spoint 0.1.80 → 0.1.82
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/package.json +1 -1
- package/src/apps/AppRuntime.js +1 -1
- package/src/connection/ConnectionManager.js +12 -0
- package/src/netcode/SnapshotEncoder.js +14 -17
- package/src/physics/GLBLoader.js +158 -0
- package/src/physics/World.js +106 -114
- package/src/sdk/BotHarness.js +87 -0
- package/src/sdk/TickHandler.js +73 -59
package/package.json
CHANGED
package/src/apps/AppRuntime.js
CHANGED
|
@@ -64,7 +64,7 @@ export class AppRuntime {
|
|
|
64
64
|
}
|
|
65
65
|
if (config.autoTrimesh && entity.model && this._physics) {
|
|
66
66
|
entity.collider = { type: 'trimesh', model: entity.model }
|
|
67
|
-
this._physics.addStaticTrimeshAsync(this.resolveAssetPath(entity.model), 0)
|
|
67
|
+
this._physics.addStaticTrimeshAsync(this.resolveAssetPath(entity.model), 0, entity.position || [0,0,0])
|
|
68
68
|
.then(id => { entity._physicsBodyId = id })
|
|
69
69
|
.catch(e => console.error(`[AppRuntime] Failed to create trimesh for ${entity.model}:`, e.message))
|
|
70
70
|
}
|
|
@@ -127,6 +127,18 @@ export class ConnectionManager extends EventEmitter {
|
|
|
127
127
|
return count
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
sendPacked(clientId, data, unreliable) {
|
|
131
|
+
const client = this.clients.get(clientId)
|
|
132
|
+
if (!client || !client.transport.isOpen) return false
|
|
133
|
+
try {
|
|
134
|
+
if (unreliable) return client.transport.sendUnreliable(data)
|
|
135
|
+
return client.transport.send(data)
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`[connection] sendPacked error to ${clientId}:`, err.message)
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
getAllStats() {
|
|
131
143
|
return {
|
|
132
144
|
activeConnections: this.clients.size,
|
|
@@ -28,21 +28,7 @@ function encodeEntity(e) {
|
|
|
28
28
|
]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function entityKey(encoded) {
|
|
32
|
-
let k = encoded[1]
|
|
33
|
-
for (let i = 2; i < 10; i++) k += '|' + encoded[i]
|
|
34
|
-
k += '|' + encoded[9]
|
|
35
|
-
if (encoded[10] !== null && encoded[10] !== undefined) k += '|' + JSON.stringify(encoded[10])
|
|
36
|
-
return k
|
|
37
|
-
}
|
|
38
|
-
|
|
39
31
|
export class SnapshotEncoder {
|
|
40
|
-
static encode(snapshot) {
|
|
41
|
-
const players = (snapshot.players || []).map(encodePlayer)
|
|
42
|
-
const entities = (snapshot.entities || []).map(encodeEntity)
|
|
43
|
-
return { tick: snapshot.tick || 0, timestamp: snapshot.timestamp || 0, players, entities }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
32
|
static encodeDelta(snapshot, prevEntityMap) {
|
|
47
33
|
const players = (snapshot.players || []).map(encodePlayer)
|
|
48
34
|
const currentIds = new Set()
|
|
@@ -50,11 +36,16 @@ export class SnapshotEncoder {
|
|
|
50
36
|
const nextMap = new Map()
|
|
51
37
|
for (const e of snapshot.entities || []) {
|
|
52
38
|
const encoded = encodeEntity(e)
|
|
53
|
-
const key = entityKey(encoded)
|
|
54
39
|
currentIds.add(e.id)
|
|
55
|
-
nextMap.set(e.id, key)
|
|
56
40
|
const prev = prevEntityMap.get(e.id)
|
|
57
|
-
|
|
41
|
+
let k = encoded[1]
|
|
42
|
+
for (let i = 2; i < 10; i++) k += '|' + encoded[i]
|
|
43
|
+
k += '|' + encoded[9]
|
|
44
|
+
const cust = encoded[10]
|
|
45
|
+
const custStr = (prev && prev[1] === cust) ? prev[2] : (cust != null ? JSON.stringify(cust) : '')
|
|
46
|
+
k += '|' + custStr
|
|
47
|
+
nextMap.set(e.id, [k, cust, custStr])
|
|
48
|
+
if (!prev || prev[0] !== k) entities.push(encoded)
|
|
58
49
|
}
|
|
59
50
|
const removed = []
|
|
60
51
|
for (const id of prevEntityMap.keys()) {
|
|
@@ -66,6 +57,12 @@ export class SnapshotEncoder {
|
|
|
66
57
|
}
|
|
67
58
|
}
|
|
68
59
|
|
|
60
|
+
static encode(snapshot) {
|
|
61
|
+
const players = (snapshot.players || []).map(encodePlayer)
|
|
62
|
+
const entities = (snapshot.entities || []).map(encodeEntity)
|
|
63
|
+
return { tick: snapshot.tick || 0, timestamp: snapshot.timestamp || 0, players, entities }
|
|
64
|
+
}
|
|
65
|
+
|
|
69
66
|
static decode(data) {
|
|
70
67
|
if (data.players && Array.isArray(data.players)) {
|
|
71
68
|
const players = data.players.map(p => {
|
package/src/physics/GLBLoader.js
CHANGED
|
@@ -364,6 +364,164 @@ async function extractMeshWithMeshopt(buf, json, prim, binOffset, meshName) {
|
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Extract ALL meshes and ALL primitives from a GLB file and combine them into
|
|
369
|
+
* a single flat vertex/index buffer suitable for a trimesh collider.
|
|
370
|
+
* Handles Draco-compressed primitives. Used for map collision where the GLB
|
|
371
|
+
* may have dozens of meshes each with many primitives.
|
|
372
|
+
*
|
|
373
|
+
* @param {string} filepath - Path to GLB file
|
|
374
|
+
* @returns {Promise<Object>} {vertices: Float32Array, indices: Uint32Array, vertexCount, triangleCount}
|
|
375
|
+
*/
|
|
376
|
+
export async function extractAllMeshesFromGLBAsync(filepath) {
|
|
377
|
+
console.log(`[GLBLoader] Extracting ALL meshes from: ${filepath}`)
|
|
378
|
+
const buf = readFileSync(filepath)
|
|
379
|
+
if (buf.toString('ascii', 0, 4) !== 'glTF') throw new Error('Not a GLB file')
|
|
380
|
+
|
|
381
|
+
const jsonLen = buf.readUInt32LE(12)
|
|
382
|
+
const json = JSON.parse(buf.toString('utf-8', 20, 20 + jsonLen))
|
|
383
|
+
const binOffset = 20 + jsonLen + 8
|
|
384
|
+
|
|
385
|
+
const allVertices = []
|
|
386
|
+
const allIndices = []
|
|
387
|
+
let vertexOffset = 0
|
|
388
|
+
let totalTriangles = 0
|
|
389
|
+
|
|
390
|
+
// Build a node->transform map for node hierarchy
|
|
391
|
+
const nodeTransforms = buildNodeTransforms(json)
|
|
392
|
+
|
|
393
|
+
for (let meshIdx = 0; meshIdx < (json.meshes || []).length; meshIdx++) {
|
|
394
|
+
const mesh = json.meshes[meshIdx]
|
|
395
|
+
// Find node referencing this mesh to get its world transform
|
|
396
|
+
const nodeIdx = (json.nodes || []).findIndex(n => n.mesh === meshIdx)
|
|
397
|
+
const worldTransform = nodeIdx >= 0 ? nodeTransforms[nodeIdx] : null
|
|
398
|
+
|
|
399
|
+
for (let primIdx = 0; primIdx < mesh.primitives.length; primIdx++) {
|
|
400
|
+
const prim = mesh.primitives[primIdx]
|
|
401
|
+
let result
|
|
402
|
+
try {
|
|
403
|
+
if (prim.extensions?.KHR_draco_mesh_compression) {
|
|
404
|
+
result = await decompressDracoMesh(buf, json, prim, binOffset, mesh.name)
|
|
405
|
+
} else {
|
|
406
|
+
result = extractStandardMesh(buf, json, prim, binOffset, mesh.name)
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
console.warn(`[GLBLoader] Skipping mesh[${meshIdx}] prim[${primIdx}]: ${e.message}`)
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!result.indices || result.triangleCount === 0) continue
|
|
414
|
+
|
|
415
|
+
// Apply world transform to vertices if present
|
|
416
|
+
let verts = result.vertices
|
|
417
|
+
if (worldTransform) {
|
|
418
|
+
verts = applyTransformMatrix(result.vertices, worldTransform)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
allVertices.push(verts)
|
|
422
|
+
// Remap indices by current vertex offset
|
|
423
|
+
const remapped = new Uint32Array(result.indices.length)
|
|
424
|
+
for (let i = 0; i < result.indices.length; i++) {
|
|
425
|
+
remapped[i] = result.indices[i] + vertexOffset
|
|
426
|
+
}
|
|
427
|
+
allIndices.push(remapped)
|
|
428
|
+
vertexOffset += result.vertexCount
|
|
429
|
+
totalTriangles += result.triangleCount
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (allVertices.length === 0) throw new Error('No valid mesh primitives found in GLB')
|
|
434
|
+
|
|
435
|
+
// Concatenate all vertex and index arrays
|
|
436
|
+
const totalVerts = vertexOffset
|
|
437
|
+
const combinedVertices = new Float32Array(totalVerts * 3)
|
|
438
|
+
let vOff = 0
|
|
439
|
+
for (const v of allVertices) { combinedVertices.set(v, vOff); vOff += v.length }
|
|
440
|
+
|
|
441
|
+
const combinedIndices = new Uint32Array(totalTriangles * 3)
|
|
442
|
+
let iOff = 0
|
|
443
|
+
for (const idx of allIndices) { combinedIndices.set(idx, iOff); iOff += idx.length }
|
|
444
|
+
|
|
445
|
+
console.log(`[GLBLoader] Combined: ${totalVerts} vertices, ${totalTriangles} triangles from ${allIndices.length} primitives`)
|
|
446
|
+
return { vertices: combinedVertices, indices: combinedIndices, vertexCount: totalVerts, triangleCount: totalTriangles }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Build world-space 4x4 transform matrices for every node in the scene graph.
|
|
451
|
+
* Returns an array indexed by node index.
|
|
452
|
+
*/
|
|
453
|
+
function buildNodeTransforms(json) {
|
|
454
|
+
const nodes = json.nodes || []
|
|
455
|
+
const matrices = new Array(nodes.length).fill(null)
|
|
456
|
+
|
|
457
|
+
function getMatrix(nodeIdx) {
|
|
458
|
+
if (matrices[nodeIdx] !== null) return matrices[nodeIdx]
|
|
459
|
+
const node = nodes[nodeIdx]
|
|
460
|
+
let local = mat4Identity()
|
|
461
|
+
if (node.matrix) {
|
|
462
|
+
local = node.matrix.slice()
|
|
463
|
+
} else {
|
|
464
|
+
const t = node.translation || [0, 0, 0]
|
|
465
|
+
const r = node.rotation || [0, 0, 0, 1]
|
|
466
|
+
const s = node.scale || [1, 1, 1]
|
|
467
|
+
local = mat4TRS(t, r, s)
|
|
468
|
+
}
|
|
469
|
+
// Find parent
|
|
470
|
+
const parentIdx = nodes.findIndex((n, i) => i !== nodeIdx && (n.children || []).includes(nodeIdx))
|
|
471
|
+
if (parentIdx >= 0) {
|
|
472
|
+
local = mat4Mul(getMatrix(parentIdx), local)
|
|
473
|
+
}
|
|
474
|
+
matrices[nodeIdx] = local
|
|
475
|
+
return local
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (let i = 0; i < nodes.length; i++) getMatrix(i)
|
|
479
|
+
return matrices
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function mat4Identity() {
|
|
483
|
+
return [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function mat4TRS(t, r, s) {
|
|
487
|
+
const [qx, qy, qz, qw] = r
|
|
488
|
+
const [sx, sy, sz] = s
|
|
489
|
+
const x2=qx+qx, y2=qy+qy, z2=qz+qz
|
|
490
|
+
const xx=qx*x2, xy=qx*y2, xz=qx*z2
|
|
491
|
+
const yy=qy*y2, yz=qy*z2, zz=qz*z2
|
|
492
|
+
const wx=qw*x2, wy=qw*y2, wz=qw*z2
|
|
493
|
+
return [
|
|
494
|
+
(1-(yy+zz))*sx, (xy+wz)*sx, (xz-wy)*sx, 0,
|
|
495
|
+
(xy-wz)*sy, (1-(xx+zz))*sy,(yz+wx)*sy, 0,
|
|
496
|
+
(xz+wy)*sz, (yz-wx)*sz, (1-(xx+yy))*sz,0,
|
|
497
|
+
t[0], t[1], t[2], 1
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function mat4Mul(a, b) {
|
|
502
|
+
const out = new Array(16)
|
|
503
|
+
for (let row = 0; row < 4; row++) {
|
|
504
|
+
for (let col = 0; col < 4; col++) {
|
|
505
|
+
let sum = 0
|
|
506
|
+
for (let k = 0; k < 4; k++) sum += a[row + k*4] * b[k + col*4]
|
|
507
|
+
out[row + col*4] = sum
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return out
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function applyTransformMatrix(vertices, m) {
|
|
514
|
+
const count = vertices.length / 3
|
|
515
|
+
const out = new Float32Array(vertices.length)
|
|
516
|
+
for (let i = 0; i < count; i++) {
|
|
517
|
+
const x = vertices[i*3], y = vertices[i*3+1], z = vertices[i*3+2]
|
|
518
|
+
out[i*3] = m[0]*x + m[4]*y + m[8]*z + m[12]
|
|
519
|
+
out[i*3+1] = m[1]*x + m[5]*y + m[9]*z + m[13]
|
|
520
|
+
out[i*3+2] = m[2]*x + m[6]*y + m[10]*z + m[14]
|
|
521
|
+
}
|
|
522
|
+
return out
|
|
523
|
+
}
|
|
524
|
+
|
|
367
525
|
/**
|
|
368
526
|
* Check if a GLB file has Draco-compressed meshes without attempting extraction.
|
|
369
527
|
* Useful for validation and error reporting.
|
package/src/physics/World.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import initJolt from 'jolt-physics/wasm-compat'
|
|
2
|
-
import { extractMeshFromGLB, extractMeshFromGLBAsync } from './GLBLoader.js'
|
|
1
|
+
import initJolt from 'jolt-physics/wasm-compat'
|
|
2
|
+
import { extractMeshFromGLB, extractMeshFromGLBAsync, extractAllMeshesFromGLBAsync } from './GLBLoader.js'
|
|
3
3
|
const LAYER_STATIC = 0, LAYER_DYNAMIC = 1, NUM_LAYERS = 2
|
|
4
4
|
let joltInstance = null
|
|
5
5
|
async function getJolt() { if (!joltInstance) joltInstance = await initJolt(); return joltInstance }
|
|
@@ -71,110 +71,92 @@ export class PhysicsWorld {
|
|
|
71
71
|
layer = motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC
|
|
72
72
|
return this._addBody(shape, position, mt, layer, { ...opts, meta: { type: motionType, shape: shapeType } })
|
|
73
73
|
}
|
|
74
|
-
addStaticTrimesh(glbPath, meshIndex = 0) {
|
|
75
|
-
const J = this.Jolt
|
|
76
|
-
const mesh = extractMeshFromGLB(glbPath, meshIndex)
|
|
77
|
-
|
|
78
|
-
// Apply node transform if present (scale, rotation, translation)
|
|
79
|
-
let vertices = mesh.vertices
|
|
80
|
-
const nodeT = mesh.nodeTransform
|
|
81
|
-
if (nodeT) {
|
|
82
|
-
const numVerts = mesh.vertexCount
|
|
83
|
-
vertices = new Float32Array(numVerts * 3)
|
|
84
|
-
|
|
85
|
-
const scale = nodeT.scale || [1, 1, 1]
|
|
86
|
-
const translation = nodeT.translation || [0, 0, 0]
|
|
87
|
-
const rotation = nodeT.rotation
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < numVerts; i++) {
|
|
90
|
-
let x = mesh.vertices[i * 3] * scale[0]
|
|
91
|
-
let y = mesh.vertices[i * 3 + 1] * scale[1]
|
|
92
|
-
let z = mesh.vertices[i * 3 + 2] * scale[2]
|
|
93
|
-
|
|
94
|
-
if (rotation) {
|
|
95
|
-
const [qx, qy, qz, qw] = rotation
|
|
96
|
-
const ix = qw * x + qy * z - qz * y
|
|
97
|
-
const iy = qw * y + qz * x - qx * z
|
|
98
|
-
const iz = qw * z + qx * y - qy * x
|
|
99
|
-
const iw = -qx * x - qy * y - qz * z
|
|
100
|
-
x = ix * qw - iw * qx - iy * qz + iz * qy
|
|
101
|
-
y = iy * qw - iw * qy - iz * qx + ix * qz
|
|
102
|
-
z = iz * qw - iw * qz - ix * qy + iy * qx
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
vertices[i * 3] = x + translation[0]
|
|
106
|
-
vertices[i * 3 + 1] = y + translation[1]
|
|
107
|
-
vertices[i * 3 + 2] = z + translation[2]
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const triangles = new J.TriangleList(); triangles.resize(mesh.triangleCount)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
74
|
+
addStaticTrimesh(glbPath, meshIndex = 0) {
|
|
75
|
+
const J = this.Jolt
|
|
76
|
+
const mesh = extractMeshFromGLB(glbPath, meshIndex)
|
|
77
|
+
|
|
78
|
+
// Apply node transform if present (scale, rotation, translation)
|
|
79
|
+
let vertices = mesh.vertices
|
|
80
|
+
const nodeT = mesh.nodeTransform
|
|
81
|
+
if (nodeT) {
|
|
82
|
+
const numVerts = mesh.vertexCount
|
|
83
|
+
vertices = new Float32Array(numVerts * 3)
|
|
84
|
+
|
|
85
|
+
const scale = nodeT.scale || [1, 1, 1]
|
|
86
|
+
const translation = nodeT.translation || [0, 0, 0]
|
|
87
|
+
const rotation = nodeT.rotation
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < numVerts; i++) {
|
|
90
|
+
let x = mesh.vertices[i * 3] * scale[0]
|
|
91
|
+
let y = mesh.vertices[i * 3 + 1] * scale[1]
|
|
92
|
+
let z = mesh.vertices[i * 3 + 2] * scale[2]
|
|
93
|
+
|
|
94
|
+
if (rotation) {
|
|
95
|
+
const [qx, qy, qz, qw] = rotation
|
|
96
|
+
const ix = qw * x + qy * z - qz * y
|
|
97
|
+
const iy = qw * y + qz * x - qx * z
|
|
98
|
+
const iz = qw * z + qx * y - qy * x
|
|
99
|
+
const iw = -qx * x - qy * y - qz * z
|
|
100
|
+
x = ix * qw - iw * qx - iy * qz + iz * qy
|
|
101
|
+
y = iy * qw - iw * qy - iz * qx + ix * qz
|
|
102
|
+
z = iz * qw - iw * qz - ix * qy + iy * qx
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
vertices[i * 3] = x + translation[0]
|
|
106
|
+
vertices[i * 3 + 1] = y + translation[1]
|
|
107
|
+
vertices[i * 3 + 2] = z + translation[2]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const triangles = new J.TriangleList(); triangles.resize(mesh.triangleCount)
|
|
112
|
+
const f3 = new J.Float3(0, 0, 0)
|
|
113
|
+
for (let t = 0; t < mesh.triangleCount; t++) {
|
|
114
|
+
const tri = triangles.at(t)
|
|
115
|
+
for (let v = 0; v < 3; v++) {
|
|
116
|
+
const idx = mesh.indices[t * 3 + v]
|
|
117
|
+
f3.x = vertices[idx * 3]; f3.y = vertices[idx * 3 + 1]; f3.z = vertices[idx * 3 + 2]
|
|
118
|
+
tri.set_mV(v, f3)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const settings = new J.MeshShapeSettings(triangles)
|
|
122
|
+
const shape = settings.Create().Get()
|
|
123
|
+
J.destroy(f3)
|
|
124
|
+
J.destroy(triangles)
|
|
125
|
+
J.destroy(settings)
|
|
126
|
+
return this._addBody(shape, [0, 0, 0], J.EMotionType_Static, LAYER_STATIC, { meta: { type: 'static', shape: 'trimesh', mesh: mesh.name, triangles: mesh.triangleCount } })
|
|
127
|
+
}
|
|
122
128
|
|
|
123
|
-
addStaticTrimeshAsync(glbPath, meshIndex = 0, position = [0, 0, 0]) {
|
|
124
|
-
return new Promise(async (resolve, reject) => {
|
|
125
|
-
try {
|
|
126
|
-
const J = this.Jolt
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
vertices[i * 3] = x + translation[0]
|
|
157
|
-
vertices[i * 3 + 1] = y + translation[1]
|
|
158
|
-
vertices[i * 3 + 2] = z + translation[2]
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const triangles = new J.TriangleList(); triangles.resize(mesh.triangleCount)
|
|
163
|
-
for (let t = 0; t < mesh.triangleCount; t++) {
|
|
164
|
-
const tri = triangles.at(t)
|
|
165
|
-
for (let v = 0; v < 3; v++) {
|
|
166
|
-
const idx = mesh.indices[t * 3 + v]
|
|
167
|
-
tri.set_mV(v, new J.Float3(vertices[idx * 3], vertices[idx * 3 + 1], vertices[idx * 3 + 2]))
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const shape = new J.MeshShapeSettings(triangles).Create().Get()
|
|
171
|
-
const id = this._addBody(shape, position, J.EMotionType_Static, LAYER_STATIC, { meta: { type: 'static', shape: 'trimesh', mesh: mesh.name, triangles: mesh.triangleCount } })
|
|
172
|
-
resolve(id)
|
|
173
|
-
} catch (e) {
|
|
174
|
-
reject(e)
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
}
|
|
129
|
+
addStaticTrimeshAsync(glbPath, meshIndex = 0, position = [0, 0, 0]) {
|
|
130
|
+
return new Promise(async (resolve, reject) => {
|
|
131
|
+
try {
|
|
132
|
+
const J = this.Jolt
|
|
133
|
+
// Use combined extraction: all meshes + all primitives (handles Draco, multi-mesh maps)
|
|
134
|
+
const mesh = await extractAllMeshesFromGLBAsync(glbPath)
|
|
135
|
+
const { vertices, indices, triangleCount } = mesh
|
|
136
|
+
|
|
137
|
+
const triangles = new J.TriangleList(); triangles.resize(triangleCount)
|
|
138
|
+
// Reuse a single Float3 to avoid WASM heap growth from per-vertex allocations
|
|
139
|
+
const f3 = new J.Float3(0, 0, 0)
|
|
140
|
+
for (let t = 0; t < triangleCount; t++) {
|
|
141
|
+
const tri = triangles.at(t)
|
|
142
|
+
for (let v = 0; v < 3; v++) {
|
|
143
|
+
const idx = indices[t * 3 + v]
|
|
144
|
+
f3.x = vertices[idx * 3]; f3.y = vertices[idx * 3 + 1]; f3.z = vertices[idx * 3 + 2]
|
|
145
|
+
tri.set_mV(v, f3)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const settings = new J.MeshShapeSettings(triangles)
|
|
149
|
+
const shape = settings.Create().Get()
|
|
150
|
+
J.destroy(f3)
|
|
151
|
+
J.destroy(triangles)
|
|
152
|
+
J.destroy(settings)
|
|
153
|
+
const id = this._addBody(shape, position, J.EMotionType_Static, LAYER_STATIC, { meta: { type: 'static', shape: 'trimesh', triangles: triangleCount } })
|
|
154
|
+
resolve(id)
|
|
155
|
+
} catch (e) {
|
|
156
|
+
reject(e)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
178
160
|
addPlayerCharacter(radius, halfHeight, position, mass) {
|
|
179
161
|
const J = this.Jolt
|
|
180
162
|
const cvs = new J.CharacterVirtualSettings()
|
|
@@ -232,9 +214,7 @@ export class PhysicsWorld {
|
|
|
232
214
|
getCharacterPosition(charId) {
|
|
233
215
|
const ch = this.characters?.get(charId); if (!ch) return [0, 0, 0]
|
|
234
216
|
const p = ch.GetPosition()
|
|
235
|
-
|
|
236
|
-
this.Jolt.destroy(p)
|
|
237
|
-
return r
|
|
217
|
+
return [p.GetX(), p.GetY(), p.GetZ()]
|
|
238
218
|
}
|
|
239
219
|
getCharacterVelocity(charId) {
|
|
240
220
|
const ch = this.characters?.get(charId); if (!ch) return [0, 0, 0]
|
|
@@ -320,8 +300,8 @@ export class PhysicsWorld {
|
|
|
320
300
|
const dir = len > 0 ? [direction[0] / len, direction[1] / len, direction[2] / len] : direction
|
|
321
301
|
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))
|
|
322
302
|
const rs = new J.RayCastSettings(), col = new J.CastRayClosestHitCollisionCollector()
|
|
323
|
-
const bp = new J.DefaultBroadPhaseLayerFilter(this.
|
|
324
|
-
const ol = new J.DefaultObjectLayerFilter(this.
|
|
303
|
+
const bp = new J.DefaultBroadPhaseLayerFilter(this.jolt.GetObjectVsBroadPhaseLayerFilter(), LAYER_DYNAMIC)
|
|
304
|
+
const ol = new J.DefaultObjectLayerFilter(this.jolt.GetObjectLayerPairFilter(), LAYER_DYNAMIC)
|
|
325
305
|
const eb = excludeBodyId != null ? this._getBody(excludeBodyId) : null
|
|
326
306
|
const bf = eb ? new J.IgnoreSingleBodyFilter(eb.GetID()) : new J.BodyFilter()
|
|
327
307
|
const sf = new J.ShapeFilter()
|
|
@@ -335,13 +315,25 @@ export class PhysicsWorld {
|
|
|
335
315
|
return result
|
|
336
316
|
}
|
|
337
317
|
destroy() {
|
|
318
|
+
if (!this.Jolt) return
|
|
319
|
+
const J = this.Jolt
|
|
338
320
|
if (this.characters) {
|
|
339
|
-
for (const
|
|
340
|
-
this.Jolt.destroy(ch)
|
|
341
|
-
}
|
|
321
|
+
for (const ch of this.characters.values()) J.destroy(ch)
|
|
342
322
|
this.characters.clear()
|
|
343
323
|
}
|
|
324
|
+
if (this._charFilters) {
|
|
325
|
+
J.destroy(this._charFilters.bp)
|
|
326
|
+
J.destroy(this._charFilters.ol)
|
|
327
|
+
J.destroy(this._charFilters.body)
|
|
328
|
+
J.destroy(this._charFilters.shape)
|
|
329
|
+
this._charFilters = null
|
|
330
|
+
}
|
|
331
|
+
if (this._charUpdateSettings) { J.destroy(this._charUpdateSettings); this._charUpdateSettings = null }
|
|
332
|
+
if (this._charGravity) { J.destroy(this._charGravity); this._charGravity = null }
|
|
344
333
|
for (const [id] of this.bodies) this.removeBody(id)
|
|
345
|
-
if (this.
|
|
334
|
+
if (this._tmpVec3) { J.destroy(this._tmpVec3); this._tmpVec3 = null }
|
|
335
|
+
if (this._tmpRVec3) { J.destroy(this._tmpRVec3); this._tmpRVec3 = null }
|
|
336
|
+
if (this.jolt) { J.destroy(this.jolt); this.jolt = null }
|
|
337
|
+
this.physicsSystem = null; this.bodyInterface = null
|
|
346
338
|
}
|
|
347
339
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { WebSocket } from 'ws'
|
|
2
|
+
import { pack, unpack } from '../protocol/msgpack.js'
|
|
3
|
+
|
|
4
|
+
const CONFIG = {
|
|
5
|
+
botCount: 100,
|
|
6
|
+
durationMs: 60000,
|
|
7
|
+
inputHz: 60,
|
|
8
|
+
serverUrl: process.env.BOT_URL || 'ws://localhost:3001/ws',
|
|
9
|
+
batchSize: 10,
|
|
10
|
+
batchDelayMs: 200
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MSG_INPUT = 0x11
|
|
14
|
+
const MSG_SNAPSHOT = 0x10
|
|
15
|
+
|
|
16
|
+
function makeInput(botId, tick) {
|
|
17
|
+
const phase = (tick / 80 + botId * 0.37) % 1
|
|
18
|
+
return {
|
|
19
|
+
forward: phase < 0.7,
|
|
20
|
+
backward: phase > 0.85,
|
|
21
|
+
left: phase > 0.72 && phase < 0.82,
|
|
22
|
+
right: phase > 0.82 && phase < 0.85,
|
|
23
|
+
jump: tick % 200 === botId % 200,
|
|
24
|
+
sprint: tick % 400 < 300,
|
|
25
|
+
yaw: (botId / CONFIG.botCount) * Math.PI * 2 + Math.sin(tick / 180) * 0.8,
|
|
26
|
+
pitch: 0,
|
|
27
|
+
crouch: false,
|
|
28
|
+
interact: false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stats = { connected: 0, snapshots: 0, errors: 0 }
|
|
33
|
+
|
|
34
|
+
function createBot(botId) {
|
|
35
|
+
let tick = 0
|
|
36
|
+
let interval = null
|
|
37
|
+
const ws = new WebSocket(CONFIG.serverUrl)
|
|
38
|
+
ws.binaryType = 'arraybuffer'
|
|
39
|
+
ws.on('open', () => {
|
|
40
|
+
stats.connected++
|
|
41
|
+
interval = setInterval(() => {
|
|
42
|
+
if (ws.readyState !== WebSocket.OPEN) return
|
|
43
|
+
ws.send(pack({ type: MSG_INPUT, payload: makeInput(botId, ++tick) }))
|
|
44
|
+
}, 1000 / CONFIG.inputHz)
|
|
45
|
+
})
|
|
46
|
+
ws.on('message', data => {
|
|
47
|
+
try {
|
|
48
|
+
const msg = unpack(data instanceof ArrayBuffer ? new Uint8Array(data) : data)
|
|
49
|
+
if (msg?.type === MSG_SNAPSHOT) stats.snapshots++
|
|
50
|
+
} catch {}
|
|
51
|
+
})
|
|
52
|
+
ws.on('error', () => { stats.errors++ })
|
|
53
|
+
ws.on('close', () => { stats.connected--; if (interval) clearInterval(interval) })
|
|
54
|
+
return ws
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const start = Date.now()
|
|
61
|
+
console.log(`[BotHarness] Connecting ${CONFIG.botCount} bots → ${CONFIG.serverUrl}`)
|
|
62
|
+
const bots = []
|
|
63
|
+
for (let i = 0; i < CONFIG.botCount; i += CONFIG.batchSize) {
|
|
64
|
+
const end = Math.min(i + CONFIG.batchSize, CONFIG.botCount)
|
|
65
|
+
for (let j = i; j < end; j++) bots.push(createBot(j))
|
|
66
|
+
await sleep(CONFIG.batchDelayMs)
|
|
67
|
+
}
|
|
68
|
+
await sleep(2000)
|
|
69
|
+
console.log(`[BotHarness] ${stats.connected}/${CONFIG.botCount} connected, running ${CONFIG.durationMs / 1000}s`)
|
|
70
|
+
const reportInterval = setInterval(() => {
|
|
71
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1)
|
|
72
|
+
const rate = (stats.snapshots / parseFloat(elapsed)).toFixed(0)
|
|
73
|
+
console.log(`[BotHarness] t=${elapsed}s conn=${stats.connected} snaps=${stats.snapshots} (${rate}/s) err=${stats.errors}`)
|
|
74
|
+
}, 5000)
|
|
75
|
+
await sleep(CONFIG.durationMs)
|
|
76
|
+
clearInterval(reportInterval)
|
|
77
|
+
const elapsed = (Date.now() - start) / 1000
|
|
78
|
+
const perBotPerSec = (stats.snapshots / CONFIG.botCount / elapsed).toFixed(2)
|
|
79
|
+
console.log(`\n[BotHarness] ── FINAL ──`)
|
|
80
|
+
console.log(`[BotHarness] Duration: ${elapsed.toFixed(1)}s | Connected: ${stats.connected}/${CONFIG.botCount}`)
|
|
81
|
+
console.log(`[BotHarness] Snapshots: ${stats.snapshots} total | ${perBotPerSec}/bot/sec | Errors: ${stats.errors}`)
|
|
82
|
+
for (const ws of bots) if (ws.readyState === WebSocket.OPEN) ws.close()
|
|
83
|
+
await sleep(500)
|
|
84
|
+
process.exit(0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch(e => { console.error('[BotHarness] fatal:', e); process.exit(1) })
|
package/src/sdk/TickHandler.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { MSG } from '../protocol/MessageTypes.js'
|
|
2
2
|
import { SnapshotEncoder } from '../netcode/SnapshotEncoder.js'
|
|
3
|
+
import { pack } from '../protocol/msgpack.js'
|
|
4
|
+
import { isUnreliable } from '../protocol/MessageTypes.js'
|
|
3
5
|
import { applyMovement as _applyMovement, DEFAULT_MOVEMENT as _DEFAULT_MOVEMENT } from '../shared/movement.js'
|
|
4
6
|
|
|
5
7
|
const KEYFRAME_INTERVAL = 128
|
|
8
|
+
const SNAP_GROUPS = 4
|
|
6
9
|
|
|
7
10
|
export function createTickHandler(deps) {
|
|
8
11
|
const {
|
|
@@ -13,23 +16,20 @@ export function createTickHandler(deps) {
|
|
|
13
16
|
const applyMovement = _movement?.applyMovement || _applyMovement
|
|
14
17
|
const DEFAULT_MOVEMENT = _movement?.DEFAULT_MOVEMENT || _DEFAULT_MOVEMENT
|
|
15
18
|
const movement = { ...DEFAULT_MOVEMENT, ...m }
|
|
16
|
-
const collisionRestitution = movement.collisionRestitution || 0.2
|
|
17
|
-
const collisionDamping = movement.collisionDamping || 0.25
|
|
18
19
|
let snapshotSeq = 0
|
|
19
|
-
|
|
20
20
|
const playerEntityMaps = new Map()
|
|
21
21
|
let broadcastEntityMap = new Map()
|
|
22
|
-
|
|
23
22
|
let profileLog = 0
|
|
24
|
-
const
|
|
23
|
+
const snapUnreliable = isUnreliable(MSG.SNAPSHOT)
|
|
24
|
+
|
|
25
25
|
return function onTick(tick, dt) {
|
|
26
26
|
const t0 = performance.now()
|
|
27
27
|
networkState.setTick(tick, Date.now())
|
|
28
28
|
const players = playerManager.getConnectedPlayers()
|
|
29
|
+
|
|
29
30
|
for (const player of players) {
|
|
30
31
|
const inputs = playerManager.getInputs(player.id)
|
|
31
32
|
const st = player.state
|
|
32
|
-
|
|
33
33
|
if (inputs.length > 0) {
|
|
34
34
|
player.lastInput = inputs[inputs.length - 1].data
|
|
35
35
|
playerManager.clearInputs(player.id)
|
|
@@ -37,12 +37,14 @@ export function createTickHandler(deps) {
|
|
|
37
37
|
const inp = player.lastInput || null
|
|
38
38
|
if (inp) {
|
|
39
39
|
const yaw = inp.yaw || 0
|
|
40
|
-
st.rotation
|
|
40
|
+
st.rotation[0] = 0
|
|
41
|
+
st.rotation[1] = Math.sin(yaw / 2)
|
|
42
|
+
st.rotation[2] = 0
|
|
43
|
+
st.rotation[3] = Math.cos(yaw / 2)
|
|
41
44
|
st.crouch = inp.crouch ? 1 : 0
|
|
42
45
|
st.lookPitch = inp.pitch || 0
|
|
43
46
|
st.lookYaw = yaw
|
|
44
47
|
}
|
|
45
|
-
|
|
46
48
|
applyMovement(st, inp, movement, dt)
|
|
47
49
|
if (inp) physicsIntegration.setCrouch(player.id, !!inp.crouch)
|
|
48
50
|
const wishedVx = st.velocity[0], wishedVz = st.velocity[2]
|
|
@@ -60,83 +62,95 @@ export function createTickHandler(deps) {
|
|
|
60
62
|
crouch: st.crouch || 0, lookPitch: st.lookPitch || 0, lookYaw: st.lookYaw || 0
|
|
61
63
|
})
|
|
62
64
|
}
|
|
65
|
+
|
|
63
66
|
const t1 = performance.now()
|
|
64
|
-
|
|
67
|
+
const cellSz = physicsIntegration.config.capsuleRadius * 8
|
|
68
|
+
const minDist = physicsIntegration.config.capsuleRadius * 2
|
|
69
|
+
const minDist2 = minDist * minDist
|
|
70
|
+
const grid = new Map()
|
|
71
|
+
for (const p of players) {
|
|
72
|
+
const cx = Math.floor(p.state.position[0] / cellSz)
|
|
73
|
+
const cz = Math.floor(p.state.position[2] / cellSz)
|
|
74
|
+
const ck = cx * 65536 + cz
|
|
75
|
+
let cell = grid.get(ck)
|
|
76
|
+
if (!cell) { cell = []; grid.set(ck, cell) }
|
|
77
|
+
cell.push(p)
|
|
78
|
+
}
|
|
65
79
|
for (const player of players) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
const px = player.state.position[0], py = player.state.position[1], pz = player.state.position[2]
|
|
81
|
+
const cx = Math.floor(px / cellSz), cz = Math.floor(pz / cellSz)
|
|
82
|
+
for (let ddx = -1; ddx <= 1; ddx++) {
|
|
83
|
+
for (let ddz = -1; ddz <= 1; ddz++) {
|
|
84
|
+
const neighbors = grid.get((cx + ddx) * 65536 + (cz + ddz))
|
|
85
|
+
if (!neighbors) continue
|
|
86
|
+
for (const other of neighbors) {
|
|
87
|
+
if (other.id <= player.id) continue
|
|
88
|
+
const ox = other.state.position[0], oy = other.state.position[1], oz = other.state.position[2]
|
|
89
|
+
const dx = ox - px, dy = oy - py, dz = oz - pz
|
|
90
|
+
const dist2 = dx * dx + dy * dy + dz * dz
|
|
91
|
+
if (dist2 >= minDist2 || dist2 === 0) continue
|
|
92
|
+
const distance = Math.sqrt(dist2)
|
|
93
|
+
const nx = dx / distance, nz = dz / distance
|
|
94
|
+
const overlap = minDist - distance
|
|
95
|
+
const halfPush = overlap * 0.5
|
|
96
|
+
const pushVel = Math.min(halfPush / dt, 3.0)
|
|
97
|
+
player.state.position[0] -= nx * halfPush
|
|
98
|
+
player.state.position[2] -= nz * halfPush
|
|
99
|
+
player.state.velocity[0] -= nx * pushVel
|
|
100
|
+
player.state.velocity[2] -= nz * pushVel
|
|
101
|
+
other.state.position[0] += nx * halfPush
|
|
102
|
+
other.state.position[2] += nz * halfPush
|
|
103
|
+
other.state.velocity[0] += nx * pushVel
|
|
104
|
+
other.state.velocity[2] += nz * pushVel
|
|
105
|
+
physicsIntegration.setPlayerPosition(player.id, player.state.position)
|
|
106
|
+
physicsIntegration.setPlayerPosition(other.id, other.state.position)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
88
109
|
}
|
|
89
110
|
}
|
|
111
|
+
|
|
90
112
|
const t2 = performance.now()
|
|
91
113
|
physics.step(dt)
|
|
92
114
|
const t3 = performance.now()
|
|
93
115
|
appRuntime.tick(tick, dt)
|
|
94
116
|
const t4 = performance.now()
|
|
117
|
+
|
|
95
118
|
if (players.length > 0) {
|
|
96
119
|
const playerSnap = networkState.getSnapshot()
|
|
97
120
|
snapshotSeq++
|
|
98
121
|
const isKeyframe = snapshotSeq % KEYFRAME_INTERVAL === 0
|
|
122
|
+
const curGroup = tick % SNAP_GROUPS
|
|
123
|
+
|
|
99
124
|
if (stageLoader && stageLoader.getActiveStage()) {
|
|
125
|
+
const relevanceRadius = stageLoader.getActiveStage().spatial.relevanceRadius
|
|
100
126
|
for (const player of players) {
|
|
101
|
-
|
|
102
|
-
const entitySnap = appRuntime.getSnapshotForPlayer(
|
|
127
|
+
if (!isKeyframe && player.id % SNAP_GROUPS !== curGroup) continue
|
|
128
|
+
const entitySnap = appRuntime.getSnapshotForPlayer(player.state.position, relevanceRadius)
|
|
103
129
|
const combined = { tick: playerSnap.tick, timestamp: playerSnap.timestamp, players: playerSnap.players, entities: entitySnap.entities }
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
connections.send(player.id, MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
|
|
109
|
-
} else {
|
|
110
|
-
const prevMap = playerEntityMaps.get(player.id)
|
|
111
|
-
const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, prevMap)
|
|
112
|
-
playerEntityMaps.set(player.id, entityMap)
|
|
113
|
-
connections.send(player.id, MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
|
|
114
|
-
}
|
|
130
|
+
const prevMap = (isKeyframe || !playerEntityMaps.has(player.id)) ? new Map() : playerEntityMaps.get(player.id)
|
|
131
|
+
const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, prevMap)
|
|
132
|
+
playerEntityMaps.set(player.id, entityMap)
|
|
133
|
+
connections.send(player.id, MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
|
|
115
134
|
}
|
|
116
135
|
} else {
|
|
117
136
|
const entitySnap = appRuntime.getSnapshot()
|
|
118
137
|
const combined = { tick: playerSnap.tick, timestamp: playerSnap.timestamp, players: playerSnap.players, entities: entitySnap.entities }
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
broadcastEntityMap = entityMap
|
|
127
|
-
connections.broadcast(MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
|
|
138
|
+
const prevMap = (isKeyframe || broadcastEntityMap.size === 0) ? new Map() : broadcastEntityMap
|
|
139
|
+
const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, prevMap)
|
|
140
|
+
broadcastEntityMap = entityMap
|
|
141
|
+
const data = pack({ type: MSG.SNAPSHOT, payload: { seq: snapshotSeq, ...encoded } })
|
|
142
|
+
for (const player of players) {
|
|
143
|
+
if (!isKeyframe && player.id % SNAP_GROUPS !== curGroup) continue
|
|
144
|
+
connections.sendPacked(player.id, data, snapUnreliable)
|
|
128
145
|
}
|
|
129
146
|
}
|
|
130
147
|
}
|
|
148
|
+
|
|
131
149
|
for (const id of playerEntityMaps.keys()) {
|
|
132
150
|
if (!playerManager.getPlayer(id)) playerEntityMaps.delete(id)
|
|
133
151
|
}
|
|
134
152
|
const t5 = performance.now()
|
|
135
|
-
try {
|
|
136
|
-
appRuntime._drainReloadQueue()
|
|
137
|
-
} catch (e) {
|
|
138
|
-
console.error('[TickHandler] reload queue error:', e.message)
|
|
139
|
-
}
|
|
153
|
+
try { appRuntime._drainReloadQueue() } catch (e) { console.error('[TickHandler] reload queue error:', e.message) }
|
|
140
154
|
profileLog++
|
|
141
155
|
if (profileLog % 1280 === 0) {
|
|
142
156
|
const total = t5 - t0
|
|
@@ -145,7 +159,7 @@ export function createTickHandler(deps) {
|
|
|
145
159
|
const rss = (mem.rss / 1048576).toFixed(1)
|
|
146
160
|
const ext = (mem.external / 1048576).toFixed(1)
|
|
147
161
|
const ab = (mem.arrayBuffers / 1048576).toFixed(1)
|
|
148
|
-
try { console.log(`[tick-profile] tick:${tick} players:${players.length} total:${total.toFixed(2)}ms | mv:${(t1-t0).toFixed(2)} col:${(t2-t1).toFixed(2)} phys:${(t3-t2).toFixed(2)} app:${(t4-t3).toFixed(2)} snap:${(t5-t4).toFixed(2)} | heap:${heap}MB rss:${rss}MB ext:${ext}MB ab:${ab}MB`) } catch (_) {}
|
|
162
|
+
try { console.log(`[tick-profile] tick:${tick} players:${players.length} total:${total.toFixed(2)}ms | mv:${(t1 - t0).toFixed(2)} col:${(t2 - t1).toFixed(2)} phys:${(t3 - t2).toFixed(2)} app:${(t4 - t3).toFixed(2)} snap:${(t5 - t4).toFixed(2)} | heap:${heap}MB rss:${rss}MB ext:${ext}MB ab:${ab}MB`) } catch (_) {}
|
|
149
163
|
}
|
|
150
164
|
}
|
|
151
165
|
}
|