Created
March 26, 2026 15:13
-
-
Save 19h/2b72c020b704665262f55934e1470390 to your computer and use it in GitHub Desktop.
cyberpunk-macos-arm64-2.3.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Cyberpunk 2077 — Frida instrumentation script (current build) | |
| * | |
| * Subsystems: | |
| * Weapon — zero ammo cost, configurable projectile fan, recoil nullification, CoF freeze | |
| * Survival — player-specific health freeze at request-builder and executor levels | |
| * Economy — money quantity multiplier on inventory-add path, XP replay on proficiency grants | |
| * Teleport — physics-proxy scan, camera-relative movement, GPS target warp, sticky tracking | |
| * Input — game input manager/backend key interception for hotkey-driven teleport | |
| * | |
| * Architecture: | |
| * Config — runtime-tunable parameters | |
| * Addresses — RVA table + lazy NativeFunction cache | |
| * Memory helpers — scratch buffers, safe reads, quaternion/vector math | |
| * PlayerTracker — entity ID + object pointer capture from ShootEvent and proxy ownership | |
| * StateProxy — physics proxy lookup, state block read/write, position commit | |
| * CameraReader — weak-handle traversal for world rotation quaternion | |
| * GpsTracker — cached quest/player GPS path targets | |
| * TeleportEngine — candidate scan, slot selection, directional/absolute warp, sticky loop | |
| * HotkeyRouter — keycode dispatch from input hooks to teleport actions | |
| * Hook installer — Interceptor.attach / .replace orchestration | |
| * Public API — globalThis.tp + rpc.exports | |
| */ | |
| "use strict"; | |
| (() => { | |
| if (globalThis.__cp2077ScriptLoaded) { | |
| console.log("[cp2077] script already loaded; skipping duplicate initialization"); | |
| return; | |
| } | |
| globalThis.__cp2077ScriptLoaded = true; | |
| // ─── Configuration ─────────────────────────────────────────────────────────── | |
| const Config = { | |
| zeroAmmoCost: true, | |
| projectileCount: 20, | |
| moneyMultiplier: 1000, | |
| experienceMultiplier: 1000, | |
| disableRecoil: true, | |
| disableConeOfFire: true, | |
| disableFallDamage: true, | |
| freezeHealth: true, | |
| enforceHealthAtExecution: true, | |
| teleportHotkeyEnabled: true, | |
| logInputKeys: false, | |
| logCameraQuatEveryMs: 0, | |
| teleportHotkeyDistance: 5.0, | |
| verbose: true, | |
| hotkeys: { | |
| forward: 104, // NumPad8 | |
| right: 102, // NumPad6 | |
| left: 100, // NumPad4 | |
| backward: 98, // NumPad2 | |
| target: 106, // NumStar | |
| playerTarget: 111, // NumSlash | |
| stickyTarget: [187, 108], // Equals | Separator | |
| increase: 107, // NumPlus | |
| decrease: 109, // NumMinus | |
| }, | |
| }; | |
| // ─── RVA Table ─────────────────────────────────────────────────────────────── | |
| const RVA = Object.freeze({ | |
| onShoot: 0x0442420C, | |
| consumeAmmo: 0x04423938, | |
| getAmmoCost: 0x0440EFC4, | |
| getProjectilesPerShot: 0x0440EFCC, | |
| recoilKick: 0x04123944, | |
| recoilReset: 0x04123E94, | |
| accuracyUpdateJob: 0x042445BC, | |
| buildSetStatPoolValue: 0x0416AE04, | |
| buildChangeStatPoolValue:0x0416B02C, | |
| executeSetStatPoolValue: 0x041767B4, | |
| executeChangeStatPoolValue: 0x04176A58, | |
| inventoryAddBackend: 0x03F53100, | |
| funcGiveMoney: 0x03FA4498, | |
| funcGiveItemByTdbid: 0x03FA5590, | |
| funcGiveItems: 0x03FA57CC, | |
| moneyItemIdHelper: 0x03FA3D84, | |
| moneyItemIdLo: 0x089322F0, | |
| moneyItemIdHi: 0x089322F8, | |
| addProficiencyPoint: 0x046F144C, | |
| isValidProxyId: 0x048D6C0C, | |
| getProxyPointer: 0x048DBE1C, | |
| getStateBlock: 0x048D79B4, | |
| getStateValue: 0x048DC1E8, | |
| buildWriteChain: 0x048DBEA0, | |
| commitWriteChain: 0x048DBFBC, | |
| writeStateValue: 0x049140D4, | |
| inputCacheInputManager: 0x02C7336C, | |
| inputCacheInputBackend: 0x02C751CC, | |
| gpsGetCurrentPathTarget: 0x02C012C4, | |
| }); | |
| const Offsets = Object.freeze({ | |
| shootEvent: { instigatorWeakHandle: 0x040, ammoCost: 0x0B0, projectilesPerShot: 0x180 }, | |
| gameObject: { entityId: 0x048 }, | |
| statRequest: { objectId: 0x020, statPoolType: 0x030, floatValue: 0x034 }, | |
| proxyLookup: { table: 0x09111CD0, generations: 1056784 }, | |
| playerObject: { cameraSystemWeakHandle: 1256, cameraComponentWeakHandle: 1192 }, | |
| cameraComponent: { worldPositionEncoded: 216, worldRotationQuat: 232 }, | |
| cameraSystem: { cachedRotationQuat: 1232 }, | |
| }); | |
| const STAT_POOL_HEALTH = 10; | |
| const HEALTH_POOL_RUNTIME = 48; | |
| const MONEY_CURRENCY_HASH = "0xF483CE936F7DF10F"; | |
| const ALL_SHAPES = uint64("0xFFFFFFFFFFFFFFFF"); | |
| // ─── Logging ───────────────────────────────────────────────────────────────── | |
| const log = (msg) => console.log(`[cp2077] ${msg}`); | |
| const tLog = (msg) => log(`tp: ${msg}`); | |
| const once = (() => { | |
| const seen = new Set(); | |
| return (msg) => { if (!seen.has(msg)) { seen.add(msg); log(msg); } }; | |
| })(); | |
| // ─── Address Resolution ────────────────────────────────────────────────────── | |
| const base = Process.mainModule?.base ?? Process.enumerateModules()[0].base; | |
| const addr = (rva) => base.add(rva); | |
| /** Lazy NativeFunction cache keyed by RVA name. */ | |
| class NativeFunctions { | |
| #cache = new Map(); | |
| get(name, retType, argTypes) { | |
| if (!this.#cache.has(name)) { | |
| this.#cache.set(name, new NativeFunction(addr(RVA[name]), retType, argTypes)); | |
| } | |
| return this.#cache.get(name); | |
| } | |
| resetRecoil = (mgr, flag) => this.get("recoilReset", "pointer", ["pointer", "char"])(mgr, flag); | |
| initMoneyItemId = (dst, hash) => this.get("moneyItemIdHelper", "uint64", ["pointer", "uint64"])(dst, hash); | |
| addProficiency = (req, sys) => this.get("addProficiencyPoint", "pointer", ["pointer", "pointer"])(req, sys); | |
| isValidProxyId = (tbl, id) => this.get("isValidProxyId", "bool", ["pointer", "uint"])(tbl, id); | |
| getProxyPointer = (id) => this.get("getProxyPointer", "pointer", ["uint"])(id); | |
| getStateBlock = (tbl, id) => this.get("getStateBlock", "pointer", ["pointer", "uint"])(tbl, id); | |
| getStateValue(block, proxyId, stateId, outBuf, outLen) { | |
| const fn = this.get("getStateValue", "int64", [ | |
| "pointer", "uint", "uint64", "uint64", "uint64", "pointer", "uint64", | |
| ]); | |
| return Number(fn(block, proxyId, u64(0), u64(0), u64(stateId), outBuf, u64(outLen))); | |
| } | |
| buildWriteChain = (block, id) => this.get("buildWriteChain", "pointer", ["pointer", "int"])(block, id); | |
| commitWriteChain = (id, delta) => this.get("commitWriteChain", "bool", ["uint", "uint64"])(id, delta); | |
| writeStateValue(proxyId, block, delta, stateId, buf, bufLen) { | |
| const fn = this.get("writeStateValue", "pointer", [ | |
| "uint", "pointer", "pointer", "uint64", "uint64", "uint64", "pointer", "uint64", "uchar", | |
| ]); | |
| return fn(proxyId, block, delta, u64(0), ALL_SHAPES, u64(stateId), buf, u64(bufLen), 0); | |
| } | |
| executeSetStatPool(sys, objId, type, instigator, a, b, c, val) { | |
| const fn = this.get("executeSetStatPoolValue", "bool", [ | |
| "pointer", "pointer", "int", "pointer", "bool", "bool", "bool", "float", | |
| ]); | |
| return fn(sys, objId, type, instigator, a, b, c, val); | |
| } | |
| executeChangeStatPool(sys, objId, type, instigator, a, b, diff) { | |
| const fn = this.get("executeChangeStatPoolValue", "bool", [ | |
| "pointer", "pointer", "int", "pointer", "bool", "bool", "float", | |
| ]); | |
| return fn(sys, objId, type, instigator, a, b, diff); | |
| } | |
| } | |
| const nf = new NativeFunctions(); | |
| // ─── Utility ───────────────────────────────────────────────────────────────── | |
| const u64 = (v) => uint64(typeof v === "string" ? v : String(v)); | |
| const safeRead = (fn) => { try { return fn(); } catch { return null; } }; | |
| const safeReadPointer = (p) => safeRead(() => p.readPointer()); | |
| const isLiveObject = (p) => { | |
| if (!p || p.isNull()) return false; | |
| const vt = safeReadPointer(p); | |
| return vt !== null && !vt.isNull(); | |
| }; | |
| // ─── Scratch Buffers ───────────────────────────────────────────────────────── | |
| const Scratch = Object.freeze({ | |
| u8: Memory.alloc(1), | |
| f32: Memory.alloc(4), | |
| vec3: Memory.alloc(12), | |
| quat: Memory.alloc(16), | |
| }); | |
| // ─── Vector / Quaternion Math ──────────────────────────────────────────────── | |
| const Vec3 = { | |
| read(ptr) { return { x: ptr.readFloat(), y: ptr.add(4).readFloat(), z: ptr.add(8).readFloat() }; }, | |
| write(ptr, v) { ptr.writeFloat(v.x); ptr.add(4).writeFloat(v.y); ptr.add(8).writeFloat(v.z); }, | |
| isFinite(v) { return Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.z); }, | |
| zero() { return { x: 0, y: 0, z: 0 }; }, | |
| add(a, b) { return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z }; }, | |
| scale(v, s) { return { x: v.x * s, y: v.y * s, z: v.z * s }; }, | |
| norm2d(v) { | |
| const len = Math.hypot(v.x, v.y); | |
| return len < 1e-6 ? { x: 0, y: 1, z: 0 } : { x: v.x / len, y: v.y / len, z: 0 }; | |
| }, | |
| format(v, prec = 2) { return `(${v.x.toFixed(prec)}, ${v.y.toFixed(prec)}, ${v.z.toFixed(prec)})`; }, | |
| }; | |
| const Quat = { | |
| read(ptr) { | |
| return { | |
| x: ptr.readFloat(), y: ptr.add(4).readFloat(), | |
| z: ptr.add(8).readFloat(), w: ptr.add(12).readFloat(), | |
| }; | |
| }, | |
| isNormalized(q) { | |
| if (!Number.isFinite(q.x) || !Number.isFinite(q.y) || !Number.isFinite(q.z) || !Number.isFinite(q.w)) | |
| return false; | |
| const len = Math.hypot(q.x, q.y, q.z, q.w); | |
| return len > 0.7 && len < 1.3; | |
| }, | |
| rotate(q, v) { | |
| const u = { x: q.x, y: q.y, z: q.z }; | |
| const s = q.w; | |
| const dot = u.x * v.x + u.y * v.y + u.z * v.z; | |
| const uu = u.x * u.x + u.y * u.y + u.z * u.z; | |
| const cx = { x: u.y * v.z - u.z * v.y, y: u.z * v.x - u.x * v.z, z: u.x * v.y - u.y * v.x }; | |
| const k = s * s - uu; | |
| return { | |
| x: 2 * dot * u.x + k * v.x + 2 * s * cx.x, | |
| y: 2 * dot * u.y + k * v.y + 2 * s * cx.y, | |
| z: 2 * dot * u.z + k * v.z + 2 * s * cx.z, | |
| }; | |
| }, | |
| format(q, prec = 6) { | |
| return `x=${q.x.toFixed(prec)} y=${q.y.toFixed(prec)} z=${q.z.toFixed(prec)} w=${q.w.toFixed(prec)}`; | |
| }, | |
| }; | |
| // ─── Weak Handle Traversal ─────────────────────────────────────────────────── | |
| function readWeakHandlePointee(basePtr, offset) { | |
| if (!isLiveObject(basePtr)) return null; | |
| const handle = basePtr.add(offset); | |
| const pointee = safeReadPointer(handle); | |
| const refCount = safeReadPointer(handle.add(Process.pointerSize)); | |
| if (!pointee || pointee.isNull() || !refCount || refCount.isNull()) return null; | |
| return isLiveObject(pointee) ? pointee : null; | |
| } | |
| // ─── Player Tracker ────────────────────────────────────────────────────────── | |
| class PlayerTracker { | |
| entityId = "1"; | |
| objectPtr = null; | |
| get hasObject() { return isLiveObject(this.objectPtr); } | |
| captureFromShootEvent(evtPtr) { | |
| try { | |
| const obj = evtPtr.add(Offsets.shootEvent.instigatorWeakHandle).readPointer(); | |
| if (obj.isNull()) return; | |
| this.#adoptObject(obj); | |
| } catch (e) { log(`Failed to capture player from ShootEvent: ${e}`); } | |
| } | |
| captureFromProxyOwner(proxyId) { | |
| const owner = this.#proxyOwner(proxyId); | |
| if (!owner) return false; | |
| this.#adoptObject(owner); | |
| return true; | |
| } | |
| matchesEntityId(entityIdStr) { return this.entityId === entityIdStr; } | |
| matchesStatRequest(reqPtr, statPoolType) { | |
| if (statPoolType !== STAT_POOL_HEALTH) return false; | |
| return safeRead(() => reqPtr.add(Offsets.statRequest.objectId).readU64().toString()) === this.entityId; | |
| } | |
| matchesStatsObjectId(idPtr) { | |
| if (!idPtr || idPtr.isNull()) return false; | |
| return safeRead(() => idPtr.readU64().toString()) === this.entityId; | |
| } | |
| matchesObjectHandle(handlePtr) { | |
| if (!this.entityId) return true; | |
| const obj = safeRead(() => handlePtr.add(72).readPointer()); | |
| if (!obj || obj.isNull()) return false; | |
| return safeRead(() => obj.add(Offsets.gameObject.entityId).readU64().toString()) === this.entityId; | |
| } | |
| #adoptObject(obj) { | |
| if (!this.objectPtr || this.objectPtr.isNull() || !obj.equals(this.objectPtr)) { | |
| this.objectPtr = obj; | |
| } | |
| const eid = safeRead(() => obj.add(Offsets.gameObject.entityId).readU64().toString()); | |
| if (eid && eid !== this.entityId) { | |
| this.entityId = eid; | |
| log(`Tracked player EntityID=${eid}`); | |
| } | |
| } | |
| #proxyOwner(proxyId) { | |
| const proxy = safeRead(() => nf.getProxyPointer(proxyId)); | |
| if (!isLiveObject(proxy)) return null; | |
| const owner = safeRead(() => proxy.add(8).readPointer()); | |
| return isLiveObject(owner) ? owner : null; | |
| } | |
| } | |
| const player = new PlayerTracker(); | |
| // ─── Money Item Cache ──────────────────────────────────────────────────────── | |
| class MoneyCache { | |
| #lo = null; | |
| #hi = null; | |
| init() { | |
| try { | |
| nf.initMoneyItemId(ptr(0), uint64(MONEY_CURRENCY_HASH)); | |
| this.#lo = addr(RVA.moneyItemIdLo).readU64().toString(); | |
| this.#hi = addr(RVA.moneyItemIdHi).readU64().toString(); | |
| log(`Tracked money ItemID=${this.#lo}:${this.#hi}`); | |
| } catch (e) { log(`Failed to init money ItemID cache: ${e}`); } | |
| } | |
| matches(itemModParamsPtr) { | |
| if (!this.#lo || !this.#hi) return false; | |
| return safeRead(() => { | |
| const lo = itemModParamsPtr.add(8).readU64().toString(); | |
| const hi = itemModParamsPtr.add(16).readU64().toString(); | |
| return lo === this.#lo && hi === this.#hi; | |
| }) ?? false; | |
| } | |
| tryMultiply(itemModParamsPtr) { | |
| if (!this.matches(itemModParamsPtr)) return false; | |
| const qtyPtr = itemModParamsPtr.add(24); | |
| const old = qtyPtr.readS32(); | |
| if (old <= 0) return false; | |
| const next = Math.min(old * Config.moneyMultiplier, 0x7FFFFFFF); | |
| if (next === old) return false; | |
| qtyPtr.writeS32(next); | |
| if (Config.verbose) log(`Money quantity ${old} → ${next}`); | |
| return true; | |
| } | |
| } | |
| const money = new MoneyCache(); | |
| // ─── State Proxy System ────────────────────────────────────────────────────── | |
| class StateProxy { | |
| get lookupTable() { return base.add(Offsets.proxyLookup.table).readPointer(); } | |
| readBlock(table, proxyId) { | |
| const block = nf.getStateBlock(table, proxyId); | |
| return block.isNull() ? null : block; | |
| } | |
| readU8(block, proxyId, stateId) { | |
| return nf.getStateValue(block, proxyId, stateId, Scratch.u8, 1) ? Scratch.u8.readU8() : null; | |
| } | |
| readF32(block, proxyId, stateId) { | |
| return nf.getStateValue(block, proxyId, stateId, Scratch.f32, 4) ? Scratch.f32.readFloat() : null; | |
| } | |
| readVec3(block, proxyId, stateId) { | |
| if (!nf.getStateValue(block, proxyId, stateId, Scratch.vec3, 12)) return null; | |
| return Vec3.read(Scratch.vec3); | |
| } | |
| readQuat(block, proxyId, stateId) { | |
| if (!nf.getStateValue(block, proxyId, stateId, Scratch.quat, 16)) return null; | |
| return Quat.read(Scratch.quat); | |
| } | |
| writePositionAndZeroVelocity(proxyId, block, position) { | |
| const delta = nf.buildWriteChain(block, proxyId); | |
| if (delta.isNull()) throw new Error("buildWriteChain failed"); | |
| Vec3.write(Scratch.vec3, position); | |
| nf.writeStateValue(proxyId, block, delta, 1, Scratch.vec3, 12); | |
| Vec3.write(Scratch.vec3, Vec3.zero()); | |
| nf.writeStateValue(proxyId, block, delta, 4, Scratch.vec3, 12); | |
| nf.commitWriteChain(proxyId, u64(delta)); | |
| } | |
| } | |
| const stateProxy = new StateProxy(); | |
| // ─── Camera Reader ─────────────────────────────────────────────────────────── | |
| class CameraReader { | |
| #timer = null; | |
| readWorldQuat() { | |
| // Prefer camera system path | |
| const sysPtr = readWeakHandlePointee(player.objectPtr, Offsets.playerObject.cameraSystemWeakHandle); | |
| if (sysPtr) { | |
| const q = safeRead(() => Quat.read(sysPtr.add(Offsets.cameraSystem.cachedRotationQuat))); | |
| if (q && Quat.isNormalized(q)) return q; | |
| } | |
| // Fall back to camera component | |
| const compPtr = readWeakHandlePointee(player.objectPtr, Offsets.playerObject.cameraComponentWeakHandle); | |
| if (!compPtr) return null; | |
| const q = safeRead(() => Quat.read(compPtr.add(Offsets.cameraComponent.worldRotationQuat))); | |
| return (q && Quat.isNormalized(q)) ? q : null; | |
| } | |
| get forward() { | |
| const q = this.readWorldQuat(); | |
| return q ? Quat.rotate(q, { x: 0, y: 1, z: 0 }) : null; | |
| } | |
| get right() { | |
| const q = this.readWorldQuat(); | |
| return q ? Quat.rotate(q, { x: 1, y: 0, z: 0 }) : null; | |
| } | |
| startLogging(ms) { | |
| this.stopLogging(); | |
| if (!Number.isFinite(ms) || ms <= 0) return false; | |
| Config.logCameraQuatEveryMs = ms; | |
| this.#timer = setInterval(() => { | |
| const q = this.readWorldQuat(); | |
| tLog(q ? `camera quat: ${Quat.format(q)}` : "camera quat: null"); | |
| }, ms); | |
| tLog(`camera quat logging every ${ms}ms`); | |
| return true; | |
| } | |
| stopLogging() { | |
| if (this.#timer !== null) { clearInterval(this.#timer); this.#timer = null; } | |
| Config.logCameraQuatEveryMs = 0; | |
| return true; | |
| } | |
| getDebugInfo() { | |
| const out = { playerObjectPtr: player.objectPtr, playerObjectValid: player.hasObject }; | |
| if (!out.playerObjectValid) return out; | |
| for (const [label, offset, quatOffset] of [ | |
| ["cameraSystem", Offsets.playerObject.cameraSystemWeakHandle, Offsets.cameraSystem.cachedRotationQuat], | |
| ["cameraComponent", Offsets.playerObject.cameraComponentWeakHandle, Offsets.cameraComponent.worldRotationQuat], | |
| ]) { | |
| const ptr = readWeakHandlePointee(player.objectPtr, offset); | |
| out[`${label}Ptr`] = ptr; | |
| out[`${label}Valid`] = isLiveObject(ptr); | |
| if (ptr) { | |
| const q = safeRead(() => Quat.read(ptr.add(quatOffset))); | |
| out[`${label}Quat`] = q; | |
| out[`${label}QuatNormalized`] = q ? Quat.isNormalized(q) : false; | |
| } | |
| } | |
| return out; | |
| } | |
| } | |
| const camera = new CameraReader(); | |
| // ─── GPS Tracker ───────────────────────────────────────────────────────────── | |
| class GpsTracker { | |
| #targets = { 0: null, 1: null }; | |
| get quest() { return this.#targets[0]; } | |
| get tracked() { return this.#targets[1]; } | |
| get(type) { return this.#targets[type] ?? null; } | |
| getAll() { return { ...this.#targets }; } | |
| get preferred() { | |
| if (this.#targets[0]) return { type: 0, position: this.#targets[0], label: "quest" }; | |
| if (this.#targets[1]) return { type: 1, position: this.#targets[1], label: "tracked" }; | |
| return null; | |
| } | |
| update(targetType, ptr) { | |
| try { | |
| const pos = Vec3.read(ptr); | |
| if (!Vec3.isFinite(pos)) return; | |
| const prev = this.#targets[targetType]; | |
| const changed = !prev || | |
| Math.abs(prev.x - pos.x) > 0.01 || | |
| Math.abs(prev.y - pos.y) > 0.01 || | |
| Math.abs(prev.z - pos.z) > 0.01; | |
| this.#targets[targetType] = pos; | |
| if (changed) tLog(`gps target[${targetType}] → ${Vec3.format(pos)}`); | |
| } catch { /* ignore */ } | |
| } | |
| } | |
| const gps = new GpsTracker(); | |
| // ─── Teleport Engine ───────────────────────────────────────────────────────── | |
| class TeleportEngine { | |
| #candidates = []; | |
| #activeSlot = -1; | |
| #saved = new Map(); | |
| #stickyTimer = null; | |
| get candidates() { return this.#candidates; } | |
| get isStickyActive() { return this.#stickyTimer !== null; } | |
| scan(limit = 0) { | |
| const table = stateProxy.lookupTable; | |
| if (table.isNull()) throw new Error("proxy lookup is null"); | |
| const out = []; | |
| for (let low = 0; low <= 0xFFFF; low++) { | |
| const gen = table.add(Offsets.proxyLookup.generations + low * 2).readU16(); | |
| const proxyId = ((gen << 16) | low) >>> 0; | |
| if (!nf.isValidProxyId(table, proxyId)) continue; | |
| const block = stateProxy.readBlock(table, proxyId); | |
| if (!block) continue; | |
| // Filter: movable humanoid proxies only | |
| if (stateProxy.readU8(block, proxyId, 26) !== 1) continue; | |
| const height = stateProxy.readF32(block, proxyId, 29); | |
| const radius = stateProxy.readF32(block, proxyId, 30); | |
| const pos = stateProxy.readVec3(block, proxyId, 1); | |
| const rot = stateProxy.readQuat(block, proxyId, 2); | |
| if (!height || !radius || !pos || !rot) continue; | |
| if (height < 1.0 || height > 2.4 || radius < 0.2 || radius > 0.7) continue; | |
| out.push({ proxyId, stateBlock: block, pos, rot, height, radius }); | |
| // Opportunistic player-object adoption | |
| if (!player.hasObject) { | |
| player.captureFromProxyOwner(proxyId); | |
| } | |
| if (limit && out.length >= limit) break; | |
| } | |
| this.#candidates = out; | |
| return out; | |
| } | |
| #ensureCandidates() { | |
| if (this.#candidates.length === 0) this.scan(); | |
| return this.#candidates; | |
| } | |
| #refresh(slot) { | |
| const c = this.#ensureCandidates()[slot]; | |
| if (!c) throw new Error(`invalid slot ${slot}`); | |
| const table = stateProxy.lookupTable; | |
| if (table.isNull() || !nf.isValidProxyId(table, c.proxyId)) | |
| throw new Error("proxy no longer valid"); | |
| c.stateBlock = stateProxy.readBlock(table, c.proxyId); | |
| c.pos = stateProxy.readVec3(c.stateBlock, c.proxyId, 1); | |
| c.rot = stateProxy.readQuat(c.stateBlock, c.proxyId, 2); | |
| if (!c.stateBlock || !c.pos || !c.rot) throw new Error("candidate state unavailable"); | |
| return c; | |
| } | |
| #requireActive() { | |
| if (this.#activeSlot < 0) { | |
| const cands = this.#ensureCandidates(); | |
| if (cands.length === 0) throw new Error("no teleport candidates found"); | |
| tLog("auto-selecting slot #0"); | |
| this.select(0); | |
| } | |
| return this.#refresh(this.#activeSlot); | |
| } | |
| select(slot) { | |
| const c = this.#refresh(slot); | |
| player.captureFromProxyOwner(c.proxyId); | |
| this.#activeSlot = slot; | |
| tLog(`selected ${this.#formatCandidate(slot, c)}${player.hasObject ? " [linked]" : " [no link]"}`); | |
| return true; | |
| } | |
| probe(slot, dz = 1.5) { | |
| const c = this.#refresh(slot); | |
| this.#saved.set(c.proxyId, { ...c.pos }); | |
| stateProxy.writePositionAndZeroVelocity(c.proxyId, c.stateBlock, Vec3.add(c.pos, { x: 0, y: 0, z: dz })); | |
| tLog(`probed slot #${slot} dz=${dz}`); | |
| return true; | |
| } | |
| restore(slot) { | |
| const c = this.#refresh(slot); | |
| const saved = this.#saved.get(c.proxyId); | |
| if (!saved) throw new Error(`no saved position for slot ${slot}`); | |
| stateProxy.writePositionAndZeroVelocity(c.proxyId, c.stateBlock, saved); | |
| tLog(`restored slot #${slot}`); | |
| return true; | |
| } | |
| forward(distance, flat = true) { | |
| const c = this.#requireActive(); | |
| let fwd = camera.forward ?? Quat.rotate(c.rot, { x: 0, y: 1, z: 0 }); | |
| if (flat) fwd = Vec3.norm2d(fwd); | |
| const dst = Vec3.add(c.pos, Vec3.scale(fwd, distance)); | |
| stateProxy.writePositionAndZeroVelocity(c.proxyId, c.stateBlock, dst); | |
| return dst; | |
| } | |
| local(lx, ly, lz) { | |
| const c = this.#requireActive(); | |
| const dst = Vec3.add(c.pos, Quat.rotate(c.rot, { x: lx, y: ly, z: lz })); | |
| stateProxy.writePositionAndZeroVelocity(c.proxyId, c.stateBlock, dst); | |
| return dst; | |
| } | |
| delta(dx, dy, dz) { | |
| const c = this.#requireActive(); | |
| const dst = Vec3.add(c.pos, { x: dx, y: dy, z: dz }); | |
| stateProxy.writePositionAndZeroVelocity(c.proxyId, c.stateBlock, dst); | |
| return dst; | |
| } | |
| abs(x, y, z) { | |
| const c = this.#requireActive(); | |
| const dst = { x, y, z }; | |
| stateProxy.writePositionAndZeroVelocity(c.proxyId, c.stateBlock, dst); | |
| return dst; | |
| } | |
| toTarget(type = 0, zOffset = 1.0) { | |
| const pos = gps.get(type); | |
| if (!pos) throw new Error(`no cached GPS target for type ${type}`); | |
| return this.abs(pos.x, pos.y, pos.z + zOffset); | |
| } | |
| toggleSticky() { return this.#stickyTimer !== null ? this.stopSticky() : this.startSticky(); } | |
| startSticky() { | |
| if (this.#stickyTimer !== null) return true; | |
| this.#stickyTimer = setInterval(() => { | |
| const t = gps.preferred; | |
| if (!t) return; | |
| try { this.toTarget(t.type); } catch (e) { tLog(`sticky failed: ${e}`); this.stopSticky(); } | |
| }, 33); | |
| tLog("sticky target enabled (30 FPS)"); | |
| return true; | |
| } | |
| stopSticky() { | |
| if (this.#stickyTimer !== null) { clearInterval(this.#stickyTimer); this.#stickyTimer = null; } | |
| tLog("sticky target disabled"); | |
| return true; | |
| } | |
| getActive() { | |
| const c = this.#requireActive(); | |
| const line = this.#formatCandidate(this.#activeSlot, c); | |
| console.log(line); | |
| return line; | |
| } | |
| list(limit = 32) { | |
| return this.scan(limit).map((c, i) => this.#formatCandidate(i, c)); | |
| } | |
| #formatCandidate(slot, c) { | |
| return `#${slot} id=0x${(c.proxyId >>> 0).toString(16).padStart(8, "0")} ` + | |
| `pos=${Vec3.format(c.pos)} h=${c.height.toFixed(2)} r=${c.radius.toFixed(2)}`; | |
| } | |
| } | |
| const tp = new TeleportEngine(); | |
| // ─── Hotkey Router ─────────────────────────────────────────────────────────── | |
| const INPUT_KEY_NAMES = { | |
| 98: "NumPad2", 100: "NumPad4", 102: "NumPad6", 104: "NumPad8", | |
| 106: "NumStar", 107: "NumPlus", 108: "Separator", 109: "NumMinus", | |
| 111: "NumSlash", 187: "Equals", | |
| }; | |
| class HotkeyRouter { | |
| #state = Object.create(null); | |
| #mgrSeen = false; | |
| #backendSeen = false; | |
| get snapshot() { | |
| return Object.fromEntries( | |
| Object.entries(Config.hotkeys).map(([name, code]) => [name, { keyCode: code, down: !!this.#state[name] }]) | |
| ); | |
| } | |
| onInput(source, key, action) { | |
| if (source === "mgr" && !this.#mgrSeen) { this.#mgrSeen = true; tLog("manager input hook alive"); } | |
| if (source === "backend" && !this.#backendSeen) { this.#backendSeen = true; tLog("backend input hook alive"); } | |
| if (Config.logInputKeys && (action === 1 || action === 2)) { | |
| const act = action === 1 ? "press" : "release"; | |
| tLog(`${source} ${act} ${INPUT_KEY_NAMES[key] ?? `Key${key}`} (${key})`); | |
| } | |
| if (!Config.teleportHotkeyEnabled) return; | |
| for (const [name, code] of Object.entries(Config.hotkeys)) { | |
| if (!(Array.isArray(code) ? code.includes(key) : code === key)) continue; | |
| if (action === 1 && !this.#state[name]) { this.#state[name] = true; this.#dispatch(name); } | |
| else if (action === 2) { this.#state[name] = false; } | |
| break; | |
| } | |
| } | |
| #dispatch(name) { | |
| const d = Config.teleportHotkeyDistance; | |
| const handlers = { | |
| increase: () => { Config.teleportHotkeyDistance += 1; tLog(`distance → ${Config.teleportHotkeyDistance.toFixed(1)}`); }, | |
| decrease: () => { Config.teleportHotkeyDistance = Math.max(1, d - 1); tLog(`distance → ${Config.teleportHotkeyDistance.toFixed(1)}`); }, | |
| forward: () => { const p = tp.forward(d, true); tLog(`forward → ${Vec3.format(p)}`); }, | |
| backward: () => { const p = tp.forward(-d, true); tLog(`backward → ${Vec3.format(p)}`); }, | |
| right: () => { const r = Vec3.norm2d(camera.right ?? { x: 1, y: 0, z: 0 }); const p = tp.delta(r.x * d, r.y * d, 0); tLog(`right → ${Vec3.format(p)}`); }, | |
| left: () => { const r = Vec3.norm2d(camera.right ?? { x: 1, y: 0, z: 0 }); const p = tp.delta(-r.x * d, -r.y * d, 0); tLog(`left → ${Vec3.format(p)}`); }, | |
| target: () => { const pos = gps.quest; if (!pos) { tLog("no quest target"); return; } const p = tp.abs(pos.x, pos.y, pos.z + 1); tLog(`target → ${Vec3.format(p)}`); }, | |
| playerTarget: () => { const pos = gps.tracked; if (!pos) { tLog("no tracked target"); return; } const p = tp.abs(pos.x, pos.y, pos.z + 1); tLog(`player target → ${Vec3.format(p)}`); }, | |
| stickyTarget: () => tp.toggleSticky(), | |
| }; | |
| try { | |
| handlers[name]?.(); | |
| } catch (e) { | |
| tLog(`${name} failed: ${e}`); | |
| } | |
| } | |
| } | |
| const hotkeys = new HotkeyRouter(); | |
| // ─── Hook Installation ─────────────────────────────────────────────────────── | |
| function installHooks() { | |
| const at = (rva) => addr(RVA[rva]); | |
| // ── Weapon: OnShoot event patching ── | |
| Interceptor.attach(at("onShoot"), { | |
| onEnter(args) { | |
| const evt = args[1]; | |
| if (evt.isNull()) return; | |
| player.captureFromShootEvent(evt); | |
| try { | |
| if (Config.zeroAmmoCost) { | |
| const p = evt.add(Offsets.shootEvent.ammoCost); | |
| const old = p.readU16(); | |
| if (old !== 0) { p.writeU16(0); if (Config.verbose) log(`OnShoot: ammo ${old} → 0`); } | |
| } | |
| if (Config.projectileCount > 0) { | |
| const p = evt.add(Offsets.shootEvent.projectilesPerShot); | |
| const n = Config.projectileCount & 0xFF; | |
| const old = p.readU8(); | |
| if (old !== n) { p.writeU8(n); if (Config.verbose) log(`OnShoot: proj ${old} → ${n}`); } | |
| } | |
| } catch (e) { log(`OnShoot patch failed: ${e}`); } | |
| }, | |
| }); | |
| // ── Weapon: ammo consumption ── | |
| if (Config.zeroAmmoCost) { | |
| Interceptor.attach(at("consumeAmmo"), { onEnter() { this.context.x1 = 0; } }); | |
| once("Ammo helper hooked: forced arg to 0"); | |
| Interceptor.attach(at("getAmmoCost"), { onLeave(r) { r.replace(0); } }); | |
| once("GetAmmoCost → 0"); | |
| } | |
| // ── Weapon: projectile fan ── | |
| if (Config.projectileCount > 0) { | |
| Interceptor.attach(at("getProjectilesPerShot"), { onLeave(r) { r.replace(Config.projectileCount & 0xFF); } }); | |
| once(`GetProjectilesPerShot → ${Config.projectileCount & 0xFF}`); | |
| } | |
| // ── Weapon: recoil nullification ── | |
| if (Config.disableRecoil) { | |
| Interceptor.attach(at("recoilKick"), { | |
| onEnter(args) { this.mgr = args[0]; }, | |
| onLeave(r) { try { nf.resetRecoil(this.mgr, 0); r.replace(0); } catch (e) { log(`Recoil reset failed: ${e}`); } }, | |
| }); | |
| once("Recoil kick → immediate reset"); | |
| } | |
| // ── Weapon: cone-of-fire freeze ── | |
| if (Config.disableConeOfFire) { | |
| Interceptor.replace(at("accuracyUpdateJob"), | |
| new NativeCallback(() => {}, "void", ["pointer", "pointer", "uint", "uint", "pointer"])); | |
| once("Accuracy update job replaced: CoF frozen"); | |
| } | |
| // ── Survival: stat pool request builders ── | |
| if (Config.freezeHealth) { | |
| Interceptor.attach(at("buildSetStatPoolValue"), { | |
| onEnter(args) { this.req = args[0]; this.type = args[2].toInt32(); }, | |
| onLeave() { if (player.matchesStatRequest(this.req, this.type)) safeRead(() => this.req.add(Offsets.statRequest.floatValue).writeFloat(1.0)); }, | |
| }); | |
| once("SetSPValue builder: player health → 100%"); | |
| Interceptor.attach(at("buildChangeStatPoolValue"), { | |
| onEnter(args) { this.req = args[0]; this.type = args[2].toInt32(); }, | |
| onLeave() { | |
| if (!player.matchesStatRequest(this.req, this.type)) return; | |
| const diff = safeRead(() => this.req.add(Offsets.statRequest.floatValue).readFloat()); | |
| if (diff !== null && diff < 0) this.req.add(Offsets.statRequest.floatValue).writeFloat(0); | |
| }, | |
| }); | |
| once("ChangeSPValue builder: negative player health deltas → 0"); | |
| } | |
| // ── Survival: execution-level health enforcement ── | |
| if (Config.enforceHealthAtExecution && (Config.freezeHealth || Config.disableFallDamage)) { | |
| Interceptor.replace(at("executeSetStatPoolValue"), | |
| new NativeCallback((sys, objId, type, ins, a, b, c, val) => { | |
| if (type === HEALTH_POOL_RUNTIME && player.matchesStatsObjectId(objId)) { | |
| if (Config.verbose) log("Blocked direct health set"); | |
| return true; | |
| } | |
| return nf.executeSetStatPool(sys, objId, type, ins, a, b, c, val); | |
| }, "bool", ["pointer", "pointer", "int", "pointer", "bool", "bool", "bool", "float"])); | |
| Interceptor.replace(at("executeChangeStatPoolValue"), | |
| new NativeCallback((sys, objId, type, ins, a, b, diff) => { | |
| if (type === HEALTH_POOL_RUNTIME && player.matchesStatsObjectId(objId) && diff < 0) { | |
| if (Config.verbose) log(`Blocked negative health Δ: ${diff}`); | |
| return true; | |
| } | |
| return nf.executeChangeStatPool(sys, objId, type, ins, a, b, diff); | |
| }, "bool", ["pointer", "pointer", "int", "pointer", "bool", "bool", "float"])); | |
| once("Execution-level health enforcement active"); | |
| } | |
| // ── Economy: money multiplier ── | |
| if (Config.moneyMultiplier > 1) { | |
| Interceptor.attach(at("inventoryAddBackend"), { | |
| onEnter(args) { if (!args[1].isNull()) try { money.tryMultiply(args[1]); } catch (e) { log(`Money mul failed: ${e}`); } }, | |
| }); | |
| once("Money multiplier on inventory add backend"); | |
| } | |
| // ── Economy: XP replay ── | |
| let xpDepth = 0; | |
| if (Config.experienceMultiplier > 1) { | |
| Interceptor.attach(at("addProficiencyPoint"), { | |
| onEnter(args) { this.req = args[0]; this.sys = args[1]; this.replayed = xpDepth !== 0; }, | |
| onLeave() { | |
| if (this.replayed) return; | |
| xpDepth++; | |
| try { for (let i = 1; i < Config.experienceMultiplier; i++) nf.addProficiency(this.req, this.sys); } | |
| catch (e) { log(`XP mul failed: ${e}`); } | |
| finally { xpDepth--; } | |
| }, | |
| }); | |
| once("XP multiplier on AddProficiencyPoint"); | |
| } | |
| // ── Input: hotkey hooks ── | |
| if (Config.teleportHotkeyEnabled) { | |
| const argU32 = (v) => (v?.toUInt32 ? v.toUInt32() : Number(v)) >>> 0; | |
| for (const rva of ["inputCacheInputManager", "inputCacheInputBackend"]) { | |
| const source = rva.includes("Manager") ? "mgr" : "backend"; | |
| Interceptor.attach(at(rva), { | |
| onEnter(args) { hotkeys.onInput(source, argU32(args[1]), argU32(args[2])); }, | |
| }); | |
| } | |
| once("Teleport hotkeys via game input manager + backend"); | |
| } | |
| // ── GPS: target cache ── | |
| Interceptor.attach(at("gpsGetCurrentPathTarget"), { | |
| onEnter(args) { this.type = (args[1]?.toUInt32 ? args[1].toUInt32() : Number(args[1])) >>> 0; this.out = args[2]; }, | |
| onLeave(r) { if (r.toInt32?.() !== 0) gps.update(this.type, this.out); }, | |
| }); | |
| once("GPS target hook active"); | |
| } | |
| // ─── Public API ────────────────────────────────────────────────────────────── | |
| function exposeApi() { | |
| globalThis.tp = { | |
| scan: (limit) => { const c = tp.scan(limit || 0); tLog(`found ${c.length} candidates`); return tp.list(c.length); }, | |
| list: (limit) => { const l = tp.list(limit || 32); l.forEach((s) => console.log(s)); return l; }, | |
| select: (slot) => tp.select(Number(slot)), | |
| probe: (slot, dz) => tp.probe(Number(slot), dz ?? 1.5), | |
| restore: (slot) => tp.restore(Number(slot)), | |
| active: () => tp.getActive(), | |
| forward: (d, flat) => tp.forward(Number(d), flat !== false), | |
| local: (x, y, z) => tp.local(Number(x), Number(y), Number(z)), | |
| delta: (dx, dy, dz) => tp.delta(Number(dx), Number(dy), Number(dz)), | |
| abs: (x, y, z) => tp.abs(Number(x), Number(y), Number(z)), | |
| look: () => { const f = camera.forward; console.log(f); return f; }, | |
| player: () => { const o = { entityId: player.entityId, object: player.objectPtr }; console.log(o); return o; }, | |
| camera: () => { const o = camera.getDebugInfo(); console.log(o); return o; }, | |
| targets: () => { const t = gps.getAll(); console.log(t); return t; }, | |
| target: (type) => { const t = gps.get(type ?? 0); console.log(t); return t; }, | |
| toTarget: (type, zOff) => tp.toTarget(type ?? 0, zOff ?? 1.0), | |
| stickyTarget:(enable) => enable === undefined ? tp.toggleSticky() : (enable ? tp.startSticky() : tp.stopSticky()), | |
| watchCamera: (ms) => camera.startLogging(ms ?? 1000), | |
| stopCamera: () => camera.stopLogging(), | |
| keys: () => { const s = hotkeys.snapshot; console.log(s); return s; }, | |
| }; | |
| rpc.exports = { | |
| tpscan: (limit) => tp.scan(limit || 0).map((c, i) => ({ slot: i, proxyId: c.proxyId >>> 0, position: c.pos, height: c.height, radius: c.radius })), | |
| tpselect: (slot) => (tp.select(Number(slot)), true), | |
| tpprobe: (slot, dz) => (tp.probe(Number(slot), dz ?? 1.5), true), | |
| tprestore: (slot) => (tp.restore(Number(slot)), true), | |
| tpforward: (d) => tp.forward(Number(d), true), | |
| tplocal: (x, y, z) => tp.local(Number(x), Number(y), Number(z)), | |
| tpdelta: (dx, dy, dz) => tp.delta(Number(dx), Number(dy), Number(dz)), | |
| tpabs: (x, y, z) => tp.abs(Number(x), Number(y), Number(z)), | |
| tptarget: (type) => gps.get(type ?? 0), | |
| tptotarget: (type, zOff) => tp.toTarget(type ?? 0, zOff ?? 1.0), | |
| tpstickytarget: (enable) => enable === undefined ? tp.toggleSticky() : (enable ? tp.startSticky() : tp.stopSticky()), | |
| tpwatchcamera: (ms) => camera.startLogging(ms ?? 1000), | |
| tpstopcamera: () => camera.stopLogging(), | |
| }; | |
| } | |
| // ─── Bootstrap ─────────────────────────────────────────────────────────────── | |
| log(`base=${base}`); | |
| log(`OnShoot=${addr(RVA.onShoot)} ConsumeAmmo=${addr(RVA.consumeAmmo)}`); | |
| log(`AmmoCost=${addr(RVA.getAmmoCost)} ProjPerShot=${addr(RVA.getProjectilesPerShot)}`); | |
| log(`RecoilKick=${addr(RVA.recoilKick)} AccuracyJob=${addr(RVA.accuracyUpdateJob)}`); | |
| log(`SetSP=${addr(RVA.buildSetStatPoolValue)} ChangeSP=${addr(RVA.buildChangeStatPoolValue)}`); | |
| log(`InvAdd=${addr(RVA.inventoryAddBackend)} Money=${addr(RVA.funcGiveMoney)} XP=${addr(RVA.addProficiencyPoint)}`); | |
| log(`Player EntityID=${player.entityId} (assumed until observed)`); | |
| log(`Fall damage suppression=${Config.freezeHealth || Config.disableFallDamage ? "on" : "off"}`); | |
| if (Config.teleportHotkeyEnabled) { | |
| log(`Teleport hotkeys: 8/6/4/2=move *=quest /=tracked ==sticky +/-=distance (${Config.teleportHotkeyDistance.toFixed(1)})`); | |
| } | |
| money.init(); | |
| installHooks(); | |
| exposeApi(); | |
| if (Config.logCameraQuatEveryMs > 0) camera.startLogging(Config.logCameraQuatEveryMs); | |
| log("All hooks active"); | |
| tLog("tp.list() tp.select(n) tp.forward(d) tp.local(x,y,z) tp.abs(x,y,z) tp.look() tp.camera() tp.target() tp.toTarget() tp.stickyTarget() tp.watchCamera(ms)"); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment