Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 2 additions & 6 deletions data/enemies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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",
Expand All @@ -507,9 +505,8 @@ def who_there_assembly(self):
asm.LDA(0x0000, asm.IMM16),
asm.STA(0x812F, asm.ABS_X),
Comment on lines 505 to 506

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In 16-bit accumulator mode (A16), you can use the STZ (Store Zero) instruction directly instead of loading 0x0000 into the accumulator and then storing it. This is more efficient, saving 3 bytes of ROM space and avoiding modifying the accumulator register.

Suggested change
asm.LDA(0x0000, asm.IMM16),
asm.STA(0x812F, asm.ABS_X),
asm.STZ(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")
Expand All @@ -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")

Expand Down
3 changes: 2 additions & 1 deletion llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.

Loading