h1z1-server 0.48.0 → 0.48.1-1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h1z1-server",
3
- "version": "0.48.0",
3
+ "version": "0.48.1-1",
4
4
  "description": "Library for emulating h1z1 servers",
5
5
  "author": "Quentin Gruber <quentingruber@gmail.com> (http://github.com/quentingruber)",
6
6
  "license": "GPL-3.0-only",
@@ -10,6 +10,9 @@ export class GridCell {
10
10
  width: number;
11
11
  height: number;
12
12
  availableScrap: number;
13
+ /** Timestamp of the last time an entity was added to this cell.
14
+ * Used to skip re-scanning cells that haven't changed since the client's last heavy-scan pass. */
15
+ lastModified = 0;
13
16
  constructor(
14
17
  server: ZoneServer2016,
15
18
  x: number,
@@ -42,6 +42,8 @@ export class ExplosiveEntity extends BaseLightweightCharacter {
42
42
 
43
43
  isAwaitingExplosion: boolean = false;
44
44
 
45
+ isArmed: boolean = false;
46
+
45
47
  constructor(
46
48
  characterId: string,
47
49
  transientId: number,
@@ -109,6 +111,7 @@ export class ExplosiveEntity extends BaseLightweightCharacter {
109
111
  async arm(server: ZoneServer2016) {
110
112
  // Wait 10 seconds before activating the trap
111
113
  await scheduler.wait(10_000);
114
+ this.isArmed = true;
112
115
  server.aiManager.addEntity(this);
113
116
  }
114
117
 
@@ -204,7 +204,6 @@ export class WaterSource extends TaskProp {
204
204
  weaponId = weapon?.itemDefinitionId,
205
205
  activatableItems = [
206
206
  Items.WEAPON_WRENCH,
207
- Items.WEAPON_BRANCH,
208
207
  Items.WEAPON_HAMMER,
209
208
  Items.WEAPON_HAMMER_DEMOLITION
210
209
  ];
@@ -113,6 +113,7 @@ export class AiManager {
113
113
  for (const cell of cells) {
114
114
  for (const obj of cell.objects) {
115
115
  if (!(obj instanceof ExplosiveEntity)) continue;
116
+ if (!obj.isArmed) continue;
116
117
  if (
117
118
  isPosInRadiusWithY(
118
119
  EXPLOSIVE_TRIGGER_RADIUS,
@@ -208,13 +208,16 @@ export class WorldDataManager {
208
208
  }
209
209
 
210
210
  async fetchWorldData(): Promise<FetchedWorldData> {
211
- const vehicles = (await this.loadVehiclesData()) as FullVehicleSaveData[];
212
- const constructionParents =
213
- (await this.loadConstructionData()) as ConstructionParentSaveData[];
214
- const freeplace =
215
- (await this.loadWorldFreeplaceConstruction()) as LootableConstructionSaveData[];
216
- const crops = (await this.loadCropData()) as PlantingDiameterSaveData[];
217
- const traps = (await this.loadTrapData()) as TrapSaveData[];
211
+ const [vehicles, constructionParents, freeplace, crops, traps] =
212
+ await Promise.all([
213
+ this.loadVehiclesData() as Promise<FullVehicleSaveData[]>,
214
+ this.loadConstructionData() as Promise<ConstructionParentSaveData[]>,
215
+ this.loadWorldFreeplaceConstruction() as Promise<
216
+ LootableConstructionSaveData[]
217
+ >,
218
+ this.loadCropData() as Promise<PlantingDiameterSaveData[]>,
219
+ this.loadTrapData() as Promise<TrapSaveData[]>
220
+ ]);
218
221
  debug("World fetched!");
219
222
  return {
220
223
  constructionParents,
@@ -255,27 +258,28 @@ export class WorldDataManager {
255
258
  }
256
259
 
257
260
  async deleteWorld() {
258
- await this.deleteServerData();
259
- await this.deleteCharacters();
261
+ await Promise.all([this.deleteServerData(), this.deleteCharacters()]);
260
262
  debug("World deleted!");
261
263
  }
262
264
 
263
265
  async saveWorld(world: WorldArg) {
264
266
  console.time("WDM: saveWorld");
265
- await this.saveVehicles(
266
- world.vehicles.filter(
267
- (vehicle) =>
268
- ![VehicleIds.SPECTATE, VehicleIds.PARACHUTE].includes(
269
- vehicle.vehicleId
270
- )
271
- )
272
- );
273
- await this.saveServerData(world.lastGuidItem);
274
- await this.saveCharacters(world.characters);
275
- await this.saveConstructionData(world.constructions);
276
- await this.saveWorldFreeplaceConstruction(world.worldConstructions);
277
- await this.saveCropData(world.crops);
278
- await this.saveTrapData(world.traps);
267
+ await Promise.all([
268
+ this.saveVehicles(
269
+ world.vehicles.filter(
270
+ (vehicle) =>
271
+ ![VehicleIds.SPECTATE, VehicleIds.PARACHUTE].includes(
272
+ vehicle.vehicleId
273
+ )
274
+ )
275
+ ),
276
+ this.saveServerData(world.lastGuidItem),
277
+ this.saveCharacters(world.characters),
278
+ this.saveConstructionData(world.constructions),
279
+ this.saveWorldFreeplaceConstruction(world.worldConstructions),
280
+ this.saveCropData(world.crops),
281
+ this.saveTrapData(world.traps)
282
+ ]);
279
283
  console.timeEnd("WDM: saveWorld");
280
284
  }
281
285
 
@@ -228,20 +228,26 @@ export class WorldObjectManager {
228
228
  if (this.gridScrapLimitEnabled) {
229
229
  this.refillScrapInChunks(server);
230
230
  }
231
+ const lootSpan = transaction?.startSpan("createLoot");
231
232
  await this.createLootThreaded(server);
232
233
  await this.createContainerLootThreaded(server);
234
+ lootSpan?.end();
233
235
  this._lastLootRespawnTime = Date.now();
234
236
  server.divideLargeCells(700);
235
237
  }
236
238
  if (this._lastNpcRespawnTime + this.npcRespawnTimer <= Date.now()) {
239
+ const npcSpan = transaction?.startSpan("createNpcs");
237
240
  await this.createNpcsThreaded(server);
241
+ npcSpan?.end();
238
242
  this._lastNpcRespawnTime = Date.now();
239
243
  }
240
244
  if (
241
245
  this._lastVehicleRespawnTime + this.vehicleRespawnTimer <=
242
246
  Date.now()
243
247
  ) {
248
+ const vehicleSpan = transaction?.startSpan("createVehicles");
244
249
  this.createVehicles(server);
250
+ vehicleSpan?.end();
245
251
  this._lastVehicleRespawnTime = Date.now();
246
252
  }
247
253
  if (
@@ -256,7 +262,9 @@ export class WorldObjectManager {
256
262
  }
257
263
 
258
264
  if (server.isSurvival()) {
265
+ const despawnSpan = transaction?.startSpan("despawnEntities");
259
266
  await this.despawnEntities(server);
267
+ despawnSpan?.end();
260
268
  }
261
269
  } finally {
262
270
  transaction.end();
@@ -1201,45 +1209,50 @@ export class WorldObjectManager {
1201
1209
  "WorldObjectManager::createVehicles",
1202
1210
  "custom"
1203
1211
  );
1204
- if (_.size(server._vehicles) >= this.vehicleSpawnCap) return;
1205
- const respawnAmount = Math.ceil(
1206
- (this.vehicleSpawnCap - _.size(server._vehicles)) / 8
1207
- );
1208
- for (let x = 0; x < respawnAmount; x++) {
1209
- const dataVehicle =
1210
- Z1_vehicles[randomIntFromInterval(0, Z1_vehicles.length - 1)];
1211
- let spawn = true;
1212
- Object.values(server._vehicles).forEach((spawnedVehicle: Vehicle2016) => {
1213
- if (!spawn) return;
1214
- if (
1215
- isPosInRadius(
1216
- this.vehicleSpawnRadius,
1217
- dataVehicle.position,
1218
- spawnedVehicle.state.position
1219
- )
1220
- ) {
1221
- spawn = false;
1212
+ try {
1213
+ if (_.size(server._vehicles) >= this.vehicleSpawnCap) return;
1214
+ const respawnAmount = Math.ceil(
1215
+ (this.vehicleSpawnCap - _.size(server._vehicles)) / 8
1216
+ );
1217
+ for (let x = 0; x < respawnAmount; x++) {
1218
+ const dataVehicle =
1219
+ Z1_vehicles[randomIntFromInterval(0, Z1_vehicles.length - 1)];
1220
+ let spawn = true;
1221
+ Object.values(server._vehicles).forEach(
1222
+ (spawnedVehicle: Vehicle2016) => {
1223
+ if (!spawn) return;
1224
+ if (
1225
+ isPosInRadius(
1226
+ this.vehicleSpawnRadius,
1227
+ dataVehicle.position,
1228
+ spawnedVehicle.state.position
1229
+ )
1230
+ ) {
1231
+ spawn = false;
1232
+ }
1233
+ }
1234
+ );
1235
+ if (!spawn) {
1236
+ continue;
1222
1237
  }
1223
- });
1224
- if (!spawn) {
1225
- continue;
1238
+ const characterId = generateRandomGuid(),
1239
+ vehicleData = new Vehicle2016(
1240
+ characterId,
1241
+ server.getTransientId(characterId),
1242
+ 0,
1243
+ new Float32Array(dataVehicle.position),
1244
+ new Float32Array(dataVehicle.rotation),
1245
+ server,
1246
+ getCurrentServerTimeWrapper().getTruncatedU32(),
1247
+ dataVehicle.vehicleId
1248
+ );
1249
+ vehicleData.positionUpdate.orientation = dataVehicle.orientation;
1250
+ this.createVehicle(server, vehicleData, maxSpawnChance); // save vehicle
1226
1251
  }
1227
- const characterId = generateRandomGuid(),
1228
- vehicleData = new Vehicle2016(
1229
- characterId,
1230
- server.getTransientId(characterId),
1231
- 0,
1232
- new Float32Array(dataVehicle.position),
1233
- new Float32Array(dataVehicle.rotation),
1234
- server,
1235
- getCurrentServerTimeWrapper().getTruncatedU32(),
1236
- dataVehicle.vehicleId
1237
- );
1238
- vehicleData.positionUpdate.orientation = dataVehicle.orientation;
1239
- this.createVehicle(server, vehicleData, maxSpawnChance); // save vehicle
1252
+ } finally {
1253
+ transaction.end();
1254
+ debug("All vehicles created");
1240
1255
  }
1241
- transaction.end();
1242
- debug("All vehicles created");
1243
1256
  }
1244
1257
 
1245
1258
  private async createNpcs(server: ZoneServer2016) {
@@ -168,6 +168,7 @@ import { LoadoutItem } from "./classes/loadoutItem";
168
168
  import { BaseItem } from "./classes/baseItem";
169
169
  import { Collection } from "mongodb";
170
170
  import { ItemObject } from "./entities/itemobject";
171
+ import { ExplosiveEntity } from "./entities/explosiveentity";
171
172
 
172
173
  function getStanceFlags(num: number): StanceFlags {
173
174
  function getBit(bin: string, bit: number) {
@@ -1500,20 +1501,10 @@ export class ZonePacketHandlers {
1500
1501
  if (stance) {
1501
1502
  const stanceFlags = getStanceFlags(stance);
1502
1503
 
1503
- if (stanceFlags.SITTING) {
1504
- server.sendData<CommandRunSpeed>(client, "Command.RunSpeed", {
1505
- runSpeed: 0.1
1506
- });
1507
- setTimeout(() => {
1508
- server.sendData<CommandRunSpeed>(client, "Command.RunSpeed", {
1509
- runSpeed: 0
1510
- });
1511
- }, 2000);
1512
- }
1513
-
1514
1504
  // Detect movements based on stance
1515
1505
  server.fairPlayManager.detectJumpXSMovement(server, client, stanceFlags);
1516
1506
  server.fairPlayManager.detectDroneMovement(server, client, stanceFlags);
1507
+ server.detectSnaking(server, client, stanceFlags);
1517
1508
  server.detectEnasMovement(server, client, stanceFlags);
1518
1509
 
1519
1510
  // Handle jump logic
@@ -1622,6 +1613,20 @@ export class ZonePacketHandlers {
1622
1613
  }
1623
1614
  client.character.state.position = position;
1624
1615
 
1616
+ // Check if player stepped on an armed landmine
1617
+ if (server.aiManager.explosiveEntities.size > 0) {
1618
+ const landmineCells = server.getGridCellsInRadius(position, 0.6);
1619
+ for (const cell of landmineCells) {
1620
+ for (const obj of cell.objects) {
1621
+ if (!(obj instanceof ExplosiveEntity)) continue;
1622
+ if (!obj.isArmed) continue;
1623
+ if (isPosInRadiusWithY(0.6, position, obj.state.position, 0.5)) {
1624
+ obj.detonate(client.character.characterId);
1625
+ }
1626
+ }
1627
+ }
1628
+ }
1629
+
1625
1630
  // Stop HUD timer if position is out of radius
1626
1631
  if (
1627
1632
  client.hudTimer &&
@@ -13,6 +13,7 @@
13
13
 
14
14
  const debugName = "ZoneServer",
15
15
  debug = require("debug")(debugName);
16
+ const apm = require("elastic-apm-node");
16
17
 
17
18
  import { EventEmitter } from "node:events";
18
19
  import { H1Z1Protocol } from "../../protocols/h1z1protocol";
@@ -762,26 +763,57 @@ export class ZoneServer2016 extends EventEmitter {
762
763
  // // If there is a lot of packet to process, it's better, if there is none then we only add like some µsec
763
764
  // await scheduler.yield();
764
765
  // }
765
- const packet = this._protocol.parse(data, flags);
766
766
 
767
- // for reversing new packets
768
- /*
769
- if(
770
- packet?.name == ""
771
- ) {
772
- let hexString = '';
773
- for (let i = 0; i < data.length; i++) {
774
- const byte = data[i].toString(16).padStart(2, '0');
775
- hexString += byte + ' ';
776
- }
777
- console.log(`<Buffer ${hexString.trim()}>`);
778
- }
779
- */
767
+ // Position broadcasts are relayed as-is and fire hundreds of times per second —
768
+ // not worth the per-packet transaction overhead.
769
+ const isPositionUpdate = flags === GatewayChannels.UpdatePosition;
770
+ let transaction = isPositionUpdate
771
+ ? null
772
+ : apm.startTransaction("PacketReceive", "game");
780
773
 
781
- if (packet) {
782
- this.onZoneDataEvent(this._clients[soeClientSessionId], packet);
783
- } else {
784
- debug("zonefailed : ", data);
774
+ try {
775
+ const parseSpan = transaction?.startSpan("parse");
776
+ const packet = this._protocol.parse(data, flags);
777
+ parseSpan?.end();
778
+
779
+ // for reversing new packets
780
+ /*
781
+ if(
782
+ packet?.name == ""
783
+ ) {
784
+ let hexString = '';
785
+ for (let i = 0; i < data.length; i++) {
786
+ const byte = data[i].toString(16).padStart(2, '0');
787
+ hexString += byte + ' ';
788
+ }
789
+ console.log(`<Buffer ${hexString.trim()}>`);
790
+ }
791
+ */
792
+
793
+ if (packet) {
794
+ if (transaction) {
795
+ if (packet.name === "PlayerUpdateManagedPosition") {
796
+ // Same reasoning as UpdatePosition flag — high-frequency relay,
797
+ // not worth the per-packet transaction overhead.
798
+ transaction.end();
799
+ transaction = null;
800
+ } else {
801
+ transaction.name = `Packet::${packet.name}`;
802
+ const client = this._clients[soeClientSessionId];
803
+ if (client?.character?.characterId) {
804
+ transaction.addLabels({
805
+ character_id: client.character.characterId,
806
+ packet_name: packet.name
807
+ });
808
+ }
809
+ }
810
+ }
811
+ this.onZoneDataEvent(this._clients[soeClientSessionId], packet);
812
+ } else {
813
+ debug("zonefailed : ", data);
814
+ }
815
+ } finally {
816
+ transaction?.end();
785
817
  }
786
818
  }
787
819
  );
@@ -1225,12 +1257,15 @@ export class ZoneServer2016 extends EventEmitter {
1225
1257
  }
1226
1258
 
1227
1259
  try {
1260
+ const processSpan = apm.currentTransaction?.startSpan("processPacket");
1228
1261
  this._packetHandlers.processPacket(
1229
1262
  this,
1230
1263
  client,
1231
1264
  packet as ReceivedPacket<any>
1232
1265
  );
1266
+ processSpan?.end();
1233
1267
  } catch (error) {
1268
+ apm.currentTransaction?.setOutcome("failure");
1234
1269
  console.error(error);
1235
1270
  console.error(`An error occurred while processing a packet : `, packet);
1236
1271
  logVersion();
@@ -2649,6 +2684,7 @@ export class ZoneServer2016 extends EventEmitter {
2649
2684
  pz <= cell.position[2] + cell.height
2650
2685
  ) {
2651
2686
  cell.objects.push(obj);
2687
+ cell.lastModified = Date.now();
2652
2688
  setImmediate(() => this.onEntityAddedToCell(obj, cell));
2653
2689
  break;
2654
2690
  }
@@ -2696,6 +2732,7 @@ export class ZoneServer2016 extends EventEmitter {
2696
2732
  const gridCell = this._gridLookup[col * this._gridNumCols + row];
2697
2733
  if (!gridCell || gridCell.objects.includes(obj)) return;
2698
2734
  gridCell.objects.push(obj);
2735
+ gridCell.lastModified = Date.now();
2699
2736
  setImmediate(() => this.onEntityAddedToCell(obj, gridCell));
2700
2737
  }
2701
2738
 
@@ -2726,14 +2763,22 @@ export class ZoneServer2016 extends EventEmitter {
2726
2763
 
2727
2764
  private async worldRoutine() {
2728
2765
  if (!this.hookManager.checkHook("OnWorldRoutine")) return;
2729
- else {
2766
+ const transaction = apm.startTransaction("worldRoutine", "game");
2767
+ try {
2730
2768
  if (this._ready) {
2769
+ const plantSpan = transaction?.startSpan("plantManager");
2731
2770
  this.constructionManager.plantManager(this);
2771
+ plantSpan?.end();
2732
2772
  await scheduler.yield();
2773
+
2733
2774
  await this.worldObjectManager.run(this);
2734
2775
  await scheduler.yield();
2776
+
2777
+ const vehicleSpan = transaction?.startSpan("checkVehiclesInMapBounds");
2735
2778
  this.checkVehiclesInMapBounds();
2779
+ vehicleSpan?.end();
2736
2780
  await scheduler.yield();
2781
+
2737
2782
  this.updateSyncTeleport();
2738
2783
  await scheduler.yield();
2739
2784
  this.updateSpectatorMap();
@@ -2745,8 +2790,10 @@ export class ZoneServer2016 extends EventEmitter {
2745
2790
  this.saveWorld();
2746
2791
  }
2747
2792
  }
2793
+ } finally {
2794
+ transaction?.end();
2795
+ this.worldRoutineTimer.refresh();
2748
2796
  }
2749
- this.worldRoutineTimer.refresh();
2750
2797
  }
2751
2798
 
2752
2799
  async deleteClient(client: Client) {
@@ -4916,7 +4963,9 @@ export class ZoneServer2016 extends EventEmitter {
4916
4963
  default:
4917
4964
  debug("send data", packetName);
4918
4965
  }
4966
+ const packSpan = apm.currentTransaction?.startSpan(`pack::${packetName}`);
4919
4967
  const data = this._protocol.pack(packetName, obj);
4968
+ packSpan?.end();
4920
4969
  if (data) {
4921
4970
  this._gatewayServer.sendTunnelData(client.soeClientId, data, channel);
4922
4971
  }
@@ -9274,6 +9323,19 @@ export class ZoneServer2016 extends EventEmitter {
9274
9323
  }
9275
9324
  }
9276
9325
 
9326
+ detectSnaking(
9327
+ server: ZoneServer2016,
9328
+ client: Client,
9329
+ stanceFlags: StanceFlags
9330
+ ) {
9331
+ if (stanceFlags.SITTING) {
9332
+ server.multiplyMovementModifier(client, 0.2);
9333
+ setTimeout(() => {
9334
+ server.divideMovementModifier(client, 0.2);
9335
+ }, 2000);
9336
+ }
9337
+ }
9338
+
9277
9339
  //#endregion
9278
9340
 
9279
9341
  async reloadZonePacketHandlers() {
@@ -9405,29 +9467,39 @@ export class ZoneServer2016 extends EventEmitter {
9405
9467
  }
9406
9468
 
9407
9469
  /** Runs the world-state update for a single client based on their current
9408
- * known position. Called from the server tick — NOT from packet handlers. */
9409
- private runClientTick(client: Client) {
9410
- if (client.isLoading) return;
9470
+ * known position. Called from the server tick — NOT from packet handlers. */
9471
+ private runClientTick(client: Client): [number, number, number] {
9472
+ if (client.isLoading) return [0, 0, 0];
9411
9473
  const pos = client.character.state.position;
9474
+ let constPermMs = 0,
9475
+ heavyScanMs = 0,
9476
+ spawnCharsMs = 0;
9412
9477
 
9413
9478
  // Construction permissions: run when player has moved 3+ units
9414
9479
  if (getDistance2d(pos, client.posAtLastPermissionCheck) >= 3) {
9480
+ const t = Date.now();
9415
9481
  this.constructionManager.constructionPermissionsManager(this, client);
9482
+ constPermMs = Date.now() - t;
9416
9483
  client.posAtLastPermissionCheck = pos.slice() as Float32Array;
9417
9484
  }
9418
9485
 
9419
9486
  // Heavy world scans: run when player has moved 10+ units
9420
9487
  if (getDistance2d(pos, client.posAtLastRoutine) >= 10) {
9488
+ const t = Date.now();
9421
9489
  if (!this.disableMapBoundsCheck) this.checkInMapBounds(client);
9422
9490
  this.assignChunkRenderDistance(client);
9423
9491
  if (!this.disablePOIManager) this.POIManager(client);
9424
9492
  this.vehicleManager(client);
9425
9493
 
9426
- // Spawn any entities in already-subscribed cells that weren't in render
9427
- // range when first subscribed e.g. NPCs that spawned nearby.
9494
+ // Re-scan only cells that received new entities since this client's last
9495
+ // heavy-scan pass. Cells unchanged since then are skipped entirely, keeping
9496
+ // this O(dirty cells) instead of O(all subscribed cells × all objects).
9428
9497
  for (const cell of client.subscribedCells) {
9429
- this.spawnCellObjectsForClient(client, cell);
9498
+ if (cell.lastModified > client.lastRoutineTime) {
9499
+ this.spawnCellObjectsForClient(client, cell);
9500
+ }
9430
9501
  }
9502
+ heavyScanMs = Date.now() - t;
9431
9503
 
9432
9504
  client.posAtLastRoutine = pos.slice() as Float32Array;
9433
9505
  client.lastRoutineTime = Date.now();
@@ -9441,7 +9513,11 @@ export class ZoneServer2016 extends EventEmitter {
9441
9513
  void this.updateClientSubscriptions(client);
9442
9514
  }
9443
9515
 
9516
+ const t = Date.now();
9444
9517
  this.spawnCharacters(client);
9518
+ spawnCharsMs = Date.now() - t;
9519
+
9520
+ return [constPermMs, heavyScanMs, spawnCharsMs];
9445
9521
  }
9446
9522
 
9447
9523
  private _worldTickTimer?: NodeJS.Timeout;
@@ -9465,17 +9541,56 @@ export class ZoneServer2016 extends EventEmitter {
9465
9541
  }
9466
9542
  }
9467
9543
 
9544
+ private _lastTickTime = 0;
9545
+
9468
9546
  startWorldTick() {
9469
9547
  const tick = () => {
9470
9548
  if (!this._ready) {
9549
+ this._lastTickTime = Date.now();
9471
9550
  this._worldTickTimer = setTimeout(tick, ZoneServer2016.WORLD_TICK_MS);
9472
9551
  return;
9473
9552
  }
9474
- this._rebuildSpatialMaps();
9475
- for (const sessionId in this._clients) {
9476
- this.runClientTick(this._clients[sessionId]);
9553
+ const now = Date.now();
9554
+ const tickLagMs = this._lastTickTime
9555
+ ? Math.max(0, now - this._lastTickTime - ZoneServer2016.WORLD_TICK_MS)
9556
+ : 0;
9557
+ this._lastTickTime = now;
9558
+
9559
+ const transaction = apm.startTransaction("WorldTick", "game");
9560
+ try {
9561
+ transaction?.addLabels({
9562
+ client_count: Object.keys(this._clients).length,
9563
+ npc_count: Object.keys(this._npcs).length,
9564
+ vehicle_count: Object.keys(this._vehicles).length,
9565
+ loot_count: Object.keys(this._lootableProps).length,
9566
+ construction_count: Object.keys(this._constructionSimple).length,
9567
+ tick_lag_ms: tickLagMs
9568
+ });
9569
+
9570
+ const spatialSpan = transaction?.startSpan("rebuildSpatialMaps");
9571
+ this._rebuildSpatialMaps();
9572
+ spatialSpan?.end();
9573
+
9574
+ const clientTickSpan = transaction?.startSpan("clientTick");
9575
+ let constPermMs = 0,
9576
+ heavyScanMs = 0,
9577
+ spawnCharsMs = 0;
9578
+ for (const sessionId in this._clients) {
9579
+ const [cp, hs, sc] = this.runClientTick(this._clients[sessionId]);
9580
+ constPermMs += cp;
9581
+ heavyScanMs += hs;
9582
+ spawnCharsMs += sc;
9583
+ }
9584
+ clientTickSpan?.addLabels({
9585
+ constPerm_ms: constPermMs,
9586
+ heavyScan_ms: heavyScanMs,
9587
+ spawnChars_ms: spawnCharsMs
9588
+ });
9589
+ clientTickSpan?.end();
9590
+ } finally {
9591
+ transaction?.end();
9592
+ this._worldTickTimer = setTimeout(tick, ZoneServer2016.WORLD_TICK_MS);
9477
9593
  }
9478
- this._worldTickTimer = setTimeout(tick, ZoneServer2016.WORLD_TICK_MS);
9479
9594
  };
9480
9595
  this._worldTickTimer = setTimeout(tick, ZoneServer2016.WORLD_TICK_MS);
9481
9596
  }