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 +1 -1
- package/src/servers/ZoneServer2016/classes/gridcell.ts +3 -0
- package/src/servers/ZoneServer2016/entities/explosiveentity.ts +3 -0
- package/src/servers/ZoneServer2016/entities/watersource.ts +0 -1
- package/src/servers/ZoneServer2016/managers/aimanager.ts +1 -0
- package/src/servers/ZoneServer2016/managers/worlddatamanager.ts +27 -23
- package/src/servers/ZoneServer2016/managers/worldobjectmanager.ts +49 -36
- package/src/servers/ZoneServer2016/zonepackethandlers.ts +16 -11
- package/src/servers/ZoneServer2016/zoneserver.ts +145 -30
package/package.json
CHANGED
|
@@ -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
|
|
|
@@ -208,13 +208,16 @@ export class WorldDataManager {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
async fetchWorldData(): Promise<FetchedWorldData> {
|
|
211
|
-
const vehicles
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
//
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
9427
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
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
|
}
|