diff --git a/agents.md b/agents.md index 3336286e..f71c3422 100644 --- a/agents.md +++ b/agents.md @@ -170,7 +170,7 @@ AssertionError 4. Changing register sizes (e.g., using `SEP`/`REP` or `A8`/`A16`) inside a subroutine without preserving and restoring the caller's processor status register `P`. This will corrupt the caller's register size state upon returning, or cause initial instructions in the hook to read garbage high bytes if the caller was in `A16` mode. **Resolution**: - Always preserve and restore modified registers using `PHX`/`PLX` or `PHY`/`PLY` at the boundaries of your subroutine. -- **Always preserve and restore the processor status register P** using `PHP` at the entry and `PLP` at all exit paths of your subroutine if register size modes (`A8`/`A16`, `I8`/`I16`) are changed, ensuring the caller's processor state is perfectly preserved. -- When beginning a custom subroutine that changes register sizes, explicitly execute `A8()` or `A16()` right after `PHP` to ensure a consistent, known accumulator size for the initial instructions, avoiding reading 16-bit garbage high bytes if the caller was in a different mode. +- **Always preserve and restore the processor status register P** using `PHP` at the entry and `PLP` at all exit paths of your subroutine if register size modes (`A8`/`A16`, `I8`/`I16`) are changed, ensuring the caller's processor state is perfectly preserved. **Exception**: If the subroutine is used as a conditional hook that must return comparison/status flags (like the Zero flag `Z` or Carry flag `C`) to the caller, do NOT use `PHP` / `PLP` as they will overwrite the return flag. Instead, manually ensure the register sizes match the caller's expectations upon exit (e.g. executing `A8()` before `RTL` if the caller expects 8-bit accumulator mode), restore any scratch registers like `X` with `PLX` first, and set/clear the target flag (e.g. with `LDA #$00` / `LDA #$01`) as the final instruction before return. +- When beginning a custom subroutine that changes register sizes, explicitly execute `A8()` or `A16()` right after `PHP` (or at the entry if not using `PHP`) to ensure a consistent, known accumulator size for the initial instructions, avoiding reading 16-bit garbage high bytes if the caller was in a different mode. - Use 24-bit long addressing opcodes (`0xAF` for `LDA long`, `0xBF` for `LDA long,X`) when referencing addresses dynamically allocated to Bank `F0`. - Always load active monster IDs directly from the active monster ID table WRAM `$2001,X` (where `X` is `slot_index * 2`) after clearing the 16-bit Accumulator (using `TDC` `0x7B`) to avoid index register pollution. WRAM `$812F,X` is used by the graphics loader to hold the overwritten graphics ID (e.g. setting it to 0 to trigger Imp graphics), and does not hold the actual active monster IDs for lookup. diff --git a/data/enemies.py b/data/enemies.py index 6ff321a8..8e781a4d 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -465,7 +465,6 @@ def who_there_assembly(self): # 3. Create the custom check_imp_graphics subroutine in Bank C0 src = [ - asm.PHP(), asm.PHX(), asm.TDC(), asm.A8(), @@ -494,9 +493,8 @@ def who_there_assembly(self): "NOT_IMP_16", asm.A8(), "NOT_IMP", - asm.LDA(0x00, asm.IMM8), asm.PLX(), - asm.PLP(), + asm.LDA(0x00, asm.IMM8), asm.RTL(), "IS_IMP", @@ -507,9 +505,8 @@ def who_there_assembly(self): asm.LDA(0x0000, asm.IMM16), asm.STA(0x812F, asm.ABS_X), asm.A8(), - asm.LDA(0x01, asm.IMM8), asm.PLX(), - asm.PLP(), + asm.LDA(0x01, asm.IMM8), asm.RTL(), ] subroutine_space = Write(Bank.C0, src, "who's there check imp graphics") @@ -522,7 +519,6 @@ def who_there_assembly(self): asm.NOP(), asm.NOP(), asm.NOP(), - asm.NOP(), ] Write(0x01207b, patch_src, "who's there imp graphics loader hook") diff --git a/llms.md b/llms.md index 185ef627..d853d5b9 100644 --- a/llms.md +++ b/llms.md @@ -192,6 +192,7 @@ All generated test data, output ROMs, log text files, and metadata manifests gen When implementing visual-only overrides (such as "Who's There?" `-who` / `--who-there` mode): - **Name Obfuscation**: Modify name fields in `data/enemies.py` directly. The data layer will serialize and write the modified text bytes to the ROM during serialization. -- **Visual Sprite Loader Hooking**: Patch the graphics loader logic at ROM offset `0x01207B` (Bank `C1`) using a hook directing to a custom assembly subroutine in Bank `C0`. The subroutine uses `PHP` and `PLP` to preserve/restore the caller's processor status register `P`, preventing register size corruption. It begins with `A8()` to ensure 8-bit accumulator mode, clears the 16-bit accumulator using `TDC`, and loads the slot index `$81A7`. It evaluates the `-who` flag status and looks up the active monster's ID directly from WRAM `$2001,X` (where `X = slot * 2` under `A16` mode) against a 384-byte `boss_table` using 24-bit absolute opcodes `0xAF`/`0xBF`. If a match is found, it overwrites WRAM `$812F,X` with `0` (the Imp ID) to redirect rendering to the Imp sprite dynamically. It uses fully consolidated exit paths (`NOT_IMP` and `NOT_IMP_16` using `PLX`/`PLP`) with all redundant `Y` register reloading completely removed for maximum efficiency. +- **Visual Sprite Loader Hooking**: Patch the graphics loader logic at ROM offset `0x01207B` (Bank `C1`) using a 9-byte hook directing to a custom assembly subroutine in Bank `C0`. The hook must be exactly 9 bytes (JSL check_imp_graphics, BEQ $09, and 3 NOPs) so as not to overwrite the subsequent vanilla JSR $202F instruction at 0x012084. The subroutine preserves and restores index register X using PHX/PLX, starts with A8() to ensure 8-bit accumulator mode, clears the 16-bit accumulator using TDC, and loads the slot index $81A7. It evaluates the -who flag status and looks up the active monster's ID directly from WRAM $2001,X (where X = slot * 2 under A16 mode) against a 384-byte boss_table using 24-bit absolute opcodes 0xAF/0xBF. If a match is found, it overwrites WRAM $812F,X with 0 (the Imp ID) to redirect rendering to the Imp sprite dynamically. It uses PLX to restore X and then sets (LDA #$00) or clears (LDA #$01) the Z flag right before RTL to return the comparison result to the caller (avoiding PHP/PLP which would overwrite the Z flag value). - **Python 3.14 Compatibility**: When selecting random items/espers from a `set` pool (such as `self.available_espers` in `data/espers.py`), never pass the `set` directly to `random.sample()`, as Python 3.9+ deprecates and Python 3.11+ (including Python 3.14) throws `TypeError`. Instead, convert the set to a tuple: `random.sample(tuple(set_pool), 1)[0]`. This satisfies Python's sequence checks while maintaining 100% identical random state consumption and seed choice sequence compatibility with all older Python versions. - **Character Gating & Reward Types**: In `event/event_reward.py`, `single_possible_type(self)` checks if a reward slot allows only a single type. Because Python's `Flag` allows checking membership for compound flag combinations (`RewardType.CHARACTER | RewardType.ESPER in RewardType` evaluates to `True`), checking `possible_types in RewardType` was a major bug that caused compound slots to be treated as single-type slots. This lead to gating deadlocks (`AssertionError` in `choose_reward`) under specific seed/flag configurations. The fix is to explicitly check against exact single-member flags: `return self.possible_types in (RewardType.CHARACTER, RewardType.ESPER, RewardType.ITEM)`. +