Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
14aad9c
STEVE!
wrjones104 Jun 3, 2026
0d80204
changes based on PR comments
wrjones104 Jun 4, 2026
9dc7fbb
Fix graphics menu alignment and visual sprite loader hook Z flag prop…
wrjones104 Jun 4, 2026
a29d13b
Fix Steveify flags menu display of None and support dynamic Arguments…
wrjones104 Jun 4, 2026
46bc2c6
Change default Steveify name override to all-caps STEVE
wrjones104 Jun 4, 2026
11751e2
first pass at "who's there" flag
wrjones104 May 18, 2026
6e080b0
Fix SrBehemoth and final battle
wrjones104 May 18, 2026
32e9117
Refactor whos-there assembly to use instruction/asm.py DSL and WRAM ,X
wrjones104 May 18, 2026
12f0a46
fix for whos there
wrjones104 May 27, 2026
7cdeb1d
Refactor whos-there assembly to use instruction/asm.py DSL and WRAM ,X
wrjones104 May 18, 2026
09d2cf1
Add "Who's There?" section to output text/flags menu
wrjones104 Jun 1, 2026
e6fee16
udpates
wrjones104 Jun 3, 2026
b5666bc
Steve (#9)
wrjones104 Jun 4, 2026
529b7cb
Merge branch 'main' into whos-there
wrjones104 Jun 4, 2026
119e36c
Remove "who_there" reference
wrjones104 Jun 4, 2026
17a5e0f
Update data/data.py
wrjones104 Jun 4, 2026
1523500
Update data/items.py
wrjones104 Jun 4, 2026
4531ee7
de-steveification
wrjones104 Jun 4, 2026
c4f3ad5
fix steveify logic and update name handling in espers and lores
wrjones104 Jun 4, 2026
0bc8615
fix and clarify logic in `graphics.py` and optimize assembly handling…
wrjones104 Jun 4, 2026
2ed772d
Merge pull request #11 from wrjones104/steve
wrjones104 Jun 4, 2026
1d2ad3d
Merge branch 'main' into whos-there
wrjones104 Jun 4, 2026
8e477ce
fix and clarify logic in `graphics.py` and optimize assembly handling…
wrjones104 Jun 4, 2026
ee83973
Merge branch 'dev' into whos-there
wrjones104 Jun 23, 2026
8724b37
Merge branch 'whos-there' of https://github.com/wrjones104/WorldsColl…
wrjones104 Jun 23, 2026
231c443
Refactor name processing in graphics.py and add NOP in enemies.py
wrjones104 Jun 23, 2026
ce91be8
Merge branch 'dev' into whos-there
wrjones104 Jun 23, 2026
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
11 changes: 7 additions & 4 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,15 @@ AssertionError

## 7. Assembly Hooks and CPU Register Preservation

**Error Signature**: Monster sprites appear as garbled noise blocks, or regular enemies have incorrect sprites.
**Cause**: Modifying registers (such as `X` or `Y`) or using incorrect data bank instruction opcodes inside custom assembly subroutines. Specifically:
**Error Signature**: Monster sprites appear as garbled noise blocks, or regular enemies have incorrect sprites, or the game crashes when returning from a hook.
**Cause**: Modifying registers (such as `X` or `Y`), changing register sizes (`A8`/`A16` or `I8`/`I16`) inside a custom assembly subroutine, or using incorrect data bank instruction opcodes. Specifically:
1. Destructively overwriting `X` inside a hook when the caller context relies on `X` containing the original graphics index.
2. Using 16-bit absolute address instructions like `LDA absolute,X` (`0xBD`) to access 24-bit dynamic ROM banks (`F0`), which reads from the incorrect data bank and treats the third address byte as the opcode of the next instruction, shifting the entire instruction stream.
3. Looking up active monster IDs in the wrong table (like general actor list WRAM `$2001,X` where slot indices do not map directly to monster offsets), leading to missing or un-rendered sprite overlays in multi-boss battles.
3. Looking up active monster IDs from the wrong table or using the wrong index, leading to missing or un-rendered sprite overlays in multi-boss battles.
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.
- 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 `$812F,Y` (indexed by slot index * 2) after clearing the 16-bit Accumulator (using `TDC` `0x7B`) at the very beginning of the subroutine to avoid index register pollution from caller active CPU registers.
- 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.
1 change: 1 addition & 0 deletions args/bosses.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def options(args):
("Boss Experience", args.boss_experience, "boss_experience"),
("No Undead", args.boss_no_undead, "boss_no_undead"),
("Marshal Keep Lobos", args.boss_marshal_keep_lobos, "boss_marshal_keep_lobos"),
("Who's There?", args.who_there, "who_there"),
("Oops All Boss ID", oops, "oops"),
]

Expand Down
24 changes: 20 additions & 4 deletions args/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ def parse(parser):
graphics.add_argument("-ahtc", "--alternate-healing-text-color", action = "store_true",
help = "Makes healing text blue, to be able to distinguish from damage.")

graphics.add_argument("-who", "--who-there", action = "store_true",
help = "Who's There? Bosses look like Imps and have the name '??????'")
Comment thread
wrjones104 marked this conversation as resolved.
Comment thread
wrjones104 marked this conversation as resolved.
graphics.add_argument("-steve", "--steveify", type = str, nargs='?', const='Steve', default=None,
help = "Steveify the seed: rename all characters, items, espers, magic, enemies, etc. to a given name (default: Steve)")

def process(args):
import graphics.palettes.palettes as palettes
import graphics.portraits.portraits as portraits
import graphics.sprites.sprites as sprites


if args.steveify is not None:
if isinstance(args.steveify, bool):
Expand Down Expand Up @@ -120,6 +123,8 @@ def process(args):
else:
args.sprite_palettes = DEFAULT_CHARACTER_SPRITE_PALETTES



def flags(args):
flags = ""

Expand All @@ -144,6 +149,8 @@ def flags(args):
flags += " -wmhc"
if args.alternate_healing_text_color:
flags += " -ahtc"
if args.who_there:
flags += " -who"

return flags

Expand Down Expand Up @@ -195,10 +202,10 @@ def _character_customization_log(args):

return log

def _other_options_log(args):
from log import format_option
log = ["Other Graphics"]
def name():
return "Graphics"

def options(args):
remove_flashes = "Original"
if args.flashes_remove_worst:
remove_flashes = "Worst"
Expand All @@ -213,13 +220,22 @@ def _other_options_log(args):
if args.alternate_healing_text_color:
healing_text = "Blue"

entries = [
return [
("Remove Flashes", remove_flashes, "remove_flashes"),
("Minimap", world_minimap, "world_minimap"),
("Healing Text", healing_text, "healing_text"),
("Steveify", args.steveify if args.steveify else "None", "steveify"),
]

def menu(args):
return (name(), options(args))

def _other_options_log(args):
from log import format_option
log = ["Other Graphics"]

entries = options(args)

for entry in entries:
log.append(format_option(*entry))

Expand Down
93 changes: 93 additions & 0 deletions data/enemies.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,13 +426,106 @@ def mod(self, maps):
self.zones.mod()
self.scripts.mod()

if self.args.who_there:
self.who_there_mod()

if self.args.scan_all:
self.scan_all()

if self.args.debug:
for enemy in self.enemies:
enemy.debug_mod()

def who_there_mod(self):
for enemy in self.enemies:
if enemy.id in bosses.enemy_name or enemy.id == 282: # 282 is Undead SrBehemoth
if enemy.id not in range(343, 352): # Exclude Final Battle Tiers (Short Arm -> Sleep)
enemy.name = "??????"
self.who_there_assembly()

def who_there_assembly(self):
from memory.space import Bank, Write
import instruction.asm as asm

# 1. Allocate a 1-byte flag in ROM representing if the flag is active (it will be 1)
who_there_flag_space = Write(Bank.F0, [1], "who's there flag")
flag_addr = who_there_flag_space.start_address_snes

# 2. Construct the 384-byte boss table
boss_table_bytes = [0] * 384
for enemy_id in bosses.enemy_name:
if 0 <= enemy_id < 384:
boss_table_bytes[enemy_id] = 1
boss_table_bytes[282] = 1 # Include SrBehemoth (Undead) Phase 2
for excluded_id in range(343, 352): # Exclude Final Battle Tiers
boss_table_bytes[excluded_id] = 0
Comment thread
wrjones104 marked this conversation as resolved.

boss_table_space = Write(Bank.F0, boss_table_bytes, "who's there boss table")
table_addr = boss_table_space.start_address_snes

# 3. Create the custom check_imp_graphics subroutine in Bank C0
src = [
asm.PHP(),
asm.PHX(),
asm.TDC(),
asm.A8(),
asm.LDA(0x81A7, asm.ABS),
asm.TAY(),
asm.LDA(0x62C2, asm.ABS_Y),
asm.BNE("IS_IMP"),
Comment thread
wrjones104 marked this conversation as resolved.

asm.LDA(0x81A7, asm.ABS),
asm.ASL(),
asm.TAX(),

asm.A16(),
asm.LDA(0x2001, asm.ABS_X), # Load the active monster ID from WRAM $2001,X
asm.CMP(384, asm.IMM16),
asm.BCS("NOT_IMP_16"),
asm.TAX(),
asm.A8(),

asm.LDA(flag_addr, asm.LNG),
asm.BEQ("NOT_IMP"),

asm.LDA(table_addr, asm.LNG_X),
asm.BNE("IS_IMP"),

"NOT_IMP_16",
asm.A8(),
"NOT_IMP",
asm.LDA(0x00, asm.IMM8),
asm.PLX(),
asm.PLP(),
asm.RTL(),

"IS_IMP",
asm.LDA(0x81A7, asm.ABS),
asm.ASL(),
asm.TAX(),
asm.A16(),
asm.LDA(0x0000, asm.IMM16),
asm.STA(0x812F, asm.ABS_X),
asm.A8(),
asm.LDA(0x01, asm.IMM8),
asm.PLX(),
asm.PLP(),
asm.RTL(),
]
Comment thread
wrjones104 marked this conversation as resolved.
Comment thread
wrjones104 marked this conversation as resolved.
subroutine_space = Write(Bank.C0, src, "who's there check imp graphics")
sub_addr = subroutine_space.start_address_snes

# 4. Patch the original graphics loader at ROM offset 0x01207B (Bank C1)
patch_src = [
asm.JSL(sub_addr),
asm.BEQ(0x09), # Branch to 0x01208A
asm.NOP(),
asm.NOP(),
asm.NOP(),
asm.NOP(),
]
Write(0x01207b, patch_src, "who's there imp graphics loader hook")
Comment thread
wrjones104 marked this conversation as resolved.
Comment thread
wrjones104 marked this conversation as resolved.
Comment thread
wrjones104 marked this conversation as resolved.
Comment thread
wrjones104 marked this conversation as resolved.

def get_event_boss(self, original_boss_name):
return self.packs.get_event_boss_replacement(original_boss_name)

Expand Down
2 changes: 1 addition & 1 deletion llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,6 @@ 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 `Reserve` to intercept the check for monster status effects (e.g. WRAM Imp Graphics flags `$62C2,Y` indexed by slot ID). A custom assembly routine in Bank `F0` (via `Allocate`) evaluates conditions (e.g., `-who` flag status and looking up the active monster's ID directly from WRAM `$812F,Y` (indexed by slot * 2) after clearing the Accumulator's high byte using `TDC` (`0x7B`) immediately after `PHX` to avoid index register pollution, against a 384-byte `boss_table` of boss IDs using 24-bit absolute opcodes `0xAF`/`0xBF`). The subroutine redirects rendering to the Imp sprite dynamically without applying the actual status, while strictly preserving Index Register X (`PHX`/`PLX`) to ensure standard graphics rendering for all other enemies remains perfectly intact.
- **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.
- **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