speedhack.js modernization
Mspeedhack.js
| @@ -1,6 +1,6 @@ | |||
|---|---|---|---|
| 1 | 1 | // ==UserScript== | |
| 2 | 2 | // @name Speedhack Panel | |
| 3 | - | // @version 1.0.0 | |
| 3 | + | // @version 1.1.0 | |
| 4 | 4 | // @description Floating, movable, resizable, minimizable time-scaling panel with an autoclicker and a Cheat-Engine-style memory scanner. Scales the page's JS timing functions by a chosen factor. Runs inside iframes. By default only the TOP frame shows a panel and broadcasts settings to child frames; any frame can be detached for its own panel. The autoclicker clicks at the cursor and auto-targets whichever (i)frame the cursor is in. The Scan tab finds/edits values in a game's WebAssembly heap or JS object graph, and can target any (i)frame from the top panel. Only the per-URL "closed" state and saved scans are remembered. | |
| 5 | 5 | // @match *://*/* | |
| 6 | 6 | // @run-at document-start | |
| @@ -13,7 +13,7 @@ | |||
|---|---|---|---|
| 13 | 13 | /* eslint-disable no-empty */ | |
| 14 | 14 | /* eslint-disable no-unused-vars */ | |
| 15 | 15 | ||
| 16 | - | (function () { | |
| 16 | + | (() => { | |
| 17 | 17 | 'use strict'; | |
| 18 | 18 | ||
| 19 | 19 | if (window.__SPEEDHACK_PANEL__) return; // guard against double-injection in the same frame | |
| @@ -31,7 +31,7 @@ | |||
|---|---|---|---|
| 31 | 31 | * ------------------------------------------------------------------ */ | |
| 32 | 32 | const RealDate = pageWin.Date; | |
| 33 | 33 | const origDateNow = RealDate.now.bind(RealDate); | |
| 34 | - | const origPerfNow = (pageWin.performance && pageWin.performance.now) | |
| 34 | + | const origPerfNow = pageWin.performance?.now | |
| 35 | 35 | ? pageWin.performance.now.bind(pageWin.performance) | |
| 36 | 36 | : origDateNow; | |
| 37 | 37 | const origSetTimeout = pageWin.setTimeout.bind(pageWin); | |
| @@ -39,7 +39,7 @@ | |||
|---|---|---|---|
| 39 | 39 | const origClearTimeout = pageWin.clearTimeout.bind(pageWin); | |
| 40 | 40 | const origClearInterval= pageWin.clearInterval.bind(pageWin); | |
| 41 | 41 | const origRAF = (pageWin.requestAnimationFrame || | |
| 42 | - | function (cb) { return origSetTimeout(function () { cb(origPerfNow()); }, 16); } | |
| 42 | + | (cb => origSetTimeout(() => cb(origPerfNow()), 16)) | |
| 43 | 43 | ).bind(pageWin); | |
| 44 | 44 | ||
| 45 | 45 | /* ------------------------------------------------------------------ * | |
| @@ -55,55 +55,38 @@ | |||
|---|---|---|---|
| 55 | 55 | (function hookWasm() { | |
| 56 | 56 | const W = pageWin.WebAssembly; | |
| 57 | 57 | if (!W) return; | |
| 58 | - | function record(m, label) { | |
| 58 | + | const record = (m, label) => { | |
| 59 | 59 | try { | |
| 60 | 60 | if (!(m instanceof W.Memory)) return; | |
| 61 | - | for (let i = 0; i < wasmMemories.length; i++) if (wasmMemories[i].memory === m) return; | |
| 62 | - | wasmMemories.push({ memory: m, label: (label || ('mem#' + wasmMemories.length)) }); | |
| 61 | + | if (wasmMemories.some(e => e.memory === m)) return; | |
| 62 | + | wasmMemories.push({ memory: m, label: label || ('mem#' + wasmMemories.length) }); | |
| 63 | 63 | } catch (e) {} | |
| 64 | - | } | |
| 65 | - | function scanExports(res) { | |
| 64 | + | }; | |
| 65 | + | const scanExports = (res) => { | |
| 66 | 66 | // res is an instantiate result ({ module, instance }) or a bare Instance. | |
| 67 | 67 | try { | |
| 68 | - | const inst = (res && res.instance) ? res.instance : res; | |
| 69 | - | const ex = inst && inst.exports; | |
| 68 | + | const ex = (res?.instance ?? res)?.exports; | |
| 70 | 69 | if (ex) for (const k in ex) { try { if (ex[k] instanceof W.Memory) record(ex[k], k); } catch (e) {} } | |
| 71 | 70 | } catch (e) {} | |
| 72 | 71 | return res; | |
| 73 | - | } | |
| 74 | - | const origInstantiate = W.instantiate, origInstantiateStreaming = W.instantiateStreaming; | |
| 75 | - | if (typeof origInstantiate === 'function') { | |
| 76 | - | W.instantiate = function () { | |
| 77 | - | const p = origInstantiate.apply(this, arguments); | |
| 78 | - | return (p && typeof p.then === 'function') ? p.then(scanExports) : p; | |
| 79 | - | }; | |
| 80 | - | } | |
| 81 | - | if (typeof origInstantiateStreaming === 'function') { | |
| 82 | - | W.instantiateStreaming = function () { | |
| 83 | - | const p = origInstantiateStreaming.apply(this, arguments); | |
| 84 | - | return (p && typeof p.then === 'function') ? p.then(scanExports) : p; | |
| 85 | - | }; | |
| 86 | - | } | |
| 87 | - | const OrigInstance = W.Instance; | |
| 88 | - | if (typeof OrigInstance === 'function') { | |
| 89 | - | function PatchedInstance() { | |
| 90 | - | const inst = new (Function.prototype.bind.apply(OrigInstance, [null].concat(Array.prototype.slice.call(arguments))))(); | |
| 91 | - | scanExports(inst); | |
| 92 | - | return inst; | |
| 93 | - | } | |
| 94 | - | PatchedInstance.prototype = OrigInstance.prototype; | |
| 95 | - | try { W.Instance = PatchedInstance; } catch (e) {} | |
| 96 | - | } | |
| 97 | - | const OrigMemory = W.Memory; | |
| 98 | - | if (typeof OrigMemory === 'function') { | |
| 99 | - | function PatchedMemory() { | |
| 100 | - | const mem = new (Function.prototype.bind.apply(OrigMemory, [null].concat(Array.prototype.slice.call(arguments))))(); | |
| 101 | - | record(mem, 'Memory()'); | |
| 102 | - | return mem; | |
| 72 | + | }; | |
| 73 | + | const wrapInstantiate = (orig) => function () { | |
| 74 | + | const p = orig.apply(this, arguments); | |
| 75 | + | return (typeof p?.then === 'function') ? p.then(scanExports) : p; | |
| 76 | + | }; | |
| 77 | + | if (typeof W.instantiate === 'function') W.instantiate = wrapInstantiate(W.instantiate); | |
| 78 | + | if (typeof W.instantiateStreaming === 'function') W.instantiateStreaming = wrapInstantiate(W.instantiateStreaming); | |
| 79 | + | const patchCtor = (Orig, onNew) => { | |
| 80 | + | function Patched(...args) { | |
| 81 | + | const obj = Reflect.construct(Orig, args, new.target || Patched); | |
| 82 | + | onNew(obj); | |
| 83 | + | return obj; | |
| 103 | 84 | } | |
| 104 | - | PatchedMemory.prototype = OrigMemory.prototype; | |
| 105 | - | try { W.Memory = PatchedMemory; } catch (e) {} | |
| 106 | - | } | |
| 85 | + | Patched.prototype = Orig.prototype; | |
| 86 | + | return Patched; | |
| 87 | + | }; | |
| 88 | + | if (typeof W.Instance === 'function') { try { W.Instance = patchCtor(W.Instance, scanExports); } catch (e) {} } | |
| 89 | + | if (typeof W.Memory === 'function') { try { W.Memory = patchCtor(W.Memory, m => record(m, 'Memory()')); } catch (e) {} } | |
| 107 | 90 | })(); | |
| 108 | 91 | ||
| 109 | 92 | /* ------------------------------------------------------------------ * | |
| @@ -116,24 +99,24 @@ | |||
|---|---|---|---|
| 116 | 99 | * are untouched. Installed at document-start so we wrap before the game does. | |
| 117 | 100 | * `panelHost` is read at event time (set once the panel is built). | |
| 118 | 101 | * ------------------------------------------------------------------ */ | |
| 119 | - | const SHIELD_TYPES = { keydown:1, keyup:1, keypress:1, pointerdown:1, pointerup:1, | |
| 120 | - | mousedown:1, mouseup:1, click:1, dblclick:1, contextmenu:1, | |
| 121 | - | wheel:1, touchstart:1, touchend:1 }; | |
| 102 | + | const SHIELD_TYPES = new Set(['keydown', 'keyup', 'keypress', 'pointerdown', 'pointerup', | |
| 103 | + | 'mousedown', 'mouseup', 'click', 'dblclick', 'contextmenu', | |
| 104 | + | 'wheel', 'touchstart', 'touchend']); | |
| 122 | 105 | const shieldedTargets = new WeakSet(); | |
| 123 | 106 | function eventInPanel(ev) { | |
| 124 | - | try { return !!(panelHost && ev && ev.composedPath && ev.composedPath().indexOf(panelHost) !== -1); } catch (e) { return false; } | |
| 107 | + | try { return !!(panelHost && ev?.composedPath && ev.composedPath().includes(panelHost)); } catch (e) { return false; } | |
| 125 | 108 | } | |
| 126 | 109 | function shieldInputTarget(target) { | |
| 127 | 110 | if (!target || typeof target.addEventListener !== 'function' || shieldedTargets.has(target)) return; | |
| 128 | 111 | shieldedTargets.add(target); | |
| 129 | 112 | const origAdd = target.addEventListener, origRemove = target.removeEventListener; | |
| 130 | 113 | const wrappers = new WeakMap(); // handler -> { typeKey -> wrapper } | |
| 114 | + | const captureOf = (opts) => (typeof opts === 'object' && opts) ? !!opts.capture : !!opts; | |
| 131 | 115 | try { | |
| 132 | 116 | target.addEventListener = function (type, handler, opts) { | |
| 133 | 117 | const usable = handler && (typeof handler === 'function' || typeof handler.handleEvent === 'function'); | |
| 134 | - | if (!usable || !SHIELD_TYPES[type] || handler.__shxNoShield) return origAdd.call(this, type, handler, opts); | |
| 135 | - | const capture = (typeof opts === 'object' && opts) ? !!opts.capture : !!opts; | |
| 136 | - | const key = type + '/' + (capture ? 1 : 0); | |
| 118 | + | if (!usable || !SHIELD_TYPES.has(type) || handler.__shxNoShield) return origAdd.call(this, type, handler, opts); | |
| 119 | + | const key = type + '/' + (captureOf(opts) ? 1 : 0); | |
| 137 | 120 | let per = wrappers.get(handler); if (!per) { per = Object.create(null); wrappers.set(handler, per); } | |
| 138 | 121 | let wrapper = per[key]; | |
| 139 | 122 | if (!wrapper) { | |
| @@ -143,16 +126,15 @@ | |||
|---|---|---|---|
| 143 | 126 | return origAdd.call(this, type, wrapper, opts); | |
| 144 | 127 | }; | |
| 145 | 128 | target.removeEventListener = function (type, handler, opts) { | |
| 146 | - | if (!handler || !SHIELD_TYPES[type]) return origRemove.call(this, type, handler, opts); | |
| 147 | - | const capture = (typeof opts === 'object' && opts) ? !!opts.capture : !!opts; | |
| 148 | - | const per = wrappers.get(handler); const wrapper = per && per[type + '/' + (capture ? 1 : 0)]; | |
| 129 | + | if (!handler || !SHIELD_TYPES.has(type)) return origRemove.call(this, type, handler, opts); | |
| 130 | + | const wrapper = wrappers.get(handler)?.[type + '/' + (captureOf(opts) ? 1 : 0)]; | |
| 149 | 131 | return origRemove.call(this, type, wrapper || handler, opts); | |
| 150 | 132 | }; | |
| 151 | 133 | } catch (e) {} | |
| 152 | 134 | } | |
| 153 | 135 | try { shieldInputTarget(pageWin); } catch (e) {} | |
| 154 | 136 | try { shieldInputTarget(pageWin.document); } catch (e) {} | |
| 155 | - | try { shieldInputTarget(pageWin.document && pageWin.document.documentElement); } catch (e) {} | |
| 137 | + | try { shieldInputTarget(pageWin.document?.documentElement); } catch (e) {} | |
| 156 | 138 | // <body> may not exist yet at document-start; it's shielded when the panel is built. | |
| 157 | 139 | ||
| 158 | 140 | /* ------------------------------------------------------------------ * | |
| @@ -162,11 +144,11 @@ | |||
|---|---|---|---|
| 162 | 144 | * ------------------------------------------------------------------ */ | |
| 163 | 145 | const PAGE = location.href.split('#')[0]; // "exact" page URL, ignoring #hash | |
| 164 | 146 | const store = { | |
| 165 | - | get: function (k, d) { | |
| 147 | + | get(k, d) { | |
| 166 | 148 | try { if (typeof GM_getValue === 'function') return GM_getValue(k, d); } catch (e) {} | |
| 167 | - | try { var v = localStorage.getItem('shx_' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } | |
| 149 | + | try { const v = localStorage.getItem('shx_' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } | |
| 168 | 150 | }, | |
| 169 | - | set: function (k, v) { | |
| 151 | + | set(k, v) { | |
| 170 | 152 | try { if (typeof GM_setValue === 'function') { GM_setValue(k, v); return; } } catch (e) {} | |
| 171 | 153 | try { localStorage.setItem('shx_' + k, JSON.stringify(v)); } catch (e) {} | |
| 172 | 154 | } | |
| @@ -185,10 +167,10 @@ | |||
|---|---|---|---|
| 185 | 167 | function makeClock(realFn) { | |
| 186 | 168 | let aReal = realFn(), aFake = aReal, s = 1; | |
| 187 | 169 | return { | |
| 188 | - | now: function () { return aFake + (realFn() - aReal) * s; }, | |
| 189 | - | setScale: function (ns) { const r = realFn(); aFake = aFake + (r - aReal) * s; aReal = r; s = ns; }, | |
| 190 | - | reanchor: function () { const r = realFn(); aReal = r; aFake = r; }, | |
| 191 | - | setTo: function (v) { aReal = realFn(); aFake = v; } // jump the fake timeline to absolute v | |
| 170 | + | now: () => aFake + (realFn() - aReal) * s, | |
| 171 | + | setScale: (ns) => { const r = realFn(); aFake = aFake + (r - aReal) * s; aReal = r; s = ns; }, | |
| 172 | + | reanchor: () => { const r = realFn(); aReal = r; aFake = r; }, | |
| 173 | + | setTo: (v) => { aReal = realFn(); aFake = v; } // jump the fake timeline to absolute v | |
| 192 | 174 | }; | |
| 193 | 175 | } | |
| 194 | 176 | const dateClock = makeClock(origDateNow); | |
| @@ -202,7 +184,7 @@ | |||
|---|---|---|---|
| 202 | 184 | dateClock.setScale(ns); | |
| 203 | 185 | perfClock.setScale(ns); | |
| 204 | 186 | } | |
| 205 | - | function perfRead() { return (turboNow !== null) ? turboNow : perfClock.now(); } | |
| 187 | + | const perfRead = () => (turboNow !== null) ? turboNow : perfClock.now(); | |
| 206 | 188 | ||
| 207 | 189 | /* ------------------------------------------------------------------ * | |
| 208 | 190 | * Pause (for the Scan tab). scale=0 is normally rejected by applyScale, | |
| @@ -231,14 +213,13 @@ | |||
|---|---|---|---|
| 231 | 213 | /* ------------------------------------------------------------------ * | |
| 232 | 214 | * Fake Date | |
| 233 | 215 | * ------------------------------------------------------------------ */ | |
| 234 | - | function FakeDate() { | |
| 235 | - | const args = Array.prototype.slice.call(arguments); | |
| 216 | + | function FakeDate(...args) { | |
| 236 | 217 | if (new.target === undefined) return new RealDate(dateClock.now()).toString(); | |
| 237 | 218 | if (args.length === 0) return new RealDate(dateClock.now()); | |
| 238 | - | return new (Function.prototype.bind.apply(RealDate, [null].concat(args)))(); | |
| 219 | + | return new RealDate(...args); | |
| 239 | 220 | } | |
| 240 | 221 | FakeDate.prototype = RealDate.prototype; | |
| 241 | - | FakeDate.now = function () { return Math.floor(dateClock.now()); }; | |
| 222 | + | FakeDate.now = () => Math.floor(dateClock.now()); | |
| 242 | 223 | FakeDate.parse = RealDate.parse.bind(RealDate); | |
| 243 | 224 | FakeDate.UTC = RealDate.UTC.bind(RealDate); | |
| 244 | 225 | try { Object.setPrototypeOf(FakeDate, RealDate); } catch (e) {} | |
| @@ -264,8 +245,8 @@ | |||
|---|---|---|---|
| 264 | 245 | if (clearHooksInstalled) return; | |
| 265 | 246 | clearHooksInstalled = true; | |
| 266 | 247 | // Transparent: only our string handles are intercepted; native numeric ids fall through. | |
| 267 | - | pageWin.clearInterval = function (id) { if (clearFake(id)) return; return origClearInterval(id); }; | |
| 268 | - | pageWin.clearTimeout = function (id) { if (clearFake(id)) return; return origClearTimeout(id); }; | |
| 248 | + | pageWin.clearInterval = (id) => { if (clearFake(id)) return; return origClearInterval(id); }; | |
| 249 | + | pageWin.clearTimeout = (id) => { if (clearFake(id)) return; return origClearTimeout(id); }; | |
| 269 | 250 | } | |
| 270 | 251 | ||
| 271 | 252 | /* ------------------------------------------------------------------ * | |
| @@ -274,39 +255,37 @@ | |||
|---|---|---|---|
| 274 | 255 | const hooks = { | |
| 275 | 256 | date: { | |
| 276 | 257 | label: 'Date (Date.now / new Date)', | |
| 277 | - | install: function () { dateClock.reanchor(); pageWin.Date = FakeDate; }, | |
| 278 | - | uninstall: function () { pageWin.Date = RealDate; } | |
| 258 | + | install: () => { dateClock.reanchor(); pageWin.Date = FakeDate; }, | |
| 259 | + | uninstall: () => { pageWin.Date = RealDate; } | |
| 279 | 260 | }, | |
| 280 | 261 | performance: { | |
| 281 | 262 | label: 'performance.now', | |
| 282 | - | install: function () { perfClock.reanchor(); if (pageWin.performance) pageWin.performance.now = function () { return perfRead(); }; }, | |
| 283 | - | uninstall: function () { if (pageWin.performance) pageWin.performance.now = origPerfNow; } | |
| 263 | + | install: () => { perfClock.reanchor(); if (pageWin.performance) pageWin.performance.now = () => perfRead(); }, | |
| 264 | + | uninstall: () => { if (pageWin.performance) pageWin.performance.now = origPerfNow; } | |
| 284 | 265 | }, | |
| 285 | 266 | setTimeout: { | |
| 286 | 267 | label: 'setTimeout', | |
| 287 | - | install: function () { | |
| 288 | - | pageWin.setTimeout = function (fn, delay) { | |
| 289 | - | const rest = Array.prototype.slice.call(arguments, 2); | |
| 268 | + | install: () => { | |
| 269 | + | pageWin.setTimeout = (fn, delay, ...rest) => { | |
| 290 | 270 | if (typeof delay === 'number' && isFinite(delay)) delay = delay / scale; | |
| 291 | - | return origSetTimeout.apply(null, [fn, delay].concat(rest)); | |
| 271 | + | return origSetTimeout(fn, delay, ...rest); | |
| 292 | 272 | }; | |
| 293 | 273 | }, | |
| 294 | - | uninstall: function () { pageWin.setTimeout = origSetTimeout; } | |
| 274 | + | uninstall: () => { pageWin.setTimeout = origSetTimeout; } | |
| 295 | 275 | }, | |
| 296 | 276 | setInterval: { | |
| 297 | 277 | label: 'setInterval', | |
| 298 | - | install: function () { | |
| 278 | + | install: () => { | |
| 299 | 279 | installClearHooks(); | |
| 300 | - | pageWin.setInterval = function (fn, delay) { | |
| 301 | - | const rest = Array.prototype.slice.call(arguments, 2); | |
| 280 | + | pageWin.setInterval = (fn, delay, ...rest) => { | |
| 302 | 281 | // Non-function callback or non-finite delay → defer to native semantics. | |
| 303 | 282 | if (typeof fn !== 'function' || typeof delay !== 'number' || !isFinite(delay)) { | |
| 304 | - | return origSetInterval.apply(null, arguments); | |
| 283 | + | return origSetInterval(fn, delay, ...rest); | |
| 305 | 284 | } | |
| 306 | 285 | const id = 'shx_int_' + (intervalSeq++); | |
| 307 | 286 | const rec = { timer: 0, cancelled: false }; | |
| 308 | 287 | fakeIntervals.set(id, rec); | |
| 309 | - | function tick() { | |
| 288 | + | const tick = () => { | |
| 310 | 289 | if (rec.cancelled) return; | |
| 311 | 290 | // Re-read scale every tick so slider changes take effect on a live interval. | |
| 312 | 291 | // When the hook is toggled OFF, eff=1 → the interval keeps running at native | |
| @@ -314,7 +293,7 @@ | |||
|---|---|---|---|
| 314 | 293 | const eff = state.setInterval ? scale : 1; | |
| 315 | 294 | rec.timer = origSetTimeout(tick, delay / eff); // schedule next BEFORE the call, | |
| 316 | 295 | try { fn.apply(pageWin, rest); } catch (e) {} // so a clear() inside fn cancels it | |
| 317 | - | } | |
| 296 | + | }; | |
| 318 | 297 | const eff0 = state.setInterval ? scale : 1; | |
| 319 | 298 | rec.timer = origSetTimeout(tick, delay / eff0); | |
| 320 | 299 | return id; | |
| @@ -322,44 +301,42 @@ | |||
|---|---|---|---|
| 322 | 301 | }, | |
| 323 | 302 | // Running fake intervals keep ticking after uninstall, but at scale 1 (see `eff`), | |
| 324 | 303 | // so toggling the hook off unscales them rather than freezing the page. | |
| 325 | - | uninstall: function () { pageWin.setInterval = origSetInterval; } | |
| 304 | + | uninstall: () => { pageWin.setInterval = origSetInterval; } | |
| 326 | 305 | }, | |
| 327 | 306 | raf: { | |
| 328 | 307 | label: 'requestAnimationFrame', | |
| 329 | - | install: function () { | |
| 330 | - | pageWin.requestAnimationFrame = function (cb) { | |
| 331 | - | return origRAF(function (realT) { | |
| 332 | - | if (!turbo) { | |
| 333 | - | // Feed one scaled timestamp. If the callback throws, swallow it — do NOT | |
| 334 | - | // re-invoke with a different time, which would double-run frame side effects. | |
| 335 | - | try { cb(perfRead()); } catch (e) {} | |
| 336 | - | return; | |
| 337 | - | } | |
| 338 | - | // turbo: run the frame callback k times per real frame on ONE monotonic | |
| 339 | - | // timeline, ~1 normal frame apart, so engines that CLAMP delta-time still | |
| 340 | - | // advance k frames. Fake time advances by exactly k*step here — it does not | |
| 341 | - | // also track real elapsed time, which is what used to cause discontinuities. | |
| 342 | - | const k = Math.max(1, Math.min(20, Math.round(scale))); // capped so the tab can't lock up | |
| 343 | - | const step = 1000 / 60; | |
| 344 | - | if (turboT === null) turboT = perfClock.now(); // seed once from current fake time | |
| 345 | - | let currentCbs = [cb]; | |
| 346 | - | for (let i = 0; i < k && currentCbs.length; i++) { | |
| 347 | - | turboT += step; // advance the single timeline | |
| 348 | - | turboNow = turboT; | |
| 349 | - | const nextCbs = []; // collect EVERY rAF re-registered this sub-step, | |
| 350 | - | const prev = pageWin.requestAnimationFrame; // not just the last (a callback may schedule several) | |
| 351 | - | pageWin.requestAnimationFrame = function (next) { if (typeof next === 'function') nextCbs.push(next); return 0; }; | |
| 352 | - | for (let j = 0; j < currentCbs.length; j++) { try { currentCbs[j](turboT); } catch (e) {} } | |
| 353 | - | pageWin.requestAnimationFrame = prev; // restore our wrapper | |
| 354 | - | currentCbs = nextCbs; // chain all re-registered callbacks | |
| 355 | - | } | |
| 356 | - | turboNow = null; | |
| 357 | - | perfClock.setTo(turboT); // keep performance.now() continuous after the burst | |
| 358 | - | for (let j = 0; j < currentCbs.length; j++) pageWin.requestAnimationFrame(currentCbs[j]); // next REAL frame | |
| 359 | - | }); | |
| 360 | - | }; | |
| 308 | + | install: () => { | |
| 309 | + | pageWin.requestAnimationFrame = (cb) => origRAF(() => { | |
| 310 | + | if (!turbo) { | |
| 311 | + | // Feed one scaled timestamp. If the callback throws, swallow it — do NOT | |
| 312 | + | // re-invoke with a different time, which would double-run frame side effects. | |
| 313 | + | try { cb(perfRead()); } catch (e) {} | |
| 314 | + | return; | |
| 315 | + | } | |
| 316 | + | // turbo: run the frame callback k times per real frame on ONE monotonic | |
| 317 | + | // timeline, ~1 normal frame apart, so engines that CLAMP delta-time still | |
| 318 | + | // advance k frames. Fake time advances by exactly k*step here — it does not | |
| 319 | + | // also track real elapsed time, which is what used to cause discontinuities. | |
| 320 | + | const k = Math.max(1, Math.min(20, Math.round(scale))); // capped so the tab can't lock up | |
| 321 | + | const step = 1000 / 60; | |
| 322 | + | if (turboT === null) turboT = perfClock.now(); // seed once from current fake time | |
| 323 | + | let currentCbs = [cb]; | |
| 324 | + | for (let i = 0; i < k && currentCbs.length; i++) { | |
| 325 | + | turboT += step; // advance the single timeline | |
| 326 | + | turboNow = turboT; | |
| 327 | + | const nextCbs = []; // collect EVERY rAF re-registered this sub-step, | |
| 328 | + | const prev = pageWin.requestAnimationFrame; // not just the last (a callback may schedule several) | |
| 329 | + | pageWin.requestAnimationFrame = (next) => { if (typeof next === 'function') nextCbs.push(next); return 0; }; | |
| 330 | + | for (const c of currentCbs) { try { c(turboT); } catch (e) {} } | |
| 331 | + | pageWin.requestAnimationFrame = prev; // restore our wrapper | |
| 332 | + | currentCbs = nextCbs; // chain all re-registered callbacks | |
| 333 | + | } | |
| 334 | + | turboNow = null; | |
| 335 | + | perfClock.setTo(turboT); // keep performance.now() continuous after the burst | |
| 336 | + | for (const c of currentCbs) pageWin.requestAnimationFrame(c); // next REAL frame | |
| 337 | + | }); | |
| 361 | 338 | }, | |
| 362 | - | uninstall: function () { pageWin.requestAnimationFrame = origRAF; } | |
| 339 | + | uninstall: () => { pageWin.requestAnimationFrame = origRAF; } | |
| 363 | 340 | } | |
| 364 | 341 | }; | |
| 365 | 342 | ||
| @@ -369,7 +346,7 @@ | |||
|---|---|---|---|
| 369 | 346 | state[name] = on; | |
| 370 | 347 | try { on ? hooks[name].install() : hooks[name].uninstall(); } catch (e) {} | |
| 371 | 348 | } | |
| 372 | - | Object.keys(hooks).forEach(function (n) { if (state[n]) { try { hooks[n].install(); } catch (e) {} } }); | |
| 349 | + | for (const n of Object.keys(hooks)) if (state[n]) { try { hooks[n].install(); } catch (e) {} } | |
| 373 | 350 | ||
| 374 | 351 | /* ------------------------------------------------------------------ * | |
| 375 | 352 | * Multi-frame coordination. | |
| @@ -379,7 +356,7 @@ | |||
|---|---|---|---|
| 379 | 356 | * from a host within 5s promotes itself (covers a top frame the manager | |
| 380 | 357 | * didn't inject into). Hooks are installed in every frame regardless. | |
| 381 | 358 | * ------------------------------------------------------------------ */ | |
| 382 | - | const isTop = (function () { try { return window.top === window.self; } catch (e) { return true; } })(); | |
| 359 | + | const isTop = (() => { try { return window.top === window.self; } catch (e) { return true; } })(); | |
| 383 | 360 | let isHost = isTop; // top frame hosts by default; a stranded child may promote | |
| 384 | 361 | let attached = !isTop; // children follow the host until detached | |
| 385 | 362 | let hostWin = null; // a child's link back to its host window | |
| @@ -392,42 +369,35 @@ | |||
|---|---|---|---|
| 392 | 369 | // which can be null/unpostable for cross-origin iframes in an isolated world. | |
| 393 | 370 | const SELF_ID = 'shx-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); | |
| 394 | 371 | ||
| 395 | - | function currentSettings() { | |
| 396 | - | return { | |
| 397 | - | scale: scale, | |
| 398 | - | turbo: turbo, | |
| 399 | - | hooks: { date: state.date, performance: state.performance, setTimeout: state.setTimeout, | |
| 400 | - | setInterval: state.setInterval, raf: state.raf } | |
| 401 | - | }; | |
| 402 | - | } | |
| 403 | - | function settingsMsg(type) { | |
| 404 | - | const s = currentSettings(); | |
| 405 | - | return { type: type, scale: s.scale, turbo: s.turbo, hooks: s.hooks }; | |
| 406 | - | } | |
| 372 | + | const settingsMsg = (type) => ({ type, scale, turbo, hooks: { ...state } }); | |
| 407 | 373 | function applySettings(s) { | |
| 408 | - | if (s.hooks) Object.keys(s.hooks).forEach(function (n) { | |
| 374 | + | if (s.hooks) for (const n of Object.keys(s.hooks)) { | |
| 409 | 375 | if (hooks[n] && state[n] !== s.hooks[n]) setHook(n, s.hooks[n]); | |
| 410 | - | }); | |
| 376 | + | } | |
| 411 | 377 | if (typeof s.turbo === 'boolean' && s.turbo !== turbo) { turbo = s.turbo; turboT = null; } | |
| 412 | 378 | if (typeof s.scale === 'number') applyScale(s.scale); | |
| 413 | - | if (panelCtl) panelCtl.sync(); | |
| 379 | + | panelCtl?.sync(); | |
| 414 | 380 | } | |
| 415 | 381 | function postTo(win, msg) { try { msg.__shx = 1; win.postMessage(msg, '*'); } catch (e) {} } | |
| 416 | 382 | ||
| 417 | - | // Deliver a scan message to EVERY frame in the tree (root = window.top, plus all | |
| 418 | - | // descendants), the same live-frame walk rollcall uses — which is proven to reach | |
| 419 | - | // cross-origin children. Only the frame whose SELF_ID matches `targetFrame` acts on | |
| 420 | - | // it; the rest ignore it. This sidesteps stored-`e.source` references entirely. | |
| 383 | + | // The one cross-origin-safe recursive walk over the live frame tree (root = | |
| 384 | + | // window.top by default, i.e. every frame on the page except the root itself). | |
| 385 | + | const topWin = () => { try { return window.top; } catch (e) { return window; } }; | |
| 386 | + | function forEachFrame(cb, root = topWin()) { | |
| 387 | + | let list; try { list = root.frames; } catch (e) { return; } | |
| 388 | + | for (let i = 0; i < list.length; i++) { | |
| 389 | + | try { cb(list[i]); } catch (e) {} | |
| 390 | + | forEachFrame(cb, list[i]); | |
| 391 | + | } | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | // Deliver a scan message to EVERY frame in the tree (root + all descendants), | |
| 395 | + | // the same live-frame walk rollcall uses — which is proven to reach cross-origin | |
| 396 | + | // children. Only the frame whose SELF_ID matches `targetFrame` acts on it; the | |
| 397 | + | // rest ignore it. This sidesteps stored-`e.source` references entirely. | |
| 421 | 398 | function broadcastScanMsg(msg) { | |
| 422 | - | let root; try { root = window.top; } catch (e) { root = window; } | |
| 423 | - | try { postTo(root, msg); } catch (e) {} | |
| 424 | - | (function visit(win) { | |
| 425 | - | let list; try { list = win.frames; } catch (e) { return; } | |
| 426 | - | for (let i = 0; i < list.length; i++) { | |
| 427 | - | try { postTo(list[i], msg); } catch (e) {} | |
| 428 | - | try { visit(list[i]); } catch (e) {} | |
| 429 | - | } | |
| 430 | - | })(root); | |
| 399 | + | postTo(topWin(), msg); | |
| 400 | + | forEachFrame(w => postTo(w, msg)); | |
| 431 | 401 | } | |
| 432 | 402 | ||
| 433 | 403 | function pruneFrames() { | |
| @@ -435,29 +405,20 @@ | |||
|---|---|---|---|
| 435 | 405 | // entries whose iframe has been removed from the tree, else they accumulate (phantom | |
| 436 | 406 | // frame-list rows + dead postMessage targets) on long-lived / SPA pages. | |
| 437 | 407 | const live = new Set(); | |
| 438 | - | (function visit(win) { | |
| 439 | - | let list; try { list = win.frames; } catch (e) { return; } | |
| 440 | - | for (let i = 0; i < list.length; i++) { try { live.add(list[i]); visit(list[i]); } catch (e) {} } | |
| 441 | - | })(window.top); | |
| 408 | + | forEachFrame(w => live.add(w)); | |
| 442 | 409 | let changed = false; | |
| 443 | - | frames.forEach(function (f, src) { if (!live.has(src)) { frames.delete(src); changed = true; } }); | |
| 410 | + | frames.forEach((f, src) => { if (!live.has(src)) { frames.delete(src); changed = true; } }); | |
| 444 | 411 | return changed; | |
| 445 | 412 | } | |
| 446 | 413 | function broadcastSettings() { | |
| 447 | 414 | if (!isHost) return; // only a host pushes settings out | |
| 448 | 415 | pruneFrames(); | |
| 449 | - | frames.forEach(function (f, src) { if (f.attached) postTo(src, settingsMsg('settings')); }); | |
| 416 | + | frames.forEach((f, src) => { if (f.attached) postTo(src, settingsMsg('settings')); }); | |
| 450 | 417 | } | |
| 451 | 418 | function rollcall() { | |
| 452 | 419 | // Ask every descendant frame to (re-)announce itself — covers a host that | |
| 453 | 420 | // booted after its children. Reaching cross-origin frames via postMessage is fine. | |
| 454 | - | (function visit(win) { | |
| 455 | - | let list; try { list = win.frames; } catch (e) { return; } | |
| 456 | - | for (let i = 0; i < list.length; i++) { | |
| 457 | - | try { postTo(list[i], { type: 'rollcall' }); } catch (e) {} | |
| 458 | - | try { visit(list[i]); } catch (e) {} | |
| 459 | - | } | |
| 460 | - | })(window.top); | |
| 421 | + | forEachFrame(w => postTo(w, { type: 'rollcall' })); | |
| 461 | 422 | } | |
| 462 | 423 | function promoteToHost() { | |
| 463 | 424 | if (isHost) return; | |
| @@ -468,100 +429,90 @@ | |||
|---|---|---|---|
| 468 | 429 | function detachFrame(src) { | |
| 469 | 430 | const f = frames.get(src); if (!f) return; | |
| 470 | 431 | f.attached = false; postTo(src, { type: 'detach' }); | |
| 471 | - | if (panelCtl) panelCtl.refreshFrames(); | |
| 432 | + | panelCtl?.refreshFrames(); | |
| 472 | 433 | } | |
| 473 | 434 | function reattachFrame(src) { | |
| 474 | 435 | const f = frames.get(src); if (!f) return; | |
| 475 | 436 | f.attached = true; postTo(src, settingsMsg('attach')); | |
| 476 | - | if (panelCtl) panelCtl.refreshFrames(); | |
| 437 | + | panelCtl?.refreshFrames(); | |
| 477 | 438 | } | |
| 478 | 439 | ||
| 479 | - | window.addEventListener('message', function (e) { | |
| 440 | + | // One named handler per message type; hostOnly/childOnly guards applied by the dispatcher. | |
| 441 | + | const MSG = { | |
| 442 | + | hello: { hostOnly: true, fn(m, e) { // host: a child announced itself | |
| 443 | + | if (hostClosed) { postTo(e.source, { type: 'host-closing' }); return; } // late frame after close → go standalone | |
| 444 | + | const f = frames.get(e.source); | |
| 445 | + | if (f) { f.url = m.url; if (m.frameId) f.id = m.frameId; } | |
| 446 | + | else frames.set(e.source, { url: m.url, attached: true, id: m.frameId }); | |
| 447 | + | postTo(e.source, settingsMsg('settings')); // sync the newcomer immediately | |
| 448 | + | postTo(e.source, clickerConfigMsg()); // ...including autoclicker config | |
| 449 | + | postTo(e.source, { type: 'clicker-run', on: clicker.running }); | |
| 450 | + | panelCtl?.refreshFrames(); | |
| 451 | + | } }, | |
| 452 | + | rollcall: { childOnly: true, fn(m, e) { // child: a host is probing for frames | |
| 453 | + | gotHost = true; hostWin = e.source; | |
| 454 | + | postTo(e.source, { type: 'hello', url: location.href, frameId: SELF_ID }); | |
| 455 | + | } }, | |
| 456 | + | settings: { childOnly: true, fn(m, e) { // child: host pushed settings | |
| 457 | + | gotHost = true; hostWin = e.source; | |
| 458 | + | if (attached) applySettings(m); | |
| 459 | + | } }, | |
| 460 | + | detach: { childOnly: true, fn(m, e) { // child: host detached us → own panel | |
| 461 | + | hostWin = e.source; attached = false; | |
| 462 | + | ensurePanel('detached'); | |
| 463 | + | } }, | |
| 464 | + | 'host-closing': { childOnly: true, fn() { // child: the host closed → become standalone | |
| 465 | + | promoteToHost(); // host mode (no re-attach button), self-sufficient | |
| 466 | + | } }, | |
| 467 | + | attach: { childOnly: true, fn(m) { // child: host folded us back in | |
| 468 | + | attached = true; destroyPanel(); applySettings(m); | |
| 469 | + | } }, | |
| 470 | + | reattach: { hostOnly: true, fn(m, e) { // host: a detached child asked to fold back | |
| 471 | + | const f = frames.get(e.source); | |
| 472 | + | if (f) { f.attached = true; postTo(e.source, settingsMsg('settings')); panelCtl?.refreshFrames(); } | |
| 473 | + | } }, | |
| 474 | + | 'clicker-config': { childOnly: true, fn(m) { // child: host pushed clicker settings | |
| 475 | + | applyClickerConfig(m); | |
| 476 | + | } }, | |
| 477 | + | 'clicker-run': { fn(m) { // shared autoclicker running state | |
| 478 | + | // host: a child toggled it → adopt + fan out to all; child: host pushed it down. | |
| 479 | + | setClickerRunning(m.on, isHost); | |
| 480 | + | } }, | |
| 481 | + | 'scan-cmd': { fn(m, e) { // any frame: if addressed to me, run + reply | |
| 482 | + | if (m.targetFrame && m.targetFrame !== SELF_ID) return; // broadcast not meant for this frame | |
| 483 | + | const dkey = (m.from || '') + ':' + m.reqId; | |
| 484 | + | if (scanHandled.has(dkey)) return; // duplicate — arrived via both channels | |
| 485 | + | scanHandled.add(dkey); scanHandledQ.push(dkey); | |
| 486 | + | if (scanHandledQ.length > 400) scanHandled.delete(scanHandledQ.shift()); | |
| 487 | + | const replyWin = e.source; | |
| 488 | + | const scmd = m.cmd || m; // command payload is nested under `cmd` (avoids type-field collision) | |
| 489 | + | let lastFrac = -2; // throttle determinate progress to ~2% steps (indeterminate always relayed) | |
| 490 | + | runScanCommand(scmd, (frac) => { | |
| 491 | + | if (frac >= 0 && frac !== 1 && frac - lastFrac < 0.02) return; | |
| 492 | + | lastFrac = frac; | |
| 493 | + | try { if (replyWin) postTo(replyWin, { type: 'scan-progress', reqId: m.reqId, targetFrame: m.from, frac }); } catch (e2) {} | |
| 494 | + | }).then((res) => { | |
| 495 | + | res.type = 'scan-result'; res.reqId = m.reqId; res.targetFrame = m.from; | |
| 496 | + | try { if (replyWin) postTo(replyWin, res); } catch (e2) {} // reply to the sender directly... | |
| 497 | + | broadcastScanMsg(res); // ...and via window.top (reliable upward) | |
| 498 | + | }); | |
| 499 | + | } }, | |
| 500 | + | 'scan-progress': { fn(m) { // controller: forward incremental progress to the UI | |
| 501 | + | if (m.targetFrame && m.targetFrame !== SELF_ID) return; | |
| 502 | + | scanProgress.get(m.reqId)?.(m.frac); | |
| 503 | + | } }, | |
| 504 | + | 'scan-result': { fn(m) { // controller: resolve the matching pending request | |
| 505 | + | if (m.targetFrame && m.targetFrame !== SELF_ID) return; | |
| 506 | + | scanProgress.delete(m.reqId); | |
| 507 | + | const resolve = scanPending.get(m.reqId); | |
| 508 | + | if (resolve) { scanPending.delete(m.reqId); resolve(m); } | |
| 509 | + | } } | |
| 510 | + | }; | |
| 511 | + | window.addEventListener('message', (e) => { | |
| 480 | 512 | const m = e.data; if (!m || m.__shx !== 1) return; | |
| 481 | - | switch (m.type) { | |
| 482 | - | case 'hello': // host: a child announced itself | |
| 483 | - | if (!isHost) break; | |
| 484 | - | if (hostClosed) { postTo(e.source, { type: 'host-closing' }); break; } // late frame after close → go standalone | |
| 485 | - | if (frames.has(e.source)) { frames.get(e.source).url = m.url; if (m.frameId) frames.get(e.source).id = m.frameId; } | |
| 486 | - | else frames.set(e.source, { url: m.url, attached: true, id: m.frameId }); | |
| 487 | - | postTo(e.source, settingsMsg('settings')); // sync the newcomer immediately | |
| 488 | - | postTo(e.source, clickerConfigMsg()); // ...including autoclicker config | |
| 489 | - | postTo(e.source, { type: 'clicker-run', on: clicker.running }); | |
| 490 | - | if (panelCtl) panelCtl.refreshFrames(); | |
| 491 | - | break; | |
| 492 | - | case 'rollcall': // child: a host is probing for frames | |
| 493 | - | if (isHost) break; | |
| 494 | - | gotHost = true; hostWin = e.source; | |
| 495 | - | postTo(e.source, { type: 'hello', url: location.href, frameId: SELF_ID }); | |
| 496 | - | break; | |
| 497 | - | case 'settings': // child: host pushed settings | |
| 498 | - | if (isHost) break; | |
| 499 | - | gotHost = true; hostWin = e.source; | |
| 500 | - | if (attached) applySettings(m); | |
| 501 | - | break; | |
| 502 | - | case 'detach': // child: host detached us → own panel | |
| 503 | - | if (isHost) break; | |
| 504 | - | hostWin = e.source; attached = false; | |
| 505 | - | ensurePanel('detached'); | |
| 506 | - | break; | |
| 507 | - | case 'host-closing': // child: the host closed → become standalone | |
| 508 | - | if (isHost) break; | |
| 509 | - | promoteToHost(); // host mode (no re-attach button), self-sufficient | |
| 510 | - | break; | |
| 511 | - | case 'attach': // child: host folded us back in | |
| 512 | - | if (isHost) break; | |
| 513 | - | attached = true; destroyPanel(); applySettings(m); | |
| 514 | - | break; | |
| 515 | - | case 'reattach': // host: a detached child asked to fold back | |
| 516 | - | if (!isHost) break; | |
| 517 | - | { const f = frames.get(e.source); | |
| 518 | - | if (f) { f.attached = true; postTo(e.source, settingsMsg('settings')); if (panelCtl) panelCtl.refreshFrames(); } } | |
| 519 | - | break; | |
| 520 | - | case 'clicker-config': // child: host pushed clicker settings | |
| 521 | - | if (isHost) break; | |
| 522 | - | applyClickerConfig(m); | |
| 523 | - | break; | |
| 524 | - | case 'clicker-run': // shared autoclicker running state | |
| 525 | - | if (isHost) { // a child toggled it → adopt + fan out to all | |
| 526 | - | setClickerRunning(m.on, true); | |
| 527 | - | } else { // host pushed the state down | |
| 528 | - | setClickerRunning(m.on, false); | |
| 529 | - | } | |
| 530 | - | break; | |
| 531 | - | case 'scan-cmd': { // any frame: if addressed to me, run + reply | |
| 532 | - | if (m.targetFrame && m.targetFrame !== SELF_ID) break; // broadcast not meant for this frame | |
| 533 | - | const dkey = (m.from || '') + ':' + m.reqId; | |
| 534 | - | if (scanHandled.has(dkey)) break; // duplicate — arrived via both channels | |
| 535 | - | scanHandled.add(dkey); scanHandledQ.push(dkey); | |
| 536 | - | if (scanHandledQ.length > 400) scanHandled.delete(scanHandledQ.shift()); | |
| 537 | - | const replyWin = e.source; | |
| 538 | - | const scmd = m.cmd || m; // command payload is nested under `cmd` (avoids type-field collision) | |
| 539 | - | let lastFrac = -2; // throttle determinate progress to ~2% steps (indeterminate always relayed) | |
| 540 | - | runScanCommand(scmd, function (frac) { | |
| 541 | - | if (frac >= 0 && frac !== 1 && frac - lastFrac < 0.02) return; | |
| 542 | - | lastFrac = frac; | |
| 543 | - | try { if (replyWin) postTo(replyWin, { type: 'scan-progress', reqId: m.reqId, targetFrame: m.from, frac: frac }); } catch (e2) {} | |
| 544 | - | }).then(function (res) { | |
| 545 | - | res.type = 'scan-result'; res.reqId = m.reqId; res.targetFrame = m.from; | |
| 546 | - | try { if (replyWin) postTo(replyWin, res); } catch (e2) {} // reply to the sender directly... | |
| 547 | - | broadcastScanMsg(res); // ...and via window.top (reliable upward) | |
| 548 | - | }); | |
| 549 | - | break; | |
| 550 | - | } | |
| 551 | - | case 'scan-progress': { // controller: forward incremental progress to the UI | |
| 552 | - | if (m.targetFrame && m.targetFrame !== SELF_ID) break; | |
| 553 | - | const cb = scanProgress.get(m.reqId); | |
| 554 | - | if (cb) cb(m.frac); | |
| 555 | - | break; | |
| 556 | - | } | |
| 557 | - | case 'scan-result': { // controller: resolve the matching pending request | |
| 558 | - | if (m.targetFrame && m.targetFrame !== SELF_ID) break; | |
| 559 | - | scanProgress.delete(m.reqId); | |
| 560 | - | const resolve = scanPending.get(m.reqId); | |
| 561 | - | if (resolve) { scanPending.delete(m.reqId); resolve(m); } | |
| 562 | - | break; | |
| 563 | - | } | |
| 564 | - | } | |
| 513 | + | const h = MSG[m.type]; if (!h) return; | |
| 514 | + | if ((h.hostOnly && !isHost) || (h.childOnly && isHost)) return; | |
| 515 | + | h.fn(m, e); | |
| 565 | 516 | }); | |
| 566 | 517 | ||
| 567 | 518 | /* ------------------------------------------------------------------ * | |
| @@ -589,11 +540,11 @@ | |||
|---|---|---|---|
| 589 | 540 | const MAX_CPS = 100; | |
| 590 | 541 | const ctxDoc = pageWin.document || document; | |
| 591 | 542 | ||
| 592 | - | function cursorInside() { return clicker.enteredDoc && !clicker.overChildFrame; } | |
| 543 | + | const cursorInside = () => clicker.enteredDoc && !clicker.overChildFrame; | |
| 593 | 544 | function trackPointer(e) { | |
| 594 | 545 | clicker.lastX = e.clientX; clicker.lastY = e.clientY; | |
| 595 | 546 | clicker.enteredDoc = true; | |
| 596 | - | const t = e.target, tag = t && t.tagName; | |
| 547 | + | const tag = e.target?.tagName; | |
| 597 | 548 | clicker.overChildFrame = (tag === 'IFRAME' || tag === 'FRAME'); | |
| 598 | 549 | } | |
| 599 | 550 | try { | |
| @@ -601,7 +552,7 @@ | |||
|---|---|---|---|
| 601 | 552 | ctxDoc.addEventListener('mousemove', trackPointer, true); // fallback where PointerEvents are absent | |
| 602 | 553 | ctxDoc.addEventListener('mouseover', trackPointer, true); // updates overChildFrame even without movement | |
| 603 | 554 | // relatedTarget == null on mouseout means the cursor left the window entirely. | |
| 604 | - | ctxDoc.addEventListener('mouseout', function (e) { if (!e.relatedTarget) clicker.enteredDoc = false; }, true); | |
| 555 | + | ctxDoc.addEventListener('mouseout', (e) => { if (!e.relatedTarget) clicker.enteredDoc = false; }, true); | |
| 605 | 556 | } catch (e) {} | |
| 606 | 557 | ||
| 607 | 558 | function fireClick() { | |
| @@ -610,16 +561,16 @@ | |||
|---|---|---|---|
| 610 | 561 | if (!el) return; | |
| 611 | 562 | const base = { bubbles: true, cancelable: true, composed: true, view: pageWin, clientX: x, clientY: y, button: 0 }; | |
| 612 | 563 | function dispatch(type, buttons, pointer) { | |
| 613 | - | const opts = Object.assign({}, base, { buttons: buttons }); | |
| 564 | + | const opts = { ...base, buttons }; | |
| 614 | 565 | let ev; | |
| 615 | 566 | if (pointer && pageWin.PointerEvent) { | |
| 616 | - | try { ev = new pageWin.PointerEvent(type, Object.assign(opts, { pointerId: 1, pointerType: 'mouse', isPrimary: true })); } catch (e) {} | |
| 567 | + | try { ev = new pageWin.PointerEvent(type, { ...opts, pointerId: 1, pointerType: 'mouse', isPrimary: true }); } catch (e) {} | |
| 617 | 568 | } | |
| 618 | 569 | if (!ev) { try { ev = new pageWin.MouseEvent(type, opts); } catch (e) { return; } } | |
| 619 | 570 | try { el.dispatchEvent(ev); } catch (e) {} | |
| 620 | 571 | } | |
| 621 | 572 | dispatch('pointerdown', 1, true); dispatch('mousedown', 1, false); | |
| 622 | - | const up = function () { | |
| 573 | + | const up = () => { | |
| 623 | 574 | clicker.upTimer = 0; | |
| 624 | 575 | dispatch('pointerup', 0, true); dispatch('mouseup', 0, false); dispatch('click', 0, false); | |
| 625 | 576 | }; | |
| @@ -650,20 +601,20 @@ | |||
|---|---|---|---|
| 650 | 601 | function setClickerRunning(on, propagate) { | |
| 651 | 602 | on = !!on; | |
| 652 | 603 | if (clicker.running !== on) { clicker.running = on; on ? startClickerLoop() : stopClickerLoop(); } | |
| 653 | - | if (panelCtl) panelCtl.syncClicker(); | |
| 604 | + | panelCtl?.syncClicker(); | |
| 654 | 605 | if (!propagate) return; | |
| 655 | - | if (isHost) frames.forEach(function (f, src) { postTo(src, { type: 'clicker-run', on: on }); }); | |
| 656 | - | else if (hostWin) postTo(hostWin, { type: 'clicker-run', on: on }); | |
| 606 | + | if (isHost) frames.forEach((f, src) => postTo(src, { type: 'clicker-run', on })); | |
| 607 | + | else if (hostWin) postTo(hostWin, { type: 'clicker-run', on }); | |
| 657 | 608 | } | |
| 658 | 609 | ||
| 659 | 610 | function clickerConfigMsg() { | |
| 660 | - | return { type: 'clicker-config', mode: clicker.mode, hotkey: clicker.hotkey, | |
| 661 | - | swallowHotkey: clicker.swallowHotkey, cps: clicker.cps, jitterMs: clicker.jitterMs, holdMs: clicker.holdMs }; | |
| 611 | + | const { mode, hotkey, swallowHotkey, cps, jitterMs, holdMs } = clicker; | |
| 612 | + | return { type: 'clicker-config', mode, hotkey, swallowHotkey, cps, jitterMs, holdMs }; | |
| 662 | 613 | } | |
| 663 | 614 | function broadcastClickerConfig() { | |
| 664 | 615 | if (!isHost) return; // config flows host → all frames (not just attached) | |
| 665 | 616 | pruneFrames(); | |
| 666 | - | frames.forEach(function (f, src) { postTo(src, clickerConfigMsg()); }); | |
| 617 | + | frames.forEach((f, src) => postTo(src, clickerConfigMsg())); | |
| 667 | 618 | } | |
| 668 | 619 | function applyClickerConfig(c) { | |
| 669 | 620 | if (c.mode === 'toggle' || c.mode === 'hold') clicker.mode = c.mode; | |
| @@ -672,7 +623,7 @@ | |||
|---|---|---|---|
| 672 | 623 | if (typeof c.cps === 'number') clicker.cps = Math.min(MAX_CPS, Math.max(0.1, c.cps)); | |
| 673 | 624 | if (typeof c.jitterMs === 'number') clicker.jitterMs = Math.max(0, c.jitterMs); | |
| 674 | 625 | if (typeof c.holdMs === 'number') clicker.holdMs = Math.max(0, c.holdMs); | |
| 675 | - | if (panelCtl) panelCtl.syncClicker(); | |
| 626 | + | panelCtl?.syncClicker(); | |
| 676 | 627 | } | |
| 677 | 628 | ||
| 678 | 629 | function hotkeyMatches(e, hk) { | |
| @@ -680,15 +631,15 @@ | |||
|---|---|---|---|
| 680 | 631 | !!e.shiftKey === !!hk.shift && !!e.metaKey === !!hk.meta; | |
| 681 | 632 | } | |
| 682 | 633 | function eventFromPanel(e) { | |
| 683 | - | return panelHost && e.composedPath && e.composedPath().indexOf(panelHost) !== -1; | |
| 634 | + | return panelHost && e.composedPath && e.composedPath().includes(panelHost); | |
| 684 | 635 | } | |
| 685 | 636 | function onClickerKeyDown(e) { | |
| 686 | 637 | if (clicker.listening) { // capturing a new binding (panel frame) | |
| 687 | - | if (e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift' || e.key === 'Meta') return; // await a real key | |
| 638 | + | if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return; // await a real key | |
| 688 | 639 | e.preventDefault(); e.stopPropagation(); | |
| 689 | 640 | clicker.hotkey = { key: e.key, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey }; | |
| 690 | 641 | clicker.listening = false; | |
| 691 | - | if (panelCtl) panelCtl.syncClicker(); | |
| 642 | + | panelCtl?.syncClicker(); | |
| 692 | 643 | broadcastClickerConfig(); | |
| 693 | 644 | return; | |
| 694 | 645 | } | |
| @@ -714,7 +665,7 @@ | |||
|---|---|---|---|
| 714 | 665 | pageWin.addEventListener('keyup', onClickerKeyUp, true); | |
| 715 | 666 | // Safeguard: in hold mode a keyup can land in a different frame than the keydown; | |
| 716 | 667 | // losing focus then releasing would otherwise leave it stuck on. | |
| 717 | - | pageWin.addEventListener('blur', function () { if (clicker.mode === 'hold' && clicker.running) setClickerRunning(false, true); }, true); | |
| 668 | + | pageWin.addEventListener('blur', () => { if (clicker.mode === 'hold' && clicker.running) setClickerRunning(false, true); }, true); | |
| 718 | 669 | } catch (e) {} | |
| 719 | 670 | ||
| 720 | 671 | /* ================================================================== * | |
| @@ -728,16 +679,18 @@ | |||
|---|---|---|---|
| 728 | 679 | * The engine runs LOCALLY in each frame and owns its own candidate state; | |
| 729 | 680 | * the panel drives it directly (this frame) or over postMessage (iframes). | |
| 730 | 681 | * ================================================================== */ | |
| 682 | + | // `arr` is the TypedArray used for bulk scanning (platform-endian == little-endian | |
| 683 | + | // on every supported browser, matching the explicit-LE DataView used for R/W). | |
| 731 | 684 | const SCAN_TYPES = { | |
| 732 | - | i8: { size: 1, get: function (d, o) { return d.getInt8(o); }, set: function (d, o, v) { d.setInt8(o, v); } }, | |
| 733 | - | u8: { size: 1, get: function (d, o) { return d.getUint8(o); }, set: function (d, o, v) { d.setUint8(o, v); } }, | |
| 734 | - | i16: { size: 2, get: function (d, o) { return d.getInt16(o, true); }, set: function (d, o, v) { d.setInt16(o, v, true); } }, | |
| 735 | - | u16: { size: 2, get: function (d, o) { return d.getUint16(o, true); }, set: function (d, o, v) { d.setUint16(o, v, true); } }, | |
| 736 | - | i32: { size: 4, get: function (d, o) { return d.getInt32(o, true); }, set: function (d, o, v) { d.setInt32(o, v, true); } }, | |
| 737 | - | u32: { size: 4, get: function (d, o) { return d.getUint32(o, true); }, set: function (d, o, v) { d.setUint32(o, v >>> 0, true); } }, | |
| 738 | - | i64: { size: 8, big: true, get: function (d, o) { return d.getBigInt64(o, true); }, set: function (d, o, v) { d.setBigInt64(o, v, true); } }, | |
| 739 | - | f32: { size: 4, float: true, get: function (d, o) { return d.getFloat32(o, true); }, set: function (d, o, v) { d.setFloat32(o, v, true); } }, | |
| 740 | - | f64: { size: 8, float: true, get: function (d, o) { return d.getFloat64(o, true); }, set: function (d, o, v) { d.setFloat64(o, v, true); } } | |
| 685 | + | i8: { size: 1, arr: Int8Array, get: (d, o) => d.getInt8(o), set: (d, o, v) => d.setInt8(o, v) }, | |
| 686 | + | u8: { size: 1, arr: Uint8Array, get: (d, o) => d.getUint8(o), set: (d, o, v) => d.setUint8(o, v) }, | |
| 687 | + | i16: { size: 2, arr: Int16Array, get: (d, o) => d.getInt16(o, true), set: (d, o, v) => d.setInt16(o, v, true) }, | |
| 688 | + | u16: { size: 2, arr: Uint16Array, get: (d, o) => d.getUint16(o, true), set: (d, o, v) => d.setUint16(o, v, true) }, | |
| 689 | + | i32: { size: 4, arr: Int32Array, get: (d, o) => d.getInt32(o, true), set: (d, o, v) => d.setInt32(o, v, true) }, | |
| 690 | + | u32: { size: 4, arr: Uint32Array, get: (d, o) => d.getUint32(o, true), set: (d, o, v) => d.setUint32(o, v >>> 0, true) }, | |
| 691 | + | i64: { size: 8, arr: BigInt64Array, big: true, get: (d, o) => d.getBigInt64(o, true), set: (d, o, v) => d.setBigInt64(o, v, true) }, | |
| 692 | + | f32: { size: 4, arr: Float32Array, float: true, get: (d, o) => d.getFloat32(o, true), set: (d, o, v) => d.setFloat32(o, v, true) }, | |
| 693 | + | f64: { size: 8, arr: Float64Array, float: true, get: (d, o) => d.getFloat64(o, true), set: (d, o, v) => d.setFloat64(o, v, true) } | |
| 741 | 694 | }; | |
| 742 | 695 | const SCAN_STORE_CAP = 500000; // max candidates TRACKED locally (refine works on all of these) | |
| 743 | 696 | const FLOAT_EPS = 1e-4; | |
| @@ -749,10 +702,8 @@ | |||
|---|---|---|---|
| 749 | 702 | allint: ['i8', 'u8', 'i16', 'u16', 'i32', 'u32', 'i64'], | |
| 750 | 703 | allfloat: ['f32', 'f64'] | |
| 751 | 704 | }; | |
| 752 | - | function expandTypes(sel) { | |
| 753 | - | if (SCAN_TYPE_GROUPS[sel]) return SCAN_TYPE_GROUPS[sel].slice(); | |
| 754 | - | return SCAN_TYPES[sel] ? [sel] : ['i32']; | |
| 755 | - | } | |
| 705 | + | const expandTypes = (sel) => | |
| 706 | + | SCAN_TYPE_GROUPS[sel] ? SCAN_TYPE_GROUPS[sel].slice() : (SCAN_TYPES[sel] ? [sel] : ['i32']); | |
| 756 | 707 | ||
| 757 | 708 | // Parse the user's typed value into the JS form the type compares with. | |
| 758 | 709 | function parseScanValue(type, raw) { | |
| @@ -786,12 +737,12 @@ | |||
|---|---|---|---|
| 786 | 737 | // "-1.5" → (-1.6, -1.5]. So magnitude grows symmetrically for either sign. | |
| 787 | 738 | function makeExactMatcher(type, raw) { | |
| 788 | 739 | const t = SCAN_TYPES[type]; if (!t) return null; | |
| 789 | - | if (t.big) { const target = parseScanValue(type, raw); if (target === null) return null; return { value: target, match: function (x) { return x === target; } }; } | |
| 740 | + | if (t.big) { const target = parseScanValue(type, raw); if (target === null) return null; return { value: target, match: x => x === target }; } | |
| 790 | 741 | const v = Number(raw); if (!isFinite(v)) return null; | |
| 791 | - | if (!t.float) return { value: v, match: function (x) { return x === v; } }; | |
| 742 | + | if (!t.float) return { value: v, match: x => x === v }; | |
| 792 | 743 | const p = precisionFromString(raw), base = Math.round(v / p) * p; // snap to the precision grid | |
| 793 | - | if (base < 0) { const lo = base - p, hi = base; return { value: v, match: function (x) { return x > lo && x <= hi; } }; } | |
| 794 | - | const lo = base, hi = base + p; return { value: v, match: function (x) { return x >= lo && x < hi; } }; | |
| 744 | + | if (base < 0) { const lo = base - p, hi = base; return { value: v, match: x => x > lo && x <= hi }; } | |
| 745 | + | const lo = base, hi = base + p; return { value: v, match: x => x >= lo && x < hi }; | |
| 795 | 746 | } | |
| 796 | 747 | // Union matcher for a type group ('all'/'allint'/'allfloat'): matches if ANY expanded | |
| 797 | 748 | // type's exact matcher does. Object-graph values are all f64, so this widens a whole | |
| @@ -799,9 +750,9 @@ | |||
|---|---|---|---|
| 799 | 750 | // already does per-type — otherwise a group would collapse to typeList[0]'s int matcher. | |
| 800 | 751 | function makeGroupMatcher(typeList, raw) { | |
| 801 | 752 | if (raw == null) return null; | |
| 802 | - | const ms = typeList.map(function (t) { return makeExactMatcher(t, raw); }).filter(Boolean); | |
| 753 | + | const ms = typeList.map(t => makeExactMatcher(t, raw)).filter(Boolean); | |
| 803 | 754 | if (!ms.length) return null; | |
| 804 | - | return { match: function (x) { for (let i = 0; i < ms.length; i++) if (ms[i].match(x)) return true; return false; } }; | |
| 755 | + | return { match: x => ms.some(m => m.match(x)) }; | |
| 805 | 756 | } | |
| 806 | 757 | // refine criteria: 'exact' uses the typed-precision matcher; the rest compare a | |
| 807 | 758 | // fresh read `cur` against the stored previous value `prev`. | |
| @@ -815,19 +766,18 @@ | |||
|---|---|---|---|
| 815 | 766 | default: return false; | |
| 816 | 767 | } | |
| 817 | 768 | } | |
| 818 | - | function scanValueToWire(v) { return (typeof v === 'bigint') ? v.toString() : v; } | |
| 769 | + | const scanValueToWire = (v) => (typeof v === 'bigint') ? v.toString() : v; | |
| 819 | 770 | ||
| 820 | 771 | // Run `body()` in slices, yielding to the event loop between them so a scan | |
| 821 | 772 | // never freezes the frame and can be cancelled mid-flight. body() returns true | |
| 822 | 773 | // while more work remains; onFinish(cancelled) fires once at the end. | |
| 823 | 774 | function chunkLoop(job, body, onFinish) { | |
| 824 | - | function tick() { | |
| 775 | + | (function tick() { | |
| 825 | 776 | if (job.cancelled) { onFinish(true); return; } | |
| 826 | 777 | let more = false; | |
| 827 | 778 | try { more = body(); } catch (e) { onFinish(false); return; } | |
| 828 | 779 | if (more) origSetTimeout(tick, 0); else onFinish(false); | |
| 829 | - | } | |
| 830 | - | tick(); | |
| 780 | + | })(); | |
| 831 | 781 | } | |
| 832 | 782 | ||
| 833 | 783 | /* ---- WASM backend -------------------------------------------------- * | |
| @@ -838,7 +788,7 @@ | |||
|---|---|---|---|
| 838 | 788 | * cancellable. Up to SCAN_STORE_CAP matches are tracked, so refining a large | |
| 839 | 789 | * first scan narrows the WHOLE set — not just the first rows shown. | |
| 840 | 790 | * ------------------------------------------------------------------- */ | |
| 841 | - | const wasmScan = (function () { | |
| 791 | + | const wasmScan = (() => { | |
| 842 | 792 | let memIndex = 0; // which wasmMemories entry we're scanning | |
| 843 | 793 | let types = ['i32']; // type list for the current scan | |
| 844 | 794 | let candidates = null; // [{ off, val, ty }] or null | |
| @@ -846,43 +796,51 @@ | |||
|---|---|---|---|
| 846 | 796 | const CHUNK = 8 * 1024 * 1024; // bytes scanned per event-loop slice | |
| 847 | 797 | const REFINE_BUDGET = 200000; // candidates re-checked per slice during refine | |
| 848 | 798 | ||
| 849 | - | function handle() { return wasmMemories[memIndex] || null; } | |
| 850 | - | function view() { const h = handle(); try { return h ? new DataView(h.memory.buffer) : null; } catch (e) { return null; } } | |
| 851 | - | function matcherCache(raw) { const c = {}; return function (t) { if (!(t in c)) c[t] = (raw != null ? makeExactMatcher(t, raw) : null); return c[t]; }; } | |
| 799 | + | const handle = () => wasmMemories[memIndex] || null; | |
| 800 | + | const buffer = () => { const h = handle(); try { return h ? h.memory.buffer : null; } catch (e) { return null; } }; | |
| 801 | + | const view = () => { const b = buffer(); return b ? new DataView(b) : null; }; | |
| 802 | + | const matcherCache = (raw) => { const c = {}; return (t) => { if (!(t in c)) c[t] = (raw != null ? makeExactMatcher(t, raw) : null); return c[t]; }; }; | |
| 852 | 803 | ||
| 853 | - | function reset() { candidates = null; snapshot = null; } | |
| 804 | + | const reset = () => { candidates = null; snapshot = null; }; | |
| 854 | 805 | ||
| 855 | 806 | // Scan the whole buffer for each type in `types`, keeping matches for whichever | |
| 856 | - | // `accept(ty, cur, prev)` returns true (prev is the snapshot byte value, or the | |
| 857 | - | // same as cur for a fresh exact scan). One CHUNK of one type per slice. | |
| 858 | - | function scanAll(accept, prevDv, prevLen, job, done) { | |
| 807 | + | // `accept(ty, cur, prev)` returns true (prev is the snapshot value at the same | |
| 808 | + | // offset, or the same as cur for a fresh exact scan). One CHUNK of one type per | |
| 809 | + | // slice. Bulk reads go through TypedArray views (recreated per slice, since | |
| 810 | + | // memory.grow() detaches the buffer) — several× faster than per-call DataView | |
| 811 | + | // getters, and every scanned offset is type-aligned because we step from 0 by size. | |
| 812 | + | function scanAll(accept, prevBuf, prevLen, job, done) { | |
| 859 | 813 | const h = handle(); if (!h) { done({ error: 'no WASM memory' }); return; } | |
| 860 | 814 | let len = 0; try { len = h.memory.buffer.byteLength; } catch (e) {} | |
| 861 | 815 | const out = []; let count = 0, capped = false, ti = 0, off = 0; | |
| 862 | - | chunkLoop(job, function () { | |
| 816 | + | chunkLoop(job, () => { | |
| 863 | 817 | if (ti >= types.length) return false; | |
| 864 | - | const dv = view(); if (!dv) return false; | |
| 865 | - | const t = types[ti], sz = SCAN_TYPES[t].size; | |
| 866 | - | const cap = prevDv ? Math.min(len, dv.byteLength, prevLen) : Math.min(len, dv.byteLength); | |
| 818 | + | const buf = buffer(); if (!buf) return false; | |
| 819 | + | const t = types[ti], { size: sz, arr: Arr } = SCAN_TYPES[t]; | |
| 820 | + | const cap = prevBuf ? Math.min(len, buf.byteLength, prevLen) : Math.min(len, buf.byteLength); | |
| 867 | 821 | const end = Math.min(cap, off + CHUNK); | |
| 868 | - | for (; off + sz <= end; off += sz) { | |
| 869 | - | let cur, prev; try { cur = SCAN_TYPES[t].get(dv, off); prev = prevDv ? SCAN_TYPES[t].get(prevDv, off) : cur; } catch (e) { continue; } | |
| 870 | - | if (accept(t, cur, prev)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ off: off, val: cur, ty: t }); else capped = true; } | |
| 822 | + | const ta = new Arr(buf, 0, Math.floor(cap / sz)); // may throw on detach → chunkLoop finishes | |
| 823 | + | const pa = prevBuf ? new Arr(prevBuf, 0, Math.floor(cap / sz)) : null; | |
| 824 | + | const stop = Math.floor(end / sz); | |
| 825 | + | for (let i = off / sz; i < stop; i++) { | |
| 826 | + | const cur = ta[i], prev = pa ? pa[i] : cur; | |
| 827 | + | if (accept(t, cur, prev)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ off: i * sz, val: cur, ty: t }); else capped = true; } | |
| 871 | 828 | } | |
| 829 | + | off = stop * sz; | |
| 872 | 830 | if (off + sz > cap) { ti++; off = 0; } | |
| 873 | - | if (job.onProgress) job.onProgress(Math.min(1, (ti + (cap > 0 ? Math.min(1, off / cap) : 1)) / types.length)); | |
| 831 | + | job.onProgress?.(Math.min(1, (ti + (cap > 0 ? Math.min(1, off / cap) : 1)) / types.length)); | |
| 874 | 832 | return ti < types.length; | |
| 875 | - | }, function (cancelled) { | |
| 833 | + | }, (cancelled) => { | |
| 876 | 834 | if (cancelled) { done({ cancelled: true }); return; } | |
| 877 | - | candidates = out; done({ count: count, capped: capped, out: out }); | |
| 835 | + | candidates = out; done({ count, capped, out }); | |
| 878 | 836 | }); | |
| 879 | 837 | } | |
| 880 | 838 | ||
| 881 | 839 | function firstExact(typeList, raw, job, done) { | |
| 882 | 840 | types = typeList.slice(); snapshot = null; candidates = null; | |
| 883 | 841 | const mfor = matcherCache(raw); | |
| 884 | - | if (types.every(function (t) { return !mfor(t); })) { done({ error: 'bad value' }); return; } | |
| 885 | - | scanAll(function (t, cur) { const m = mfor(t); return m && m.match(cur); }, null, 0, job, done); | |
| 842 | + | if (types.every(t => !mfor(t))) { done({ error: 'bad value' }); return; } | |
| 843 | + | scanAll((t, cur) => { const m = mfor(t); return m && m.match(cur); }, null, 0, job, done); | |
| 886 | 844 | } | |
| 887 | 845 | ||
| 888 | 846 | function firstUnknown(typeList, job, done) { | |
| @@ -895,8 +853,8 @@ | |||
|---|---|---|---|
| 895 | 853 | // Build the first candidate list from the unknown-scan snapshot by diffing the buffer. | |
| 896 | 854 | function materialize(criteria, raw, job, done) { | |
| 897 | 855 | if (!snapshot) { done({ error: 'no snapshot' }); return; } | |
| 898 | - | const mfor = matcherCache(raw), prevDv = new DataView(snapshot.buffer), snapLen = snapshot.byteLength; | |
| 899 | - | scanAll(function (t, cur, prev) { return passesCriteria(t, criteria, cur, prev, mfor(t)); }, prevDv, snapLen, job, function (r) { | |
| 856 | + | const mfor = matcherCache(raw); | |
| 857 | + | scanAll((t, cur, prev) => passesCriteria(t, criteria, cur, prev, mfor(t)), snapshot.buffer, snapshot.byteLength, job, (r) => { | |
| 900 | 858 | if (!r.cancelled && !r.error) { try { snapshot = new Uint8Array(handle().memory.buffer.slice(0)); } catch (e) {} } // re-baseline | |
| 901 | 859 | done(r); | |
| 902 | 860 | }); | |
| @@ -905,20 +863,19 @@ | |||
|---|---|---|---|
| 905 | 863 | function refine(criteria, raw, job, done) { | |
| 906 | 864 | if (candidates === null && snapshot !== null) { materialize(criteria, raw, job, done); return; } | |
| 907 | 865 | if (candidates === null) { done({ error: 'no scan in progress' }); return; } | |
| 908 | - | const dv = view(); if (!dv) { done({ error: 'no WASM memory' }); return; } | |
| 866 | + | if (!view()) { done({ error: 'no WASM memory' }); return; } | |
| 909 | 867 | const mfor = matcherCache(raw), src = candidates, kept = []; let i = 0; | |
| 910 | - | chunkLoop(job, function () { | |
| 868 | + | chunkLoop(job, () => { | |
| 911 | 869 | const d = view(); if (!d) return false; | |
| 912 | - | let n = 0; | |
| 913 | - | for (; i < src.length && n < REFINE_BUDGET; i++, n++) { | |
| 914 | - | const c = src[i], sz = SCAN_TYPES[c.ty].size; | |
| 870 | + | for (let n = 0; i < src.length && n < REFINE_BUDGET; i++, n++) { | |
| 871 | + | const c = src[i], { size: sz, get } = SCAN_TYPES[c.ty]; | |
| 915 | 872 | if (c.off + sz > d.byteLength) continue; | |
| 916 | - | let cur; try { cur = SCAN_TYPES[c.ty].get(d, c.off); } catch (e) { continue; } | |
| 873 | + | let cur; try { cur = get(d, c.off); } catch (e) { continue; } | |
| 917 | 874 | if (passesCriteria(c.ty, criteria, cur, c.val, mfor(c.ty))) kept.push({ off: c.off, val: cur, ty: c.ty }); | |
| 918 | 875 | } | |
| 919 | - | if (job.onProgress) job.onProgress(src.length ? i / src.length : 1); | |
| 876 | + | job.onProgress?.(src.length ? i / src.length : 1); | |
| 920 | 877 | return i < src.length; | |
| 921 | - | }, function (cancelled) { | |
| 878 | + | }, (cancelled) => { | |
| 922 | 879 | if (cancelled) { done({ cancelled: true }); return; } | |
| 923 | 880 | candidates = kept; done({ count: kept.length, capped: false }); | |
| 924 | 881 | }); | |
| @@ -934,24 +891,23 @@ | |||
|---|---|---|---|
| 934 | 891 | return out; | |
| 935 | 892 | } | |
| 936 | 893 | function readAddress(address, t) { | |
| 937 | - | const p = String(address).split(':'); const mi = +p[0], off = +p[1], ty = p[2] || t; | |
| 938 | - | const h = wasmMemories[mi]; if (!h || !SCAN_TYPES[ty]) return null; | |
| 939 | - | try { return scanValueToWire(SCAN_TYPES[ty].get(new DataView(h.memory.buffer), off)); } catch (e) { return null; } | |
| 894 | + | const [mi, off, aty] = String(address).split(':'); const ty = aty || t; | |
| 895 | + | const h = wasmMemories[+mi]; if (!h || !SCAN_TYPES[ty]) return null; | |
| 896 | + | try { return scanValueToWire(SCAN_TYPES[ty].get(new DataView(h.memory.buffer), +off)); } catch (e) { return null; } | |
| 940 | 897 | } | |
| 941 | 898 | function writeAddress(address, t, raw) { | |
| 942 | - | const p = String(address).split(':'); const mi = +p[0], off = +p[1], ty = p[2] || t; | |
| 943 | - | const h = wasmMemories[mi]; if (!h || !SCAN_TYPES[ty]) return false; | |
| 899 | + | const [mi, off, aty] = String(address).split(':'); const ty = aty || t; | |
| 900 | + | const h = wasmMemories[+mi]; if (!h || !SCAN_TYPES[ty]) return false; | |
| 944 | 901 | const v = parseScanValue(ty, raw); if (v === null) return false; | |
| 945 | - | try { SCAN_TYPES[ty].set(new DataView(h.memory.buffer), off, v); return true; } catch (e) { return false; } | |
| 902 | + | try { SCAN_TYPES[ty].set(new DataView(h.memory.buffer), +off, v); return true; } catch (e) { return false; } | |
| 946 | 903 | } | |
| 947 | - | function memories() { return wasmMemories.map(function (m) { let mb = 0; try { mb = m.memory.buffer.byteLength >> 20; } catch (e) {} return { label: m.label + ' (' + mb + ' MB)' }; }); } | |
| 948 | - | function setMem(i) { memIndex = i | 0; reset(); } | |
| 904 | + | const memories = () => wasmMemories.map(m => { | |
| 905 | + | let mb = 0; try { mb = m.memory.buffer.byteLength >> 20; } catch (e) {} | |
| 906 | + | return { label: m.label + ' (' + mb + ' MB)' }; | |
| 907 | + | }); | |
| 908 | + | const setMem = (i) => { memIndex = i | 0; reset(); }; | |
| 949 | 909 | ||
| 950 | - | return { | |
| 951 | - | memories: memories, setMem: setMem, reset: reset, | |
| 952 | - | firstExact: firstExact, firstUnknown: firstUnknown, refine: refine, | |
| 953 | - | rows: rows, readAddress: readAddress, writeAddress: writeAddress | |
| 954 | - | }; | |
| 910 | + | return { memories, setMem, reset, firstExact, firstUnknown, refine, rows, readAddress, writeAddress }; | |
| 955 | 911 | })(); | |
| 956 | 912 | ||
| 957 | 913 | /* ---- Object-graph backend ----------------------------------------- * | |
| @@ -960,27 +916,35 @@ | |||
|---|---|---|---|
| 960 | 916 | * keys). JS numbers are all f64, so the int/float type only tunes equality | |
| 961 | 917 | * (integer match vs. epsilon) here — noted in the UI. | |
| 962 | 918 | * ------------------------------------------------------------------- */ | |
| 963 | - | const objectScan = (function () { | |
| 919 | + | const objectScan = (() => { | |
| 964 | 920 | const NODE_CAP = 200000, DEPTH_CAP = 12, BUDGET = 15000; // nodes per event-loop slice | |
| 965 | 921 | let type = 'f64'; // display / default-write type (typeList[0]); values are all f64 | |
| 966 | 922 | let types = ['f64']; // full expanded type list of the current scan (for group matchers) | |
| 967 | 923 | let candidates = null; // [{ path:[...], val }] | |
| 968 | 924 | let snapshot = null; // Map(pathKey -> { path, val }) for "unknown initial value" | |
| 969 | 925 | ||
| 970 | - | function pathKey(path) { return path.join(' '); } | |
| 926 | + | const pathKey = (path) => path.join(' '); | |
| 971 | 927 | function resolve(path) { | |
| 972 | 928 | let o = pageWin; | |
| 973 | 929 | for (let i = 0; i < path.length; i++) { if (o == null) return undefined; o = o[path[i]]; } | |
| 974 | 930 | return o; | |
| 975 | 931 | } | |
| 976 | 932 | // Iterative, chunked DFS over numeric leaves. cb(path, value); match(value) filters. | |
| 933 | + | // Stack frames are parent-pointer nodes [obj, key, parentNode, depth]; the full path | |
| 934 | + | // array is only materialized for leaves that actually match (path.concat per node | |
| 935 | + | // used to allocate hundreds of thousands of throwaway arrays on a full walk). | |
| 977 | 936 | function walkAsync(cb, match, job, done) { | |
| 978 | 937 | const seen = new WeakSet(); let nodes = 0; | |
| 979 | - | const stack = [[pageWin, [], 0]]; | |
| 980 | - | chunkLoop(job, function () { | |
| 938 | + | const stack = [[pageWin, null, null, 0]]; | |
| 939 | + | const pathOf = (node, leafKey) => { | |
| 940 | + | const p = [leafKey]; | |
| 941 | + | for (let n = node; n && n[1] !== null; n = n[2]) p.push(n[1]); | |
| 942 | + | return p.reverse(); | |
| 943 | + | }; | |
| 944 | + | chunkLoop(job, () => { | |
| 981 | 945 | let processed = 0; | |
| 982 | 946 | while (stack.length && processed < BUDGET && nodes < NODE_CAP) { | |
| 983 | - | const fr = stack.pop(); const obj = fr[0], path = fr[1], depth = fr[2]; processed++; | |
| 947 | + | const fr = stack.pop(); const obj = fr[0], depth = fr[3]; processed++; | |
| 984 | 948 | if (obj == null || depth > DEPTH_CAP) continue; | |
| 985 | 949 | let keys; try { keys = Object.keys(obj); } catch (e) { continue; } | |
| 986 | 950 | for (let i = 0; i < keys.length; i++) { | |
| @@ -988,23 +952,22 @@ | |||
|---|---|---|---|
| 988 | 952 | const k = keys[i]; let v; | |
| 989 | 953 | try { v = obj[k]; } catch (e) { continue; } | |
| 990 | 954 | const tv = typeof v; | |
| 991 | - | if (tv === 'number') { if (isFinite(v) && (!match || match(v))) cb(path.concat(k), v); } | |
| 955 | + | if (tv === 'number') { if (isFinite(v) && (!match || match(v))) cb(pathOf(fr, k), v); } | |
| 992 | 956 | else if (tv === 'object' || tv === 'function') { | |
| 993 | 957 | if (v === null || seen.has(v) || v === pageWin || v === window) continue; | |
| 994 | 958 | try { if (v.nodeType && v.nodeName) continue; } catch (e) {} // DOM nodes | |
| 995 | 959 | try { if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer) continue; } catch (e) {} | |
| 996 | 960 | seen.add(v); nodes++; | |
| 997 | - | stack.push([v, path.concat(k), depth + 1]); | |
| 961 | + | stack.push([v, k, fr, depth + 1]); | |
| 998 | 962 | } | |
| 999 | 963 | } | |
| 1000 | 964 | } | |
| 1001 | - | if (job.onProgress) job.onProgress(-1); // total is unknown up front → indeterminate | |
| 965 | + | job.onProgress?.(-1); // total is unknown up front → indeterminate | |
| 1002 | 966 | return stack.length > 0 && nodes < NODE_CAP; | |
| 1003 | 967 | }, done); | |
| 1004 | 968 | } | |
| 1005 | 969 | ||
| 1006 | - | function reset() { candidates = null; snapshot = null; } | |
| 1007 | - | function setMem() {} // n/a for object graph | |
| 970 | + | const reset = () => { candidates = null; snapshot = null; }; | |
| 1008 | 971 | ||
| 1009 | 972 | // All JS numbers are f64, so the width only tunes equality (exact int vs. float | |
| 1010 | 973 | // cell); a multi-type selection just uses the first type's matcher here. | |
| @@ -1013,56 +976,50 @@ | |||
|---|---|---|---|
| 1013 | 976 | const matcher = makeGroupMatcher(types, raw); | |
| 1014 | 977 | if (!matcher) { done({ error: 'bad value' }); return; } | |
| 1015 | 978 | const out = []; let count = 0, capped = false; | |
| 1016 | - | walkAsync(function (path, v) { count++; if (out.length < SCAN_STORE_CAP) out.push({ path: path, val: v }); else capped = true; }, | |
| 1017 | - | function (v) { return matcher.match(v); }, job, function (cancelled) { | |
| 979 | + | walkAsync((path, v) => { count++; if (out.length < SCAN_STORE_CAP) out.push({ path, val: v }); else capped = true; }, | |
| 980 | + | v => matcher.match(v), job, (cancelled) => { | |
| 1018 | 981 | if (cancelled) { done({ cancelled: true }); return; } | |
| 1019 | - | candidates = out; done({ count: count, capped: capped }); | |
| 982 | + | candidates = out; done({ count, capped }); | |
| 1020 | 983 | }); | |
| 1021 | 984 | } | |
| 1022 | 985 | function firstUnknown(typeList, job, done) { | |
| 1023 | 986 | types = typeList.slice(); type = typeList[0] || 'f64'; candidates = null; | |
| 1024 | 987 | const snap = new Map(); | |
| 1025 | - | walkAsync(function (path, v) { snap.set(pathKey(path), { path: path, val: v }); }, null, job, function (cancelled) { | |
| 988 | + | walkAsync((path, v) => snap.set(pathKey(path), { path, val: v }), null, job, (cancelled) => { | |
| 1026 | 989 | if (cancelled) { done({ cancelled: true }); return; } | |
| 1027 | 990 | snapshot = snap; done({ count: -1, capped: true }); | |
| 1028 | 991 | }); | |
| 1029 | 992 | } | |
| 1030 | - | function materialize(criteria, raw, job, done) { | |
| 1031 | - | if (!snapshot) { done({ error: 'no snapshot' }); return; } | |
| 993 | + | // Re-check `entries` ({ path, val }) against a fresh read, chunked so huge candidate | |
| 994 | + | // sets never freeze the frame; used by both materialize (from snapshot, re-baselining) | |
| 995 | + | // and refine (from candidates). | |
| 996 | + | function recheck(entries, criteria, raw, rebaseline, job, done) { | |
| 1032 | 997 | const matcher = (raw != null) ? makeGroupMatcher(types, raw) : null; | |
| 1033 | - | const entries = Array.from(snapshot.values()); | |
| 1034 | 998 | const out = []; let count = 0, capped = false, i = 0; | |
| 1035 | - | chunkLoop(job, function () { | |
| 1036 | - | let processed = 0; | |
| 1037 | - | for (; i < entries.length && processed < BUDGET; i++, processed++) { | |
| 999 | + | chunkLoop(job, () => { | |
| 1000 | + | for (let processed = 0; i < entries.length && processed < BUDGET; i++, processed++) { | |
| 1038 | 1001 | const entry = entries[i]; const cur = resolve(entry.path); | |
| 1039 | 1002 | if (typeof cur !== 'number' || !isFinite(cur)) continue; | |
| 1040 | 1003 | if (passesCriteria(type, criteria, cur, entry.val, matcher)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ path: entry.path, val: cur }); else capped = true; } | |
| 1041 | - | entry.val = cur; // re-baseline | |
| 1004 | + | if (rebaseline) entry.val = cur; | |
| 1042 | 1005 | } | |
| 1043 | - | if (job.onProgress) job.onProgress(entries.length ? i / entries.length : 1); | |
| 1006 | + | job.onProgress?.(entries.length ? i / entries.length : 1); | |
| 1044 | 1007 | return i < entries.length; | |
| 1045 | - | }, function (cancelled) { | |
| 1008 | + | }, (cancelled) => { | |
| 1046 | 1009 | if (cancelled) { done({ cancelled: true }); return; } | |
| 1047 | - | candidates = out; done({ count: count, capped: capped }); | |
| 1010 | + | candidates = out; done({ count, capped }); | |
| 1048 | 1011 | }); | |
| 1049 | 1012 | } | |
| 1050 | 1013 | function refine(criteria, raw, job, done) { | |
| 1051 | - | if (candidates === null && snapshot !== null) { materialize(criteria, raw, job, done); return; } | |
| 1014 | + | if (candidates === null && snapshot !== null) { recheck(Array.from(snapshot.values()), criteria, raw, true, job, done); return; } | |
| 1052 | 1015 | if (candidates === null) { done({ error: 'no scan in progress' }); return; } | |
| 1053 | - | const matcher = (raw != null) ? makeGroupMatcher(types, raw) : null; | |
| 1054 | - | const kept = []; | |
| 1055 | - | for (let i = 0; i < candidates.length; i++) { | |
| 1056 | - | const c = candidates[i]; const cur = resolve(c.path); | |
| 1057 | - | if (typeof cur === 'number' && isFinite(cur) && passesCriteria(type, criteria, cur, c.val, matcher)) kept.push({ path: c.path, val: cur }); | |
| 1058 | - | } | |
| 1059 | - | candidates = kept; done({ count: kept.length, capped: false }); | |
| 1016 | + | recheck(candidates, criteria, raw, false, job, done); | |
| 1060 | 1017 | } | |
| 1061 | 1018 | function rows(limit) { | |
| 1062 | 1019 | const out = [], list = candidates || []; | |
| 1063 | 1020 | for (let i = 0; i < list.length && i < limit; i++) { | |
| 1064 | 1021 | const cur = resolve(list[i].path); | |
| 1065 | - | out.push({ address: JSON.stringify(list[i].path), value: (typeof cur === 'number' ? cur : null), type: type }); | |
| 1022 | + | out.push({ address: JSON.stringify(list[i].path), value: (typeof cur === 'number' ? cur : null), type }); | |
| 1066 | 1023 | } | |
| 1067 | 1024 | return out; | |
| 1068 | 1025 | } | |
| @@ -1076,22 +1033,19 @@ | |||
|---|---|---|---|
| 1076 | 1033 | try { const parent = resolve(path.slice(0, -1)); if (parent == null) return false; parent[path[path.length - 1]] = (typeof v === 'bigint' ? Number(v) : v); return true; } | |
| 1077 | 1034 | catch (e) { return false; } | |
| 1078 | 1035 | } | |
| 1079 | - | function memories() { return []; } | |
| 1036 | + | const memories = () => []; | |
| 1037 | + | const setMem = () => {}; // n/a for object graph | |
| 1080 | 1038 | ||
| 1081 | - | return { | |
| 1082 | - | memories: memories, setMem: setMem, reset: reset, | |
| 1083 | - | firstExact: firstExact, firstUnknown: firstUnknown, refine: refine, | |
| 1084 | - | rows: rows, readAddress: readAddress, writeAddress: writeAddress | |
| 1085 | - | }; | |
| 1039 | + | return { memories, setMem, reset, firstExact, firstUnknown, refine, rows, readAddress, writeAddress }; | |
| 1086 | 1040 | })(); | |
| 1087 | 1041 | ||
| 1088 | - | function scanEngine(name) { return name === 'object' ? objectScan : wasmScan; } | |
| 1042 | + | const scanEngine = (name) => name === 'object' ? objectScan : wasmScan; | |
| 1089 | 1043 | ||
| 1090 | 1044 | // Address labels for display (wasm "mi:off:ty" → "mem#mi +0x.. (ty)"; object path → "window..."). | |
| 1091 | 1045 | function scanAddressLabel(engine, address) { | |
| 1092 | 1046 | if (engine === 'object') { try { return 'window.' + JSON.parse(address).join('.'); } catch (e) { return String(address); } } | |
| 1093 | - | const p = String(address).split(':'); | |
| 1094 | - | return 'mem#' + p[0] + ' +0x' + (Number(p[1]) || 0).toString(16) + (p[2] ? ' (' + p[2] + ')' : ''); | |
| 1047 | + | const [mi, off, ty] = String(address).split(':'); | |
| 1048 | + | return 'mem#' + mi + ' +0x' + (Number(off) || 0).toString(16) + (ty ? ' (' + ty + ')' : ''); | |
| 1095 | 1049 | } | |
| 1096 | 1050 | ||
| 1097 | 1051 | // Execute one scan command against the LOCAL engines; resolves a wire-safe result. | |
| @@ -1100,42 +1054,42 @@ | |||
|---|---|---|---|
| 1100 | 1054 | const SCAN_ROW_LIMIT = 200; // most rows we ship/render at once | |
| 1101 | 1055 | let scanJob = null; | |
| 1102 | 1056 | function runScanCommand(cmd, onProgress) { | |
| 1103 | - | return new Promise(function (resolve) { | |
| 1057 | + | return new Promise((resolve) => { | |
| 1104 | 1058 | try { | |
| 1105 | 1059 | const eng = scanEngine(cmd.engine); | |
| 1060 | + | const cancelJob = () => { if (scanJob) scanJob.cancelled = true; }; | |
| 1106 | 1061 | switch (cmd.op) { | |
| 1107 | 1062 | case 'ping': resolve({ ok: true, pong: true }); return; | |
| 1108 | 1063 | case 'list-memories': resolve({ ok: true, memories: wasmScan.memories() }); return; | |
| 1109 | - | case 'set-mem': if (scanJob) scanJob.cancelled = true; eng.setMem(cmd.mem | 0); resolve({ ok: true }); return; | |
| 1110 | - | case 'reset': if (scanJob) scanJob.cancelled = true; eng.reset(); resolve({ ok: true, count: 0, rows: [] }); return; | |
| 1111 | - | case 'cancel': if (scanJob) scanJob.cancelled = true; resolve({ ok: true, cancelled: true }); return; | |
| 1112 | - | case 'set-paused': setPaused(cmd.value); resolve({ ok: true, paused: paused }); return; | |
| 1113 | - | case 'read': { | |
| 1114 | - | const vals = (cmd.addresses || []).map(function (a) { return { address: a, value: scanValueToWire(eng.readAddress(a, cmd.type)) }; }); | |
| 1115 | - | resolve({ ok: true, values: vals }); return; | |
| 1116 | - | } | |
| 1064 | + | case 'set-mem': cancelJob(); eng.setMem(cmd.mem | 0); resolve({ ok: true }); return; | |
| 1065 | + | case 'reset': cancelJob(); eng.reset(); resolve({ ok: true, count: 0, rows: [] }); return; | |
| 1066 | + | case 'cancel': cancelJob(); resolve({ ok: true, cancelled: true }); return; | |
| 1067 | + | case 'set-paused': setPaused(cmd.value); resolve({ ok: true, paused }); return; | |
| 1068 | + | case 'read': | |
| 1069 | + | resolve({ ok: true, values: (cmd.addresses || []).map(a => ({ address: a, value: scanValueToWire(eng.readAddress(a, cmd.type)) })) }); | |
| 1070 | + | return; | |
| 1117 | 1071 | case 'write': { | |
| 1118 | 1072 | const ok = eng.writeAddress(cmd.address, cmd.type, cmd.value); | |
| 1119 | - | resolve({ ok: ok, value: scanValueToWire(eng.readAddress(cmd.address, cmd.type)) }); return; | |
| 1073 | + | resolve({ ok, value: scanValueToWire(eng.readAddress(cmd.address, cmd.type)) }); return; | |
| 1120 | 1074 | } | |
| 1121 | 1075 | case 'first-exact': case 'first-unknown': case 'refine': { | |
| 1122 | - | if (scanJob) scanJob.cancelled = true; // supersede any prior scan | |
| 1076 | + | cancelJob(); // supersede any prior scan | |
| 1123 | 1077 | const job = { cancelled: false, onProgress: onProgress || null }; scanJob = job; | |
| 1124 | - | const done = function (r) { | |
| 1078 | + | const done = (r) => { | |
| 1125 | 1079 | if (scanJob === job) scanJob = null; | |
| 1126 | - | if (!r || r.error) { resolve({ ok: false, error: (r && r.error) || 'scan failed' }); return; } | |
| 1080 | + | if (!r || r.error) { resolve({ ok: false, error: r?.error || 'scan failed' }); return; } | |
| 1127 | 1081 | if (r.cancelled) { resolve({ ok: true, cancelled: true }); return; } | |
| 1128 | 1082 | resolve({ ok: true, count: r.count, capped: r.capped, rows: eng.rows(SCAN_ROW_LIMIT) }); | |
| 1129 | 1083 | }; | |
| 1130 | 1084 | const types = expandTypes(cmd.type); | |
| 1131 | 1085 | if (cmd.op === 'first-exact') eng.firstExact(types, cmd.value, job, done); | |
| 1132 | 1086 | else if (cmd.op === 'first-unknown') eng.firstUnknown(types, job, done); | |
| 1133 | - | else eng.refine(cmd.criteria, (cmd.value != null ? cmd.value : null), job, done); | |
| 1087 | + | else eng.refine(cmd.criteria, cmd.value ?? null, job, done); | |
| 1134 | 1088 | return; | |
| 1135 | 1089 | } | |
| 1136 | 1090 | default: resolve({ ok: false, error: 'unknown op' }); return; | |
| 1137 | 1091 | } | |
| 1138 | - | } catch (e) { resolve({ ok: false, error: String(e && e.message || e) }); } | |
| 1092 | + | } catch (e) { resolve({ ok: false, error: String(e?.message || e) }); } | |
| 1139 | 1093 | }); | |
| 1140 | 1094 | } | |
| 1141 | 1095 | ||
| @@ -1155,7 +1109,7 @@ | |||
|---|---|---|---|
| 1155 | 1109 | const scanHandledQ = []; // FIFO to bound scanHandled | |
| 1156 | 1110 | function sendScan(targetId, targetWin, cmd, onProgress) { | |
| 1157 | 1111 | if (!targetId) return runScanCommand(cmd, onProgress); // null → this frame (local engine, no messaging) | |
| 1158 | - | return new Promise(function (resolve) { | |
| 1112 | + | return new Promise((resolve) => { | |
| 1159 | 1113 | const reqId = scanSeq++; | |
| 1160 | 1114 | scanPending.set(reqId, resolve); | |
| 1161 | 1115 | if (onProgress) scanProgress.set(reqId, onProgress); | |
| @@ -1163,12 +1117,11 @@ | |||
|---|---|---|---|
| 1163 | 1117 | // the scan value-type field is also called `type` and would otherwise overwrite | |
| 1164 | 1118 | // the envelope's `type: 'scan-cmd'`, so the target saw an unknown message type | |
| 1165 | 1119 | // and silently dropped every remote scan (v1.4.5 fix). | |
| 1166 | - | const msg = { type: 'scan-cmd', reqId: reqId, targetFrame: targetId, from: SELF_ID, cmd: cmd }; | |
| 1167 | - | if (targetWin) { try { postTo(targetWin, msg); } catch (e) {} } // proven channel (clicker/settings use it) | |
| 1168 | - | broadcastScanMsg(msg); // + frame-tree broadcast as backup | |
| 1120 | + | const msg = { type: 'scan-cmd', reqId, targetFrame: targetId, from: SELF_ID, cmd }; | |
| 1121 | + | if (targetWin) postTo(targetWin, msg); // proven channel (clicker/settings use it) | |
| 1122 | + | broadcastScanMsg(msg); // + frame-tree broadcast as backup | |
| 1169 | 1123 | }); | |
| 1170 | 1124 | } | |
| 1171 | - | ||
| 1172 | 1125 | /* ------------------------------------------------------------------ * | |
| 1173 | 1126 | * UI (Shadow DOM) | |
| 1174 | 1127 | * ------------------------------------------------------------------ */ | |
| @@ -1447,104 +1400,105 @@ | |||
|---|---|---|---|
| 1447 | 1400 | // listener registered on window/document in the CAPTURE phase still sees the event — | |
| 1448 | 1401 | // nothing in the same DOM can prevent that; detaching into the game's own frame, or | |
| 1449 | 1402 | // an input-isolating iframe, would be the only full fix.) | |
| 1450 | - | ['pointerdown','pointerup','mousedown','mouseup','click','dblclick','contextmenu', | |
| 1451 | - | 'keydown','keyup','keypress','wheel','touchstart','touchend','pointermove','mousemove' | |
| 1452 | - | ].forEach(function (t) { host.addEventListener(t, function (e) { e.stopPropagation(); }, false); }); | |
| 1453 | - | ||
| 1454 | - | const $ = function (s) { return root.querySelector(s); }; | |
| 1455 | - | const panel = $('#panel'), bar = $('#bar'), badge = $('#badge'), title = $('#title'); | |
| 1456 | - | const range = $('#scaleRange'), num = $('#scaleNum'), out = $('#scaleOut'); | |
| 1457 | - | const btnMin = $('#min'), btnClose = $('#close'), dlg = $('#dlg'), status = $('#status'); | |
| 1458 | - | const framesBox = $('#framesBox'), frameList = $('#frameList'), reattachBtn = $('#reattach'); | |
| 1403 | + | for (const t of ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click', 'dblclick', 'contextmenu', | |
| 1404 | + | 'keydown', 'keyup', 'keypress', 'wheel', 'touchstart', 'touchend', 'pointermove', 'mousemove']) | |
| 1405 | + | host.addEventListener(t, e => e.stopPropagation(), false); | |
| 1406 | + | ||
| 1407 | + | // Every [id] in the template, keyed by id: ui.panel, ui.scaleRange, ui.clkCps, … | |
| 1408 | + | const ui = {}; | |
| 1409 | + | root.querySelectorAll('[id]').forEach(n => { ui[n.id] = n; }); | |
| 1410 | + | const el = (tag, cls, text) => { | |
| 1411 | + | const n = document.createElement(tag); | |
| 1412 | + | if (cls) n.className = cls; | |
| 1413 | + | if (text != null) n.textContent = text; | |
| 1414 | + | return n; | |
| 1415 | + | }; | |
| 1416 | + | const { panel, bar } = ui; | |
| 1459 | 1417 | ||
| 1460 | 1418 | // status line — confirms we reached the page's real context | |
| 1461 | - | if (hasUnsafe) { status.className = 'status ok'; status.textContent = '✓ patching page context (unsafeWindow)'; } | |
| 1462 | - | else { status.className = 'status warn'; status.textContent = '⚠ unsafeWindow not found — using this window. If nothing speeds up, the manager is sandboxing the script.'; } | |
| 1419 | + | if (hasUnsafe) { ui.status.className = 'status ok'; ui.status.textContent = '✓ patching page context (unsafeWindow)'; } | |
| 1420 | + | else { ui.status.className = 'status warn'; ui.status.textContent = '⚠ unsafeWindow not found — using this window. If nothing speeds up, the manager is sandboxing the script.'; } | |
| 1463 | 1421 | ||
| 1464 | 1422 | // toggles (the 5 hooks + turbo) | |
| 1465 | - | const tg = $('#toggles'); | |
| 1466 | - | const hookInputs = {}; | |
| 1467 | - | Object.keys(hooks).forEach(function (name) { | |
| 1468 | - | const lab = document.createElement('label'); | |
| 1469 | - | lab.className = 'tg'; | |
| 1470 | - | lab.innerHTML = '<input type="checkbox" ' + (state[name] ? 'checked' : '') + '><span>' + hooks[name].label + '</span>'; | |
| 1423 | + | const makeToggle = (text, checked, onChange) => { | |
| 1424 | + | const lab = el('label', 'tg'); | |
| 1425 | + | lab.innerHTML = '<input type="checkbox"><span></span>'; | |
| 1471 | 1426 | const inp = lab.querySelector('input'); | |
| 1472 | - | hookInputs[name] = inp; | |
| 1473 | - | inp.addEventListener('change', function (e) { | |
| 1474 | - | setHook(name, e.target.checked); | |
| 1427 | + | inp.checked = checked; | |
| 1428 | + | lab.querySelector('span').textContent = text; | |
| 1429 | + | inp.addEventListener('change', e => onChange(e.target.checked)); | |
| 1430 | + | ui.toggles.appendChild(lab); | |
| 1431 | + | return { lab, inp }; | |
| 1432 | + | }; | |
| 1433 | + | const hookInputs = {}; | |
| 1434 | + | for (const name of Object.keys(hooks)) { | |
| 1435 | + | hookInputs[name] = makeToggle(hooks[name].label, state[name], (on) => { | |
| 1436 | + | setHook(name, on); | |
| 1475 | 1437 | if (name === 'raf') updateTurboEnabled(); | |
| 1476 | 1438 | broadcastSettings(); | |
| 1477 | - | }); | |
| 1478 | - | tg.appendChild(lab); | |
| 1479 | - | }); | |
| 1480 | - | const tlab = document.createElement('label'); | |
| 1481 | - | tlab.className = 'tg'; | |
| 1482 | - | tlab.innerHTML = '<input type="checkbox"><span>rAF turbo — multi-step (experimental)</span>'; | |
| 1483 | - | const turboInput = tlab.querySelector('input'); | |
| 1484 | - | turboInput.checked = turbo; | |
| 1485 | - | turboInput.addEventListener('change', function (e) { turbo = e.target.checked; turboT = null; broadcastSettings(); }); | |
| 1486 | - | tg.appendChild(tlab); | |
| 1439 | + | }).inp; | |
| 1440 | + | } | |
| 1441 | + | const turboTg = makeToggle('rAF turbo — multi-step (experimental)', turbo, (on) => { turbo = on; turboT = null; broadcastSettings(); }); | |
| 1442 | + | const turboInput = turboTg.inp; | |
| 1487 | 1443 | ||
| 1488 | 1444 | // turbo only does anything while the rAF hook is installed | |
| 1489 | 1445 | function updateTurboEnabled() { | |
| 1490 | 1446 | const on = !!state.raf; | |
| 1491 | 1447 | turboInput.disabled = !on; | |
| 1492 | - | tlab.classList.toggle('disabled', !on); | |
| 1448 | + | turboTg.lab.classList.toggle('disabled', !on); | |
| 1493 | 1449 | } | |
| 1494 | 1450 | updateTurboEnabled(); | |
| 1495 | 1451 | ||
| 1496 | 1452 | // scale | |
| 1497 | - | function reflect(v) { out.textContent = v + '×'; badge.textContent = v + '×'; } | |
| 1453 | + | function reflect(v) { ui.scaleOut.textContent = v + '×'; ui.badge.textContent = v + '×'; } | |
| 1498 | 1454 | const MAX_SCALE = 1000, SLIDER_MAX = 100, SLIDER_MIN = 0.1; | |
| 1455 | + | const clampSlider = (v) => Math.min(SLIDER_MAX, Math.max(SLIDER_MIN, v)); | |
| 1499 | 1456 | // writeNum=false while the user is typing into the number field, so we don't | |
| 1500 | 1457 | // clobber the caret / intermediate input — that field is normalized on commit. | |
| 1501 | 1458 | function onScale(v, writeNum) { | |
| 1502 | 1459 | v = Number(v); if (!isFinite(v) || v <= 0) return; | |
| 1503 | 1460 | if (v > MAX_SCALE) v = MAX_SCALE; // hard cap, incl. typed-in numbers | |
| 1504 | 1461 | applyScale(v); | |
| 1505 | - | range.value = Math.min(SLIDER_MAX, Math.max(SLIDER_MIN, v)); | |
| 1506 | - | if (writeNum !== false) num.value = v; | |
| 1462 | + | ui.scaleRange.value = clampSlider(v); | |
| 1463 | + | if (writeNum !== false) ui.scaleNum.value = v; | |
| 1507 | 1464 | reflect(v); | |
| 1508 | 1465 | broadcastSettings(); | |
| 1509 | 1466 | } | |
| 1510 | - | range.addEventListener('input', function (e) { onScale(e.target.value); }); | |
| 1511 | - | num.addEventListener('input', function (e) { onScale(e.target.value, false); }); | |
| 1512 | - | num.addEventListener('change', function (e) { onScale(e.target.value); }); // normalize + cap on commit | |
| 1513 | - | root.querySelectorAll('.presets button').forEach(function (b) { | |
| 1514 | - | b.addEventListener('click', function () { onScale(b.dataset.s); }); | |
| 1467 | + | ui.scaleRange.addEventListener('input', e => onScale(e.target.value)); | |
| 1468 | + | ui.scaleNum.addEventListener('input', e => onScale(e.target.value, false)); | |
| 1469 | + | ui.scaleNum.addEventListener('change', e => onScale(e.target.value)); // normalize + cap on commit | |
| 1470 | + | root.querySelectorAll('.presets button').forEach(b => { | |
| 1471 | + | b.addEventListener('click', () => onScale(b.dataset.s)); | |
| 1515 | 1472 | }); | |
| 1516 | 1473 | ||
| 1517 | 1474 | // pull controls back in line with current state (used when settings arrive remotely) | |
| 1518 | 1475 | function sync() { | |
| 1519 | - | Object.keys(hookInputs).forEach(function (n) { hookInputs[n].checked = !!state[n]; }); | |
| 1476 | + | for (const n of Object.keys(hookInputs)) hookInputs[n].checked = !!state[n]; | |
| 1520 | 1477 | turboInput.checked = turbo; | |
| 1521 | 1478 | updateTurboEnabled(); | |
| 1522 | - | range.value = Math.min(SLIDER_MAX, Math.max(SLIDER_MIN, scale)); | |
| 1523 | - | num.value = scale; | |
| 1479 | + | ui.scaleRange.value = clampSlider(scale); | |
| 1480 | + | ui.scaleNum.value = scale; | |
| 1524 | 1481 | reflect(scale); | |
| 1525 | 1482 | } | |
| 1526 | 1483 | ||
| 1527 | 1484 | // frame list (host mode) — one row per child frame that has announced itself | |
| 1528 | 1485 | function refreshFrames() { | |
| 1529 | 1486 | refreshScanTargets(); | |
| 1530 | - | if (curMode !== 'host') { framesBox.hidden = true; return; } | |
| 1487 | + | if (curMode !== 'host') { ui.framesBox.hidden = true; return; } | |
| 1531 | 1488 | pruneFrames(); | |
| 1532 | - | frameList.textContent = ''; | |
| 1533 | - | if (frames.size === 0) { framesBox.hidden = true; return; } | |
| 1534 | - | framesBox.hidden = false; | |
| 1535 | - | frames.forEach(function (f, src) { | |
| 1536 | - | const row = document.createElement('div'); row.className = 'frow'; | |
| 1537 | - | const label = document.createElement('span'); label.className = 'furl'; | |
| 1538 | - | label.textContent = shortUrl(f.url); label.title = f.url; | |
| 1539 | - | const btn = document.createElement('button'); | |
| 1540 | - | btn.textContent = f.attached ? 'Detach' : 'Re-attach'; | |
| 1541 | - | btn.addEventListener('click', function () { f.attached ? detachFrame(src) : reattachFrame(src); }); | |
| 1542 | - | row.appendChild(label); row.appendChild(btn); | |
| 1543 | - | frameList.appendChild(row); | |
| 1489 | + | ui.frameList.textContent = ''; | |
| 1490 | + | ui.framesBox.hidden = frames.size === 0; | |
| 1491 | + | frames.forEach((f, src) => { | |
| 1492 | + | const row = el('div', 'frow'); | |
| 1493 | + | const label = el('span', 'furl', shortUrl(f.url)); label.title = f.url; | |
| 1494 | + | const btn = el('button', '', f.attached ? 'Detach' : 'Re-attach'); | |
| 1495 | + | btn.addEventListener('click', () => { f.attached ? detachFrame(src) : reattachFrame(src); }); | |
| 1496 | + | row.append(label, btn); | |
| 1497 | + | ui.frameList.appendChild(row); | |
| 1544 | 1498 | }); | |
| 1545 | 1499 | } | |
| 1546 | 1500 | ||
| 1547 | - | reattachBtn.addEventListener('click', function () { | |
| 1501 | + | ui.reattach.addEventListener('click', () => { | |
| 1548 | 1502 | if (hostWin) postTo(hostWin, { type: 'reattach' }); | |
| 1549 | 1503 | attached = true; | |
| 1550 | 1504 | destroyPanel(); // back to headless; host will resend settings | |
| @@ -1552,17 +1506,15 @@ | |||
|---|---|---|---|
| 1552 | 1506 | ||
| 1553 | 1507 | // tabs | |
| 1554 | 1508 | const panes = {}; | |
| 1555 | - | root.querySelectorAll('.pane').forEach(function (p) { panes[p.dataset.pane] = p; }); | |
| 1509 | + | root.querySelectorAll('.pane').forEach(p => { panes[p.dataset.pane] = p; }); | |
| 1556 | 1510 | const tabBtns = root.querySelectorAll('.tab'); | |
| 1557 | 1511 | function setTab(name) { | |
| 1558 | - | tabBtns.forEach(function (b) { b.classList.toggle('active', b.dataset.tab === name); }); | |
| 1559 | - | Object.keys(panes).forEach(function (n) { panes[n].hidden = (n !== name); }); | |
| 1512 | + | tabBtns.forEach(b => b.classList.toggle('active', b.dataset.tab === name)); | |
| 1513 | + | for (const n of Object.keys(panes)) panes[n].hidden = (n !== name); | |
| 1560 | 1514 | } | |
| 1561 | - | tabBtns.forEach(function (b) { b.addEventListener('click', function () { setTab(b.dataset.tab); }); }); | |
| 1515 | + | tabBtns.forEach(b => b.addEventListener('click', () => setTab(b.dataset.tab))); | |
| 1562 | 1516 | ||
| 1563 | 1517 | /* ----- clicker controls ----- */ | |
| 1564 | - | const clkStatus = $('#clkStatus'), clkKey = $('#clkKey'), clkSet = $('#clkSet'), clkClear = $('#clkClear'); | |
| 1565 | - | const clkSwallow = $('#clkSwallow'), clkCps = $('#clkCps'), clkJitter = $('#clkJitter'), clkHold = $('#clkHold'); | |
| 1566 | 1518 | const clkModeBtns = root.querySelectorAll('#clkMode button'); | |
| 1567 | 1519 | ||
| 1568 | 1520 | function keyLabel(hk) { | |
| @@ -1571,60 +1523,50 @@ | |||
|---|---|---|---|
| 1571 | 1523 | (hk.key === ' ' ? 'Space' : hk.key); | |
| 1572 | 1524 | } | |
| 1573 | 1525 | function syncClicker() { | |
| 1574 | - | clkModeBtns.forEach(function (b) { b.classList.toggle('on', b.dataset.mode === clicker.mode); }); | |
| 1575 | - | clkKey.textContent = clicker.listening ? 'press a key…' : keyLabel(clicker.hotkey); | |
| 1576 | - | clkKey.classList.toggle('listening', clicker.listening); | |
| 1577 | - | clkSwallow.checked = clicker.swallowHotkey; | |
| 1526 | + | clkModeBtns.forEach(b => b.classList.toggle('on', b.dataset.mode === clicker.mode)); | |
| 1527 | + | ui.clkKey.textContent = clicker.listening ? 'press a key…' : keyLabel(clicker.hotkey); | |
| 1528 | + | ui.clkKey.classList.toggle('listening', clicker.listening); | |
| 1529 | + | ui.clkSwallow.checked = clicker.swallowHotkey; | |
| 1578 | 1530 | // root.activeElement (not document.activeElement) sees focus *inside* the shadow root, | |
| 1579 | 1531 | // so we don't overwrite a field the user is currently typing into. | |
| 1580 | - | if (root.activeElement !== clkCps) clkCps.value = clicker.cps; | |
| 1581 | - | if (root.activeElement !== clkJitter) clkJitter.value = clicker.jitterMs; | |
| 1582 | - | if (root.activeElement !== clkHold) clkHold.value = clicker.holdMs; | |
| 1583 | - | clkStatus.className = 'status ' + (clicker.running ? 'run' : 'idle'); | |
| 1584 | - | clkStatus.textContent = clicker.running | |
| 1532 | + | if (root.activeElement !== ui.clkCps) ui.clkCps.value = clicker.cps; | |
| 1533 | + | if (root.activeElement !== ui.clkJitter) ui.clkJitter.value = clicker.jitterMs; | |
| 1534 | + | if (root.activeElement !== ui.clkHold) ui.clkHold.value = clicker.holdMs; | |
| 1535 | + | ui.clkStatus.className = 'status ' + (clicker.running ? 'run' : 'idle'); | |
| 1536 | + | ui.clkStatus.textContent = clicker.running | |
| 1585 | 1537 | ? '● clicking — ' + Math.round(clicker.cps) + '/s at cursor' | |
| 1586 | 1538 | : (clicker.hotkey ? '○ idle — press ' + keyLabel(clicker.hotkey) + ' to ' + (clicker.mode === 'hold' ? 'hold' : 'toggle') | |
| 1587 | 1539 | : '○ idle — set a hotkey to start'); | |
| 1588 | 1540 | } | |
| 1589 | - | clkModeBtns.forEach(function (b) { | |
| 1590 | - | b.addEventListener('click', function () { | |
| 1541 | + | clkModeBtns.forEach(b => { | |
| 1542 | + | b.addEventListener('click', () => { | |
| 1591 | 1543 | clicker.mode = b.dataset.mode; | |
| 1592 | 1544 | if (clicker.running) setClickerRunning(false, true); // mode switch is a clean stop | |
| 1593 | 1545 | syncClicker(); broadcastClickerConfig(); | |
| 1594 | 1546 | }); | |
| 1595 | 1547 | }); | |
| 1596 | - | clkSet.addEventListener('click', function () { clicker.listening = true; syncClicker(); }); | |
| 1597 | - | clkClear.addEventListener('click', function () { | |
| 1548 | + | ui.clkSet.addEventListener('click', () => { clicker.listening = true; syncClicker(); }); | |
| 1549 | + | ui.clkClear.addEventListener('click', () => { | |
| 1598 | 1550 | if (clicker.running) setClickerRunning(false, true); | |
| 1599 | 1551 | clicker.hotkey = null; clicker.listening = false; syncClicker(); broadcastClickerConfig(); | |
| 1600 | 1552 | }); | |
| 1601 | - | clkSwallow.addEventListener('change', function (e) { clicker.swallowHotkey = e.target.checked; broadcastClickerConfig(); }); | |
| 1602 | - | function commitNum(input, key, min, max) { | |
| 1603 | - | let v = Number(input.value); | |
| 1604 | - | if (!isFinite(v)) return; | |
| 1605 | - | v = Math.min(max, Math.max(min, v)); | |
| 1606 | - | clicker[key] = v; | |
| 1607 | - | broadcastClickerConfig(); | |
| 1553 | + | ui.clkSwallow.addEventListener('change', e => { clicker.swallowHotkey = e.target.checked; broadcastClickerConfig(); }); | |
| 1554 | + | // Each numeric field commits live on input (clamped) and snaps back to the | |
| 1555 | + | // accepted value on change (blur/Enter). | |
| 1556 | + | for (const [input, key, min, max] of [[ui.clkCps, 'cps', 0.1, MAX_CPS], [ui.clkJitter, 'jitterMs', 0, 2000], [ui.clkHold, 'holdMs', 0, 2000]]) { | |
| 1557 | + | input.addEventListener('input', () => { | |
| 1558 | + | const v = Number(input.value); | |
| 1559 | + | if (!isFinite(v)) return; | |
| 1560 | + | clicker[key] = Math.min(max, Math.max(min, v)); | |
| 1561 | + | broadcastClickerConfig(); | |
| 1562 | + | }); | |
| 1563 | + | input.addEventListener('change', () => { input.value = clicker[key]; }); | |
| 1608 | 1564 | } | |
| 1609 | - | clkCps.addEventListener('input', function () { commitNum(clkCps, 'cps', 0.1, MAX_CPS); }); | |
| 1610 | - | clkCps.addEventListener('change', function () { clkCps.value = clicker.cps; }); | |
| 1611 | - | clkJitter.addEventListener('input', function () { commitNum(clkJitter, 'jitterMs', 0, 2000); }); | |
| 1612 | - | clkJitter.addEventListener('change', function () { clkJitter.value = clicker.jitterMs; }); | |
| 1613 | - | clkHold.addEventListener('input', function () { commitNum(clkHold, 'holdMs', 0, 2000); }); | |
| 1614 | - | clkHold.addEventListener('change', function () { clkHold.value = clicker.holdMs; }); | |
| 1615 | 1565 | syncClicker(); | |
| 1616 | 1566 | ||
| 1617 | 1567 | /* ----- scan controls ----- */ | |
| 1618 | - | const scTarget = $('#scTarget'), scPause = $('#scPause'), scStatus = $('#scStatus'); | |
| 1619 | - | const scMem = $('#scMem'), scMemDetect = $('#scMemDetect'), scType = $('#scType'), scValue = $('#scValue'); | |
| 1620 | - | const scFirst = $('#scFirst'), scCancel = $('#scCancel'); | |
| 1621 | - | const scProgress = $('#scProgress'), scBar = $('#scBar'); | |
| 1622 | 1568 | const scModeInputs = root.querySelectorAll('#scMode input'); | |
| 1623 | - | const scRefine = $('#scRefine'), scCount = $('#scCount'), scResults = $('#scResults'), scSaved = $('#scSaved'); | |
| 1624 | - | function getScMode() { | |
| 1625 | - | for (let i = 0; i < scModeInputs.length; i++) if (scModeInputs[i].checked) return scModeInputs[i].value; | |
| 1626 | - | return 'exact'; | |
| 1627 | - | } | |
| 1569 | + | const getScMode = () => [...scModeInputs].find(r => r.checked)?.value || 'exact'; | |
| 1628 | 1570 | const scEngineBtns = root.querySelectorAll('#scEngine button'); | |
| 1629 | 1571 | const scRefineBtns = root.querySelectorAll('#scRefine button'); | |
| 1630 | 1572 | ||
| @@ -1638,36 +1580,34 @@ | |||
|---|---|---|---|
| 1638 | 1580 | let pausedLocalView = false; | |
| 1639 | 1581 | ||
| 1640 | 1582 | function scCmd(extra, onProgress) { | |
| 1641 | - | const cmd = { engine: scEngineName, type: scType.value }; | |
| 1642 | - | for (const k in extra) cmd[k] = extra[k]; | |
| 1643 | - | return sendScan(scTargetId, scTargetWin, cmd, onProgress); | |
| 1583 | + | return sendScan(scTargetId, scTargetWin, { engine: scEngineName, type: ui.scType.value, ...extra }, onProgress); | |
| 1644 | 1584 | } | |
| 1645 | 1585 | ||
| 1646 | 1586 | // Two independent status lines so they never clobber each other: | |
| 1647 | 1587 | // - setConn(): frame / WASM-memory / pause state (top, #scStatus) | |
| 1648 | 1588 | // - setScan(): scan progress + result counts (#scCount) | |
| 1649 | - | function setConn(kind, text) { scStatus.className = 'status ' + kind; scStatus.textContent = text; } | |
| 1650 | - | function setScan(text) { scCount.textContent = text; } | |
| 1589 | + | function setConn(kind, text) { ui.scStatus.className = 'status ' + kind; ui.scStatus.textContent = text; } | |
| 1590 | + | function setScan(text) { ui.scCount.textContent = text; } | |
| 1651 | 1591 | ||
| 1652 | 1592 | function refreshScanTargets() { | |
| 1653 | - | const prev = scTarget.value; | |
| 1593 | + | const prev = ui.scTarget.value; | |
| 1654 | 1594 | scTargetList.length = 0; | |
| 1655 | - | scTarget.textContent = ''; | |
| 1656 | - | const self = document.createElement('option'); self.value = 'self'; self.textContent = 'This frame'; | |
| 1657 | - | scTarget.appendChild(self); | |
| 1595 | + | ui.scTarget.textContent = ''; | |
| 1596 | + | const self = el('option', '', 'This frame'); self.value = 'self'; | |
| 1597 | + | ui.scTarget.appendChild(self); | |
| 1658 | 1598 | // `frames` is only populated in host mode; a detached child just sees itself. | |
| 1659 | - | frames.forEach(function (f, src) { | |
| 1599 | + | frames.forEach((f, src) => { | |
| 1660 | 1600 | if (!f.id) return; | |
| 1661 | 1601 | const idx = scTargetList.push({ id: f.id, win: src }) - 1; | |
| 1662 | - | const opt = document.createElement('option'); | |
| 1663 | - | opt.value = 'f' + idx; opt.textContent = shortUrl(f.url); opt.title = f.url; | |
| 1664 | - | scTarget.appendChild(opt); | |
| 1602 | + | const opt = el('option', '', shortUrl(f.url)); | |
| 1603 | + | opt.value = 'f' + idx; opt.title = f.url; | |
| 1604 | + | ui.scTarget.appendChild(opt); | |
| 1665 | 1605 | }); | |
| 1666 | - | scTarget.value = Array.prototype.some.call(scTarget.options, function (o) { return o.value === prev; }) ? prev : 'self'; | |
| 1606 | + | ui.scTarget.value = [...ui.scTarget.options].some(o => o.value === prev) ? prev : 'self'; | |
| 1667 | 1607 | applyTargetSelection(); | |
| 1668 | 1608 | } | |
| 1669 | 1609 | function applyTargetSelection() { | |
| 1670 | - | const v = scTarget.value; | |
| 1610 | + | const v = ui.scTarget.value; | |
| 1671 | 1611 | const ent = (v === 'self') ? null : scTargetList[+v.slice(1)]; | |
| 1672 | 1612 | scTargetId = ent ? ent.id : null; | |
| 1673 | 1613 | scTargetWin = ent ? ent.win : null; | |
| @@ -1675,27 +1615,27 @@ | |||
|---|---|---|---|
| 1675 | 1615 | ||
| 1676 | 1616 | // Reflect scanActive / scanRunning onto the buttons. | |
| 1677 | 1617 | function updateButtons() { | |
| 1678 | - | scFirst.textContent = scanActive ? 'New scan' : 'First scan'; | |
| 1679 | - | scFirst.disabled = scanRunning; | |
| 1618 | + | ui.scFirst.textContent = scanActive ? 'New scan' : 'First scan'; | |
| 1619 | + | ui.scFirst.disabled = scanRunning; | |
| 1680 | 1620 | // Scan mode (Known value / Unknown initial) is locked once a scan exists — it only | |
| 1681 | 1621 | // applies to starting a NEW scan. Start a new scan to change it. | |
| 1682 | - | scModeInputs.forEach(function (r) { r.disabled = scanRunning || scanActive; }); | |
| 1683 | - | scCancel.hidden = !scanRunning; | |
| 1684 | - | scRefine.hidden = !scanActive; | |
| 1685 | - | scRefineBtns.forEach(function (b) { b.disabled = scanRunning || !scanActive; }); | |
| 1686 | - | scType.disabled = scanRunning; | |
| 1622 | + | scModeInputs.forEach(r => { r.disabled = scanRunning || scanActive; }); | |
| 1623 | + | ui.scCancel.hidden = !scanRunning; | |
| 1624 | + | ui.scRefine.hidden = !scanActive; | |
| 1625 | + | scRefineBtns.forEach(b => { b.disabled = scanRunning || !scanActive; }); | |
| 1626 | + | ui.scType.disabled = scanRunning; | |
| 1687 | 1627 | // The value box stays editable during refine (so "Exact" refine can take a new value); | |
| 1688 | 1628 | // it's disabled only while a scan runs, or for a fresh "Unknown initial" scan (no value). | |
| 1689 | - | scValue.disabled = scanRunning || (!scanActive && getScMode() === 'unknown'); | |
| 1690 | - | scMemDetect.disabled = scanRunning; | |
| 1629 | + | ui.scValue.disabled = scanRunning || (!scanActive && getScMode() === 'unknown'); | |
| 1630 | + | ui.scMemDetect.disabled = scanRunning; | |
| 1691 | 1631 | } | |
| 1692 | 1632 | ||
| 1693 | 1633 | function setEngine(name) { | |
| 1694 | 1634 | scEngineName = name; | |
| 1695 | - | scEngineBtns.forEach(function (b) { b.classList.toggle('on', b.dataset.engine === name); }); | |
| 1635 | + | scEngineBtns.forEach(b => b.classList.toggle('on', b.dataset.engine === name)); | |
| 1696 | 1636 | const wasm = (name === 'wasm'); | |
| 1697 | - | scMemDetect.hidden = !wasm; | |
| 1698 | - | scMem.hidden = true; | |
| 1637 | + | ui.scMemDetect.hidden = !wasm; | |
| 1638 | + | ui.scMem.hidden = true; | |
| 1699 | 1639 | newScan(); | |
| 1700 | 1640 | if (wasm) { refreshMemList(); return; } | |
| 1701 | 1641 | // object-graph engine: confirm a remote frame is reachable (same short | |
| @@ -1703,13 +1643,13 @@ | |||
|---|---|---|---|
| 1703 | 1643 | if (!scTargetId) { setConn('idle', 'Walking values reachable from this frame’s window.'); return; } | |
| 1704 | 1644 | const id = scTargetId; let settled = false; | |
| 1705 | 1645 | setConn('idle', 'Connecting to frame…'); | |
| 1706 | - | origSetTimeout(function () { | |
| 1646 | + | origSetTimeout(() => { | |
| 1707 | 1647 | if (settled || scTargetId !== id || scEngineName !== 'object') return; | |
| 1708 | 1648 | settled = true; setConn('warn', 'No response from that frame. If it just loaded, re-select it; otherwise open the frame’s own panel and scan there.'); | |
| 1709 | 1649 | }, 2500); | |
| 1710 | - | scCmd({ op: 'ping' }).then(function (res) { | |
| 1650 | + | scCmd({ op: 'ping' }).then((res) => { | |
| 1711 | 1651 | if (settled || scTargetId !== id || scEngineName !== 'object') return; | |
| 1712 | - | settled = true; setConn(res && res.ok ? 'ok' : 'warn', res && res.ok ? 'Frame connected — walking values reachable from its window.' : 'Frame not responding.'); | |
| 1652 | + | settled = true; setConn(res?.ok ? 'ok' : 'warn', res?.ok ? 'Frame connected — walking values reachable from its window.' : 'Frame not responding.'); | |
| 1713 | 1653 | }); | |
| 1714 | 1654 | } | |
| 1715 | 1655 | ||
| @@ -1721,22 +1661,22 @@ | |||
|---|---|---|---|
| 1721 | 1661 | if (scEngineName !== 'wasm') return; | |
| 1722 | 1662 | const id = scTargetId; let settled = false; | |
| 1723 | 1663 | setConn('idle', 'Detecting WASM memory…'); | |
| 1724 | - | if (id) origSetTimeout(function () { | |
| 1664 | + | if (id) origSetTimeout(() => { | |
| 1725 | 1665 | if (settled || scTargetId !== id || scEngineName !== 'wasm') return; | |
| 1726 | - | settled = true; scMem.hidden = true; | |
| 1666 | + | settled = true; ui.scMem.hidden = true; | |
| 1727 | 1667 | setConn('warn', 'No response from that frame. If the game is still loading, click “Detect” again; otherwise open the frame’s own panel and scan there.'); | |
| 1728 | 1668 | }, 2500); | |
| 1729 | - | scCmd({ op: 'list-memories' }).then(function (res) { | |
| 1669 | + | scCmd({ op: 'list-memories' }).then((res) => { | |
| 1730 | 1670 | if (settled || scEngineName !== 'wasm' || scTargetId !== id) return; // stale/superseded | |
| 1731 | 1671 | settled = true; | |
| 1732 | - | if (!res || res.ok === false) { scMem.hidden = true; setConn('warn', id ? 'Target frame not responding — try detaching its panel and scanning there.' : 'Frame not responding.'); return; } | |
| 1672 | + | if (!res || res.ok === false) { ui.scMem.hidden = true; setConn('warn', id ? 'Target frame not responding — try detaching its panel and scanning there.' : 'Frame not responding.'); return; } | |
| 1733 | 1673 | const mems = res.memories || []; | |
| 1734 | - | const prev = scMem.value; | |
| 1735 | - | scMem.textContent = ''; | |
| 1736 | - | mems.forEach(function (m, i) { const o = document.createElement('option'); o.value = String(i); o.textContent = m.label; scMem.appendChild(o); }); | |
| 1737 | - | if (prev && mems[+prev]) scMem.value = prev; | |
| 1738 | - | scMem.hidden = mems.length === 0; | |
| 1739 | - | scMemDetect.textContent = mems.length ? '↻ Re-detect WASM memory' : '↻ Detect WASM memory'; | |
| 1674 | + | const prev = ui.scMem.value; | |
| 1675 | + | ui.scMem.textContent = ''; | |
| 1676 | + | mems.forEach((m, i) => { const o = el('option', '', m.label); o.value = String(i); ui.scMem.appendChild(o); }); | |
| 1677 | + | if (prev && mems[+prev]) ui.scMem.value = prev; | |
| 1678 | + | ui.scMem.hidden = mems.length === 0; | |
| 1679 | + | ui.scMemDetect.textContent = mems.length ? '↻ Re-detect WASM memory' : '↻ Detect WASM memory'; | |
| 1740 | 1680 | if (mems.length === 0) setConn('warn', 'No WASM memory' + (id ? ' in that frame' : '') + ' yet. If the game is still loading, click “Detect” again.'); | |
| 1741 | 1681 | else setConn('ok', mems.length + ' WASM memor' + (mems.length === 1 ? 'y' : 'ies') + ' found.'); | |
| 1742 | 1682 | }); | |
| @@ -1744,7 +1684,7 @@ | |||
|---|---|---|---|
| 1744 | 1684 | ||
| 1745 | 1685 | function newScan() { | |
| 1746 | 1686 | scanActive = false; | |
| 1747 | - | scResults.textContent = ''; | |
| 1687 | + | ui.scResults.textContent = ''; | |
| 1748 | 1688 | setScan('No scan yet.'); | |
| 1749 | 1689 | showProgress(false); | |
| 1750 | 1690 | updateButtons(); | |
| @@ -1755,7 +1695,7 @@ | |||
|---|---|---|---|
| 1755 | 1695 | ||
| 1756 | 1696 | function onScanDone(res) { | |
| 1757 | 1697 | setScanRunning(false); | |
| 1758 | - | if (!res || !res.ok) { setScan('Scan failed: ' + ((res && res.error) || 'unknown')); return; } | |
| 1698 | + | if (!res || !res.ok) { setScan('Scan failed: ' + (res?.error || 'unknown')); return; } | |
| 1759 | 1699 | if (res.cancelled) { setScan('Scan cancelled.'); updateButtons(); return; } | |
| 1760 | 1700 | scanActive = true; | |
| 1761 | 1701 | updateButtons(); | |
| @@ -1774,51 +1714,51 @@ | |||
|---|---|---|---|
| 1774 | 1714 | setScanRunning(true); | |
| 1775 | 1715 | setScan(progress); | |
| 1776 | 1716 | showProgress(true); | |
| 1777 | - | scCmd(cmd, onScanProgress).then(function (res) { showProgress(false); onScanDone(res); }); | |
| 1717 | + | scCmd(cmd, onScanProgress).then((res) => { showProgress(false); onScanDone(res); }); | |
| 1778 | 1718 | } | |
| 1779 | 1719 | // Progress bar: starts indeterminate (animated stripe); flips to a determinate fill the | |
| 1780 | 1720 | // first time the engine reports a real fraction (frac >= 0). frac < 0 stays indeterminate | |
| 1781 | 1721 | // (the object-graph walk, whose total isn't known up front). | |
| 1782 | 1722 | function showProgress(on) { | |
| 1783 | - | scProgress.hidden = !on; | |
| 1784 | - | if (on) { scBar.classList.add('indet'); scBar.style.width = '0%'; } | |
| 1723 | + | ui.scProgress.hidden = !on; | |
| 1724 | + | if (on) { ui.scBar.classList.add('indet'); ui.scBar.style.width = '0%'; } | |
| 1785 | 1725 | } | |
| 1786 | 1726 | function onScanProgress(frac) { | |
| 1787 | 1727 | if (typeof frac !== 'number' || frac < 0) return; | |
| 1788 | - | scBar.classList.remove('indet'); | |
| 1789 | - | scBar.style.width = Math.max(0, Math.min(1, frac)) * 100 + '%'; | |
| 1728 | + | ui.scBar.classList.remove('indet'); | |
| 1729 | + | ui.scBar.style.width = Math.max(0, Math.min(1, frac)) * 100 + '%'; | |
| 1790 | 1730 | } | |
| 1791 | 1731 | ||
| 1792 | 1732 | function renderRows(rows) { | |
| 1793 | - | scResults.textContent = ''; | |
| 1794 | - | rows.forEach(function (r) { | |
| 1795 | - | const row = document.createElement('div'); row.className = 'sc-row'; row.dataset.addr = r.address; | |
| 1796 | - | const addr = document.createElement('span'); addr.className = 'sc-addr'; | |
| 1797 | - | addr.textContent = scanAddressLabel(scEngineName, r.address); addr.title = addr.textContent; | |
| 1798 | - | const val = document.createElement('input'); val.className = 'sc-val'; val.value = (r.value == null ? '?' : r.value); | |
| 1799 | - | val.addEventListener('change', function () { scCmd({ op: 'write', address: r.address, value: val.value }); }); | |
| 1800 | - | const save = document.createElement('button'); save.className = 'sc-mini'; save.textContent = '★'; | |
| 1801 | - | save.title = 'Save this result'; | |
| 1802 | - | save.addEventListener('click', function () { addSaved(r.address, r.type); }); | |
| 1803 | - | row.appendChild(addr); row.appendChild(val); row.appendChild(save); | |
| 1804 | - | scResults.appendChild(row); | |
| 1805 | - | }); | |
| 1733 | + | ui.scResults.textContent = ''; | |
| 1734 | + | for (const r of rows) { | |
| 1735 | + | const row = el('div', 'sc-row'); row.dataset.addr = r.address; | |
| 1736 | + | const addr = el('span', 'sc-addr', scanAddressLabel(scEngineName, r.address)); addr.title = addr.textContent; | |
| 1737 | + | const val = el('input', 'sc-val'); val.value = (r.value == null ? '?' : r.value); | |
| 1738 | + | val.addEventListener('change', () => scCmd({ op: 'write', address: r.address, value: val.value })); | |
| 1739 | + | const save = el('button', 'sc-mini', '★'); save.title = 'Save this result'; | |
| 1740 | + | save.addEventListener('click', () => addSaved(r.address, r.type)); | |
| 1741 | + | row.append(addr, val, save); | |
| 1742 | + | ui.scResults.appendChild(row); | |
| 1743 | + | } | |
| 1806 | 1744 | } | |
| 1807 | 1745 | ||
| 1808 | 1746 | // live re-read of shown result rows + saved rows (real-time; unaffected by pause) | |
| 1809 | - | const scPane = root.querySelector('.pane[data-pane="scan"]'); | |
| 1747 | + | const scPane = panes.scan; | |
| 1810 | 1748 | function pollValues() { | |
| 1811 | - | if (scPane && scPane.hidden) return; // only poll while the Scan tab is open | |
| 1812 | - | const rowEls = Array.prototype.slice.call(scResults.querySelectorAll('.sc-row')); | |
| 1813 | - | const addrs = rowEls.map(function (el) { return el.dataset.addr; }); | |
| 1749 | + | // Only poll while the Scan tab is actually visible (not tab-hidden, not minimized). | |
| 1750 | + | if ((scPane && scPane.hidden) || panel.classList.contains('min')) return; | |
| 1751 | + | const rowEls = [...ui.scResults.querySelectorAll('.sc-row')]; | |
| 1752 | + | const addrs = rowEls.map(e2 => e2.dataset.addr); | |
| 1814 | 1753 | if (addrs.length) { | |
| 1815 | - | scCmd({ op: 'read', addresses: addrs }).then(function (res) { | |
| 1754 | + | scCmd({ op: 'read', addresses: addrs }).then((res) => { | |
| 1816 | 1755 | if (!res || !res.values) return; | |
| 1817 | - | const byAddr = {}; res.values.forEach(function (v) { byAddr[v.address] = v.value; }); | |
| 1818 | - | rowEls.forEach(function (el) { | |
| 1819 | - | const inp = el.querySelector('.sc-val'); | |
| 1820 | - | if (inp && root.activeElement !== inp) inp.value = (byAddr[el.dataset.addr] == null ? '?' : byAddr[el.dataset.addr]); | |
| 1821 | - | }); | |
| 1756 | + | const byAddr = new Map(res.values.map(v => [v.address, v.value])); | |
| 1757 | + | for (const rowEl of rowEls) { | |
| 1758 | + | const inp = rowEl.querySelector('.sc-val'); | |
| 1759 | + | const v = byAddr.get(rowEl.dataset.addr); | |
| 1760 | + | if (inp && root.activeElement !== inp) inp.value = (v == null ? '?' : v); | |
| 1761 | + | } | |
| 1822 | 1762 | }); | |
| 1823 | 1763 | } | |
| 1824 | 1764 | pollSaved(); | |
| @@ -1827,70 +1767,81 @@ | |||
|---|---|---|---|
| 1827 | 1767 | /* ----- saved list ----- */ | |
| 1828 | 1768 | function persistSaved() { store.set('scan:' + PAGE, savedScans); } | |
| 1829 | 1769 | function addSaved(address, ty) { | |
| 1830 | - | savedScans.push({ name: scanAddressLabel(scEngineName, address), engine: scEngineName, type: (ty || scType.value), address: address }); | |
| 1770 | + | savedScans.push({ name: scanAddressLabel(scEngineName, address), engine: scEngineName, type: (ty || ui.scType.value), address }); | |
| 1831 | 1771 | persistSaved(); renderSaved(); | |
| 1832 | 1772 | } | |
| 1833 | 1773 | function renderSaved() { | |
| 1834 | - | scSaved.textContent = ''; | |
| 1835 | - | savedScans.forEach(function (s, i) { | |
| 1836 | - | const row = document.createElement('div'); row.className = 'sc-srow'; row.dataset.idx = i; | |
| 1837 | - | const name = document.createElement('input'); name.className = 'sc-name'; name.value = s.name; | |
| 1774 | + | ui.scSaved.textContent = ''; | |
| 1775 | + | savedScans.forEach((s, i) => { | |
| 1776 | + | const row = el('div', 'sc-srow'); row.dataset.idx = i; | |
| 1777 | + | const name = el('input', 'sc-name'); name.value = s.name; | |
| 1838 | 1778 | name.title = scanAddressLabel(s.engine, s.address) + ' (' + s.type + ')'; | |
| 1839 | - | name.addEventListener('change', function () { s.name = name.value; persistSaved(); }); | |
| 1840 | - | const val = document.createElement('input'); val.className = 'sc-val'; val.value = '?'; | |
| 1841 | - | val.addEventListener('change', function () { sendScan(scTargetId, scTargetWin, { engine: s.engine, type: s.type, op: 'write', address: s.address, value: val.value }); }); | |
| 1842 | - | const del = document.createElement('button'); del.className = 'sc-mini'; del.textContent = '×'; del.title = 'Delete'; | |
| 1843 | - | del.addEventListener('click', function () { savedScans.splice(i, 1); persistSaved(); renderSaved(); }); | |
| 1844 | - | row.appendChild(name); row.appendChild(val); row.appendChild(del); | |
| 1845 | - | scSaved.appendChild(row); | |
| 1779 | + | name.addEventListener('change', () => { s.name = name.value; persistSaved(); }); | |
| 1780 | + | const val = el('input', 'sc-val'); val.value = '?'; | |
| 1781 | + | val.addEventListener('change', () => sendScan(scTargetId, scTargetWin, { engine: s.engine, type: s.type, op: 'write', address: s.address, value: val.value })); | |
| 1782 | + | const del = el('button', 'sc-mini', '×'); del.title = 'Delete'; | |
| 1783 | + | del.addEventListener('click', () => { savedScans.splice(i, 1); persistSaved(); renderSaved(); }); | |
| 1784 | + | row.append(name, val, del); | |
| 1785 | + | ui.scSaved.appendChild(row); | |
| 1846 | 1786 | }); | |
| 1847 | 1787 | } | |
| 1788 | + | // One batched `read` per (engine, type) group instead of a postMessage round-trip | |
| 1789 | + | // per saved row every poll tick. | |
| 1848 | 1790 | function pollSaved() { | |
| 1849 | - | const rowEls = Array.prototype.slice.call(scSaved.querySelectorAll('.sc-srow')); | |
| 1850 | - | rowEls.forEach(function (el) { | |
| 1851 | - | const s = savedScans[+el.dataset.idx]; if (!s) return; | |
| 1852 | - | sendScan(scTargetId, scTargetWin, { engine: s.engine, type: s.type, op: 'read', addresses: [s.address] }).then(function (res) { | |
| 1853 | - | const inp = el.querySelector('.sc-val'); if (!inp || root.activeElement === inp) return; | |
| 1854 | - | const v = res && res.values && res.values[0] ? res.values[0].value : null; | |
| 1855 | - | inp.value = (v == null ? '∅' : v); | |
| 1856 | - | el.querySelector('.sc-name').classList.toggle('stale', v == null); | |
| 1791 | + | const groups = new Map(); // "engine type" -> { engine, type, addrs, els } | |
| 1792 | + | for (const rowEl of ui.scSaved.querySelectorAll('.sc-srow')) { | |
| 1793 | + | const s = savedScans[+rowEl.dataset.idx]; if (!s) continue; | |
| 1794 | + | const key = s.engine + ' ' + s.type; | |
| 1795 | + | let g = groups.get(key); | |
| 1796 | + | if (!g) { g = { engine: s.engine, type: s.type, addrs: [], els: [] }; groups.set(key, g); } | |
| 1797 | + | g.addrs.push(s.address); g.els.push(rowEl); | |
| 1798 | + | } | |
| 1799 | + | groups.forEach((g) => { | |
| 1800 | + | sendScan(scTargetId, scTargetWin, { engine: g.engine, type: g.type, op: 'read', addresses: g.addrs }).then((res) => { | |
| 1801 | + | const byAddr = new Map((res?.values || []).map(v => [v.address, v.value])); | |
| 1802 | + | g.els.forEach((rowEl, i) => { | |
| 1803 | + | const inp = rowEl.querySelector('.sc-val'); if (!inp || root.activeElement === inp) return; | |
| 1804 | + | const v = byAddr.get(g.addrs[i]); | |
| 1805 | + | inp.value = (v == null ? '∅' : v); | |
| 1806 | + | rowEl.querySelector('.sc-name').classList.toggle('stale', v == null); | |
| 1807 | + | }); | |
| 1857 | 1808 | }); | |
| 1858 | 1809 | }); | |
| 1859 | 1810 | } | |
| 1860 | 1811 | ||
| 1861 | 1812 | // wire scan controls | |
| 1862 | - | scEngineBtns.forEach(function (b) { b.addEventListener('click', function () { setEngine(b.dataset.engine); }); }); | |
| 1863 | - | scTarget.addEventListener('change', function () { applyTargetSelection(); setEngine(scEngineName); }); | |
| 1864 | - | scMem.addEventListener('change', function () { scCmd({ op: 'set-mem', mem: +scMem.value }).then(newScan); }); | |
| 1865 | - | scMemDetect.addEventListener('click', function () { refreshMemList(); }); | |
| 1866 | - | scType.addEventListener('change', function () { const o = scType.options[scType.selectedIndex]; scType.title = o ? o.title : ''; }); | |
| 1867 | - | scModeInputs.forEach(function (r) { r.addEventListener('change', updateButtons); }); | |
| 1868 | - | scFirst.addEventListener('click', function () { | |
| 1813 | + | scEngineBtns.forEach(b => b.addEventListener('click', () => setEngine(b.dataset.engine))); | |
| 1814 | + | ui.scTarget.addEventListener('change', () => { applyTargetSelection(); setEngine(scEngineName); }); | |
| 1815 | + | ui.scMem.addEventListener('change', () => { scCmd({ op: 'set-mem', mem: +ui.scMem.value }).then(newScan); }); | |
| 1816 | + | ui.scMemDetect.addEventListener('click', refreshMemList); | |
| 1817 | + | ui.scType.addEventListener('change', () => { ui.scType.title = ui.scType.options[ui.scType.selectedIndex]?.title || ''; }); | |
| 1818 | + | scModeInputs.forEach(r => r.addEventListener('change', updateButtons)); | |
| 1819 | + | ui.scFirst.addEventListener('click', () => { | |
| 1869 | 1820 | if (scanActive) { newScan(); return; } // acts as "New scan" once a scan exists | |
| 1870 | 1821 | if (getScMode() === 'unknown') { startScan({ op: 'first-unknown' }, 'Snapshotting…'); return; } | |
| 1871 | - | const raw = scValue.value.trim(); | |
| 1822 | + | const raw = ui.scValue.value.trim(); | |
| 1872 | 1823 | if (raw === '') { setScan('Enter a value, or choose “Unknown initial value”.'); return; } | |
| 1873 | 1824 | startScan({ op: 'first-exact', value: raw }, 'Scanning…'); | |
| 1874 | 1825 | }); | |
| 1875 | - | scCancel.addEventListener('click', function () { setScan('Cancelling…'); scCmd({ op: 'cancel' }); }); | |
| 1876 | - | scRefineBtns.forEach(function (b) { | |
| 1877 | - | b.addEventListener('click', function () { | |
| 1878 | - | const crit = b.dataset.crit, raw = scValue.value.trim(); | |
| 1826 | + | ui.scCancel.addEventListener('click', () => { setScan('Cancelling…'); scCmd({ op: 'cancel' }); }); | |
| 1827 | + | scRefineBtns.forEach(b => { | |
| 1828 | + | b.addEventListener('click', () => { | |
| 1829 | + | const crit = b.dataset.crit, raw = ui.scValue.value.trim(); | |
| 1879 | 1830 | startScan({ op: 'refine', criteria: crit, value: (crit === 'exact' && raw !== '') ? raw : undefined }, 'Refining…'); | |
| 1880 | 1831 | }); | |
| 1881 | 1832 | }); | |
| 1882 | - | scPause.addEventListener('click', function () { | |
| 1883 | - | scCmd({ op: 'set-paused', value: !pausedLocalView }).then(function (res) { | |
| 1884 | - | pausedLocalView = !!(res && res.paused); | |
| 1833 | + | ui.scPause.addEventListener('click', () => { | |
| 1834 | + | scCmd({ op: 'set-paused', value: !pausedLocalView }).then((res) => { | |
| 1835 | + | pausedLocalView = !!res?.paused; | |
| 1885 | 1836 | reflectPause(); | |
| 1886 | 1837 | }); | |
| 1887 | 1838 | }); | |
| 1888 | 1839 | function reflectPause() { | |
| 1889 | - | scPause.textContent = pausedLocalView ? '▶ Resume' : '⏸ Pause'; | |
| 1890 | - | scPause.classList.toggle('on', pausedLocalView); | |
| 1840 | + | ui.scPause.textContent = pausedLocalView ? '▶ Resume' : '⏸ Pause'; | |
| 1841 | + | ui.scPause.classList.toggle('on', pausedLocalView); | |
| 1891 | 1842 | } | |
| 1892 | 1843 | ||
| 1893 | - | scType.title = scType.options[scType.selectedIndex] ? scType.options[scType.selectedIndex].title : ''; | |
| 1844 | + | ui.scType.title = ui.scType.options[ui.scType.selectedIndex]?.title || ''; | |
| 1894 | 1845 | renderSaved(); | |
| 1895 | 1846 | setEngine('wasm'); | |
| 1896 | 1847 | reflectPause(); | |
| @@ -1899,66 +1850,71 @@ | |||
|---|---|---|---|
| 1899 | 1850 | let curMode; | |
| 1900 | 1851 | function setMode(m) { | |
| 1901 | 1852 | curMode = m; | |
| 1902 | - | if (m === 'host') { title.textContent = 'Speedhack'; reattachBtn.hidden = true; refreshFrames(); } | |
| 1903 | - | else { title.textContent = 'Speedhack (frame)'; framesBox.hidden = true; reattachBtn.hidden = false; } | |
| 1853 | + | if (m === 'host') { ui.title.textContent = 'Speedhack'; ui.reattach.hidden = true; refreshFrames(); } | |
| 1854 | + | else { ui.title.textContent = 'Speedhack (frame)'; ui.framesBox.hidden = true; ui.reattach.hidden = false; } | |
| 1904 | 1855 | } | |
| 1905 | 1856 | setMode(mode || 'host'); | |
| 1906 | 1857 | ||
| 1907 | 1858 | // minimize (start minimized) | |
| 1908 | - | function setMin(m) { panel.classList.toggle('min', m); btnMin.textContent = m ? '▢' : '–'; } | |
| 1859 | + | function setMin(m) { panel.classList.toggle('min', m); ui.min.textContent = m ? '▢' : '–'; } | |
| 1909 | 1860 | setMin(true); | |
| 1910 | - | btnMin.addEventListener('click', function (e) { e.stopPropagation(); setMin(!panel.classList.contains('min')); }); | |
| 1861 | + | ui.min.addEventListener('click', (e) => { e.stopPropagation(); setMin(!panel.classList.contains('min')); }); | |
| 1911 | 1862 | ||
| 1912 | 1863 | // close + remember dialog | |
| 1913 | - | btnClose.addEventListener('click', function (e) { e.stopPropagation(); $('#dlgurl').textContent = PAGE; dlg.hidden = false; }); | |
| 1914 | - | $('#dlgCancel').addEventListener('click', function () { dlg.hidden = true; }); | |
| 1915 | - | $('#dlgNo').addEventListener('click', function () { doClose(false); }); | |
| 1916 | - | $('#dlgYes').addEventListener('click', function () { doClose(true); }); | |
| 1864 | + | ui.close.addEventListener('click', (e) => { e.stopPropagation(); ui.dlgurl.textContent = PAGE; ui.dlg.hidden = false; }); | |
| 1865 | + | ui.dlgCancel.addEventListener('click', () => { ui.dlg.hidden = true; }); | |
| 1866 | + | ui.dlgNo.addEventListener('click', () => doClose(false)); | |
| 1867 | + | ui.dlgYes.addEventListener('click', () => doClose(true)); | |
| 1917 | 1868 | function doClose(remember) { | |
| 1918 | 1869 | if (remember) store.set(CLOSED_KEY, true); | |
| 1919 | - | Object.keys(hooks).forEach(function (n) { try { hooks[n].uninstall(); } catch (e) {} }); | |
| 1870 | + | for (const n of Object.keys(hooks)) { try { hooks[n].uninstall(); } catch (e) {} } | |
| 1920 | 1871 | // The host is going away, so there's nothing left to re-attach to. Tell EVERY | |
| 1921 | 1872 | // frame (attached or already-detached) to become its own standalone main panel | |
| 1922 | 1873 | // rather than a detached one with a dead "re-attach" button. `hostClosed` also | |
| 1923 | 1874 | // makes us hand off any iframe that announces itself AFTER this point. | |
| 1924 | 1875 | if (isHost) { | |
| 1925 | 1876 | hostClosed = true; | |
| 1926 | - | frames.forEach(function (f, src) { f.attached = false; postTo(src, { type: 'host-closing' }); }); | |
| 1877 | + | frames.forEach((f, src) => { f.attached = false; postTo(src, { type: 'host-closing' }); }); | |
| 1927 | 1878 | } | |
| 1928 | 1879 | destroyPanel(); | |
| 1929 | 1880 | } | |
| 1930 | 1881 | ||
| 1931 | 1882 | // drag (and tap-to-expand when minimized) | |
| 1932 | 1883 | let dragging = false, moved = false, sx, sy, ox, oy; | |
| 1933 | - | bar.addEventListener('pointerdown', function (e) { | |
| 1884 | + | bar.addEventListener('pointerdown', (e) => { | |
| 1934 | 1885 | if (e.target.closest('button')) return; | |
| 1935 | 1886 | dragging = true; moved = false; | |
| 1936 | 1887 | const r = panel.getBoundingClientRect(); | |
| 1937 | 1888 | panel.style.left = r.left + 'px'; panel.style.top = r.top + 'px'; | |
| 1938 | 1889 | panel.style.right = 'auto'; panel.style.bottom = 'auto'; | |
| 1939 | 1890 | sx = e.clientX; sy = e.clientY; ox = r.left; oy = r.top; | |
| 1940 | - | try { bar.setPointerCapture(e.pointerId); } catch (_) {} | |
| 1891 | + | try { bar.setPointerCapture(e.pointerId); } catch (e2) {} | |
| 1941 | 1892 | }); | |
| 1942 | - | bar.addEventListener('pointermove', function (e) { | |
| 1893 | + | bar.addEventListener('pointermove', (e) => { | |
| 1943 | 1894 | if (!dragging) return; | |
| 1944 | 1895 | const dx = e.clientX - sx, dy = e.clientY - sy; | |
| 1945 | 1896 | if (Math.abs(dx) > 4 || Math.abs(dy) > 4) moved = true; | |
| 1946 | 1897 | panel.style.left = Math.max(0, Math.min(window.innerWidth - 30, ox + dx)) + 'px'; | |
| 1947 | 1898 | panel.style.top = Math.max(0, Math.min(window.innerHeight - 20, oy + dy)) + 'px'; | |
| 1948 | 1899 | }); | |
| 1949 | - | bar.addEventListener('pointerup', function (e) { | |
| 1900 | + | bar.addEventListener('pointerup', (e) => { | |
| 1950 | 1901 | if (!dragging) return; dragging = false; | |
| 1951 | - | try { bar.releasePointerCapture(e.pointerId); } catch (_) {} | |
| 1902 | + | try { bar.releasePointerCapture(e.pointerId); } catch (e2) {} | |
| 1952 | 1903 | if (!moved && panel.classList.contains('min')) setMin(false); | |
| 1953 | 1904 | }); | |
| 1954 | 1905 | ||
| 1955 | 1906 | return { | |
| 1956 | - | removeNode: function () { try { origClearInterval(scPoll); } catch (e) {} try { host.remove(); } catch (e) {} if (panelHost === host) panelHost = null; clicker.listening = false; }, | |
| 1957 | - | refreshFrames: refreshFrames, | |
| 1958 | - | refreshScan: refreshScanTargets, | |
| 1959 | - | setMode: setMode, | |
| 1960 | - | sync: sync, | |
| 1961 | - | syncClicker: syncClicker | |
| 1907 | + | removeNode() { | |
| 1908 | + | try { origClearInterval(scPoll); } catch (e) {} | |
| 1909 | + | try { host.remove(); } catch (e) {} | |
| 1910 | + | if (panelHost === host) panelHost = null; | |
| 1911 | + | clicker.listening = false; | |
| 1912 | + | }, | |
| 1913 | + | refreshFrames, | |
| 1914 | + | refreshScan: refreshScanTargets, | |
| 1915 | + | setMode, | |
| 1916 | + | sync, | |
| 1917 | + | syncClicker | |
| 1962 | 1918 | }; | |
| 1963 | 1919 | } | |
| 1964 | 1920 | ||
| @@ -1970,14 +1926,14 @@ | |||
|---|---|---|---|
| 1970 | 1926 | let pendingMode = 'host'; | |
| 1971 | 1927 | function whenBody(fn) { | |
| 1972 | 1928 | if (document.body) { fn(); return; } | |
| 1973 | - | const obs = new MutationObserver(function () { if (document.body) { obs.disconnect(); fn(); } }); | |
| 1929 | + | const obs = new MutationObserver(() => { if (document.body) { obs.disconnect(); fn(); } }); | |
| 1974 | 1930 | try { obs.observe(document.documentElement, { childList: true, subtree: true }); } catch (e) {} | |
| 1975 | - | document.addEventListener('DOMContentLoaded', function () { try { obs.disconnect(); } catch (e) {} fn(); }, { once: true }); | |
| 1931 | + | document.addEventListener('DOMContentLoaded', () => { try { obs.disconnect(); } catch (e) {} fn(); }, { once: true }); | |
| 1976 | 1932 | } | |
| 1977 | 1933 | function ensurePanel(mode) { | |
| 1978 | 1934 | pendingMode = mode; | |
| 1979 | 1935 | if (panelCtl) { panelCtl.setMode(mode); return; } | |
| 1980 | - | whenBody(function () { | |
| 1936 | + | whenBody(() => { | |
| 1981 | 1937 | if (panelCtl) { panelCtl.setMode(pendingMode); return; } | |
| 1982 | 1938 | if (!(document.body || document.documentElement)) return; | |
| 1983 | 1939 | panelCtl = buildUI(pendingMode); | |
| @@ -1997,6 +1953,6 @@ | |||
|---|---|---|---|
| 1997 | 1953 | } else { | |
| 1998 | 1954 | postTo(window.top, { type: 'hello', url: location.href, frameId: SELF_ID }); | |
| 1999 | 1955 | // origSetTimeout (real time) so scaling can't distort the 5s fallback window. | |
| 2000 | - | origSetTimeout(function () { if (!gotHost) promoteToHost(); }, 5000); | |
| 1956 | + | origSetTimeout(() => { if (!gotHost) promoteToHost(); }, 5000); | |
| 2001 | 1957 | } | |
| 2002 | 1958 | })(); | |