oettaM123 commited on
Commit
4e2d71d
·
1 Parent(s): 9ac90c6

add passive swordmasters timing probe

Browse files
Files changed (1) hide show
  1. tools/swordmasters_timing_probe.js +366 -0
tools/swordmasters_timing_probe.js ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Passive SwordMasters.io timing probe.
3
+
4
+ It reads the local Colyseus room state and incoming damage messages only.
5
+ No game actions or network messages are sent.
6
+ */
7
+ (() => {
8
+ "use strict";
9
+
10
+ const VERSION = "0.1.0";
11
+ const DAMAGE_MESSAGE = "Server:EnemyController:getDamage";
12
+ const POLL_MS = 10;
13
+
14
+ if (window.SM_TIMING_PROBE && typeof window.SM_TIMING_PROBE.stop === "function") {
15
+ window.SM_TIMING_PROBE.stop({ quiet: true });
16
+ }
17
+
18
+ const state = {
19
+ startedAt: new Date().toISOString(),
20
+ room: null,
21
+ roomSource: "",
22
+ enemies: new Map(),
23
+ deaths: [],
24
+ respawns: [],
25
+ damagePackets: [],
26
+ hookErrors: [],
27
+ pollTimer: 0,
28
+ dispatchWrapper: null,
29
+ originalDispatchMessage: null,
30
+ };
31
+
32
+ function finite(value) {
33
+ const number = Number(value);
34
+ return Number.isFinite(number) ? number : null;
35
+ }
36
+
37
+ function median(values) {
38
+ const sorted = values.filter(Number.isFinite).slice().sort((a, b) => a - b);
39
+ if (!sorted.length) return null;
40
+ const middle = Math.floor(sorted.length / 2);
41
+ return sorted.length % 2
42
+ ? sorted[middle]
43
+ : (sorted[middle - 1] + sorted[middle]) / 2;
44
+ }
45
+
46
+ function mapEntries(mapLike) {
47
+ if (!mapLike) return [];
48
+ if (typeof mapLike.entries === "function") return Array.from(mapLike.entries());
49
+ const rows = [];
50
+ if (typeof mapLike.forEach === "function") {
51
+ mapLike.forEach((value, key) => rows.push([key, value]));
52
+ return rows;
53
+ }
54
+ return Object.entries(mapLike);
55
+ }
56
+
57
+ function readMapValue(mapLike, key) {
58
+ if (!mapLike) return null;
59
+ if (typeof mapLike.get === "function") {
60
+ const value = mapLike.get(key);
61
+ if (value) return value;
62
+ }
63
+ if (mapLike[key]) return mapLike[key];
64
+ for (const [itemKey, value] of mapEntries(mapLike)) {
65
+ if (String(itemKey) === String(key)) return value;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function readField(value, names) {
71
+ if (!value) return null;
72
+ for (const name of names) {
73
+ try {
74
+ if (value[name] != null) return value[name];
75
+ } catch (_) {
76
+ // Try the next known schema field name.
77
+ }
78
+ }
79
+ try {
80
+ const json = typeof value.toJSON === "function" ? value.toJSON() : null;
81
+ if (json) {
82
+ for (const name of names) {
83
+ if (json[name] != null) return json[name];
84
+ }
85
+ }
86
+ } catch (_) {
87
+ // Detached Schema objects can throw while serializing.
88
+ }
89
+ return null;
90
+ }
91
+
92
+ function enemySnapshot(enemyId, enemy) {
93
+ return {
94
+ enemyId: String(enemyId),
95
+ enemyType: finite(readField(enemy, ["type", "enemyType", "enemyTypeId", "enemyTypeIndex"])),
96
+ hp: finite(readField(enemy, ["health", "hp", "currentHp", "currentHP"])),
97
+ maxHp: finite(readField(enemy, ["maxHealth", "maxHp", "maxHP"])),
98
+ };
99
+ }
100
+
101
+ function registerRoom(room, source) {
102
+ if (!room || !room.state) return;
103
+ state.room = room;
104
+ state.roomSource = source;
105
+ }
106
+
107
+ function findRoom() {
108
+ const candidates = [
109
+ () => window.pc && window.pc.app && window.pc.app.networkManager && window.pc.app.networkManager.room,
110
+ () => window.pc && window.pc.Application && window.pc.Application.getApplication &&
111
+ window.pc.Application.getApplication().networkManager &&
112
+ window.pc.Application.getApplication().networkManager.room,
113
+ () => window.app && window.app.networkManager && window.app.networkManager.room,
114
+ ];
115
+ for (const build of candidates) {
116
+ try {
117
+ const room = build();
118
+ if (room && room.state) return room;
119
+ } catch (_) {
120
+ // Keep trying fallback paths.
121
+ }
122
+ }
123
+ for (const key of Object.keys(window)) {
124
+ let value;
125
+ try {
126
+ value = window[key];
127
+ } catch (_) {
128
+ continue;
129
+ }
130
+ if (value && value.room && value.room.state) return value.room;
131
+ if (value && value.networkManager && value.networkManager.room && value.networkManager.room.state) {
132
+ return value.networkManager.room;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function markDeath(tracker, atMs, reason) {
139
+ if (!tracker || tracker.deathAtMs != null) return;
140
+ tracker.deathAtMs = atMs;
141
+ tracker.deathReason = reason;
142
+ state.deaths.push({
143
+ enemyId: tracker.enemyId,
144
+ enemyType: tracker.enemyType,
145
+ atMs,
146
+ reason,
147
+ });
148
+ }
149
+
150
+ function markRespawn(tracker, snapshot, atMs, reason) {
151
+ if (!tracker || tracker.deathAtMs == null) return;
152
+ const respawnMs = atMs - tracker.deathAtMs;
153
+ if (respawnMs >= 100) {
154
+ state.respawns.push({
155
+ enemyId: tracker.enemyId,
156
+ enemyType: snapshot.enemyType == null ? tracker.enemyType : snapshot.enemyType,
157
+ respawnMs,
158
+ deathAtMs: tracker.deathAtMs,
159
+ respawnAtMs: atMs,
160
+ deathReason: tracker.deathReason,
161
+ respawnReason: reason,
162
+ });
163
+ }
164
+ tracker.deathAtMs = null;
165
+ tracker.deathReason = "";
166
+ }
167
+
168
+ function observeDamage(room, message) {
169
+ registerRoom(room, "colyseus-dispatch");
170
+ const atMs = Date.now();
171
+ const enemyId = String(message && message.enemyId);
172
+ const tracker = state.enemies.get(enemyId);
173
+ const damageAmount = finite(message && message.damageAmount);
174
+ const damagers = Array.isArray(message && message.damagers)
175
+ ? message.damagers.length
176
+ : Array.from((message && message.damagers) || []).length;
177
+
178
+ state.damagePackets.push({
179
+ atMs,
180
+ enemyId,
181
+ enemyType: tracker ? tracker.enemyType : null,
182
+ damageAmount,
183
+ damagers,
184
+ });
185
+
186
+ if (tracker && tracker.hp != null && damageAmount != null && damageAmount >= tracker.hp) {
187
+ markDeath(tracker, atMs, "lethal-damage-packet");
188
+ }
189
+ }
190
+
191
+ function pollRoomState() {
192
+ if (!state.room) registerRoom(findRoom(), "window-scan");
193
+ const enemies = state.room && state.room.state && state.room.state.enemies;
194
+ if (!enemies) return;
195
+
196
+ const atMs = Date.now();
197
+ const visible = new Set();
198
+ for (const [enemyId, enemy] of mapEntries(enemies)) {
199
+ const snapshot = enemySnapshot(enemyId, enemy);
200
+ visible.add(snapshot.enemyId);
201
+ let tracker = state.enemies.get(snapshot.enemyId);
202
+ if (!tracker) {
203
+ tracker = {
204
+ ...snapshot,
205
+ present: true,
206
+ deathAtMs: null,
207
+ deathReason: "",
208
+ };
209
+ state.enemies.set(snapshot.enemyId, tracker);
210
+ continue;
211
+ }
212
+
213
+ const wasPresent = tracker.present;
214
+ const previousHp = tracker.hp;
215
+ if (previousHp != null && previousHp > 0 && snapshot.hp != null && snapshot.hp <= 0) {
216
+ markDeath(tracker, atMs, "hp-zero");
217
+ }
218
+ if (
219
+ snapshot.hp != null &&
220
+ snapshot.hp > 0 &&
221
+ (
222
+ !wasPresent ||
223
+ (previousHp != null && snapshot.hp > previousHp) ||
224
+ (previousHp != null && previousHp <= 0)
225
+ )
226
+ ) {
227
+ markRespawn(tracker, snapshot, atMs, !wasPresent ? "enemy-readded" : "hp-reset");
228
+ }
229
+
230
+ Object.assign(tracker, snapshot, { present: true });
231
+ }
232
+
233
+ for (const tracker of state.enemies.values()) {
234
+ if (tracker.present && !visible.has(tracker.enemyId)) {
235
+ tracker.present = false;
236
+ markDeath(tracker, atMs, "enemy-removed");
237
+ }
238
+ }
239
+ }
240
+
241
+ function inventorySummary() {
242
+ const room = state.room;
243
+ const inventory = room && room.state && readMapValue(room.state.playerInventories, room.sessionId);
244
+ const attackSpeed = finite(readField(inventory, ["attackSpeed"]));
245
+ return {
246
+ sessionId: room && room.sessionId || "",
247
+ attackSpeed,
248
+ attackIntervalMs: attackSpeed && attackSpeed > 0 ? 1000 / attackSpeed : null,
249
+ };
250
+ }
251
+
252
+ function packetRows() {
253
+ const byEnemy = new Map();
254
+ for (const packet of state.damagePackets) {
255
+ const key = `${packet.enemyType}:${packet.enemyId}`;
256
+ if (!byEnemy.has(key)) byEnemy.set(key, []);
257
+ byEnemy.get(key).push(packet);
258
+ }
259
+ const byType = new Map();
260
+ for (const packets of byEnemy.values()) {
261
+ packets.sort((a, b) => a.atMs - b.atMs);
262
+ for (let index = 1; index < packets.length; index += 1) {
263
+ const interval = packets[index].atMs - packets[index - 1].atMs;
264
+ if (interval <= 0 || interval > 900) continue;
265
+ const type = packets[index].enemyType;
266
+ if (!byType.has(type)) byType.set(type, []);
267
+ byType.get(type).push(interval);
268
+ }
269
+ }
270
+ return Array.from(byType, ([enemyType, intervals]) => ({
271
+ enemyType,
272
+ shortPacketIntervals: intervals.length,
273
+ shortPacketIntervalMedianMs: median(intervals),
274
+ })).sort((a, b) => Number(a.enemyType) - Number(b.enemyType));
275
+ }
276
+
277
+ function respawnRows() {
278
+ const byType = new Map();
279
+ for (const row of state.respawns) {
280
+ if (!byType.has(row.enemyType)) byType.set(row.enemyType, []);
281
+ byType.get(row.enemyType).push(row.respawnMs);
282
+ }
283
+ return Array.from(byType, ([enemyType, values]) => ({
284
+ enemyType,
285
+ respawns: values.length,
286
+ respawnMedianMs: median(values),
287
+ respawnMinMs: Math.min(...values),
288
+ respawnMaxMs: Math.max(...values),
289
+ likelyWholeSeconds: Math.round(median(values) / 1000),
290
+ })).sort((a, b) => Number(a.enemyType) - Number(b.enemyType));
291
+ }
292
+
293
+ function summary() {
294
+ pollRoomState();
295
+ const result = {
296
+ version: VERSION,
297
+ startedAt: state.startedAt,
298
+ roomFound: !!state.room,
299
+ roomSource: state.roomSource,
300
+ inventory: inventorySummary(),
301
+ enemiesTracked: state.enemies.size,
302
+ damagePackets: state.damagePackets.length,
303
+ deaths: state.deaths.length,
304
+ respawns: state.respawns.length,
305
+ attackValidation: packetRows(),
306
+ respawnTimings: respawnRows(),
307
+ recentRespawns: state.respawns.slice(-12),
308
+ hookErrors: state.hookErrors.slice(),
309
+ };
310
+ console.log("Inventory attack speed is the exact client cooldown input. Respawn rows use passive room-state reset timings.");
311
+ console.table(result.attackValidation);
312
+ console.table(result.respawnTimings);
313
+ console.table(result.recentRespawns);
314
+ return result;
315
+ }
316
+
317
+ function reset() {
318
+ state.enemies.clear();
319
+ state.deaths.length = 0;
320
+ state.respawns.length = 0;
321
+ state.damagePackets.length = 0;
322
+ pollRoomState();
323
+ console.log("SM timing probe reset.");
324
+ }
325
+
326
+ function stop(options = {}) {
327
+ if (state.pollTimer) window.clearInterval(state.pollTimer);
328
+ state.pollTimer = 0;
329
+ const roomProto = window.Colyseus && window.Colyseus.Room && window.Colyseus.Room.prototype;
330
+ if (roomProto && roomProto.dispatchMessage === state.dispatchWrapper && state.originalDispatchMessage) {
331
+ roomProto.dispatchMessage = state.originalDispatchMessage;
332
+ }
333
+ if (!options.quiet) console.log("SM timing probe stopped.");
334
+ }
335
+
336
+ function install() {
337
+ const roomProto = window.Colyseus && window.Colyseus.Room && window.Colyseus.Room.prototype;
338
+ if (roomProto && typeof roomProto.dispatchMessage === "function") {
339
+ state.originalDispatchMessage = roomProto.dispatchMessage;
340
+ state.dispatchWrapper = function timingProbeDispatchMessage(type, message) {
341
+ try {
342
+ registerRoom(this, "colyseus-dispatch");
343
+ if (String(type) === DAMAGE_MESSAGE) observeDamage(this, message);
344
+ } catch (error) {
345
+ state.hookErrors.push(String(error && error.message || error));
346
+ }
347
+ return state.originalDispatchMessage.apply(this, arguments);
348
+ };
349
+ roomProto.dispatchMessage = state.dispatchWrapper;
350
+ }
351
+ registerRoom(findRoom(), "window-scan");
352
+ state.pollTimer = window.setInterval(pollRoomState, POLL_MS);
353
+ pollRoomState();
354
+ }
355
+
356
+ window.SM_TIMING_PROBE = {
357
+ version: VERSION,
358
+ summary,
359
+ reset,
360
+ stop,
361
+ state,
362
+ };
363
+
364
+ install();
365
+ console.log("SM timing probe running. Stay on one mob for several kills, then run SM_TIMING_PROBE.summary().");
366
+ })();