From aa0325e3a1a4966af5b77462918f3744c8bac6e3 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Mon, 3 Nov 2025 00:49:01 -0600 Subject: [PATCH 01/15] Add Final Fantast XIII --- PC_Steam_FINAL_FANTASY_XIII.js | 1305 ++++++++++++++++++++++++++++++++ 1 file changed, 1305 insertions(+) create mode 100644 PC_Steam_FINAL_FANTASY_XIII.js diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js new file mode 100644 index 00000000..a0db5473 --- /dev/null +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -0,0 +1,1305 @@ +// ==UserScript== +// @name FINAL FANTASY XIII (ファイナルファンタジーXIII) +// @version 1.0.0 +// @author Mansive +// @description Steam +// * Square Enix +// https://store.steampowered.com/app/292120/FINAL_FANTASY_XIII/ +// ==/UserScript== + +//#region Types + +/** + * @callback TreasureArgsFunction + * @param {Object} treasure + * @param {InvocationArguments} treasure.args + * @returns {NativePointer} + */ + +/** + * @callback TreasureContextFunction + * @param {Object} treasure + * @param {X64CpuContext} treasure.context + * @returns {NativePointer} + */ + +/** + * @typedef {Object} TargetHook + * @property {string} name + * @property {string | MatchPattern} pattern + * @property {NativePointer} address - Mainly used for debugging + * @property {string} register + * @property {number} argIndex + * @property {TreasureArgsFunction | TreasureContextFunction} getTreasureAddress + */ + +/** + * @typedef {Object} Hook + * @property {string | MatchPattern} pattern + * @property {string=} register + * @property {number=} argIndex + * @property {TargetHook=} target + * @property {string[]=} origins + * @property {HookHandler} handler + */ + +/** + * New InvocationContext with specified Ia32CpuContext because VSCode can't + * perfectly resolve the generic CpuContext + * @typedef {Omit & { context: Ia32CpuContext }} Ia32InvocationContext + */ + +/** + * @callback HookHandler + * @this {Ia32InvocationContext} + * @param {NativePointer} address + * @returns {string | null=} + */ + +//#endregion + +//#region Some Globals + +const __e = Process.enumerateModules()[0]; + +const BACKTRACE = false; +const DEBUG_LOGS = true; +const INSPECT_ARGS_REGS = true; + +const SETTINGS = { + singleSentence: true, + // enableHooksName: true, + // enableHooksTips: true, + // enableHooksMenuExplanation: true, + // enableHooksArcadeItems: true, +}; + +let hooksPrimaryCount = 0; +let hooksAuxCount = 0; + +let timer1 = null; +let timer3 = null; + +const encoder = new TextEncoder("utf-8"); +const decoder = new TextDecoder("utf-8"); + +const texts1 = new Set(); + +const topTexts = new Set(); +const middleTexts = new Set(); +const bottomTexts = new Set(); +const deepTexts = new Set(); + +let previous = ""; + +/** @param {NativePointer} address */ +function readString(address) { + const text = address.readMonoString(); + + DEBUG_LOGS && logText(text); + + return text; +} + +//#endregion + +//#region Hooks + +const hooksStatus = { + // exampleHookName: { enabled: true, characters: 0 }, +}; + +// ASLR disabled +/** @type {Object.} */ +const targetHooks = { + // ENCY: { + // name: "ENCY", + // pattern: "48 8B D8 48 85 C0 74 53 48 8B D0 48 89 74 24 30 48 8D 4F 08 E8", + // address: ptr(0x140303454), + // register: "rax", + // argIndex: -1, + // /** @type {TreasureContextFunction} */ + // getTreasureAddress({ context }) { + // return context[this.register]; + // }, + // }, + MYSTERY: { + name: "MYSTERY", + // pattern: "E8 0D 01 00 00 8B 45 E8 8B E5", + pattern: + "55 8B EC 81 EC D0 01 00 00 A1 84 5A 6E 02 33 C5 89 45 FC 89 8D 30 FE FF FF 83 7D 0C 00 75 07 C7 45 10 FF FF FF FF 8B 85 30 FE FF FF 8B 48 20", + address: NULL, + // register: "ecx", // message type + register: "edx", // write buffer, nothings in it yet until onLeave() + argIndex: -1, + /** @type {TreasureContextFunction} */ + getTreasureAddress({ context }) { + return context[this.register]; + }, + strategy: mysteryHookStrategy, + }, +}; + +//#region Hooks: Main + +const hooksMain = { + CutsceneDialogue: { + pattern: "E8 A1 60 FC FF", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + DatalogEntry: { + pattern: "E8 FB 1A 06 00", + register: "edx", + handler: mainHandler, + }, + Tutorial: { + pattern: "83 BC 10 B8 00 00 00 00 75 11 6A 01 68 24 9C 19 01 8D 4D DC", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + DifficultySelection: { + pattern: "E8 74 8A ED FF", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + LoadingChapter: { + pattern: "E8 27 43 EE FF 8B 8D 90 FE FF FF 8B 51 74 52 68 10 8C 19 01", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + LoadingReview: { + pattern: "E8 F3 42 EE FF 68 28 DF 09 01 8D 4D 88 E8 26 8D 9F FF", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + AreaName: { + pattern: "E8 29 32 EE FF 8B 8D E0 FE FF FF 83 79 74 00 0F 84 AE 02 00 00", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + SideDialogue: { + pattern: "E8 A3 B9 00 00 0F BF 55 FE 8B 45 F4 0F BE 0C 10", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + ShopSlidingMessage: { + pattern: + "E8 CE 5D F2 FF C6 85 D8 FB FF FF 00 0F BF 4D E6 03 4D DC 89 8D C4 FB FF FF 0F 84 F8 00 00 00", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + ShopItemDescription: { + pattern: "E8 16 D6 F2 FF 8B 45 10 50 0F BF 4D DA", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, + MenuOptionDescription: { + pattern: "E8 BB ED E9 FF", + // pattern: "E8 6C 6F 0A 00", + target: targetHooks.MYSTERY, + handler: MenuOptionDescriptionHandler, + }, +}; + +//#endregion + +//#region Hooks: Ency + +const hooksEncyclopedia = { + // EncyclopediaHelpInfo: { + // pattern: "48 89 5C 24 10 57 48 83 EC 20 48 8B F9 8B CA E8 7C 0E F3 FF", // above ency + // origins: [ + // "49 8B 4C 24 28 33 D2 48 8B 01 FF 50 20 49 8B 8C 24 98 00 00 00 41 83 C9 FF 45 33 C0 48 8B 89 20 02 00 00 41 8D 51 02 E8 11 47 1B 00", // open help menu + // "48 8B 7C 24 38 33 C0 48 83 C4 20 5B C3 49 8B 8A 20 02 00 00 41 83 C9 FF 45 33 C0 33 D2 E8 6A 45 1B 00", // switch entry + // "48 8B 7C 24 38 33 C0 48 83 C4 20 5B C3 48 8B CB 89 BB A8 00 00 00 E8 EC F7 FF FF", // flip entry page + // // "39 BB AC 00 00 00 0F 8E A0 00 00 00", // flip menu page + // ], + // target: targetHooks.ENCY, + // handler: encyclopediaHelpInfoHandler, + // }, +}; + +//#endregion + +//#region Hooks: All + +// Combine all sets of hooks into one object for ease of use +/** @type {Object.} */ +const hooks = Object.assign({}, hooksMain); + +const hooksPrimaryTotal = Object.keys(hooks).length; + +//#endregion + +//#endregion + +//#region Strategies + +/** + * Returns a NativePointer from either the arguments or registers depending + * on how the targeted hook extracts text. + * @param {Object} options + * @param {TargetHook} options.target + * @param {InvocationArguments} options.args + * @param {X64CpuContext} options.context + * @returns {NativePointer} + */ +function getTreasureAddress({ target, args, context }) { + return target.getTreasureAddress({ args, context }); +} + +/** + * Hooks an address and checks the return addresses before invoking the handler. + * Expects the address to be the function prologue. + * @param {Hook & {name: string} & {address: NativePointer}} + */ +function filterReturnsStrategy({ address, name, register, handler }) { + Breakpoint.add(address, function () { + const returnAddress = this.context.rsp.readPointer(); + // console.warn("filtering: " + returnAddress); + + if (returnAddresses.has(returnAddress.toInt32())) { + DEBUG_LOGS && console.warn("passedFilter: " + name); + + if (hooksStatus[name].enabled === false) { + logDim("skipped: " + name); + return false; + } + + if (INSPECT_ARGS_REGS === true) { + console.log("in: ORIGIN"); + inspectRegs(this.context); + } + + const text = handler.call(this, this.context[register]); + setHookCharacterCount(name, text); + } else { + // console.warn(`Current return address: ${this.returnAddress} + // \rreturnAddresses Set: ${JSON.stringify(returnAddresses)}`); + } + }); +} + +// The function is called twice, but we only want the second call. +// Only the first call have EAX and EDX equal to each other, +// so we can use that behavior to skip the first call. +// The beginning of the function stores the string's memory location in EDX, +// and EDX contains the completed string only after the function finishes. +function mysteryHookStrategy({ address, name, target, handler }) { + Breakpoint.add(address, function () { + if (hooksStatus[name].enabled === false) { + logDim("skipped: " + name); + return false; + } + console.log("onEnter: " + name); + if (INSPECT_ARGS_REGS === true) { + console.log("in: ORIGIN"); + inspectRegs(this.context); + } + + const outerContext = this.context; + + let isDetached = false; + const hook = Interceptor.attach(target.address, { + onEnter(args) { + if (this.context.eax.equals(this.context.edx)) { + console.warn("skipped, equal eax and edx"); + this.shouldSkip = true; + return null; + } + this.outerContext = outerContext; + this.edx = this.context.edx; + }, + onLeave(retval) { + if (this.shouldSkip) { + console.warn("skipped in onleave"); + return null; + } + + hook.detach(); + isDetached = true; + Interceptor.flush(); + + console.log("onLeave: " + name); + + const text = handler.call(this, this.edx) ?? null; + // console.log(hexdump(this.edx, { header: false, ansi: false, length: 0x100 })); + setHookCharacterCount(name, text); + }, + }); + + // Manually detach in case onLeave never gets called + setTimeout(() => { + if (isDetached === false) { + hook.detach(); + Interceptor.flush(); + console.warn("timeout: detached hook for " + name); + } + }, 10); + }); +} + +/** + * Hooks an address as the origin, then temporarily hooks a target address + * whenever the origin is accessed. + * @param {Hook & {name: string} & {address: NativePointer}} + */ +function nestedHooksStrategy({ address, name, target, handler }) { + Breakpoint.add(address, function () { + if (hooksStatus[name].enabled === false) { + logDim("skipped: " + name); + return false; + } + console.log("onEnter: " + name); + if (INSPECT_ARGS_REGS === true) { + console.log("in: ORIGIN"); + inspectRegs(this.context); + } + // this.outerArgs = outerArgs; + hotAttach(target.address, function () { + if (INSPECT_ARGS_REGS === true) { + console.log("in: TARGET"); + inspectRegs(this.context); + } + const text = handler(getTreasureAddress({ target, context: this.context })); + setHookCharacterCount(name, text); + }); + }); +} + +/** + * @param {Hook & {name: string} & {address: NativePointer}} + */ +function nestedHooksOnLeaveStrategy({ address, name, target, handler }) { + Breakpoint.add(address, function () { + const hook = Interceptor.attach(target.address, { + onEnter(args) { + console.log("onEnter: " + name); + this.enterContext = this.context; + }, + onLeave(retval) { + hook.detach(); + Interceptor.flush(); + + console.log("onLeave: " + name); + // const text = handler(getTreasureAddress({ target, context: this.enterContext })); + const text = handler(this.enterContext.edx); + setHookCharacterCount(name, text); + }, + }); + }); +} + +/** + * Combination of {@link nestedHooksStrategy} and {@link filterReturnsStrategy}. + * @param {Hook & {name: string} & {address: NativePointer}} + */ +function filterReturnsNestedHooksStrategy({ address, name, target, handler }) { + Breakpoint.add(address, function () { + const returnAddress = this.context.rsp.readPointer(); + // console.warn("filtering: " + returnAddress); + + if (returnAddresses.has(returnAddress.toInt32())) { + DEBUG_LOGS && console.warn("passedFilter: " + name); + + if (hooksStatus[name].enabled === false) { + logDim("skipped: " + name); + return false; + } + + console.log("onEnter: " + name); + + if (INSPECT_ARGS_REGS === true) { + console.log("in: ORIGIN"); + inspectRegs(this.context); + } + + // const outerContext = this.context; + + hotAttach(target.address, function () { + if (INSPECT_ARGS_REGS === true) { + console.log("in: TARGET"); + inspectRegs(this.context); + } + + // this.outerContext = outerContext; + + const text = handler(getTreasureAddress({ target, context: this.context })); + setHookCharacterCount(name, text); + }); + } else { + // ... + } + }); +} + +/** @param {Hook & {name: string} & {address: NativePointer}} */ +function normalStrategy({ address, name, register, handler }) { + Breakpoint.add(address, function () { + if (hooksStatus[name].enabled === false) { + logDim("skipped: " + name); + return false; + } + + console.log("onEnter: " + name); + + if (INSPECT_ARGS_REGS === true) { + inspectRegs(this.context); + } + + const text = handler.call(this, this.context[register]) ?? null; + setHookCharacterCount(name, text); + }); +} + +//#endregion + +//#region Attach + +/** + * Wrapper around "Interceptor.attach". Quickly detach after attaching. + * @param {NativePointer} address + * @param {Function} callback + */ +function hotAttach(address, callback) { + const hook = Interceptor.attach(address, function (args) { + hook.detach(); + Interceptor.flush(); + + this.args = args; + + callback.call(this, args); + }); +} + +/** + * Scans a pattern in memory and returns a NativePointer for first match. + * @param {string} name + * @param {string} pattern + * @returns {NativePointer} + */ +function getPatternAddress(name, pattern) { + let results = ""; + + try { + results = Memory.scanSync(__e.base, __e.size, pattern); + } catch (err) { + throw new Error(`Error ocurred with [${name}]: ${err.message}`, { + cause: err, + }); + } + + if (results.length === 0) { + throw new Error(`[${name}] Not found!`); + } + + const address = results[0].address; + + console.log(`\x1b[32m[${name}] @ ${address}\x1b[0m`); + if (results.length > 1) { + console.warn(`${name} has ${results.length} results`); + // console.log(results[0].address, results[1].address); + } + + return address; +} + +function setupHooks() { + for (const hook in targetHooks) { + const name = hook; + const pattern = targetHooks[name].pattern; + targetHooks[hook].address = getPatternAddress(name, pattern); + hooksAuxCount += 1; + } + + for (const hook in hooks) { + const name = hook; + const origins = hooks[hook].origins; + + if (origins) { + for (const origin of origins) { + returnAddresses.add(getPatternAddress(name + "RETURN", origin).toUInt32()); + hooksAuxCount += 1; + } + } + + const result = attachHook({ name, ...hooks[hook] }); + + if (result === true) { + hooksStatus[name] = { enabled: true, characters: 0 }; + hooksPrimaryCount += 1; + } else { + console.log("FAIL"); + } + } + + console.log(` +${hooksPrimaryCount} primary hooks attached +${hooksAuxCount} auxiliary hooks on standby +${hooksPrimaryCount + hooksAuxCount} total hooks + `); +} + +/** + * In order from least to greatest priority:\ + * If {@link target} is provided, the hook will use it.\ + * If {@link origins} is provided, return addresses will filter the hook. + * @param {Hook & {name: string}} params + * @returns {boolean} + */ +function attachHook(params) { + const { name, pattern, target, origins } = params; + const address = getPatternAddress(name, pattern); + const args = { address, ...params }; + + if (target?.strategy) { + DEBUG_LOGS && console.log(`[${name}] using custom strategy`); + target.strategy(args); + } else if (origins && target) { + DEBUG_LOGS && + console.log(`[${name}] filtered with return addresses and targeting [${target.name}]`); + filterReturnsNestedHooksStrategy(args); + } else if (origins) { + DEBUG_LOGS && console.log(`[${name}] filtered with return addresses`); + filterReturnsStrategy(args); + } else if (target) { + DEBUG_LOGS && console.log(`[${name}] targeting [${target.name}]`); + nestedHooksStrategy(args); + } else { + normalStrategy(args); + } + + return true; +} + +//#endregion + +//#region Handlers + +// https://github.com/LR-Research-Team/Datalog/wiki/ZTR +const encodingKeys = { + icons: { + f0_40: "{Icon Clock}", + f0_41: "{Icon Warning}", + f0_42: "{Icon Notification}", + f0_43: "{Icon Gil}", + f0_44: "{Icon Arrow_Right}", + f0_45: "{Icon Arrow_Left}", + f0_46: "{Icon Mission_Note}", + f0_47: "{Icon Check_Mark}", + f0_48: "{Icon Ability_Synthesized}", + f2_40: "{Icon Gunblade}", + f2_41: "{Icon Pistol}", + f2_42: "{Icon Emblem}", + f2_43: "{Icon Boomerang}", + f2_44: "{Icon Staff}", + f2_45: "{Icon Spear}", + f2_46: "{Icon Knife}", + f2_47: "{Icon Water_Drop}", + f2_48: "{Icon Datalog}", + f2_49: "{Icon Eidolith_Crystal}", + f2_4a: "{Icon Omni_Kit}", + f2_4b: "{Icon Shop_Pass}", + f2_4c: "{Icon Synthetic_Component}", + f2_4d: "{Icon Organic_Component}", + f2_4e: "{Icon Catalyst_Component}", + f2_4f: "{Icon Accessory_Type1}", + f2_50: "{Icon Accessory_Type2}", + f2_51: "{Icon Accessory_Type3}", + f2_52: "{Icon Accessory_Type4}", + f2_53: "{Icon Potion}", + f2_54: "{Icon Container_Type1}", + f2_55: "{Icon Container_Type2}", + f2_56: "{Icon Phoenix_Down}", + f2_57: "{Icon Shroud}", + f2_58: "{Icon Sack}", + f2_59: "{Icon Ability_Passive}", + f2_5a: "{Icon Ability_Physical}", + f2_5b: "{Icon Ability_Magic}", + f2_5c: "{Icon Ability_Defense}", + f2_5d: "{Icon Ability_Heal}", + f2_5e: "{Icon Ability_Debuff}", + f2_5f: "{Icon Status_Ailment}", + f2_60: "{Icon Ability_Buff}", + f2_61: "{Icon Alert}", + f2_62: "{Icon Sword}", + f2_63: "{Icon Shield}", + f2_64: "{Icon Magic_Staff}", + f2_65: "{Icon Unknown1}", + f2_66: "{Icon Unknown2}", + f2_67: "{Icon Unknown3}", + f2_68: "{Icon Ability_Eidolon}", + f2_69: "{Icon Ability_Technique}", + f2_6a: "{Icon Ribbon}", + f2_6b: "{Icon Amulet}", + f2_6c: "{Icon Necklace}", + }, + buttonPrompts: { + f1_40: "{Btn A}", + f1_41: "{Btn B}", + f1_42: "{Btn X}", + f1_43: "{Btn Y}", + f1_44: "{Btn Start}", + f1_45: "{Btn Back}", + f1_46: "{Btn LB}", + f1_47: "{Btn RB}", + f1_48: "{Btn LT}", + f1_49: "{Btn RT}", + f1_4a: "{Btn DPadLeft}", + f1_4b: "{Btn DPadDown}", + f1_4c: "{Btn DPadRight}", + f1_4d: "{Btn DPadUp}", + f1_4e: "{Btn LSLeft}", + f1_4f: "{Btn LSDown}", + f1_50: "{Btn LSRight}", + f1_51: "{Btn LSUp}", + f1_52: "{Btn LSLeftRight}", + f1_53: "{Btn LSUpDown}", + f1_54: "{Btn LSPress}", + f1_55: "{Btn RSPress}", + f1_56: "{Btn RSLeft}", + f1_57: "{Btn RSDown}", + f1_58: "{Btn RSRight}", + f1_59: "{Btn RSUp}", + f1_5a: "{Btn RSLeftRight}", + f1_5b: "{Btn RSUpDown}", + f1_5c: "{Btn LStick}", + f1_5d: "{Btn RStick}", + f1_5e: "{Btn DPadUpDown}", + f1_5f: "{Btn DPadLeftRight}", + f1_60: "{Btn DPad}", + }, + colors: { + f9_32: "{Color Ex00}", + f9_33: "{Color Ex01}", + f9_34: "{Color Ex02}", + f9_35: "{Color Ex03}", + f9_36: "{Color Ex04}", + f9_37: "{Color Ex05}", + f9_38: "{Color Ex06}", + f9_39: "{Color Ex07}", + f9_3a: "{Color Ex08}", + f9_3b: "{Color Ex09}", + f9_3c: "{Color Ex10}", + f9_3d: "{Color Ex11}", + f9_3e: "{Color Ex12}", + f9_3f: "{Color Ex13}", + f9_40: "{Color White}", + f9_41: "{Color IceBlue}", + f9_42: "{Color Gold}", + f9_43: "{Color LightRed}", + f9_44: "{Color Yellow}", + f9_45: "{Color Green}", + f9_46: "{Color Gray}", + f9_47: "{Color LightGold}", + f9_48: "{Color Rose}", + f9_49: "{Color Purple}", + f9_4a: "{Color DarkYellow}", + f9_4b: "{Color Gray2}", + f9_4c: "{Color Voilet}", + f9_4d: "{Color LightGreen}", + f9_4f: "{Color Ex14}", + f9_50: "{Color Ex15}", + f9_51: "{Color Ex16}", + f9_52: "{Color Ex17}", + f9_53: "{Color Ex18}", + f9_54: "{Color Ex19}", + f9_55: "{Color Ex20}", + f9_56: "{Color Ex21}", + f9_57: "{Color Ex22}", + f9_58: "{Color Ex23}", + f9_59: "{Color Ex24}", + f9_5a: "{Color Ex25}", + f9_5b: "{Color Ex26}", + f9_5e: "{Color Ex27}", + f9_5f: "{Color Ex28}", + }, + characters: { + "85_40": "€", + "85_42": "‚", + "85_44": "„", + "85_45": "…", + "85_46": "†", + "85_47": "‡", + "85_49": "‰", + "85_4a": "Š", + "85_4b": "‹", + "85_4c": "Œ", + "85_4e": "Ž", + "85_51": "‘", + "85_52": "’", + "85_53": "“", + "85_54": "”", + "85_55": "•", + "85_56": "-", + "85_57": "—", + "85_59": "™", + "85_5a": "š", + "85_5b": "›", + "85_5c": "œ", + "85_5e": "ž", + "85_5f": "Ÿ", + "85_61": "¡", + "85_62": "¢", + "85_63": "£", + "85_64": "¤", + "85_65": "¥", + "85_66": "¦", + "85_67": "§", + "85_68": "¨", + "85_69": "©", + "85_6a": "ª", + "85_6b": "«", + "85_6c": "¬", + "85_6e": "®", + "85_6f": "¯", + "85_70": "°", + "85_71": "±", + "85_72": "²", + "85_73": "³", + "85_74": "´", + "85_75": "µ", + "85_76": "¶", + "85_77": "·", + "85_78": "¸", + "85_79": "¹", + "85_7a": "º", + "85_7b": "»", + "85_7c": "¼", + "85_7d": "½", + "85_7e": "¾", + "85_7f": "¿", + "85_9f": "À", + "85_81": "Á", + "85_82": "Â", + "85_83": "Ã", + "85_84": "Ä", + "85_85": "Å", + "85_86": "Æ", + "85_87": "Ç", + "85_88": "È", + "85_89": "É", + "85_8a": "Ê", + "85_8b": "Ë", + "85_8c": "Ì", + "85_8d": "Í", + "85_8e": "Î", + "85_8f": "Ï", + "85_90": "Ð", + "85_91": "Ñ", + "85_92": "Ò", + "85_93": "Ó", + "85_94": "Ô", + "85_95": "Õ", + "85_96": "Ö", + "85_b6": "×", + "85_98": "Ø", + "85_99": "Ù", + "85_9a": "Ú", + "85_9b": "Û", + "85_9c": "Ü", + "85_9d": "Ý", + "85_bd": "Þ", + "85_be": "ß", + "85_bf": "à", + "85_c0": "á", + "85_c1": "â", + "85_c2": "ã", + "85_c3": "ä", + "85_c4": "å", + "85_c5": "æ", + "85_c6": "ç", + "85_c7": "è", + "85_c8": "é", + "85_c9": "ê", + "85_ca": "ë", + "85_cb": "ì", + "85_cc": "í", + "85_cd": "î", + "85_ce": "ï", + "85_cf": "ð", + "85_d0": "ñ", + "85_d1": "ò", + "85_d2": "ó", + "85_d3": "ô", + "85_d4": "õ", + "85_d5": "ö", + "85_d6": "÷", + "85_d7": "ø", + "85_d8": "ù", + "85_d9": "ú", + "85_da": "û", + "85_db": "ü", + "85_dc": "ý", + "85_dd": "þ", + "85_de": "ÿ", + }, +}; + +// encodingKeys is for human readability, while encodingKeys2 is for actual use +// keys as numbers avoids needing to convert hex to strings for object lookups +const encodingKeys2 = new Map(); +for (const category in encodingKeys) { + for (const [key, value] of Object.entries(encodingKeys[category])) { + const [hex1, hex2] = key.split("_"); + const newKey = parseInt(hex1, 16) + parseInt(hex2, 16); + encodingKeys2.set(newKey, value); + } +} + +// ev_comn_xxx -> cutscene dialogue with high-quality character models +// ev_hang -> cutscene dialogue without high-quality character models +// system -> system message + +/** @param {NativePointer} address */ +function readString(address) { + let limit = 500; + let count = 0; + let byte1 = 0; + let byte2 = 0; + let s = ""; + let c = ""; + + while (true) { + byte1 = address.readU8(); + byte2 = address.add(1).readU8(); + + if (byte1 >= 0xf0 && byte1 <= 0xf2 && byte2 >= 0x40 && byte2 <= 0x70) { + const controlCode = encodingKeys2.get(byte1 + byte2); + if (!controlCode) { + console.warn("Unknown control code:", byte1, byte2); + c = `[${byte1} ${byte2}]`; + s += c; + } else { + s += "▢"; // placeholder + } + address = address.add(2); + } + + // colors + else if (byte1 === 0xf9 && byte2 >= 0x32 && byte2 <= 0x5b) { + const colorCode = encodingKeys2.get(byte1 + byte2); + if (!colorCode) { + console.warn("Unknown color code:", byte1, byte2); + c = `[${byte1} ${byte2}]`; + s += c; + } + address = address.add(2); + } + // special characters + else if (byte1 === 0x85 && byte2 >= 0x40 && byte2 <= 0xde) { + const specialChar = encodingKeys2.get(byte1 + byte2); + if (!specialChar) { + console.warn("Unknown special character:", byte1, byte2); + c = `[${byte1} ${byte2}]`; + s += c; + } else { + s += specialChar; + } + address = address.add(2); + } + // newline + else if (byte1 === 0x40 && byte2 === 0x72) { + s += "\n"; + address = address.add(2); + } + // choices + else if (byte1 === 0x7c) { + s += "\n"; + address = address.add(1); + } + // null terminator + else if (byte1 === 0x0) { + // console.warn("Found null terminator"); + break; + } else { + c = decoder.decode(address.readByteArray(4))[0]; // utf-8: 1->4 bytes. + s += c; + address = address.add(encoder.encode(c).byteLength); + } + } + + const text = s; + + DEBUG_LOGS && console.log(`${color.FgYellow}${JSON.stringify(text)}${color.Reset}`); + + return text; +} + +/** @param {string} text */ +function genericHandler(text) { + texts1.add(text); + + clearTimeout(timer1); + timer1 = setTimeout(() => { + trans.send([...texts1].join("\r\n")); + texts1.clear(); + }, 200); +} + +function orderedHandler() { + clearTimeout(timer3); + timer3 = setTimeout(() => { + trans.send([...topTexts, ...middleTexts, ...bottomTexts, ...deepTexts].join("\n")); + + topTexts.clear(); + middleTexts.clear(); + bottomTexts.clear(); + deepTexts.clear(); + }, 600); +} + +/** + * @param {string} text + * @param {Set} set + * @param {boolean} list + */ +function textSetControl(text, set, list = false) { + if (list === false) { + set.clear(); + } + set.add(text); +} + +/** @type {HookHandler & {list: boolean}} */ +function positionTopHandler(text, list = false) { + bottomTexts.clear(); + + textSetControl(text, topTexts, list); + orderedHandler(); + + return text; +} + +/** @type {HookHandler & {list: boolean}} */ +function positionMiddleHandler(text, list = false) { + bottomTexts.clear(); + + textSetControl(text, middleTexts, list); + orderedHandler(); + + return text; +} + +/** @type {HookHandler & {list: boolean}} */ +function positionBottomHandler(text, list = false) { + textSetControl(text, bottomTexts, list); + orderedHandler(); + + return text; +} + +/** @type {HookHandler & {list: boolean}} */ +function positionDeepHandler(text, list = false) { + textSetControl(text, deepTexts, list); + orderedHandler(); + + return text; +} + +/** @type {HookHandler} */ +function mainHandler(address) { + // console.log(hexdump(address, { header: false, ansi: false, length: 0x200 })); + + let text = readString(address); + + genericHandler(text); + return text; +} + +/** @type {HookHandler} */ +function MenuOptionDescriptionHandler(address) { + const clue = this.outerContext.eax.readShiftJisString(); + + if (clue.startsWith("$")) { + const text = readString(address); + + positionTopHandler(text); + return text; + } else { + positionTopHandler(clue); + + return clue; + } +} + +trans.replace((/**@type {string}*/ s) => { + // if (s === previous || s === "") { + // return null; + // } + // previous = s; + + // s = s.replace(/@r/g, "\n"); // 0x40 0x72 + + return s.trim(); +}); + +//#endregion + +//#region Miscellaneous + +const color = { + Reset: "\x1b[0m", + Bright: "\x1b[1m", + Dim: "\x1b[2m", + Underscore: "\x1b[4m", + Blink: "\x1b[5m", + Reverse: "\x1b[7m", + Hidden: "\x1b[8m", + + FgBlack: "\x1b[30m", + FgRed: "\x1b[31m", + FgGreen: "\x1b[32m", + FgYellow: "\x1b[33m", + FgBlue: "\x1b[34m", + FgMagenta: "\x1b[35m", + FgCyan: "\x1b[36m", + FgWhite: "\x1b[37m", + FgGray: "\x1b[90m", + + BgBlack: "\x1b[40m", + BgRed: "\x1b[41m", + BgGreen: "\x1b[42m", + BgYellow: "\x1b[43m", + BgBlue: "\x1b[44m", + BgMagenta: "\x1b[45m", + BgCyan: "\x1b[46m", + BgWhite: "\x1b[47m", + BgGray: "\x1b[100m", +}; + +function logText(message) { + console.log(`${color.FgYellow}${JSON.stringify(message)}${color.Reset}`); +} + +function logDim(message) { + console.log(`${color.Dim}${message}${color.Reset}`); +} + +function validateHooks() { + function expose(name, property) { + throw new TypeError(`[${name}] ${property} is of type ${typeof property}`); + } + + for (const hookName in hooks) { + const hook = hooks[hookName]; + const { pattern, register, argIndex, target, origins, handler } = hook; + + if (typeof pattern !== "string") { + expose(hookName, pattern); + } + if (typeof handler !== "function") { + expose(hookName, handler); + } + if (register && argIndex) { + expose(hookName, argIndex); + } + if (argIndex && !target && typeof argIndex !== "number") { + expose(hookName, argIndex); + } + if (register && !target && typeof register !== "string") { + expose(hookName, register); + } else if (!register && target && typeof target !== "object") { + expose(hookName, target); + } else if (register && target && origins) { + expose(hookName, origins); + } + if (!register && !target) { + expose(hookName, target); + } + } +} + +/** + * Attempts to print arguments' values as strings. + * @param {InvocationArguments} args + */ +function inspectArgs(args) { + const argsTexts = []; + + for (let i = 0; i <= 10; i++) { + let type = ""; + let text = ""; + + // yeehaw + try { + type = "P"; + text = args[i].readPointer().readShiftJisString(); + } catch (err) { + try { + type = "PP"; + text = args[i].readPointer().readPointer().readShiftJisString(); + } catch (err) { + try { + type = "S"; + text = args[i].readShiftJisString(); + } catch (err) { + // type = "A"; + // text = args[i].toString(); + continue; + } + } + } + + if (text === null || text.length === 0 || /^\\/g.test()) { + continue; + } + + // text += args[i].toString(); + argsTexts.push(`${type}|args[${i}]=${JSON.stringify(text)}`); + } + + for (const text of argsTexts) { + console.log(`${color.BgMagenta}${text}${color.Reset}`); + } + argsTexts.length = 0; +} + +/** + * Attempts to print registers' values as strings. + * @param {X64CpuContext} context + */ +function inspectRegs(context) { + const regsTexts = []; + const regs = [ + "eax", + "ebx", + "ecx", + "edx", + "esi", + "edi", + "ebp", + "esp", + //"eip", + ]; + + let text = ""; + let address = NULL; + + for (const reg of regs) { + address = context[reg]; + try { + text = address.readShiftJisString(); + } catch (err) { + continue; + } + + if (text === null || text.length === 0 || /^\\/g.test()) { + continue; + } + + regsTexts.push(`${reg}=${JSON.stringify(text)}`); + } + + for (const text of regsTexts) { + console.log(`${color.BgBlue}${text}${color.Reset}`); + } + regsTexts.length = 0; +} + +/** Prints the backtrace or callstack for a hook. */ +function startTrace() { + console.warn("Tracing!!"); + + const traceTarget = targetHooks.MYSTERY; + + const traceAddress = getPatternAddress(traceTarget.name, traceTarget.pattern); + traceTarget.address = traceAddress; + const previousTexts = new Set(); + + // Interceptor.attach(traceAddress, { + // onEnter(args) { + // let text = ""; + // const context = this.context; + // try { + // text = getTreasureAddress({ + // target: traceTarget, + // args, + // context, + // }).readShiftJisString(); + // } catch (err) { + // // console.error("Reading from address failed:", err.message); + // return null; + // } + + // if (previousTexts.has(text)) { + // return null; + // } + // previousTexts.add(text); + + // const callstack = Thread.backtrace(this.context, Backtracer.ACCURATE); + + // console.log(` + // \rONENTER: ${traceTarget.name} + // \r${text} + // \rCallstack: ${callstack.splice(0, 8)} + // \rReturn: ${this.returnAddress}`); + + // if (INSPECT_ARGS_REGS === true) { + // inspectArgs(args); + // inspectRegs(this.context); + // } + // }, + // }); + + Interceptor.attach(traceAddress, { + onEnter(args) { + this.edx = this.context.edx; + + // const callstack = Thread.backtrace(this.context, Backtracer.ACCURATE); + + // console.log(` + // \rONENTER: ${traceTarget.name} + // \rCallstack: ${callstack.splice(0, 8)}`); + }, + onLeave(retval) { + let text = ""; + try { + text = readString(this.edx); + } catch (err) { + // console.error("Reading from address failed:", err.message); + return null; + } + + if (previousTexts.has(text)) { + return null; + } + previousTexts.add(text); + + const callstack = Thread.backtrace(this.context, Backtracer.ACCURATE); + + console.log(`ONLEAVE: ${traceTarget.name} + \r${text} + \rCallstack: ${callstack.splice(0, 8)}`); + }, + }); +} + +function setHookCharacterCount(name, text) { + if (text === null || text === "") { + return null; + } + + const cleanedText = text.replace(/[。…、?!「」―ー・]|<[^>]+>|\r|\n|\u3000/gu, ""); + hooksStatus[name].characters += cleanedText.length; +} + +//#endregion + +//#region Start + +function start() { + if (BACKTRACE === true) { + startTrace(); + return true; + } + + validateHooks(); + setupHooks(); + // uiStart(); +} + +start(); + +//#endregion From 8353c72557532a976a3fe2147c2363577c9f3ce5 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Mon, 3 Nov 2025 01:13:30 -0600 Subject: [PATCH 02/15] Make position handlers less weird, better menu filtering --- PC_Steam_FINAL_FANTASY_XIII.js | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index a0db5473..105e203e 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -151,7 +151,7 @@ const hooksMain = { DatalogEntry: { pattern: "E8 FB 1A 06 00", register: "edx", - handler: mainHandler, + handler: positionMiddleHandler, }, Tutorial: { pattern: "83 BC 10 B8 00 00 00 00 75 11 6A 01 68 24 9C 19 01 8D 4D DC", @@ -304,7 +304,6 @@ function mysteryHookStrategy({ address, name, target, handler }) { const hook = Interceptor.attach(target.address, { onEnter(args) { if (this.context.eax.equals(this.context.edx)) { - console.warn("skipped, equal eax and edx"); this.shouldSkip = true; return null; } @@ -313,7 +312,6 @@ function mysteryHookStrategy({ address, name, target, handler }) { }, onLeave(retval) { if (this.shouldSkip) { - console.warn("skipped in onleave"); return null; } @@ -963,9 +961,10 @@ function textSetControl(text, set, list = false) { } /** @type {HookHandler & {list: boolean}} */ -function positionTopHandler(text, list = false) { +function positionTopHandler(address, list = false) { bottomTexts.clear(); + const text = readString(address); textSetControl(text, topTexts, list); orderedHandler(); @@ -973,9 +972,10 @@ function positionTopHandler(text, list = false) { } /** @type {HookHandler & {list: boolean}} */ -function positionMiddleHandler(text, list = false) { +function positionMiddleHandler(address, list = false) { bottomTexts.clear(); + const text = readString(address); textSetControl(text, middleTexts, list); orderedHandler(); @@ -983,7 +983,8 @@ function positionMiddleHandler(text, list = false) { } /** @type {HookHandler & {list: boolean}} */ -function positionBottomHandler(text, list = false) { +function positionBottomHandler(address, list = false) { + const text = readString(address); textSetControl(text, bottomTexts, list); orderedHandler(); @@ -991,7 +992,8 @@ function positionBottomHandler(text, list = false) { } /** @type {HookHandler & {list: boolean}} */ -function positionDeepHandler(text, list = false) { +function positionDeepHandler(address, list = false) { + const text = readString(address); textSetControl(text, deepTexts, list); orderedHandler(); @@ -1008,19 +1010,23 @@ function mainHandler(address) { return text; } +let menuOptionDescriptionPrevious = NULL; /** @type {HookHandler} */ function MenuOptionDescriptionHandler(address) { - const clue = this.outerContext.eax.readShiftJisString(); + /** @type {NativePointer} */ + const eax = this.outerContext.eax; - if (clue.startsWith("$")) { - const text = readString(address); + if (eax.equals(menuOptionDescriptionPrevious) || eax.isNull()) { + return null; + } + menuOptionDescriptionPrevious = eax; - positionTopHandler(text); - return text; - } else { - positionTopHandler(clue); + const clue = eax.readShiftJisString(); - return clue; + if (clue.startsWith("$")) { + return positionTopHandler(address); + } else { + return positionTopHandler(eax); } } From bfe0eacaacda8833a42cc6cf9a44030db38ff2d6 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 4 Nov 2025 00:35:08 -0600 Subject: [PATCH 03/15] Single bytes, add support for Japanese text --- PC_Steam_FINAL_FANTASY_XIII.js | 44 +++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 105e203e..d1ab0bb7 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -62,9 +62,9 @@ const __e = Process.enumerateModules()[0]; -const BACKTRACE = false; -const DEBUG_LOGS = true; -const INSPECT_ARGS_REGS = true; +const BACKTRACE = true; +const DEBUG_LOGS = false; +const INSPECT_ARGS_REGS = false; const SETTINGS = { singleSentence: true, @@ -80,8 +80,8 @@ let hooksAuxCount = 0; let timer1 = null; let timer3 = null; -const encoder = new TextEncoder("utf-8"); -const decoder = new TextDecoder("utf-8"); +const encoder = new TextEncoder("shift_jis"); +const decoder = new TextDecoder("shift_jis"); const texts1 = new Set(); @@ -153,7 +153,7 @@ const hooksMain = { register: "edx", handler: positionMiddleHandler, }, - Tutorial: { + Popups: { pattern: "83 BC 10 B8 00 00 00 00 75 11 6A 01 68 24 9C 19 01 8D 4D DC", target: targetHooks.MYSTERY, handler: mainHandler, @@ -321,8 +321,9 @@ function mysteryHookStrategy({ address, name, target, handler }) { console.log("onLeave: " + name); - const text = handler.call(this, this.edx) ?? null; // console.log(hexdump(this.edx, { header: false, ansi: false, length: 0x100 })); + + const text = handler.call(this, this.edx) ?? null; setHookCharacterCount(name, text); }, }); @@ -577,6 +578,15 @@ function attachHook(params) { // https://github.com/LR-Research-Team/Datalog/wiki/ZTR const encodingKeys = { + singleByte: { + "00": "{End}", + "01": "{Escape}", + "02": "{Italic}", + "03": "{StraightLine}", + "04": "{Article}", + "05": "{ArticleMany}", + ff: "{FF}", + }, icons: { f0_40: "{Icon Clock}", f0_41: "{Icon Warning}", @@ -838,6 +848,18 @@ const encodingKeys = { // encodingKeys is for human readability, while encodingKeys2 is for actual use // keys as numbers avoids needing to convert hex to strings for object lookups const encodingKeys2 = new Map(); +for (const category in encodingKeys) { + for (const [key, value] of Object.entries(encodingKeys[category])) { + let newKey; + if (key.includes("_")) { + const [hex1, hex2] = key.split("_"); + newKey = parseInt(hex1, 16) + parseInt(hex2, 16); + } else { + newKey = parseInt(key, 16); + } + encodingKeys2.set(newKey, value); + } +} for (const category in encodingKeys) { for (const [key, value] of Object.entries(encodingKeys[category])) { const [hex1, hex2] = key.split("_"); @@ -907,12 +929,17 @@ function readString(address) { s += "\n"; address = address.add(1); } + // Single byte key + else if (byte1 >= 0x01 && byte1 <= 0x05) { + console.warn("Single byte key:", encodingKeys2.get(byte1)); + address = address.add(1); + } // null terminator else if (byte1 === 0x0) { // console.warn("Found null terminator"); break; } else { - c = decoder.decode(address.readByteArray(4))[0]; // utf-8: 1->4 bytes. + c = decoder.decode([byte1, byte2])[0]; s += c; address = address.add(encoder.encode(c).byteLength); } @@ -1017,6 +1044,7 @@ function MenuOptionDescriptionHandler(address) { const eax = this.outerContext.eax; if (eax.equals(menuOptionDescriptionPrevious) || eax.isNull()) { + console.warn("Skipping duplicate or null MenuOptionDescription"); return null; } menuOptionDescriptionPrevious = eax; From e667477114e2b02d9e7c77696c78737978513211 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 4 Nov 2025 00:43:55 -0600 Subject: [PATCH 04/15] Add tutorial hook, improve filter for menu option hook --- PC_Steam_FINAL_FANTASY_XIII.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index d1ab0bb7..69d5a9a3 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -62,8 +62,8 @@ const __e = Process.enumerateModules()[0]; -const BACKTRACE = true; -const DEBUG_LOGS = false; +const BACKTRACE = false; +const DEBUG_LOGS = true; const INSPECT_ARGS_REGS = false; const SETTINGS = { @@ -158,6 +158,11 @@ const hooksMain = { target: targetHooks.MYSTERY, handler: mainHandler, }, + Tutorial: { + pattern: "E8 C0 80 F0 FF", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, DifficultySelection: { pattern: "E8 74 8A ED FF", target: targetHooks.MYSTERY, @@ -321,7 +326,7 @@ function mysteryHookStrategy({ address, name, target, handler }) { console.log("onLeave: " + name); - // console.log(hexdump(this.edx, { header: false, ansi: false, length: 0x100 })); + DEBUG_LOGS && console.log(hexdump(this.edx, { header: false, ansi: false, length: 0x100 })); const text = handler.call(this, this.edx) ?? null; setHookCharacterCount(name, text); @@ -333,7 +338,7 @@ function mysteryHookStrategy({ address, name, target, handler }) { if (isDetached === false) { hook.detach(); Interceptor.flush(); - console.warn("timeout: detached hook for " + name); + DEBUG_LOGS && console.warn("Timeout: detached hook for " + name); } }, 10); }); @@ -1043,14 +1048,19 @@ function MenuOptionDescriptionHandler(address) { /** @type {NativePointer} */ const eax = this.outerContext.eax; - if (eax.equals(menuOptionDescriptionPrevious) || eax.isNull()) { - console.warn("Skipping duplicate or null MenuOptionDescription"); + if (eax.isNull()) { + DEBUG_LOGS && console.warn("Skip null eax"); return null; } - menuOptionDescriptionPrevious = eax; const clue = eax.readShiftJisString(); + if (menuOptionDescriptionPrevious === clue) { + DEBUG_LOGS && console.warn("Skip duplicate clue"); + return null; + } + menuOptionDescriptionPrevious = clue; + if (clue.startsWith("$")) { return positionTopHandler(address); } else { From 314f0098a8cdbcb783d15966771814c3e526e1d8 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:38:28 -0600 Subject: [PATCH 05/15] Refactor, improve menu option filter --- PC_Steam_FINAL_FANTASY_XIII.js | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 69d5a9a3..1eee9043 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -158,7 +158,7 @@ const hooksMain = { target: targetHooks.MYSTERY, handler: mainHandler, }, - Tutorial: { + Popups1: { pattern: "E8 C0 80 F0 FF", target: targetHooks.MYSTERY, handler: mainHandler, @@ -851,7 +851,7 @@ const encodingKeys = { }; // encodingKeys is for human readability, while encodingKeys2 is for actual use -// keys as numbers avoids needing to convert hex to strings for object lookups +// keys as numbers avoids needing to convert hex to strings in readString() const encodingKeys2 = new Map(); for (const category in encodingKeys) { for (const [key, value] of Object.entries(encodingKeys[category])) { @@ -879,8 +879,6 @@ for (const category in encodingKeys) { /** @param {NativePointer} address */ function readString(address) { - let limit = 500; - let count = 0; let byte1 = 0; let byte2 = 0; let s = ""; @@ -890,25 +888,23 @@ function readString(address) { byte1 = address.readU8(); byte2 = address.add(1).readU8(); + // icons if (byte1 >= 0xf0 && byte1 <= 0xf2 && byte2 >= 0x40 && byte2 <= 0x70) { const controlCode = encodingKeys2.get(byte1 + byte2); if (!controlCode) { console.warn("Unknown control code:", byte1, byte2); - c = `[${byte1} ${byte2}]`; - s += c; + s += `[${byte1} ${byte2}]`; } else { s += "▢"; // placeholder } address = address.add(2); } - // colors else if (byte1 === 0xf9 && byte2 >= 0x32 && byte2 <= 0x5b) { const colorCode = encodingKeys2.get(byte1 + byte2); if (!colorCode) { console.warn("Unknown color code:", byte1, byte2); - c = `[${byte1} ${byte2}]`; - s += c; + s += `[${byte1} ${byte2}]`; } address = address.add(2); } @@ -917,8 +913,7 @@ function readString(address) { const specialChar = encodingKeys2.get(byte1 + byte2); if (!specialChar) { console.warn("Unknown special character:", byte1, byte2); - c = `[${byte1} ${byte2}]`; - s += c; + s += `[${byte1} ${byte2}]`; } else { s += specialChar; } @@ -934,7 +929,7 @@ function readString(address) { s += "\n"; address = address.add(1); } - // Single byte key + // single byte key else if (byte1 >= 0x01 && byte1 <= 0x05) { console.warn("Single byte key:", encodingKeys2.get(byte1)); address = address.add(1); @@ -944,12 +939,12 @@ function readString(address) { // console.warn("Found null terminator"); break; } else { - c = decoder.decode([byte1, byte2])[0]; - s += c; + s += decoder.decode([byte1, byte2])[0]; address = address.add(encoder.encode(c).byteLength); } } + // $(t_dpad) -> 方向キー const text = s; DEBUG_LOGS && console.log(`${color.FgYellow}${JSON.stringify(text)}${color.Reset}`); @@ -982,8 +977,8 @@ function orderedHandler() { /** * @param {string} text - * @param {Set} set - * @param {boolean} list + * @param {Set} set Positional text queue + * @param {boolean} list Whether to append text to the list instead of overwriting */ function textSetControl(text, set, list = false) { if (list === false) { @@ -1055,6 +1050,11 @@ function MenuOptionDescriptionHandler(address) { const clue = eax.readShiftJisString(); + if (clue === "") { + DEBUG_LOGS && console.warn("Skip empty clue"); + return null; + } + if (menuOptionDescriptionPrevious === clue) { DEBUG_LOGS && console.warn("Skip duplicate clue"); return null; @@ -1169,15 +1169,15 @@ function inspectArgs(args) { // yeehaw try { type = "P"; - text = args[i].readPointer().readShiftJisString(); + text = readString(args[i].readPointer()); } catch (err) { try { type = "PP"; - text = args[i].readPointer().readPointer().readShiftJisString(); + text = readString(args[i].readPointer().readPointer()); } catch (err) { try { type = "S"; - text = args[i].readShiftJisString(); + text = readString(args[i]); } catch (err) { // type = "A"; // text = args[i].toString(); @@ -1224,7 +1224,7 @@ function inspectRegs(context) { for (const reg of regs) { address = context[reg]; try { - text = address.readShiftJisString(); + text = readString(address); } catch (err) { continue; } From 439f4e0021fb9a04e73c11443df047bfd68c46c0 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Wed, 5 Nov 2025 23:21:00 -0600 Subject: [PATCH 06/15] Fix readString not increasing address --- PC_Steam_FINAL_FANTASY_XIII.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 1eee9043..0269d1e9 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -882,7 +882,6 @@ function readString(address) { let byte1 = 0; let byte2 = 0; let s = ""; - let c = ""; while (true) { byte1 = address.readU8(); @@ -939,7 +938,8 @@ function readString(address) { // console.warn("Found null terminator"); break; } else { - s += decoder.decode([byte1, byte2])[0]; + const c = decoder.decode([byte1, byte2])[0]; + s += c; address = address.add(encoder.encode(c).byteLength); } } From 2929e76a1e558bcde6f7f8622f1ae953a84262e1 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:02:37 -0600 Subject: [PATCH 07/15] Refactor, add another popup hook --- PC_Steam_FINAL_FANTASY_XIII.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 0269d1e9..af27f1c8 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -92,15 +92,6 @@ const deepTexts = new Set(); let previous = ""; -/** @param {NativePointer} address */ -function readString(address) { - const text = address.readMonoString(); - - DEBUG_LOGS && logText(text); - - return text; -} - //#endregion //#region Hooks @@ -163,6 +154,11 @@ const hooksMain = { target: targetHooks.MYSTERY, handler: mainHandler, }, + Popups2: { + pattern: "e8 b0 2b 00 00", + target: targetHooks.MYSTERY, + handler: mainHandler, + }, DifficultySelection: { pattern: "E8 74 8A ED FF", target: targetHooks.MYSTERY, From 8c064cf24fe02d556ec6e00c7147cf7aab207140 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:03:10 -0600 Subject: [PATCH 08/15] Automatically create match patterns for callstack --- PC_Steam_FINAL_FANTASY_XIII.js | 108 +++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 38 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index af27f1c8..9bedbfac 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -63,6 +63,7 @@ const __e = Process.enumerateModules()[0]; const BACKTRACE = false; +const BACKTRACE = true; const DEBUG_LOGS = true; const INSPECT_ARGS_REGS = false; @@ -943,7 +944,7 @@ function readString(address) { // $(t_dpad) -> 方向キー const text = s; - DEBUG_LOGS && console.log(`${color.FgYellow}${JSON.stringify(text)}${color.Reset}`); + DEBUG_LOGS && !BACKTRACE && console.log(`${color.FgYellow}${JSON.stringify(text)}${color.Reset}`); return text; } @@ -1238,6 +1239,57 @@ function inspectRegs(context) { regsTexts.length = 0; } +/** + * @param {NativePointer} relativeOffset + * @returns {String} + */ +function createCallPattern(relativeOffset) { + // 1. Convert the numeric offset to a hex string. + let hexOperand = relativeOffset.toString(16); + + // 2. Pad with leading zeros to ensure it's 8 characters (4 bytes). + // This is crucial for offsets smaller than 0x10000000. + hexOperand = hexOperand.padStart(8, "0"); // e.g., "0027c3ad" + + // 3. Split the hex string into an array of byte pairs. + const bytes = hexOperand.match(/../g); + if (!bytes) { + return "e8 00 00 00 00"; // Return a default or handle error + } + + // 4. Reverse the byte order to create the little-endian pattern. + const littleEndianPattern = bytes.reverse().join(" "); // e.g., "ad c3 27 00" + + // 5. Prepend the 'call' opcode (e8) and a space. + const fullPattern = "e8 " + littleEndianPattern; + + return fullPattern; +} + +/** + * @param {Instruction} ins + * @returns {NativePointer} + */ +function getCallRelativeOffset(ins) { + if (ins.mnemonic !== "call") { + // console.warn(`Instruction ${ins.address} is not a call`); + return NULL; + } + + const operand = ins.operands[0]; + if (operand.type !== "imm") { + // console.warn("Call operand is not immediate"); + return NULL; + } + + const insNext = ins.next; + const functionAddress = ptr(operand.value); + + const relativeOffset = functionAddress.sub(insNext); + + return relativeOffset; +} + /** Prints the backtrace or callstack for a hook. */ function startTrace() { console.warn("Tracing!!"); @@ -1247,41 +1299,7 @@ function startTrace() { const traceAddress = getPatternAddress(traceTarget.name, traceTarget.pattern); traceTarget.address = traceAddress; const previousTexts = new Set(); - - // Interceptor.attach(traceAddress, { - // onEnter(args) { - // let text = ""; - // const context = this.context; - // try { - // text = getTreasureAddress({ - // target: traceTarget, - // args, - // context, - // }).readShiftJisString(); - // } catch (err) { - // // console.error("Reading from address failed:", err.message); - // return null; - // } - - // if (previousTexts.has(text)) { - // return null; - // } - // previousTexts.add(text); - - // const callstack = Thread.backtrace(this.context, Backtracer.ACCURATE); - - // console.log(` - // \rONENTER: ${traceTarget.name} - // \r${text} - // \rCallstack: ${callstack.splice(0, 8)} - // \rReturn: ${this.returnAddress}`); - - // if (INSPECT_ARGS_REGS === true) { - // inspectArgs(args); - // inspectRegs(this.context); - // } - // }, - // }); + const previousAddresses = new Set(); Interceptor.attach(traceAddress, { onEnter(args) { @@ -1307,11 +1325,25 @@ function startTrace() { } previousTexts.add(text); - const callstack = Thread.backtrace(this.context, Backtracer.ACCURATE); + const callstack = Thread.backtrace(this.context, Backtracer.ACCURATE).splice(2, 8); + const result = []; + + for (let i = 0; i < callstack.length; i++) { + const addr = callstack[i]; + const ins = Instruction.parse(addr.sub(0x5)); + const offset = getCallRelativeOffset(ins); + + if (offset.isNull()) { + result.push(` ${i + 1}. ${addr.toString()}`); + } else { + const pattern = createCallPattern(offset); + result.push(` ${i + 1}. ${addr.toString()} - ${pattern}`); + } + } console.log(`ONLEAVE: ${traceTarget.name} \r${text} - \rCallstack: ${callstack.splice(0, 8)}`); + \rCallstack: \n${result.join("\r\n")}\n`); }, }); } From 5e88941dc671f9b3734325acfea16a2e0c324ecf Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:40:27 -0600 Subject: [PATCH 09/15] Fix origin filter strategy, refactor, highlight... ...callstack addresses that are already hooked --- PC_Steam_FINAL_FANTASY_XIII.js | 50 +++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 9bedbfac..7c288675 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -62,7 +62,6 @@ const __e = Process.enumerateModules()[0]; -const BACKTRACE = false; const BACKTRACE = true; const DEBUG_LOGS = true; const INSPECT_ARGS_REGS = false; @@ -101,6 +100,8 @@ const hooksStatus = { // exampleHookName: { enabled: true, characters: 0 }, }; +const returnAddresses = new Set(); + // ASLR disabled /** @type {Object.} */ const targetHooks = { @@ -510,7 +511,8 @@ function setupHooks() { for (const hook in targetHooks) { const name = hook; const pattern = targetHooks[name].pattern; - targetHooks[hook].address = getPatternAddress(name, pattern); + const targetHookAddress = getPatternAddress(name, pattern); + targetHooks[hook].address = targetHookAddress; hooksAuxCount += 1; } @@ -520,7 +522,8 @@ function setupHooks() { if (origins) { for (const origin of origins) { - returnAddresses.add(getPatternAddress(name + "RETURN", origin).toUInt32()); + const returnAddress = getPatternAddress(name + "RETURN", origin); + returnAddresses.add(returnAddress.toUInt32()); hooksAuxCount += 1; } } @@ -944,7 +947,9 @@ function readString(address) { // $(t_dpad) -> 方向キー const text = s; - DEBUG_LOGS && !BACKTRACE && console.log(`${color.FgYellow}${JSON.stringify(text)}${color.Reset}`); + DEBUG_LOGS && + !BACKTRACE && + console.log(`${colors.FgYellow}${JSON.stringify(text)}${colors.Reset}`); return text; } @@ -1080,7 +1085,7 @@ trans.replace((/**@type {string}*/ s) => { //#region Miscellaneous -const color = { +const colors = { Reset: "\x1b[0m", Bright: "\x1b[1m", Dim: "\x1b[2m", @@ -1111,11 +1116,11 @@ const color = { }; function logText(message) { - console.log(`${color.FgYellow}${JSON.stringify(message)}${color.Reset}`); + console.log(`${colors.FgYellow}${JSON.stringify(message)}${colors.Reset}`); } function logDim(message) { - console.log(`${color.Dim}${message}${color.Reset}`); + console.log(`${colors.Dim}${message}${colors.Reset}`); } function validateHooks() { @@ -1192,7 +1197,7 @@ function inspectArgs(args) { } for (const text of argsTexts) { - console.log(`${color.BgMagenta}${text}${color.Reset}`); + console.log(`${colors.BgMagenta}${text}${colors.Reset}`); } argsTexts.length = 0; } @@ -1234,7 +1239,7 @@ function inspectRegs(context) { } for (const text of regsTexts) { - console.log(`${color.BgBlue}${text}${color.Reset}`); + console.log(`${colors.BgBlue}${text}${colors.Reset}`); } regsTexts.length = 0; } @@ -1294,6 +1299,14 @@ function getCallRelativeOffset(ins) { function startTrace() { console.warn("Tracing!!"); + console.warn("Storing hooked addresses..."); + const hookedAddresses = new Set(); + for (const hook in hooks) { + const hookInfo = hooks[hook]; + const address = getPatternAddress(hook, hookInfo.pattern); + hookedAddresses.add(address.toUInt32()); + } + const traceTarget = targetHooks.MYSTERY; const traceAddress = getPatternAddress(traceTarget.name, traceTarget.pattern); @@ -1325,19 +1338,30 @@ function startTrace() { } previousTexts.add(text); + // first two are redundant const callstack = Thread.backtrace(this.context, Backtracer.ACCURATE).splice(2, 8); const result = []; for (let i = 0; i < callstack.length; i++) { - const addr = callstack[i]; - const ins = Instruction.parse(addr.sub(0x5)); + let color = ""; + const returnAddress = callstack[i]; + const callInsAddress = returnAddress.sub(0x5); // not quite right but good enough + + // Highlight if the call instruction is from a hooked function + if (hookedAddresses.has(callInsAddress.toUInt32())) { + color = colors.BgBlue; + } + + const ins = Instruction.parse(callInsAddress); const offset = getCallRelativeOffset(ins); + const returnAddressString = returnAddress.toString(); + const relations = ` ${i + 1}. ${callInsAddress.toString()}`; if (offset.isNull()) { - result.push(` ${i + 1}. ${addr.toString()}`); + result.push(relations); } else { const pattern = createCallPattern(offset); - result.push(` ${i + 1}. ${addr.toString()} - ${pattern}`); + result.push(`${color}${relations} -> ${returnAddressString} - ${pattern}${colors.Reset}`); } } From 27798afe6f0ca3fb1e9fc23101b9ebd0ddb81898 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:24:32 -0600 Subject: [PATCH 10/15] Refactor, add YEEHAW Mode --- PC_Steam_FINAL_FANTASY_XIII.js | 262 +++++++++------------------------ 1 file changed, 67 insertions(+), 195 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 7c288675..5ff88ee4 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -4,6 +4,9 @@ // @author Mansive // @description Steam // * Square Enix +// +// Only supports game languages English and Japanese +// Works with Nova Chrysalia // https://store.steampowered.com/app/292120/FINAL_FANTASY_XIII/ // ==/UserScript== @@ -30,6 +33,7 @@ * @property {NativePointer} address - Mainly used for debugging * @property {string} register * @property {number} argIndex + * @property {Function} [strategy] * @property {TreasureArgsFunction | TreasureContextFunction} getTreasureAddress */ @@ -62,12 +66,13 @@ const __e = Process.enumerateModules()[0]; -const BACKTRACE = true; -const DEBUG_LOGS = true; +const BACKTRACE = false; +const DEBUG_LOGS = false; const INSPECT_ARGS_REGS = false; const SETTINGS = { singleSentence: true, + YEEHAWMode: false, // enableHooksName: true, // enableHooksTips: true, // enableHooksMenuExplanation: true, @@ -105,17 +110,6 @@ const returnAddresses = new Set(); // ASLR disabled /** @type {Object.} */ const targetHooks = { - // ENCY: { - // name: "ENCY", - // pattern: "48 8B D8 48 85 C0 74 53 48 8B D0 48 89 74 24 30 48 8D 4F 08 E8", - // address: ptr(0x140303454), - // register: "rax", - // argIndex: -1, - // /** @type {TreasureContextFunction} */ - // getTreasureAddress({ context }) { - // return context[this.register]; - // }, - // }, MYSTERY: { name: "MYSTERY", // pattern: "E8 0D 01 00 00 8B 45 E8 8B E5", @@ -146,26 +140,26 @@ const hooksMain = { register: "edx", handler: positionMiddleHandler, }, - Popups: { + Popups1: { pattern: "83 BC 10 B8 00 00 00 00 75 11 6A 01 68 24 9C 19 01 8D 4D DC", target: targetHooks.MYSTERY, handler: mainHandler, }, - Popups1: { + Popups2: { pattern: "E8 C0 80 F0 FF", target: targetHooks.MYSTERY, handler: mainHandler, }, - Popups2: { + Popups3: { pattern: "e8 b0 2b 00 00", target: targetHooks.MYSTERY, handler: mainHandler, }, - DifficultySelection: { - pattern: "E8 74 8A ED FF", - target: targetHooks.MYSTERY, - handler: mainHandler, - }, + // DifficultySelection: { + // pattern: "E8 74 8A ED FF", + // target: targetHooks.MYSTERY, + // handler: mainHandler, + // }, LoadingChapter: { pattern: "E8 27 43 EE FF 8B 8D 90 FE FF FF 8B 51 74 52 68 10 8C 19 01", target: targetHooks.MYSTERY, @@ -195,7 +189,7 @@ const hooksMain = { ShopItemDescription: { pattern: "E8 16 D6 F2 FF 8B 45 10 50 0F BF 4D DA", target: targetHooks.MYSTERY, - handler: mainHandler, + handler: positionMiddleHandler, }, MenuOptionDescription: { pattern: "E8 BB ED E9 FF", @@ -207,24 +201,6 @@ const hooksMain = { //#endregion -//#region Hooks: Ency - -const hooksEncyclopedia = { - // EncyclopediaHelpInfo: { - // pattern: "48 89 5C 24 10 57 48 83 EC 20 48 8B F9 8B CA E8 7C 0E F3 FF", // above ency - // origins: [ - // "49 8B 4C 24 28 33 D2 48 8B 01 FF 50 20 49 8B 8C 24 98 00 00 00 41 83 C9 FF 45 33 C0 48 8B 89 20 02 00 00 41 8D 51 02 E8 11 47 1B 00", // open help menu - // "48 8B 7C 24 38 33 C0 48 83 C4 20 5B C3 49 8B 8A 20 02 00 00 41 83 C9 FF 45 33 C0 33 D2 E8 6A 45 1B 00", // switch entry - // "48 8B 7C 24 38 33 C0 48 83 C4 20 5B C3 48 8B CB 89 BB A8 00 00 00 E8 EC F7 FF FF", // flip entry page - // // "39 BB AC 00 00 00 0F 8E A0 00 00 00", // flip menu page - // ], - // target: targetHooks.ENCY, - // handler: encyclopediaHelpInfoHandler, - // }, -}; - -//#endregion - //#region Hooks: All // Combine all sets of hooks into one object for ease of use @@ -252,40 +228,8 @@ function getTreasureAddress({ target, args, context }) { return target.getTreasureAddress({ args, context }); } -/** - * Hooks an address and checks the return addresses before invoking the handler. - * Expects the address to be the function prologue. - * @param {Hook & {name: string} & {address: NativePointer}} - */ -function filterReturnsStrategy({ address, name, register, handler }) { - Breakpoint.add(address, function () { - const returnAddress = this.context.rsp.readPointer(); - // console.warn("filtering: " + returnAddress); - - if (returnAddresses.has(returnAddress.toInt32())) { - DEBUG_LOGS && console.warn("passedFilter: " + name); - - if (hooksStatus[name].enabled === false) { - logDim("skipped: " + name); - return false; - } - - if (INSPECT_ARGS_REGS === true) { - console.log("in: ORIGIN"); - inspectRegs(this.context); - } - - const text = handler.call(this, this.context[register]); - setHookCharacterCount(name, text); - } else { - // console.warn(`Current return address: ${this.returnAddress} - // \rreturnAddresses Set: ${JSON.stringify(returnAddresses)}`); - } - }); -} - // The function is called twice, but we only want the second call. -// Only the first call have EAX and EDX equal to each other, +// Only the first call has EAX and EDX equal to each other, // so we can use that behavior to skip the first call. // The beginning of the function stores the string's memory location in EDX, // and EDX contains the completed string only after the function finishes. @@ -295,15 +239,15 @@ function mysteryHookStrategy({ address, name, target, handler }) { logDim("skipped: " + name); return false; } - console.log("onEnter: " + name); + // console.log("onEnter: " + name); if (INSPECT_ARGS_REGS === true) { console.log("in: ORIGIN"); inspectRegs(this.context); } const outerContext = this.context; - let isDetached = false; + const hook = Interceptor.attach(target.address, { onEnter(args) { if (this.context.eax.equals(this.context.edx)) { @@ -319,11 +263,10 @@ function mysteryHookStrategy({ address, name, target, handler }) { } hook.detach(); - isDetached = true; Interceptor.flush(); + isDetached = true; console.log("onLeave: " + name); - DEBUG_LOGS && console.log(hexdump(this.edx, { header: false, ansi: false, length: 0x100 })); const text = handler.call(this, this.edx) ?? null; @@ -342,100 +285,6 @@ function mysteryHookStrategy({ address, name, target, handler }) { }); } -/** - * Hooks an address as the origin, then temporarily hooks a target address - * whenever the origin is accessed. - * @param {Hook & {name: string} & {address: NativePointer}} - */ -function nestedHooksStrategy({ address, name, target, handler }) { - Breakpoint.add(address, function () { - if (hooksStatus[name].enabled === false) { - logDim("skipped: " + name); - return false; - } - console.log("onEnter: " + name); - if (INSPECT_ARGS_REGS === true) { - console.log("in: ORIGIN"); - inspectRegs(this.context); - } - // this.outerArgs = outerArgs; - hotAttach(target.address, function () { - if (INSPECT_ARGS_REGS === true) { - console.log("in: TARGET"); - inspectRegs(this.context); - } - const text = handler(getTreasureAddress({ target, context: this.context })); - setHookCharacterCount(name, text); - }); - }); -} - -/** - * @param {Hook & {name: string} & {address: NativePointer}} - */ -function nestedHooksOnLeaveStrategy({ address, name, target, handler }) { - Breakpoint.add(address, function () { - const hook = Interceptor.attach(target.address, { - onEnter(args) { - console.log("onEnter: " + name); - this.enterContext = this.context; - }, - onLeave(retval) { - hook.detach(); - Interceptor.flush(); - - console.log("onLeave: " + name); - // const text = handler(getTreasureAddress({ target, context: this.enterContext })); - const text = handler(this.enterContext.edx); - setHookCharacterCount(name, text); - }, - }); - }); -} - -/** - * Combination of {@link nestedHooksStrategy} and {@link filterReturnsStrategy}. - * @param {Hook & {name: string} & {address: NativePointer}} - */ -function filterReturnsNestedHooksStrategy({ address, name, target, handler }) { - Breakpoint.add(address, function () { - const returnAddress = this.context.rsp.readPointer(); - // console.warn("filtering: " + returnAddress); - - if (returnAddresses.has(returnAddress.toInt32())) { - DEBUG_LOGS && console.warn("passedFilter: " + name); - - if (hooksStatus[name].enabled === false) { - logDim("skipped: " + name); - return false; - } - - console.log("onEnter: " + name); - - if (INSPECT_ARGS_REGS === true) { - console.log("in: ORIGIN"); - inspectRegs(this.context); - } - - // const outerContext = this.context; - - hotAttach(target.address, function () { - if (INSPECT_ARGS_REGS === true) { - console.log("in: TARGET"); - inspectRegs(this.context); - } - - // this.outerContext = outerContext; - - const text = handler(getTreasureAddress({ target, context: this.context })); - setHookCharacterCount(name, text); - }); - } else { - // ... - } - }); -} - /** @param {Hook & {name: string} & {address: NativePointer}} */ function normalStrategy({ address, name, register, handler }) { Breakpoint.add(address, function () { @@ -560,13 +409,6 @@ function attachHook(params) { if (target?.strategy) { DEBUG_LOGS && console.log(`[${name}] using custom strategy`); target.strategy(args); - } else if (origins && target) { - DEBUG_LOGS && - console.log(`[${name}] filtered with return addresses and targeting [${target.name}]`); - filterReturnsNestedHooksStrategy(args); - } else if (origins) { - DEBUG_LOGS && console.log(`[${name}] filtered with return addresses`); - filterReturnsStrategy(args); } else if (target) { DEBUG_LOGS && console.log(`[${name}] targeting [${target.name}]`); nestedHooksStrategy(args); @@ -577,6 +419,37 @@ function attachHook(params) { return true; } +// last resort +function activateYeehaw() { + const { name, pattern } = targetHooks.MYSTERY; + const address = getPatternAddress(name, pattern); + + const previousTexts = new Set(); + + Interceptor.attach(address, { + onEnter() { + if (this.context.eax.equals(this.context.edx)) { + this.shouldSkip = true; + return null; + } + this.edx = this.context.edx; + }, + onLeave() { + if (this.shouldSkip) { + return null; + } + const text = readString(this.edx); + + if (previousTexts.has(text)) { + return null; + } + previousTexts.add(text); + + genericHandler(text); + }, + }); +} + //#endregion //#region Handlers @@ -850,7 +723,7 @@ const encodingKeys = { }, }; -// encodingKeys is for human readability, while encodingKeys2 is for actual use +// encodingKeys is for human readability, while encodingKeys2 is for actual use. // keys as numbers avoids needing to convert hex to strings in readString() const encodingKeys2 = new Map(); for (const category in encodingKeys) { @@ -956,6 +829,7 @@ function readString(address) { /** @param {string} text */ function genericHandler(text) { + console.warn(text); texts1.add(text); clearTimeout(timer1); @@ -1071,14 +945,16 @@ function MenuOptionDescriptionHandler(address) { } trans.replace((/**@type {string}*/ s) => { - // if (s === previous || s === "") { - // return null; - // } - // previous = s; + if (s === previous || s === "") { + return null; + } + previous = s; // s = s.replace(/@r/g, "\n"); // 0x40 0x72 + s = s.replace(/\$\([^)]+\)/g, "▢"); // $(t_dpad) -> 方向キー + s = s.trim(); - return s.trim(); + return s; }); //#endregion @@ -1272,7 +1148,7 @@ function createCallPattern(relativeOffset) { } /** - * @param {Instruction} ins + * @param {Instruction} ins Call instruction * @returns {NativePointer} */ function getCallRelativeOffset(ins) { @@ -1312,7 +1188,6 @@ function startTrace() { const traceAddress = getPatternAddress(traceTarget.name, traceTarget.pattern); traceTarget.address = traceAddress; const previousTexts = new Set(); - const previousAddresses = new Set(); Interceptor.attach(traceAddress, { onEnter(args) { @@ -1343,25 +1218,22 @@ function startTrace() { const result = []; for (let i = 0; i < callstack.length; i++) { - let color = ""; const returnAddress = callstack[i]; const callInsAddress = returnAddress.sub(0x5); // not quite right but good enough - // Highlight if the call instruction is from a hooked function - if (hookedAddresses.has(callInsAddress.toUInt32())) { - color = colors.BgBlue; - } - const ins = Instruction.parse(callInsAddress); const offset = getCallRelativeOffset(ins); - const returnAddressString = returnAddress.toString(); const relations = ` ${i + 1}. ${callInsAddress.toString()}`; if (offset.isNull()) { result.push(relations); } else { + // Highlight if the call instruction is from a hooked function + const color = hookedAddresses.has(callInsAddress.toUInt32()) ? colors.BgBlue : ""; const pattern = createCallPattern(offset); - result.push(`${color}${relations} -> ${returnAddressString} - ${pattern}${colors.Reset}`); + result.push( + `${color}${relations} -> ${returnAddress.toString()} - ${pattern}${colors.Reset}` + ); } } @@ -1392,7 +1264,7 @@ function start() { } validateHooks(); - setupHooks(); + SETTINGS.YEEHAWMode ? activateYeehaw() : setupHooks(); // uiStart(); } From e4d64b1a1aba3c1cd3e086e1077af3a11993febf Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:30:28 -0600 Subject: [PATCH 11/15] Add notice about YEEHAW Mode --- PC_Steam_FINAL_FANTASY_XIII.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 5ff88ee4..cd079257 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -1264,7 +1264,15 @@ function start() { } validateHooks(); - SETTINGS.YEEHAWMode ? activateYeehaw() : setupHooks(); + + // SETTINGS.YEEHAWMode ? activateYeehaw() : {setupHooks(); + if (SETTINGS.YEEHAWMode === true) { + activateYeehaw(); + } else { + setupHooks(); + console.warn("Enable YEEHAWMode in the script for a hook that gets more text.\n"); + } + // uiStart(); } From 516d876da188fe0c74feac71860dcfcf842a4fe7 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:36:01 -0600 Subject: [PATCH 12/15] Refactor --- PC_Steam_FINAL_FANTASY_XIII.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index cd079257..ee61e9dd 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -71,7 +71,7 @@ const DEBUG_LOGS = false; const INSPECT_ARGS_REGS = false; const SETTINGS = { - singleSentence: true, + // singleSentence: true, YEEHAWMode: false, // enableHooksName: true, // enableHooksTips: true, @@ -728,7 +728,7 @@ const encodingKeys = { const encodingKeys2 = new Map(); for (const category in encodingKeys) { for (const [key, value] of Object.entries(encodingKeys[category])) { - let newKey; + let newKey = -1; if (key.includes("_")) { const [hex1, hex2] = key.split("_"); newKey = parseInt(hex1, 16) + parseInt(hex2, 16); @@ -738,13 +738,6 @@ for (const category in encodingKeys) { encodingKeys2.set(newKey, value); } } -for (const category in encodingKeys) { - for (const [key, value] of Object.entries(encodingKeys[category])) { - const [hex1, hex2] = key.split("_"); - const newKey = parseInt(hex1, 16) + parseInt(hex2, 16); - encodingKeys2.set(newKey, value); - } -} // ev_comn_xxx -> cutscene dialogue with high-quality character models // ev_hang -> cutscene dialogue without high-quality character models From 6357c15b1e40ae5f4ddd620efdf5ed4028c31ae7 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:43:58 -0600 Subject: [PATCH 13/15] Fix typo --- PC_Steam_FINAL_FANTASY_XIII.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index ee61e9dd..4ef70acc 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -195,7 +195,7 @@ const hooksMain = { pattern: "E8 BB ED E9 FF", // pattern: "E8 6C 6F 0A 00", target: targetHooks.MYSTERY, - handler: MenuOptionDescriptionHandler, + handler: menuOptionDescriptionHandler, }, }; @@ -908,7 +908,7 @@ function mainHandler(address) { let menuOptionDescriptionPrevious = NULL; /** @type {HookHandler} */ -function MenuOptionDescriptionHandler(address) { +function menuOptionDescriptionHandler(address) { /** @type {NativePointer} */ const eax = this.outerContext.eax; From 3cd4b45e41ce6af18cd3386931bfe2799c6be103 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:26:00 -0600 Subject: [PATCH 14/15] Refactor --- PC_Steam_FINAL_FANTASY_XIII.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 4ef70acc..50f9c838 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -96,6 +96,7 @@ const bottomTexts = new Set(); const deepTexts = new Set(); let previous = ""; +let menuOptionDescriptionPrevious = NULL; //#endregion @@ -906,7 +907,6 @@ function mainHandler(address) { return text; } -let menuOptionDescriptionPrevious = NULL; /** @type {HookHandler} */ function menuOptionDescriptionHandler(address) { /** @type {NativePointer} */ From 2ad270caf8ac70afa2b456edb134832c76c5bc51 Mon Sep 17 00:00:00 2001 From: Mansive <33560917+Mansive@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:30:30 -0600 Subject: [PATCH 15/15] Fix typo --- PC_Steam_FINAL_FANTASY_XIII.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PC_Steam_FINAL_FANTASY_XIII.js b/PC_Steam_FINAL_FANTASY_XIII.js index 50f9c838..5c300ab9 100644 --- a/PC_Steam_FINAL_FANTASY_XIII.js +++ b/PC_Steam_FINAL_FANTASY_XIII.js @@ -337,7 +337,7 @@ function getPatternAddress(name, pattern) { try { results = Memory.scanSync(__e.base, __e.size, pattern); } catch (err) { - throw new Error(`Error ocurred with [${name}]: ${err.message}`, { + throw new Error(`Error occurred with [${name}]: ${err.message}`, { cause: err, }); }