spoint 0.1.81 → 0.1.83
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/apps/tps-game/schwust.glb +0 -0
- package/apps/world/index.js +15 -3
- package/package.json +1 -1
- package/src/connection/ConnectionManager.js +12 -0
- package/src/netcode/SnapshotEncoder.js +14 -17
- package/src/sdk/BotHarness.js +87 -0
- package/src/sdk/ServerAPI.js +3 -1
- package/src/sdk/ServerHandlers.js +2 -1
- package/src/sdk/TickHandler.js +78 -60
- package/src/sdk/server.js +2 -1
|
Binary file
|
package/apps/world/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export default {
|
|
|
2
2
|
port: 3001,
|
|
3
3
|
tickRate: 128,
|
|
4
4
|
gravity: [0, -9.81, 0],
|
|
5
|
+
relevanceRadius: 150,
|
|
5
6
|
movement: {
|
|
6
7
|
maxSpeed: 4.0,
|
|
7
8
|
groundAccel: 10.0,
|
|
@@ -58,8 +59,19 @@ export default {
|
|
|
58
59
|
fadeTime: 0.15
|
|
59
60
|
},
|
|
60
61
|
entities: [
|
|
61
|
-
{ id: '
|
|
62
|
+
{ id: 'env-schwust', model: './apps/tps-game/schwust.glb', position: [0, 0, 0], app: 'environment' },
|
|
63
|
+
{ id: 'env-kosova', model: './apps/maps/aim_kosova_ak47.glb', position: [200, 0, 0], app: 'environment' },
|
|
64
|
+
{ id: 'env-sillos', model: './apps/maps/aim_sillos.glb', position: [400, 0, 0], app: 'environment' },
|
|
65
|
+
{ id: 'env-dust2', model: './apps/maps/de_dust2_kosovo.glb', position: [600, 0, 0], app: 'environment' },
|
|
66
|
+
{ id: 'env-gash', model: './apps/maps/de_gash.glb', position: [800, 0, 0], app: 'environment' }
|
|
62
67
|
],
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
spawnPoints: [
|
|
69
|
+
[-30, 20, -30],
|
|
70
|
+
[212, 20, 12],
|
|
71
|
+
[412, 20, 12],
|
|
72
|
+
[612, 20, 12],
|
|
73
|
+
[812, 20, 12]
|
|
74
|
+
],
|
|
75
|
+
spawnPoint: [-30, 20, -30],
|
|
76
|
+
playerModel: './apps/tps-game/cleetus.vrm'
|
|
65
77
|
}
|
package/package.json
CHANGED
|
@@ -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 => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { WebSocket } from 'ws'
|
|
2
|
+
import { pack, unpack } from '../protocol/msgpack.js'
|
|
3
|
+
|
|
4
|
+
const CONFIG = {
|
|
5
|
+
botCount: parseInt(process.env.BOT_COUNT || '100'),
|
|
6
|
+
durationMs: parseInt(process.env.BOT_DURATION || '60000'),
|
|
7
|
+
inputHz: parseInt(process.env.BOT_HZ || '60'),
|
|
8
|
+
serverUrl: process.env.BOT_URL || 'ws://localhost:3001/ws',
|
|
9
|
+
batchSize: parseInt(process.env.BOT_BATCH || '20'),
|
|
10
|
+
batchDelayMs: parseInt(process.env.BOT_DELAY || '100')
|
|
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/ServerAPI.js
CHANGED
|
@@ -33,7 +33,9 @@ export function createServerAPI(ctx) {
|
|
|
33
33
|
|
|
34
34
|
async loadWorld(worldDef) {
|
|
35
35
|
ctx.currentWorldDef = worldDef
|
|
36
|
-
if (worldDef.
|
|
36
|
+
if (worldDef.spawnPoints?.length) ctx.worldSpawnPoints = worldDef.spawnPoints
|
|
37
|
+
else if (worldDef.spawnPoint) ctx.worldSpawnPoints = [worldDef.spawnPoint]
|
|
38
|
+
ctx.worldSpawnPoint = ctx.worldSpawnPoints?.[0] || worldDef.spawnPoint || [0, 5, 0]
|
|
37
39
|
await appLoader.loadAll()
|
|
38
40
|
const stage = stageLoader.loadFromDefinition('main', worldDef)
|
|
39
41
|
return { entities: new Map(), apps: new Map(), count: stage.entityCount }
|
|
@@ -5,7 +5,8 @@ export function createConnectionHandlers(ctx) {
|
|
|
5
5
|
const { tickSystem, playerManager, networkState, lagCompensator, physicsIntegration, connections, sessions, appLoader, appRuntime, emitter, inspector } = ctx
|
|
6
6
|
|
|
7
7
|
function onClientConnect(transport) {
|
|
8
|
-
const
|
|
8
|
+
const spawnPoints = ctx.worldSpawnPoints || [ctx.worldSpawnPoint]
|
|
9
|
+
const sp = [...spawnPoints[Math.floor(Math.random() * spawnPoints.length)]]
|
|
9
10
|
const playerConfig = ctx.currentWorldDef?.player || {}
|
|
10
11
|
const playerId = playerManager.addPlayer(transport, { position: sp, health: playerConfig.health })
|
|
11
12
|
networkState.addPlayer(playerId, { position: sp })
|
package/src/sdk/TickHandler.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
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 MAX_SENDS_PER_TICK = 25
|
|
6
9
|
|
|
7
10
|
export function createTickHandler(deps) {
|
|
8
11
|
const {
|
|
9
12
|
networkState, playerManager, physicsIntegration,
|
|
10
13
|
lagCompensator, physics, appRuntime, connections,
|
|
11
|
-
movement: m = {}, stageLoader, eventLog, _movement
|
|
14
|
+
movement: m = {}, stageLoader, eventLog, _movement, getRelevanceRadius
|
|
12
15
|
} = 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,82 +62,98 @@ 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
|
|
99
|
-
|
|
122
|
+
const snapGroups = Math.max(1, Math.ceil(players.length / MAX_SENDS_PER_TICK))
|
|
123
|
+
const curGroup = tick % snapGroups
|
|
124
|
+
|
|
125
|
+
const relevanceRadius = (stageLoader && stageLoader.getActiveStage())
|
|
126
|
+
? stageLoader.getActiveStage().spatial.relevanceRadius
|
|
127
|
+
: (getRelevanceRadius ? getRelevanceRadius() : 0)
|
|
128
|
+
if (relevanceRadius > 0) {
|
|
100
129
|
for (const player of players) {
|
|
101
|
-
|
|
102
|
-
const entitySnap = appRuntime.getSnapshotForPlayer(
|
|
130
|
+
if (!isKeyframe && player.id % snapGroups !== curGroup) continue
|
|
131
|
+
const entitySnap = appRuntime.getSnapshotForPlayer(player.state.position, relevanceRadius)
|
|
103
132
|
const combined = { tick: playerSnap.tick, timestamp: playerSnap.timestamp, players: playerSnap.players, entities: entitySnap.entities }
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} else {
|
|
109
|
-
const prevMap = playerEntityMaps.get(player.id)
|
|
110
|
-
const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, prevMap)
|
|
111
|
-
playerEntityMaps.set(player.id, entityMap)
|
|
112
|
-
connections.send(player.id, MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
|
|
113
|
-
}
|
|
133
|
+
const prevMap = (isKeyframe || !playerEntityMaps.has(player.id)) ? new Map() : playerEntityMaps.get(player.id)
|
|
134
|
+
const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, prevMap)
|
|
135
|
+
playerEntityMaps.set(player.id, entityMap)
|
|
136
|
+
connections.send(player.id, MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
|
|
114
137
|
}
|
|
115
138
|
} else {
|
|
116
139
|
const entitySnap = appRuntime.getSnapshot()
|
|
117
140
|
const combined = { tick: playerSnap.tick, timestamp: playerSnap.timestamp, players: playerSnap.players, entities: entitySnap.entities }
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
broadcastEntityMap = entityMap
|
|
126
|
-
connections.broadcast(MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
|
|
141
|
+
const prevMap = (isKeyframe || broadcastEntityMap.size === 0) ? new Map() : broadcastEntityMap
|
|
142
|
+
const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, prevMap)
|
|
143
|
+
broadcastEntityMap = entityMap
|
|
144
|
+
const data = pack({ type: MSG.SNAPSHOT, payload: { seq: snapshotSeq, ...encoded } })
|
|
145
|
+
for (const player of players) {
|
|
146
|
+
if (!isKeyframe && player.id % snapGroups !== curGroup) continue
|
|
147
|
+
connections.sendPacked(player.id, data, snapUnreliable)
|
|
127
148
|
}
|
|
128
149
|
}
|
|
129
150
|
}
|
|
151
|
+
|
|
130
152
|
for (const id of playerEntityMaps.keys()) {
|
|
131
153
|
if (!playerManager.getPlayer(id)) playerEntityMaps.delete(id)
|
|
132
154
|
}
|
|
133
155
|
const t5 = performance.now()
|
|
134
|
-
try {
|
|
135
|
-
appRuntime._drainReloadQueue()
|
|
136
|
-
} catch (e) {
|
|
137
|
-
console.error('[TickHandler] reload queue error:', e.message)
|
|
138
|
-
}
|
|
156
|
+
try { appRuntime._drainReloadQueue() } catch (e) { console.error('[TickHandler] reload queue error:', e.message) }
|
|
139
157
|
profileLog++
|
|
140
158
|
if (profileLog % 1280 === 0) {
|
|
141
159
|
const total = t5 - t0
|
|
@@ -144,7 +162,7 @@ export function createTickHandler(deps) {
|
|
|
144
162
|
const rss = (mem.rss / 1048576).toFixed(1)
|
|
145
163
|
const ext = (mem.external / 1048576).toFixed(1)
|
|
146
164
|
const ab = (mem.arrayBuffers / 1048576).toFixed(1)
|
|
147
|
-
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 (_) {}
|
|
165
|
+
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 (_) {}
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
}
|
package/src/sdk/server.js
CHANGED
|
@@ -171,7 +171,8 @@ export async function createServer(config = {}) {
|
|
|
171
171
|
connections,
|
|
172
172
|
movement,
|
|
173
173
|
stageLoader,
|
|
174
|
-
eventLog
|
|
174
|
+
eventLog,
|
|
175
|
+
getRelevanceRadius: () => ctx.currentWorldDef?.relevanceRadius || 0
|
|
175
176
|
}))
|
|
176
177
|
|
|
177
178
|
const { onClientConnect } = createConnectionHandlers(ctx)
|