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.
Binary file
@@ -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: 'environment', model: './apps/tps-game/schwust.glb', position: [0, 0, 0], app: 'environment' }
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
- playerModel: './apps/tps-game/cleetus.vrm',
64
- spawnPoint: [-30, 7.6, -30]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
- if (prev !== key) entities.push(encoded)
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) })
@@ -33,7 +33,9 @@ export function createServerAPI(ctx) {
33
33
 
34
34
  async loadWorld(worldDef) {
35
35
  ctx.currentWorldDef = worldDef
36
- if (worldDef.spawnPoint) ctx.worldSpawnPoint = [...worldDef.spawnPoint]
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 sp = [...ctx.worldSpawnPoint]
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 })
@@ -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 separated = new Set()
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 = [0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)]
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
- separated.clear()
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 collisions = physicsIntegration.checkCollisionWithOthers(player.id, players)
67
- for (const collision of collisions) {
68
- const pairKey = player.id < collision.playerId ? `${player.id}-${collision.playerId}` : `${collision.playerId}-${player.id}`
69
- if (separated.has(pairKey)) continue
70
- separated.add(pairKey)
71
- const other = playerManager.getPlayer(collision.playerId)
72
- if (!other) continue
73
- const nx = collision.normal[0], nz = collision.normal[2]
74
- const minDist = physicsIntegration.config.capsuleRadius * 2
75
- const overlap = minDist - collision.distance
76
- const halfPush = overlap * 0.5
77
- const pushVel = Math.min(halfPush / dt, 3.0)
78
- player.state.position[0] -= nx * halfPush
79
- player.state.position[2] -= nz * halfPush
80
- player.state.velocity[0] -= nx * pushVel
81
- player.state.velocity[2] -= nz * pushVel
82
- other.state.position[0] += nx * halfPush
83
- other.state.position[2] += nz * halfPush
84
- other.state.velocity[0] += nx * pushVel
85
- other.state.velocity[2] += nz * pushVel
86
- physicsIntegration.setPlayerPosition(player.id, player.state.position)
87
- physicsIntegration.setPlayerPosition(other.id, other.state.position)
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
- if (stageLoader && stageLoader.getActiveStage()) {
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
- const pos = player.state.position
102
- const entitySnap = appRuntime.getSnapshotForPlayer(pos, stageLoader.getActiveStage().spatial.relevanceRadius)
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
- if (isKeyframe || !playerEntityMaps.has(player.id)) {
105
- const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, new Map())
106
- playerEntityMaps.set(player.id, entityMap)
107
- connections.send(player.id, MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
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
- if (isKeyframe || broadcastEntityMap.size === 0) {
119
- const encoded = SnapshotEncoder.encode(combined)
120
- const { entityMap } = SnapshotEncoder.encodeDelta(combined, new Map())
121
- broadcastEntityMap = entityMap
122
- connections.broadcast(MSG.SNAPSHOT, { seq: snapshotSeq, ...encoded })
123
- } else {
124
- const { encoded, entityMap } = SnapshotEncoder.encodeDelta(combined, broadcastEntityMap)
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)