#!/usr/bin/env python3 """Apply the App Launcher patch (v2.1) to the stock hiby_player (v1.4). Replaces the Qobuz streaming entry with a "MeowAPPs" app list page: 1. checks the TF card (/usr/data/mnt/sd_0 via /proc/mounts); if missing shows "未检测到存储卡" (no hang) 2. scans /usr/data/mnt/sd_0/MeowAPPs/*/app.json; if absent/empty shows "无可用应用" + directory example (no hang); malformed app.json is skipped (no crash) 3. shows app names (UTF-8 -> UTF-16 runtime conversion) with per-row click callbacks 4. on row click: access(X_OK) check -> "无法启动该应用" hint on failure (no hang); else fork(4002)+execve(4011), parent blocks in waitpid(4007) = "让位"; on app exit the UI resumes Keeps the v2.0 fixes: "viewgroup" type @ 0x7ee7a0 (not "imageview"), 0x46c800(parent, view), text setter vtable[0x198], child enumeration. Usage: python3 apply_patch_v2.py [stock] [output] """ import sys, struct STOCK = sys.argv[1] if len(sys.argv) > 1 else "hiby_player_stock" OUT = sys.argv[2] if len(sys.argv) > 2 else "hiby_player_patched_v2" # ---------------------------------------------------------------- encoders def enc_lw(rt, off, rs): return (0x23 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) def enc_sw(rt, off, rs): return (0x2B << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) def enc_sb(rt, off, rs): return (0x28 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) def enc_sh(rt, off, rs): return (0x29 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) def enc_lbu(rt, off, rs): return (0x24 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) def enc_slt(rd, rs, rt): return 0x0000002A | (rs << 21) | (rt << 16) | (rd << 11) def enc_slti(rt, rs, imm): return (0x0A << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) def enc_sltiu(rt, rs, imm): return (0x0B << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) def enc_addiu(rt, rs, imm): return (0x09 << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) def enc_andi(rt, rs, imm): return (0x0C << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) def enc_lui(rt, imm): return (0x0F << 26) | (rt << 16) | (imm & 0xFFFF) def enc_beq(rs, rt, off): return (0x04 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) def enc_bne(rs, rt, off): return (0x05 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) def enc_blez(rs, off): return (0x06 << 26) | (rs << 21) | (off & 0xFFFF) def enc_j(target): return (0x02 << 26) | ((target >> 2) & 0x03FFFFFF) def enc_jal(target): return (0x03 << 26) | ((target >> 2) & 0x03FFFFFF) def enc_jr(rs): return 0x00000008 | (rs << 21) def enc_jalr(rd, rs): return 0x00000009 | (rs << 21) | (rd << 11) def enc_move(rd, rs): return 0x00000021 | (rs << 21) | (rd << 11) def enc_addu(rd, rs, rt): return 0x00000021 | (rs << 21) | (rt << 16) | (rd << 11) def enc_or(rd, rs, rt): return 0x00000025 | (rs << 21) | (rt << 16) | (rd << 11) def enc_sll(rd, rt, sa): return 0x00000000 | (rt << 16) | (rd << 11) | (sa << 6) NOP = 0x00000000 SYSCALL = 0x0000000C # ---------------------------------------------------------------- globals # .data zero region 0x9636bc (verified zeros in stock; see ANALYSIS.md §17) # 全局区迁移(2026-08-28): 0x9636bc 被 hiby_player 运行时复用(实测 g_page 被写 1) # -> 0x9060e4 (.data 大零区, 真机 gdb 实测启动后 60s 全零未被写) G_BASE_HI = 0x90 # lui 高 16 位 G_BASE_LO = 0x60E4 # addiu 低 16 位(正偏移) G_STATE = 0x0000 # 0=ok 1=no card 2=no apps G_PAGE = 0x0004 G_QOBUZ = 0x0008 G_COUNT = 0x000C G_ROWS = 0x0010 # 8x u32 G_NAMES = 0x0030 # 8x16B G_EXECS = 0x00B0 # 8x48B G_SCAN = 0x0230 # 256B G_UTF16 = 0x0330 # 8x48B G_HINT = 0x04B0 # 48B utf16 G_NOTE = 0x04E0 # 48B utf16 G_PENDING = 0x0510 # autotest pending flag G_TRACE = 0x0514 # loop_hook 执行计数器(调试) G_SIGREG = 0x0518 # SIGUSR1 handler 已注册标志 G_PARENT = 0x051C # entry 回调原始 parent(0x46c800/0x46c600 需要) PLT = dict(fread=0x8E6180, access=0x8E6240, strstr=0x8E6280, fopen=0x8E62E0, open=0x8E6120, read=0x8E6F00, signal=0x8E6970, fgets=0x8E6440, strlen=0x8E6510, readdir=0x8E65B0, strcmp=0x8E6650, fclose=0x8E6760, opendir=0x8E6C00, closedir=0x8E6C40, strcpy=0x8E70B0) CREATE_VIEW = 0x49BE00 ACTIVATE_VIEW = 0x46C800 SET_CALLBACK = 0x46C2C0 CAVE = 0x41B8B0 STR_BASE = CAVE + 0x1000 STR_VIEWGROUP = 0x7EE7A0 # ---------------------------------------------------------------- assembler class Asm: def __init__(self, base): self.base = base self.words = [] self.patches = [] self.syms = {} def here(self): return self.base + 4 * len(self.words) def label(self, name): self.syms[name] = self.here() def emit(self, word): self.words.append((self.here(), word)) return self.here() - 4 def raw(self, words): for w in words: self.emit(w) def b(self, rs, rt, label, eq=True): a = self.emit(None) self.patches.append((a, "beq" if eq else "bne", rs, rt, label)) def blez(self, rs, label): a = self.emit(None) self.patches.append((a, "blez", rs, 0, label)) def j(self, label): a = self.emit(None) self.patches.append((a, "j", 0, 0, label)) def jal(self, label_or_abs): if isinstance(label_or_abs, int): self.emit(enc_jal(label_or_abs)) else: a = self.emit(None) self.patches.append((a, "jal", 0, 0, label_or_abs)) def load_cstr(self, rt, label): a1 = self.emit(None) a2 = self.emit(None) self.patches.append((a1, "lui", rt, rt, label)) self.patches.append((a2, "addiu", rt, rt, label)) def load_abs(self, rt, addr): hi, lo = (addr >> 16) & 0xFFFF, addr & 0xFFFF if lo & 0x8000: self.raw([enc_lui(rt, (hi + 1) & 0xFFFF), enc_addiu(rt, rt, lo - 0x10000)]) else: self.raw([enc_lui(rt, hi), enc_addiu(rt, rt, lo)]) def gp(self): self.raw([enc_lui(28, 0x97), enc_addiu(28, 28, -0x3BC0)]) def load_g(self, rt, off): self.raw([enc_lui(rt, G_BASE_HI), enc_lw(rt, G_BASE_LO + off, rt)]) def addr_g(self, rt, off): self.raw([enc_lui(rt, G_BASE_HI), enc_addiu(rt, rt, G_BASE_LO + off)]) def store_g(self, rs, off): self.raw([enc_lui(8, G_BASE_HI), enc_sw(rs, G_BASE_LO + off, 8)]) def li(self, rt, imm): if 0 <= imm <= 0x7FFF: self.raw([enc_addiu(rt, 0, imm)]) else: self.raw([enc_lui(rt, (imm >> 16) & 0xFFFF), enc_addiu(rt, rt, imm & 0xFFFF)]) def resolve(self): for a, kind, rs, rt, arg in self.patches: t = self.syms[arg] if isinstance(arg, str) else arg if kind in ("beq", "bne"): off = (t - (a + 4)) // 4 assert -0x8000 <= off < 0x8000, f"{kind}@{a:#x}->{t:#x} off {off}" w = enc_beq(rs, rt, off) if kind == "beq" else enc_bne(rs, rt, off) elif kind == "blez": off = (t - (a + 4)) // 4 assert -0x8000 <= off < 0x8000 w = enc_blez(rs, off) elif kind == "j": w = enc_j(t) elif kind == "jal": w = enc_jal(t) elif kind == "lui": w = enc_lui(rt, (t >> 16) & 0xFFFF) elif kind == "addiu": lo = t & 0xFFFF if lo & 0x8000: lo -= 0x10000 for j in range(len(self.words)): va, ww = self.words[j] if va == a - 4 and ww is not None and (ww >> 26) == 0x0F \ and ((ww >> 16) & 0x1F) == rt: self.words[j] = (va, enc_lui(rt, ((t >> 16) + 1) & 0xFFFF)) break else: for j in range(len(self.words)): va, ww = self.words[j] if va == a - 4 and ww is not None and (ww >> 26) == 0x0F \ and ((ww >> 16) & 0x1F) == rt: self.words[j] = (va, enc_lui(rt, (t >> 16) & 0xFFFF)) break w = enc_addiu(rt, rt, lo & 0xFFFF) self.words[self.words.index((a, None))] = (a, w) for a, w in self.words: assert w is not None, f"unresolved @{a:#x}" def blob(self): self.resolve() out = bytearray() for a, w in self.words: out.extend(struct.pack("=7(扩展事件)才返回; type<7 继续 A.raw([NOP]) A.raw([enc_addiu(3, 0, 4)]) A.b(2, 3, "entry_ret", eq=False) A.raw([NOP]) # parent fallback: if $a0==0 use layout ctx (*(0x96d124)) A.b(4, 0, "entry_have_parent", eq=False) A.raw([NOP]) A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) A.label("entry_have_parent") A.store_g(4, G_PARENT) # g_parent = effective parent # view guard: if $a1==0 skip mark/g_qobuz A.b(5, 0, "entry_noview", eq=True) A.raw([NOP]) A.raw([enc_addiu(2, 0, 1), enc_sb(2, 0xC1, 5)]) # mark selected A.addr_g(8, G_QOBUZ) A.raw([enc_sw(5, 0, 8)]) # g_qobuz = view A.label("entry_noview") A.load_g(8, G_PAGE) A.b(8, 0, "entry_activate", eq=False) A.raw([NOP]) # ---- build page ---- A.raw([enc_addiu(29, 29, -0x20)]) A.raw([enc_sw(31, 0x1C, 29), enc_sw(4, 0x18, 29), enc_sw(16, 0x14, 29), enc_sw(17, 0x10, 29)]) # card check A.jal("check_card") A.raw([NOP]) A.store_g(2, G_STATE) # state = 0/1 # scan MeowAPPs A.jal("scan") A.raw([NOP]) # if count==0 and card present -> state=2 A.load_g(8, G_COUNT) A.b(8, 0, "entry_have", eq=False) A.raw([NOP]) A.load_g(8, G_STATE) A.b(8, 0, "entry_have", eq=False) # card missing -> keep state=1 A.raw([NOP]) A.li(8, 2) A.store_g(8, G_STATE) A.label("entry_have") # create page: 0x49be00(parent, "hiby_apps_main", "viewgroup") A.load_abs(6, STR_VIEWGROUP) A.load_cstr(5, "s_apps_main") A.raw([enc_lw(4, 0x18, 29)]) A.jal(CREATE_VIEW) A.raw([NOP]) A.gp() A.b(2, 0, "entry_skip_setup", eq=True) A.raw([NOP]) A.addr_g(8, G_PAGE) A.raw([enc_sw(2, 0, 8)]) # g_page = view A.raw([enc_move(4, 2)]) A.jal("setup") A.raw([NOP]) # 两条路径(建页成功/失败)在此汇聚恢复栈帧, 避免 v0==0 时 $ra 指向 # CREATE_VIEW 返回地址跳回 entry 中间(潜在崩溃) A.label("entry_skip_setup") A.raw([enc_lw(17, 0x10, 29), enc_lw(16, 0x14, 29), enc_lw(31, 0x1C, 29)]) A.raw([enc_addiu(29, 29, 0x20)]) A.label("entry_activate") A.load_g(4, G_PAGE) A.b(4, 0, "entry_ret", eq=True) A.raw([NOP]) # 0x46c800(a0=parent, a1=view): 沿 a1 的 0xf8 链找 layout_main 并显示 a1 # (0x46c800 反汇编实证: s0=s2=a1 起链; 原版回调 0x4f0120 参数原样传递) A.load_g(5, G_PAGE) # a1 = page (激活应用页) A.load_g(4, G_PARENT) # a0 = parent A.jal(ACTIVATE_VIEW) A.raw([NOP]) A.label("entry_ret") A.gp() A.raw([enc_jr(31), NOP]) # ---------------- check_card() -> $v0=1 if sd_0 mounted ---------------- A.label("check_card") A.gp() A.raw([enc_addiu(29, 29, -0x20)]) A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) A.load_cstr(4, "s_mounts") A.load_cstr(5, "s_r") A.jal(PLT["fopen"]) A.raw([NOP]) A.gp() A.b(2, 0, "cc_nofile", eq=True) A.raw([NOP]) A.raw([enc_move(16, 2)]) # s0 = FILE* A.raw([enc_addiu(17, 0, 0)]) # s1 = found = 0 A.label("cc_loop") A.addr_g(4, G_SCAN) # a0 = buf (fgets(buf, size, FILE)) A.raw([enc_addiu(5, 0, 128)]) # a1 = size A.raw([enc_move(6, 16)]) # a2 = FILE* (原顺序错误: a0=FILE,a1=buf,a2=size) A.jal(PLT["fgets"]) A.raw([NOP]) A.gp() A.b(2, 0, "cc_done", eq=True) A.raw([NOP]) A.addr_g(4, G_SCAN) A.load_cstr(5, "s_sd0") A.jal(PLT["strstr"]) A.raw([NOP]) A.gp() A.b(2, 0, "cc_found", eq=False) A.raw([NOP]) A.j("cc_loop") A.raw([NOP]) A.label("cc_found") A.raw([enc_addiu(17, 0, 1)]) A.label("cc_done") A.raw([enc_move(4, 16)]) A.jal(PLT["fclose"]) A.raw([NOP]) A.gp() A.raw([enc_move(2, 17)]) A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) A.label("cc_nofile") A.raw([enc_addiu(2, 0, 0), enc_jr(31), NOP]) # ---------------- setup($a0=page): rows + hints + back ---------------- A.label("setup") A.gp() A.raw([enc_addiu(29, 29, -0x20)]) A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29), enc_sw(18, 0x10, 29)]) A.raw([enc_move(16, 4)]) A.raw([enc_addiu(17, 0, 0)]) # i A.label("setup_loop") A.load_g(2, G_COUNT) A.raw([enc_slt(3, 17, 2)]) A.b(3, 0, "setup_hints", eq=True) A.raw([NOP]) A.load_cstr(4, "s_tv_prefix") A.addr_g(5, G_SCAN) A.jal(PLT["strcpy"]) A.raw([NOP]) A.gp() A.addr_g(4, G_SCAN) A.jal(PLT["strlen"]) A.raw([NOP]) A.addr_g(8, G_SCAN) A.raw([enc_addu(8, 8, 2)]) A.raw([enc_addiu(2, 17, 0x30), enc_sb(2, 0, 8), enc_sb(0, 1, 8)]) A.raw([enc_move(5, 8)]) A.raw([enc_move(4, 16)]) A.jal("find") A.raw([NOP]) A.b(2, 0, "setup_next", eq=True) A.raw([NOP]) A.addr_g(8, G_ROWS) A.raw([enc_sll(9, 17, 2), enc_addu(8, 8, 9), enc_sw(2, 0, 8)]) A.raw([enc_move(18, 2)]) A.addr_g(5, G_NAMES) A.raw([enc_sll(9, 17, 4), enc_addu(5, 5, 9)]) A.addr_g(6, G_UTF16) A.raw([enc_sll(9, 17, 5), enc_sll(10, 17, 4), enc_addu(6, 6, 9)]) A.raw([enc_move(4, 18)]) A.jal("settext") A.raw([NOP]) A.raw([enc_move(4, 18)]) A.load_cstr(5, "rowcb") A.jal(SET_CALLBACK) A.raw([NOP]) A.label("setup_next") A.raw([enc_addiu(17, 17, 1)]) A.j("setup_loop") A.raw([NOP]) # hints by state A.label("setup_hints") A.load_g(8, G_STATE) A.raw([enc_addiu(9, 0, 1)]) A.b(8, 9, "setup_nocard", eq=True) A.raw([NOP]) A.raw([enc_addiu(9, 0, 2)]) A.b(8, 9, "setup_noapp", eq=True) A.raw([NOP]) # state 0 A.load_cstr(4, "s_t_hint_ok") A.jal("set_hint") A.raw([NOP]) A.j("setup_backrow") A.raw([NOP]) A.label("setup_nocard") A.load_cstr(4, "s_t_no_card") A.jal("set_hint") A.raw([NOP]) A.j("setup_backrow") A.raw([NOP]) A.label("setup_noapp") A.load_cstr(4, "s_t_no_app") A.jal("set_hint") A.raw([NOP]) A.load_cstr(4, "s_t_example") A.jal("set_note") A.raw([NOP]) # back row A.label("setup_backrow") A.raw([enc_move(4, 16)]) A.load_cstr(5, "s_tv_back") A.jal("find") A.raw([NOP]) A.b(2, 0, "setup_skipback", eq=True) A.raw([NOP]) A.raw([enc_move(4, 2)]) A.load_cstr(5, "backcb") A.jal(SET_CALLBACK) A.raw([NOP]) A.label("setup_skipback") A.raw([enc_lw(18, 0x10, 29), enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29)]) A.raw([enc_lw(31, 0x1C, 29), enc_addiu(29, 29, 0x20)]) A.raw([enc_jr(31), NOP]) # ---------------- set_hint($a0=utf8) ---------------- A.label("set_hint") A.raw([enc_addiu(29, 29, -0x20)]) A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) A.raw([enc_move(17, 4)]) A.load_g(4, G_PAGE) A.b(4, 0, "hint_out", eq=True) A.raw([NOP]) A.load_cstr(5, "s_tv_hint") A.jal("find") A.raw([NOP]) A.b(2, 0, "hint_out", eq=True) A.raw([NOP]) A.raw([enc_move(4, 2), enc_move(5, 17)]) A.addr_g(6, G_HINT) A.jal("settext") A.raw([NOP]) A.label("hint_out") A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) # ---------------- set_note($a0=utf8) ---------------- A.label("set_note") A.raw([enc_addiu(29, 29, -0x20)]) A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) A.raw([enc_move(17, 4)]) A.load_g(4, G_PAGE) A.b(4, 0, "note_out", eq=True) A.raw([NOP]) A.load_cstr(5, "s_tv_note") A.jal("find") A.raw([NOP]) A.b(2, 0, "note_out", eq=True) A.raw([NOP]) A.raw([enc_move(4, 2), enc_move(5, 17)]) A.addr_g(6, G_NOTE) A.jal("settext") A.raw([NOP]) A.label("note_out") A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) # ---------------- settext($a0=row, $a1=utf8, $a2=buf) ---------------- A.label("settext") A.raw([enc_addiu(29, 29, -0x20)]) A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29), enc_sw(18, 0x10, 29)]) A.raw([enc_move(16, 4), enc_move(17, 5), enc_move(18, 6)]) A.raw([enc_move(4, 5), enc_move(5, 6)]) A.jal("u2u") A.raw([NOP]) A.raw([enc_lw(4, 0x48, 16)]) A.b(4, 0, "settext_out", eq=True) A.raw([NOP]) A.raw([enc_lw(9, 0x198, 4)]) A.raw([enc_move(5, 18)]) A.raw([enc_jalr(31, 9), NOP]) A.label("settext_out") A.raw([enc_lw(18, 0x10, 29), enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29)]) A.raw([enc_lw(31, 0x1C, 29), enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) # ---------------- u2u($a0=utf8, $a1=utf16buf): utf8->utf16le ---------------- A.label("u2u") A.label("u2u_loop") A.raw([enc_lbu(9, 0, 4)]) A.b(9, 0, "u2u_end", eq=True) A.raw([NOP]) A.raw([enc_andi(10, 9, 0x80)]) A.b(10, 0, "u2u_ascii", eq=True) A.raw([NOP]) A.raw([enc_andi(10, 9, 0xE0)]) A.raw([enc_addiu(11, 0, 0xC0)]) A.b(10, 11, "u2u_two", eq=True) A.raw([NOP]) # 3-byte A.raw([enc_lbu(11, 1, 4), enc_lbu(12, 2, 4)]) A.raw([enc_andi(10, 9, 0x0F), enc_sll(10, 10, 12)]) A.raw([enc_andi(13, 11, 0x3F), enc_sll(13, 13, 6)]) A.raw([enc_or(10, 10, 13)]) A.raw([enc_andi(13, 12, 0x3F), enc_or(10, 10, 13)]) A.raw([enc_sh(10, 0, 5)]) A.raw([enc_addiu(4, 4, 3), enc_addiu(5, 5, 2)]) A.j("u2u_loop") A.raw([NOP]) A.label("u2u_two") A.raw([enc_lbu(11, 1, 4)]) A.raw([enc_andi(10, 9, 0x1F), enc_sll(10, 10, 6)]) A.raw([enc_andi(13, 11, 0x3F), enc_or(10, 10, 13)]) A.raw([enc_sh(10, 0, 5)]) A.raw([enc_addiu(4, 4, 2), enc_addiu(5, 5, 2)]) A.j("u2u_loop") A.raw([NOP]) A.label("u2u_ascii") A.raw([enc_sh(9, 0, 5)]) A.raw([enc_addiu(4, 4, 1), enc_addiu(5, 5, 2)]) A.j("u2u_loop") A.raw([NOP]) A.label("u2u_end") A.raw([enc_sh(0, 0, 5), enc_jr(31), NOP]) # ---------------- find($a0=parent, $a1=name) -> $v0 ---------------- A.label("find") A.raw([enc_lw(8, 0xFC, 4)]) A.b(8, 0, "find_nf", eq=True) A.raw([NOP]) A.raw([enc_move(13, 5)]) A.raw([enc_lw(9, 0x30, 8)]) A.raw([enc_jalr(31, 9), enc_move(4, 8)]) A.blez(2, "find_nf") A.raw([enc_move(12, 2)]) A.raw([enc_addiu(10, 0, 0)]) A.label("find_loop") A.raw([enc_lw(9, 0x2C, 8)]) A.raw([enc_move(5, 10)]) A.raw([enc_jalr(31, 9), enc_move(4, 8)]) A.b(2, 0, "find_next", eq=True) A.raw([NOP]) A.raw([enc_move(11, 2)]) A.raw([enc_addiu(4, 11, 0x20)]) A.raw([enc_move(5, 13)]) A.jal(PLT["strcmp"]) A.raw([NOP]) A.gp() A.b(2, 0, "find_found", eq=True) A.raw([NOP]) A.label("find_next") A.raw([enc_addiu(10, 10, 1)]) A.raw([enc_slt(3, 10, 12)]) A.b(3, 0, "find_loop", eq=False) A.raw([NOP]) A.label("find_nf") A.raw([enc_addiu(2, 0, 0), enc_jr(31), NOP]) A.label("find_found") A.raw([enc_move(2, 11), enc_jr(31), NOP]) # ---------------- rowcb(parent, view, &type) ---------------- A.label("rowcb") A.gp() A.raw([enc_lw(2, 0, 6), enc_slti(3, 2, 7)]) A.b(3, 0, "rowcb_ret", eq=True) # type>=7 返回 A.raw([NOP]) A.raw([enc_addiu(3, 0, 4)]) A.b(2, 3, "rowcb_ret", eq=False) A.raw([NOP]) A.addr_g(8, G_ROWS) A.raw([enc_addiu(9, 0, 0)]) A.label("rowcb_loop") A.raw([enc_sltiu(3, 9, 8)]) A.b(3, 0, "rowcb_ret", eq=True) A.raw([NOP]) A.raw([enc_lw(2, 0, 8)]) A.b(2, 5, "rowcb_launch", eq=True) A.raw([NOP]) A.raw([enc_addiu(8, 8, 4), enc_addiu(9, 9, 1)]) A.j("rowcb_loop") A.raw([NOP]) A.label("rowcb_launch") A.raw([enc_move(4, 9)]) A.jal("launch") A.raw([NOP]) A.label("rowcb_ret") A.gp() A.raw([enc_jr(31), NOP]) # ---------------- backcb(parent, view, &type) ---------------- # 返回流媒体页: 0x46c800(parent, qobuz 入口 view) — 沿 qobuz 链显示流媒体页 A.label("backcb") A.gp() A.raw([enc_lw(2, 0, 6), enc_slti(3, 2, 7)]) A.b(3, 0, "backcb_ret", eq=True) # type>=7 返回 A.raw([NOP]) A.raw([enc_addiu(3, 0, 4)]) A.b(2, 3, "backcb_ret", eq=False) A.raw([NOP]) A.load_g(5, G_QOBUZ) A.b(5, 0, "backcb_ret", eq=True) A.raw([NOP]) A.load_g(4, G_PARENT) A.jal(ACTIVATE_VIEW) A.raw([NOP]) A.label("backcb_ret") A.gp() A.raw([enc_jr(31), NOP]) # ---------------- launch($a0=index) ---------------- # frame: ra@0x24 s0@app_pid@0x20 s1@wd_pid@0x1c appargv@0x08,0x0c # wdargv@0x10,0x14,0x18 status@0x04 A.label("launch") A.gp() A.raw([enc_addiu(29, 29, -0x28)]) A.raw([enc_sw(31, 0x24, 29), enc_sw(16, 0x20, 29), enc_sw(17, 0x1C, 29)]) A.addr_g(8, G_EXECS) A.raw([enc_sll(9, 4, 5), enc_sll(10, 4, 4), enc_addu(9, 9, 10)]) A.raw([enc_addu(8, 8, 9)]) # access(path, X_OK=1) A.raw([enc_move(4, 8)]) A.raw([enc_addiu(5, 0, 1)]) A.jal(PLT["access"]) A.raw([NOP]) A.gp() A.b(2, 0, "launch_fail", eq=False) A.raw([NOP]) # fork app A.raw([enc_addiu(2, 0, 4002), SYSCALL]) A.b(2, 0, "launch_app_parent", eq=False) A.raw([NOP]) # app child: execve(path, {path,0}, 0) A.raw([enc_sw(8, 0x08, 29), enc_sw(0, 0x0C, 29)]) A.raw([enc_move(4, 8)]) A.raw([enc_addiu(5, 29, 0x08)]) A.raw([enc_addiu(6, 0, 0)]) A.raw([enc_addiu(2, 0, 4011), SYSCALL]) A.raw([enc_addiu(2, 0, 4001), enc_addiu(4, 0, 1), SYSCALL]) A.label("launch_app_parent") A.raw([enc_move(16, 2)]) # s0 = app_pid # itoa(app_pid) -> g_scan A.addr_g(5, G_SCAN) A.raw([enc_move(4, 16)]) A.jal("itoa") A.raw([NOP]) # fork watchdog A.raw([enc_addiu(2, 0, 4002), SYSCALL]) A.b(2, 0, "launch_wd_parent", eq=False) A.raw([NOP]) # wd child: execve("/usr/data/meow_watchdog", {wd, g_scan, 0}, 0) A.load_cstr(4, "s_wd") A.raw([enc_sw(4, 0x10, 29)]) A.addr_g(8, G_SCAN) A.raw([enc_sw(8, 0x14, 29), enc_sw(0, 0x18, 29)]) A.raw([enc_addiu(5, 29, 0x10)]) A.raw([enc_addiu(6, 0, 0)]) A.raw([enc_addiu(2, 0, 4011), SYSCALL]) A.raw([enc_addiu(2, 0, 4001), enc_addiu(4, 0, 1), SYSCALL]) A.label("launch_wd_parent") A.raw([enc_move(17, 2)]) # s1 = wd_pid # waitpid(app_pid, &status, 0) — blocks = 让位 A.raw([enc_move(4, 16)]) A.raw([enc_addiu(5, 29, 0x04)]) A.raw([enc_addiu(6, 0, 0)]) A.raw([enc_addiu(2, 0, 4007), SYSCALL]) # kill(wd_pid, SIGTERM=15) A.raw([enc_move(4, 17)]) A.raw([enc_addiu(5, 0, 15)]) A.raw([enc_addiu(2, 0, 4037), SYSCALL]) A.raw([enc_addiu(2, 0, 0)]) # v0 = 0 (ok) A.raw([enc_lw(17, 0x1C, 29), enc_lw(16, 0x20, 29), enc_lw(31, 0x24, 29)]) A.raw([enc_addiu(29, 29, 0x28), enc_jr(31), NOP]) A.label("launch_fail") # hint "无法启动该应用" A.raw([enc_sw(31, 0x24, 29)]) A.load_cstr(4, "s_t_fail") A.jal("set_hint") A.raw([NOP]) A.raw([enc_addiu(2, 0, 1)]) # v0 = 1 (failed) A.raw([enc_lw(31, 0x24, 29), enc_addiu(29, 29, 0x28), enc_jr(31), NOP]) # ---------------- itoa($a0=value, $a1=buf) ---------------- # t0=value t1=end t2=tmp t3=quot t4=rem; writes digits reversed then copies A.label("itoa") A.raw([enc_move(8, 4)]) # t0 = value A.raw([enc_addiu(9, 5, 12)]) # t1 = buf+12 (temp tail) A.raw([enc_sb(0, 1, 9)]) # temp[13] = 0 terminator A.label("itoa_loop") A.raw([enc_addiu(10, 0, 10)]) A.raw([0x0000001B | (8 << 21) | (10 << 16)]) # divu $t0, $t2 A.raw([NOP]) A.raw([0x00000012 | (11 << 11)]) # mflo $t3 (商) A.raw([0x00000010 | (12 << 11)]) # mfhi $t4 (余) A.raw([enc_addiu(12, 12, 0x30)]) # t4 = rem + '0' A.raw([enc_sb(12, 0, 9)]) A.raw([enc_addiu(9, 9, -1)]) A.raw([enc_move(8, 11)]) # t0 = quotient A.b(11, 0, "itoa_loop", eq=False) # quotient!=0 -> loop A.raw([NOP]) A.label("itoa_copy") A.raw([enc_addiu(10, 9, 1)]) # t2 = end+1 A.label("itoa_cp") A.raw([enc_lbu(12, 0, 10)]) A.raw([enc_sb(12, 0, 5)]) A.raw([enc_addiu(10, 10, 1)]) A.raw([enc_addiu(5, 5, 1)]) A.b(12, 0, "itoa_done", eq=False) A.raw([NOP]) A.j("itoa_cp") A.raw([NOP]) A.label("itoa_done") A.raw([enc_jr(31), NOP]) # ---------------- scan() ---------------- A.label("scan") A.store_g(0, G_COUNT) A.load_cstr(4, "s_appsdir") A.jal(PLT["opendir"]) A.raw([NOP]) A.gp() A.b(2, 0, "scan_done", eq=True) A.raw([NOP]) A.raw([enc_addiu(29, 29, -0x20)]) A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29), enc_sw(18, 0x10, 29)]) A.raw([enc_move(16, 2)]) A.raw([enc_addiu(17, 0, 0)]) A.label("scan_loop") A.raw([enc_move(4, 16)]) A.jal(PLT["readdir"]) A.raw([NOP]) A.gp() A.b(2, 0, "scan_close", eq=True) A.raw([NOP]) A.raw([enc_move(18, 2)]) A.raw([enc_lbu(2, 0x0B, 18)]) A.raw([enc_addiu(3, 0, 0x2E)]) A.b(2, 3, "scan_proc", eq=False) A.raw([NOP]) A.raw([enc_lbu(2, 0x0C, 18)]) A.b(2, 0, "scan_loop", eq=True) A.raw([NOP]) A.raw([enc_addiu(3, 0, 0x2E)]) A.b(2, 3, "scan_proc", eq=False) A.raw([NOP]) A.raw([enc_lbu(2, 0x0D, 18)]) A.b(2, 0, "scan_proc", eq=False) A.raw([NOP]) A.j("scan_loop") A.raw([NOP]) A.label("scan_proc") A.raw([enc_sltiu(2, 17, 8)]) A.b(2, 0, "scan_close", eq=True) A.raw([NOP]) A.addr_g(4, G_SCAN) A.load_cstr(5, "s_appsdir") A.jal(PLT["strcpy"]) A.raw([NOP]) A.gp() A.addr_g(4, G_SCAN) A.jal(PLT["strlen"]) A.raw([NOP]) A.addr_g(8, G_SCAN) A.raw([enc_addu(8, 8, 2)]) A.raw([enc_move(4, 8)]) A.raw([enc_addiu(5, 18, 0x0B)]) A.jal(PLT["strcpy"]) A.raw([NOP]) A.gp() A.addr_g(4, G_SCAN) A.jal(PLT["strlen"]) A.raw([NOP]) A.addr_g(8, G_SCAN) A.raw([enc_addu(4, 8, 2)]) A.load_cstr(5, "s_appjson") A.jal(PLT["strcpy"]) A.raw([NOP]) A.gp() A.addr_g(4, G_SCAN) A.load_cstr(5, "s_r") A.jal(PLT["fopen"]) A.raw([NOP]) A.gp() A.b(2, 0, "scan_next", eq=True) A.raw([NOP]) A.raw([enc_move(18, 2)]) A.addr_g(4, G_SCAN) # a0 = buf (fgets(buf, size, FILE)) A.raw([enc_addiu(4, 4, 0x60)]) A.raw([enc_addiu(5, 0, 96)]) # a1 = size A.raw([enc_move(6, 18)]) # a2 = FILE* A.jal(PLT["fgets"]) A.raw([NOP]) A.gp() A.raw([enc_move(4, 18)]) A.jal(PLT["fclose"]) A.raw([NOP]) A.gp() # parse name A.addr_g(4, G_SCAN) A.raw([enc_addiu(4, 4, 0x60)]) A.load_cstr(5, "s_name_k") A.jal(PLT["strstr"]) A.raw([NOP]) A.gp() A.b(2, 0, "scan_next", eq=True) A.raw([NOP]) A.raw([enc_move(18, 2)]) A.label("scan_skip1") A.raw([enc_lbu(2, 0, 18)]) A.raw([enc_addiu(3, 0, 0x22)]) A.b(2, 3, "scan_got1", eq=True) A.raw([NOP]) A.raw([enc_addiu(18, 18, 1)]) A.j("scan_skip1") A.raw([NOP]) A.label("scan_got1") A.raw([enc_addiu(18, 18, 1)]) A.addr_g(8, G_NAMES) A.raw([enc_sll(9, 17, 4), enc_addu(8, 8, 9)]) A.raw([enc_move(9, 18)]) A.label("scan_copy1") A.raw([enc_lbu(2, 0, 9)]) A.raw([enc_addiu(3, 0, 0x22)]) A.b(2, 3, "scan_end1", eq=True) A.raw([NOP]) A.b(2, 0, "scan_end1", eq=True) A.raw([NOP]) A.raw([enc_sb(2, 0, 8), enc_addiu(8, 8, 1), enc_addiu(9, 9, 1)]) A.j("scan_copy1") A.raw([NOP]) A.label("scan_end1") A.raw([enc_sb(0, 0, 8)]) # parse exec (missing -> skip app: no count++) A.addr_g(4, G_SCAN) A.raw([enc_addiu(4, 4, 0x60)]) A.load_cstr(5, "s_exec_k") A.jal(PLT["strstr"]) A.raw([NOP]) A.gp() A.b(2, 0, "scan_next", eq=True) A.raw([NOP]) A.raw([enc_move(18, 2)]) A.label("scan_skip2") A.raw([enc_lbu(2, 0, 18)]) A.raw([enc_addiu(3, 0, 0x22)]) A.b(2, 3, "scan_got2", eq=True) A.raw([NOP]) A.raw([enc_addiu(18, 18, 1)]) A.j("scan_skip2") A.raw([NOP]) A.label("scan_got2") A.raw([enc_addiu(18, 18, 1)]) A.addr_g(8, G_EXECS) A.raw([enc_sll(9, 17, 5), enc_sll(10, 17, 4), enc_addu(9, 9, 10)]) A.raw([enc_addu(8, 8, 9)]) A.raw([enc_move(9, 18)]) A.label("scan_copy2") A.raw([enc_lbu(2, 0, 9)]) A.raw([enc_addiu(3, 0, 0x22)]) A.b(2, 3, "scan_end2", eq=True) A.raw([NOP]) A.b(2, 0, "scan_end2", eq=True) A.raw([NOP]) A.raw([enc_sb(2, 0, 8), enc_addiu(8, 8, 1), enc_addiu(9, 9, 1)]) A.j("scan_copy2") A.raw([NOP]) A.label("scan_end2") A.raw([enc_sb(0, 0, 8)]) A.raw([enc_addiu(17, 17, 1)]) A.label("scan_next") A.j("scan_loop") A.raw([NOP]) A.label("scan_close") A.raw([enc_move(4, 16)]) A.jal(PLT["closedir"]) A.raw([NOP]) A.gp() A.store_g(17, G_COUNT) A.raw([enc_lw(18, 0x10, 29), enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29)]) A.raw([enc_lw(31, 0x1C, 29), enc_addiu(29, 29, 0x20)]) A.label("scan_done") A.raw([enc_jr(31), NOP]) # ---------------- touch_hook / autotest_check ---------------- # 0x47b820 (hgl_ts 触摸读取, lg_ial 线程, 息屏时也在 poll 触摸) 入口被 # patch 为 "j touch_hook; nop" —— nop 消除 delay slot, 避免原 prologue # 第 2 条 (sw ra) 以未调整的 sp 先执行(乱写调用者栈, 上一版 SIGTRAP 主嫌)。 # hook 内补执行原 prologue 两条后跳回 0x47b828。 A.label("touch_hook") A.raw([enc_addiu(29, 29, -0x80)]) # 原 0x47b820 prologue 指令1 A.raw([enc_sw(31, 0x7C, 29)]) # 原 prologue 指令2 A.raw([enc_sw(28, 0x08, 29)]) # 保存 gp (0x08(sp) 原函数未用) # 无信号方案: 早期 lg_ial 线程与主线程并发 signal() 疑触发 glibc 竞态 SIGSEGV。 # touch_hook 只置 pending, 由主循环 loop_hook 轮询消费(主循环带超时, 无需唤醒信号)。 A.jal("autotest_check") A.raw([NOP]) A.raw([enc_lw(28, 0x08, 29)]) # 恢复 gp A.raw([enc_j(0x47B828)]) # 回原函数继续 A.raw([NOP]) # loop_hook: 0x436fc0 (system_if 主循环, 主线程) -> "j loop_hook; nop" # pending 置位时: 解析 qobuz 入口视图(0x49be00), 调它的回调 (view+0x140) # cb(parent, view, &type=4) —— 与真实点击完全同路径, 主线程执行无 UI 竞态。 A.label("loop_hook") A.raw([enc_addiu(29, 29, -0x20)]) # 原 0x436fc0 prologue 指令1 A.raw([enc_sw(31, 0x1C, 29)]) # 原 prologue 指令2 A.raw([enc_sw(28, 0x08, 29)]) # 保存 gp (0x08(sp) 原函数未用) # trace 计数器(调试用, gdb 读 0x906614) A.raw([enc_lui(8, G_BASE_HI), enc_lw(9, G_BASE_LO + G_TRACE, 8)]) A.raw([enc_addiu(9, 9, 1), enc_sw(9, G_TRACE, 8)]) A.load_g(8, G_PENDING) A.b(8, 0, "lh_done", eq=True) A.raw([NOP]) A.store_g(0, G_PENDING) # 清 pending # type=4 @ sp+0x0c A.raw([enc_addiu(2, 0, 4), enc_sw(2, 0x0C, 29)]) # a0 = ctx = *(0x96d124) A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) A.b(4, 0, "lh_done", eq=True) # ctx 未就绪 -> 放弃 A.raw([NOP]) # 0x49be00(ctx, "stream_media_vg_qobuz", "viewgroup") -> qobuz 入口视图 A.load_abs(6, STR_VIEWGROUP) A.load_cstr(5, "s_qobuz_name") A.jal(CREATE_VIEW) A.raw([NOP]) A.gp() A.b(2, 0, "lh_entry_fallback", eq=True) # 视图未找到(未进过流媒体页) -> 直接 entry A.raw([NOP]) # 调视图回调: lw t9, 0x140(view); t9(ctx, view, &type) A.raw([enc_move(10, 2)]) # t2 = view (t 寄存器, 不污染 callee-saved) A.raw([enc_lw(9, 0x140, 10)]) A.b(9, 0, "lh_entry_fallback", eq=True) A.raw([NOP]) A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) # a0 = ctx A.raw([enc_move(5, 10)]) # a1 = view A.raw([enc_addiu(6, 29, 0x0C)]) # a2 = &type A.raw([enc_jalr(31, 9), NOP]) A.j("lh_done") A.raw([NOP]) # fallback: entry(ctx, 0, &type) —— 从任意页面直接创建+激活应用列表页。 # view=0 跳过 g_qobuz/选中标记; g_parent 由 entry 内取 ctx 兜底。 A.label("lh_entry_fallback") A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) # a0 = ctx A.raw([enc_addiu(5, 0, 0)]) # a1 = 0 A.raw([enc_addiu(6, 29, 0x0C)]) # a2 = &type A.jal("entry") A.raw([NOP]) A.gp() A.label("lh_done") A.raw([enc_lw(28, 0x08, 29)]) # 恢复 gp A.raw([enc_j(0x436FC8)]) # 回原函数继续 A.raw([NOP]) # ---------------- pending_hook (v3, 2026-08-28) ---------------- # 触发机制修正: 0x436fc0 不是主循环(init 一次性调用, 已实证 G_TRACE 恒 0)。 # 真正的主线程周期循环 = LiteGUI 主循环 0x44a820/0x44a900(同构双副本), 每轮 # 调用 0x488020(触摸轮询, 已实证 X/Y 全局更新)。把两处 "jal 0x488020" # (0x44a898/0x44a950) 改为 "j pending_hook_{a,b}; nop", hook 内: # 执行原 delay slot → 检查 G_PENDING → 有则清 + entry(ctx, 0, &type=4) # → jal 0x488020(原调用) → jr t0 回各自循环继续。主线程执行, 无 UI 竞态。 A.label("pending_hook_a") A.load_abs(8, 0x44A8A0) # t0 = 0x44a8a0 (循环 A 续点) A.b(0, 0, "pending_common", eq=True) # 无条件跳公共段 A.raw([NOP]) A.label("pending_hook_b") A.load_abs(8, 0x44A958) # t0 = 0x44a958 (循环 B 续点) A.label("pending_common") A.raw([enc_sw(8, 0x14, 29)]) # 续点存 sp+0x14 (entry 会践踏 t0) A.raw([enc_addiu(4, 29, 0x18)]) # a0 = sp+0x18 (原 delay slot) A.raw([enc_lui(9, 0x90), enc_lw(9, 0x65F4, 9)]) # t9 = G_PENDING (0x9065f4) A.b(9, 0, "ph_call", eq=True) # pending == 0 → 直接原调用 A.raw([NOP]) A.raw([enc_lui(9, 0x90), enc_sw(0, 0x65F4, 9)]) # 清 pending A.raw([enc_addiu(2, 0, 4), enc_sw(2, 0x0C, 29)]) # type=4 @ sp+0x0c A.raw([enc_lui(9, 0x97), enc_lw(4, -0x2EDC, 9)]) # a0 = ctx = *(0x96d124) A.b(4, 0, "ph_call", eq=True) # ctx 未就绪 → 跳过 A.raw([NOP]) A.raw([enc_addiu(5, 0, 0)]) # a1 = 0 (view: 跳过 g_qobuz/选中) A.raw([enc_addiu(6, 29, 0x0C)]) # a2 = &type A.raw([enc_lui(8, 0x90), enc_lw(9, 0x65F8, 8)]) # 诊断: G_TRACE++ A.raw([enc_addiu(9, 9, 1), enc_sw(9, 0x65F8, 8)]) A.jal("entry") A.raw([NOP]) A.gp() A.label("ph_call") A.jal(0x488020) # 原调用 (触摸轮询, 保持 UI 状态) A.raw([enc_addiu(4, 29, 0x18)]) # 原 delay slot A.raw([enc_lw(8, 0x14, 29)]) # jal 返回后恢复续点 (0x488020 践踏 t0) A.raw([enc_jr(8)]) # 回各自循环续点 A.raw([NOP]) A.label("autotest_check") # 注意: 此函数被插在 0x47b820 prologue 中间执行, 原函数依赖 $a0 (touch obj) # 必须保存/恢复所有 caller-saved: a0-a3 A.raw([enc_addiu(29, 29, -0x20)]) # 独立临时帧 A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) A.raw([enc_sw(4, 0x10, 29), enc_sw(5, 0x0C, 29), enc_sw(6, 0x08, 29), enc_sw(7, 0x04, 29)]) # 防抖: 布局上下文(0x96d124)必须已就绪(UI 初始化完成)才触发 A.raw([enc_lui(8, 0x97), enc_lw(8, -0x2EDC, 8)]) A.b(8, 0, "at_done", eq=True) A.raw([NOP]) A.load_cstr(4, "s_autotest") A.raw([enc_addiu(5, 0, 0)]) # F_OK A.jal(PLT["access"]) A.raw([NOP]) A.gp() A.b(2, 0, "at_done", eq=False) # 文件不存在 -> done A.raw([NOP]) # 存在: 删除(一次性) + pending=1 (主循环 loop_hook 轮询消费, 无信号) A.load_cstr(4, "s_autotest") A.jal(0x8E6E60) # remove A.raw([NOP]) A.gp() A.raw([enc_addiu(2, 0, 1)]) A.store_g(2, G_PENDING) A.label("at_done") A.raw([enc_lw(7, 0x04, 29), enc_lw(6, 0x08, 29), enc_lw(5, 0x0C, 29), enc_lw(4, 0x10, 29)]) A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) # ---------------- assemble ---------------- cave_code = A.blob() assert len(cave_code) <= 0x1000, f"cave code overflow: {len(cave_code):#x}" cave_code += b"\x00" * (STR_BASE - (CAVE + len(cave_code))) assert len(cave_code) == STR_BASE - CAVE for name, bs in STRINGS: cave_code += bs while len(cave_code) % 4: cave_code += b"\x00" print(f"cave total: {len(cave_code)} bytes") for k in ["entry", "check_card", "setup", "set_hint", "set_note", "settext", "u2u", "find", "rowcb", "backcb", "launch", "scan"]: print(f" {k}: {A.syms[k]:#x}") PATCH_LUI_OFF = 0x4EFE28 - 0x400000 PATCH_ADD_OFF = 0x4EFE2C - 0x400000 PATCH_TOUCH_OFF = 0x47B820 - 0x400000 PATCH_LOOP_OFF = 0x436FC0 - 0x400000 CAVE_OFF = CAVE - 0x400000 def main(): data = bytearray(open(STOCK, "rb").read()) orig_lui = struct.unpack_from(" {OUT} ({len(data)} bytes)") if __name__ == "__main__": main()