From 14aad9c51d7cb5d19917f432c3cd8a8e765f2d9b Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Wed, 3 Jun 2026 16:27:37 -0400 Subject: [PATCH 01/21] STEVE! --- args/graphics.py | 16 ++++++++++++++++ data/characters.py | 4 ++++ data/dances.py | 4 ++++ data/data.py | 12 ++++++++++++ data/enemies.py | 9 +++++++++ data/espers.py | 6 ++++++ data/items.py | 5 +++++ data/lores.py | 4 ++++ data/magiteks.py | 4 ++++ data/spells.py | 17 +++++++++++++++++ data/swdtechs.py | 4 ++++ 11 files changed, 85 insertions(+) diff --git a/args/graphics.py b/args/graphics.py index 7f3f7b13..852928fc 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -22,11 +22,20 @@ 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("-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 not args.steveify or args.steveify.isspace(): + args.steveify = "Steve" + if len(args.steveify) > 6: + args.steveify = args.steveify[:6] + if args.character_names is not None: args.names = args.character_names.split('.') if len(args.names) != Characters.CHARACTER_COUNT: @@ -40,6 +49,9 @@ def process(args): else: args.names = Characters.DEFAULT_NAME + if args.steveify is not None: + args.names = [args.steveify] * Characters.CHARACTER_COUNT + args.palettes = [] if args.character_palettes: args.palette_ids = [int(palette_id) for palette_id in args.character_palettes.split('.')] @@ -101,6 +113,8 @@ def flags(args): if args.character_names: flags += " -name " + args.character_names + if args.steveify: + flags += " -steve " + args.steveify if args.character_palettes: flags += " -cpal " + args.character_palettes if args.character_portraits: @@ -191,6 +205,8 @@ def _other_options_log(args): ("Remove Flashes", remove_flashes, "remove_flashes"), ("World Minimap", world_minimap, "world_minimap"), ("Healing Text", healing_text, "healing_text"), + ("Who's There?", who_there, "who_there"), + ("Steveify", args.steveify if args.steveify else "None", "steveify"), ] for entry in entries: diff --git a/data/characters.py b/data/characters.py index c5f258d5..761075b0 100644 --- a/data/characters.py +++ b/data/characters.py @@ -133,6 +133,10 @@ def mod(self): if self.args.character_names: self.mod_names() + if self.args.steveify: + for character in self.characters: + character.name = self.args.steveify + if self.args.original_name_display: characters_asm.show_original_names() diff --git a/data/dances.py b/data/dances.py index 843249ea..f26cfec3 100644 --- a/data/dances.py +++ b/data/dances.py @@ -164,6 +164,10 @@ def shuffle(self): dance.dances = abilities[ability_index : ability_index + self.DATA_SIZE] def mod(self): + if self.args.steveify: + for dance in self.dances: + dance.name = self.args.steveify + self.write_learners_table() self.write_is_learner() self.after_battle_check_mod() diff --git a/data/data.py b/data/data.py index 3624b986..700ad10c 100644 --- a/data/data.py +++ b/data/data.py @@ -21,6 +21,8 @@ class Data: def __init__(self, rom, args): + self.rom = rom + self.args = args self.dialogs = dialogs self.spells = spells.Spells(rom, args) @@ -81,6 +83,16 @@ def __init__(self, rom, args): self.title_graphics.mod() def write(self): + if self.args.steveify: + import data.text as text + ability_name_bytes = bytearray() + for i in range(175): + name_bytes = text.get_bytes(self.args.steveify, text.TEXT2) + name_bytes = name_bytes[:10] + name_bytes.extend([0xff] * (10 - len(name_bytes))) + ability_name_bytes.extend(name_bytes) + self.rom.set_bytes(0x26f7b9, ability_name_bytes) + self.dialogs.write() self.characters.write() self.items.write() diff --git a/data/enemies.py b/data/enemies.py index 7e16dd83..b384cf45 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -75,6 +75,8 @@ def get_enemy(self, name): return enemy.id def get_name(self, enemy_id): + if self.args.steveify: + return self.args.steveify if enemy_id in bosses.enemy_name: return bosses.enemy_name[enemy_id] return self.enemies[enemy_id].name @@ -439,6 +441,13 @@ def print(self): enemy.print() def write(self): + if self.args.steveify: + for enemy in self.enemies: + if enemy.name: + enemy.name = self.args.steveify + if enemy.special_name: + enemy.special_name = self.args.steveify + for enemy_index in range(len(self.enemies)): self.enemy_data[enemy_index] = self.enemies[enemy_index].data() self.enemy_name_data[enemy_index] = self.enemies[enemy_index].name_data() diff --git a/data/espers.py b/data/espers.py index bf2c0957..b99fb44c 100644 --- a/data/espers.py +++ b/data/espers.py @@ -275,6 +275,10 @@ def multi_summon(self): space = Reserve(0x24da3, 0x24da5, "espers set used in battle bit", asm.NOP()) def mod(self, dialogs): + if self.args.steveify: + for esper in self.espers: + esper.name = self.args.steveify + self.receive_dialogs_mod(dialogs) if self.args.esper_spells_shuffle or self.args.esper_spells_shuffle_random_rates: @@ -353,6 +357,8 @@ def get_receive_esper_dialog(self, esper): return self.receive_dialogs[esper] def get_name(self, esper): + if self.args.steveify: + return self.args.steveify return self.esper_names[esper] def log(self): diff --git a/data/items.py b/data/items.py index f90bf5b1..06aaec5e 100644 --- a/data/items.py +++ b/data/items.py @@ -220,6 +220,11 @@ def moogle_starting_equipment(self): self.characters.characters[index].init_head = random.choice(tiers[Item.HELMET][1]) def mod(self): + if self.args.steveify: + for item in self.items: + if item.id != self.EMPTY: + item.name = self.args.steveify + not_relic_condition = lambda x: x != Item.RELIC if self.args.item_equipable_random: self.equipable_random(not_relic_condition, self.args.item_equipable_random_min, diff --git a/data/lores.py b/data/lores.py index c4cfa602..d0eff7ea 100644 --- a/data/lores.py +++ b/data/lores.py @@ -253,6 +253,10 @@ def show_mp_mod(self): ) def mod(self, dialogs): + if self.args.steveify: + for lore in self.lores: + lore.name = self.args.steveify + self.write_learners_table() self.write_is_learner() self.after_battle_check_mod() diff --git a/data/magiteks.py b/data/magiteks.py index 1dadccec..8cb93327 100644 --- a/data/magiteks.py +++ b/data/magiteks.py @@ -34,6 +34,10 @@ def fix_reflectable_beams(self): self.magiteks[self.ICE_BEAM].flags2 = 0x22 def mod(self): + if self.args.steveify: + for magitek in self.magiteks: + magitek.name = self.args.steveify + self.fix_reflectable_beams() pass diff --git a/data/spells.py b/data/spells.py index ad82b430..47374109 100644 --- a/data/spells.py +++ b/data/spells.py @@ -102,6 +102,23 @@ def alternate_healing_text_color(self): space.write(0x44, 0x7f) #default: F6 4B def mod(self): + if self.args.steveify: + for spell in self.spells: + icon = "" + for tag in ['', '', '']: + if spell.name.startswith(tag): + icon = tag + break + suffix = "" + for s in [" 2", " 3", "2"]: + if spell.name.endswith(s): + suffix = s + break + + max_steve_len = 6 - len(suffix) + steve_part = self.args.steveify[:max_steve_len] + spell.name = f"{icon}{steve_part}{suffix}" + if self.args.magic_mp_shuffle: self.shuffle_mp() elif self.args.magic_mp_random_value: diff --git a/data/swdtechs.py b/data/swdtechs.py index 847cbe85..866c2d5a 100644 --- a/data/swdtechs.py +++ b/data/swdtechs.py @@ -130,6 +130,10 @@ def enable_fast_swdtech(self): space.write(0x00) def mod(self): + if self.args.steveify: + for swdtech in self.swdtechs: + swdtech.name = self.args.steveify + self.write_learners_table() self.write_is_learner() From 0d80204a605cd77b1af6797fb2f0e3f5db44053e Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 09:18:47 -0400 Subject: [PATCH 02/21] changes based on PR comments --- data/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/data.py b/data/data.py index 700ad10c..372a6ac9 100644 --- a/data/data.py +++ b/data/data.py @@ -87,7 +87,7 @@ def write(self): import data.text as text ability_name_bytes = bytearray() for i in range(175): - name_bytes = text.get_bytes(self.args.steveify, text.TEXT2) + name_bytes = bytearray(text.get_bytes(self.args.steveify, text.TEXT2)) name_bytes = name_bytes[:10] name_bytes.extend([0xff] * (10 - len(name_bytes))) ability_name_bytes.extend(name_bytes) From 9dc7fbbf95ab8b3c886647e981c5b1f710636dd3 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 09:59:06 -0400 Subject: [PATCH 03/21] Fix graphics menu alignment and visual sprite loader hook Z flag propagation --- args/graphics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/args/graphics.py b/args/graphics.py index 852928fc..8cff0dd8 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -203,7 +203,7 @@ def _other_options_log(args): entries = [ ("Remove Flashes", remove_flashes, "remove_flashes"), - ("World Minimap", world_minimap, "world_minimap"), + ("Minimap", world_minimap, "world_minimap"), ("Healing Text", healing_text, "healing_text"), ("Who's There?", who_there, "who_there"), ("Steveify", args.steveify if args.steveify else "None", "steveify"), From a29d13befde10b9f727ce4e446b822e61ff102d6 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 10:25:48 -0400 Subject: [PATCH 04/21] Fix Steveify flags menu display of None and support dynamic Arguments propagation --- args/arguments.py | 9 +++++++++ args/graphics.py | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/args/arguments.py b/args/arguments.py index cc372d13..9bcce51a 100644 --- a/args/arguments.py +++ b/args/arguments.py @@ -67,6 +67,15 @@ def __init__(self): if self.debug: self.spoiler_log = True + # Update the global args module attributes to match this Arguments instance, + # which is helpful if a wrapper script instantiates Arguments dynamically + # instead of importing the args module directly. + import sys + if "args" in sys.modules: + module = sys.modules["args"] + for name, value in self.__dict__.items(): + setattr(module, name, value) + def _process_min_max(self, arg_name): values = getattr(self, arg_name) if values: diff --git a/args/graphics.py b/args/graphics.py index 8cff0dd8..169a99a9 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -31,10 +31,20 @@ def process(args): import graphics.sprites.sprites as sprites if args.steveify is not None: - if not args.steveify or args.steveify.isspace(): - args.steveify = "Steve" - if len(args.steveify) > 6: - args.steveify = args.steveify[:6] + if isinstance(args.steveify, bool): + if args.steveify: + args.steveify = "Steve" + else: + args.steveify = None + elif not args.steveify or args.steveify.isspace() or args.steveify.lower() in ("none", "false"): + if args.steveify.lower() in ("none", "false"): + args.steveify = None + else: + args.steveify = "Steve" + + if args.steveify is not None: + if len(args.steveify) > 6: + args.steveify = args.steveify[:6] if args.character_names is not None: args.names = args.character_names.split('.') From 46bc2c6a3ef6f005d24bc32cef1ed85a7f816590 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 10:35:21 -0400 Subject: [PATCH 05/21] Change default Steveify name override to all-caps STEVE --- args/graphics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/args/graphics.py b/args/graphics.py index 169a99a9..76efd16a 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -33,14 +33,14 @@ def process(args): if args.steveify is not None: if isinstance(args.steveify, bool): if args.steveify: - args.steveify = "Steve" + args.steveify = "STEVE" else: args.steveify = None elif not args.steveify or args.steveify.isspace() or args.steveify.lower() in ("none", "false"): if args.steveify.lower() in ("none", "false"): args.steveify = None else: - args.steveify = "Steve" + args.steveify = "STEVE" if args.steveify is not None: if len(args.steveify) > 6: From 11751e26730d507d23c82446ffa7296bdda0f32b Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Mon, 18 May 2026 12:50:51 -0400 Subject: [PATCH 06/21] first pass at "who's there" flag --- args/graphics.py | 10 ++++ data/enemies.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/args/graphics.py b/args/graphics.py index 7f3f7b13..d23580c7 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -22,6 +22,9 @@ 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 '??????'") + def process(args): import graphics.palettes.palettes as palettes import graphics.portraits.portraits as portraits @@ -118,6 +121,8 @@ def flags(args): flags += " -wmhc" if args.alternate_healing_text_color: flags += " -ahtc" + if args.who_there: + flags += " -who" return flags @@ -187,10 +192,15 @@ def _other_options_log(args): if args.alternate_healing_text_color: healing_text = "Blue" + who_there = "Original" + if args.who_there: + who_there = "Imps" + entries = [ ("Remove Flashes", remove_flashes, "remove_flashes"), ("World Minimap", world_minimap, "world_minimap"), ("Healing Text", healing_text, "healing_text"), + ("Who's There?", who_there, "who_there"), ] for entry in entries: diff --git a/data/enemies.py b/data/enemies.py index 7e16dd83..5d5b8638 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -424,6 +424,9 @@ 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() @@ -431,6 +434,152 @@ def mod(self, maps): 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: + enemy.name = "??????" + self.who_there_assembly() + + def who_there_assembly(self): + from memory.space import Allocate, Reserve, Bank + 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 = Allocate(Bank.F0, 1, "who's there flag") + who_there_flag_space.write([1]) + + # 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_space = Allocate(Bank.F0, 384, "who's there boss table") + boss_table_space.write(boss_table_bytes) + + # 3. Create the custom check_imp_graphics subroutine in Bank F0 + # Write custom assembly code in C0 to check the flag and table + subroutine_space = Allocate(Bank.C0, 120, "who's there check imp graphics") + sub_addr = subroutine_space.start_address_snes + + subroutine_bytes = [] + + # PHX (0xDA) + subroutine_bytes.append(0xda) + # TDC (0x7B) - Clear 16-bit Accumulator + subroutine_bytes.append(0x7b) + + # LDA $81A7 (0xAD, 0xA7, 0x81) + subroutine_bytes.extend([0xad, 0xa7, 0x81]) + # TAY (0xA8) + subroutine_bytes.append(0xa8) + # LDA $62C2,Y (0xB9, 0xC2, 0x62) + subroutine_bytes.extend([0xb9, 0xc2, 0x62]) + # BNE .is_imp (offset 0x30) + subroutine_bytes.extend([0xd0, 0x30]) + + # LDA $81A7 (0xAD, 0xA7, 0x81) + subroutine_bytes.extend([0xad, 0xa7, 0x81]) + # ASL A (0x0A) + subroutine_bytes.append(0x0a) + # TAX (0xAA) + subroutine_bytes.append(0xaa) + + # REP #$20 (0xC2, 0x20) + subroutine_bytes.extend([0xc2, 0x20]) + # LDA $2001,X (0xBD, 0x01, 0x20) + subroutine_bytes.extend([0xbd, 0x01, 0x20]) + # CMP #384 (0xC9, 0x80, 0x01) + subroutine_bytes.extend([0xc9, 0x80, 0x01]) + # BCS .not_imp_16 (offset 0x17) + subroutine_bytes.extend([0xb0, 0x17]) + # TAX (0xAA) + subroutine_bytes.append(0xaa) + # SEP #$20 (0xE2, 0x20) + subroutine_bytes.extend([0xe2, 0x20]) + + # LDA long who_there_flag (0xAF) + flag_addr = who_there_flag_space.start_address_snes + subroutine_bytes.extend([0xaf, flag_addr & 0xff, (flag_addr >> 8) & 0xff, (flag_addr >> 16) & 0xff]) + # BEQ .not_imp (offset 0x06) + subroutine_bytes.extend([0xf0, 0x06]) + + # LDA long boss_table,X (0xBF) + table_addr = boss_table_space.start_address_snes + subroutine_bytes.extend([0xbf, table_addr & 0xff, (table_addr >> 8) & 0xff, (table_addr >> 16) & 0xff]) + # BNE .is_imp (offset 0x12) + subroutine_bytes.extend([0xd0, 0x12]) + + # .not_imp: + # PLX (0xFA) + subroutine_bytes.append(0xfa) + # LDA $81A7 (0xAD, 0xA7, 0x81) + subroutine_bytes.extend([0xad, 0xa7, 0x81]) + # TAY (0xA8) + subroutine_bytes.append(0xa8) + # LDA #$00 (0xA9, 0x00) + subroutine_bytes.extend([0xa9, 0x00]) + # RTL (0x6B) + subroutine_bytes.append(0x6b) + + # .not_imp_16: + # SEP #$20 (0xE2, 0x20) + subroutine_bytes.extend([0xe2, 0x20]) + # PLX (0xFA) + subroutine_bytes.append(0xfa) + # LDA $81A7 (0xAD, 0xA7, 0x81) + subroutine_bytes.extend([0xad, 0xa7, 0x81]) + # TAY (0xA8) + subroutine_bytes.append(0xa8) + # LDA #$00 (0xA9, 0x00) + subroutine_bytes.extend([0xa9, 0x00]) + # RTL (0x6B) + subroutine_bytes.append(0x6b) + + # .is_imp: + # LDA $81A7 (0xAD, 0xA7, 0x81) + subroutine_bytes.extend([0xad, 0xa7, 0x81]) + # ASL A (0x0A) + subroutine_bytes.append(0x0a) + # TAX (0xAA) + subroutine_bytes.append(0xaa) + # REP #$20 (0xC2, 0x20) + subroutine_bytes.extend([0xc2, 0x20]) + # LDA #$0000 (0xA9, 0x00, 0x00) + subroutine_bytes.extend([0xa9, 0x00, 0x00]) + # STA $812F,X (0x9D, 0x2F, 0x81) + subroutine_bytes.extend([0x9d, 0x2f, 0x81]) + # SEP #$20 (0xE2, 0x20) + subroutine_bytes.extend([0xe2, 0x20]) + + # PLX (0xFA) + subroutine_bytes.append(0xfa) + # LDA $81A7 (0xAD, 0xA7, 0x81) + subroutine_bytes.extend([0xad, 0xa7, 0x81]) + # TAY (0xA8) + subroutine_bytes.append(0xa8) + # LDA #$01 (0xA9, 0x01) + subroutine_bytes.extend([0xa9, 0x01]) + # RTL (0x6B) + subroutine_bytes.append(0x6b) + + subroutine_space.write(subroutine_bytes) + + # 4. Patch the original graphics loader at ROM offset 0x01207B (Bank C1) + # We replace 9 bytes from 0x01207B to 0x012083 with: + # JSL check_imp_graphics (4 bytes) + # BEQ $09 (2 bytes) + # NOP (3 bytes) + patch_space = Reserve(0x01207b, 0x012083, "who's there imp graphics loader hook", asm.NOP()) + + sub_addr = subroutine_space.start_address_snes + patch_bytes = [ + 0x22, sub_addr & 0xff, (sub_addr >> 8) & 0xff, (sub_addr >> 16) & 0xff, # JSL check_imp_graphics + 0xf0, 0x09, # BEQ $09 (to 0x01208A) + 0xea, 0xea, 0xea # NOPs + ] + patch_space.write(patch_bytes) + def get_event_boss(self, original_boss_name): return self.packs.get_event_boss_replacement(original_boss_name) From 6e080b027071c0bb6dd8f28430a63094ee65008e Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Mon, 18 May 2026 13:05:07 -0400 Subject: [PATCH 07/21] Fix SrBehemoth and final battle --- data/enemies.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/data/enemies.py b/data/enemies.py index 5d5b8638..f5a908ef 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -436,8 +436,9 @@ def mod(self, maps): def who_there_mod(self): for enemy in self.enemies: - if enemy.id in bosses.enemy_name: - enemy.name = "??????" + 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): @@ -454,6 +455,13 @@ def who_there_assembly(self): if 0 <= enemy_id < 384: boss_table_bytes[enemy_id] = 1 + # Include SrBehemoth (Undead) Phase 2 + boss_table_bytes[282] = 1 + + # Exclude Final Battle Tiers + for excluded_id in range(343, 352): + boss_table_bytes[excluded_id] = 0 + boss_table_space = Allocate(Bank.F0, 384, "who's there boss table") boss_table_space.write(boss_table_bytes) From 32e911733fe3c73ccd9e1773056b51585c72bb98 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Mon, 18 May 2026 15:17:11 -0400 Subject: [PATCH 08/21] Refactor whos-there assembly to use instruction/asm.py DSL and WRAM ,X --- data/enemies.py | 200 +++++++++++++++++------------------------------- 1 file changed, 71 insertions(+), 129 deletions(-) diff --git a/data/enemies.py b/data/enemies.py index f5a908ef..8d874531 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -442,151 +442,93 @@ def who_there_mod(self): self.who_there_assembly() def who_there_assembly(self): - from memory.space import Allocate, Reserve, Bank + 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 = Allocate(Bank.F0, 1, "who's there flag") - who_there_flag_space.write([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 - - # Include SrBehemoth (Undead) Phase 2 - boss_table_bytes[282] = 1 - - # Exclude Final Battle Tiers - for excluded_id in range(343, 352): + 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 - boss_table_space = Allocate(Bank.F0, 384, "who's there boss table") - boss_table_space.write(boss_table_bytes) + 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 F0 - # Write custom assembly code in C0 to check the flag and table - subroutine_space = Allocate(Bank.C0, 120, "who's there check imp graphics") + # 3. Create the custom check_imp_graphics subroutine in Bank C0 + src = [ + asm.PHX(), + asm.TDC(), + asm.LDA(0x81A7, asm.ABS), + asm.TAY(), + asm.LDA(0x62C2, asm.ABS_Y), + asm.BNE("IS_IMP"), + + asm.LDA(0x81A7, asm.ABS), + asm.ASL(), + asm.TAX(), + + asm.A16(), + asm.LDA(0x812F, asm.ABS_X), # Use 812F as per documentation in agents.md + 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", + asm.PLX(), + asm.LDA(0x81A7, asm.ABS), + asm.TAY(), + asm.LDA(0x00, asm.IMM8), + asm.RTL(), + + "NOT_IMP_16", + asm.A8(), + asm.PLX(), + asm.LDA(0x81A7, asm.ABS), + asm.TAY(), + asm.LDA(0x00, asm.IMM8), + 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.PLX(), + asm.LDA(0x81A7, asm.ABS), + asm.TAY(), + asm.LDA(0x01, asm.IMM8), + asm.RTL(), + ] + subroutine_space = Write(Bank.C0, src, "who's there check imp graphics") sub_addr = subroutine_space.start_address_snes - subroutine_bytes = [] - - # PHX (0xDA) - subroutine_bytes.append(0xda) - # TDC (0x7B) - Clear 16-bit Accumulator - subroutine_bytes.append(0x7b) - - # LDA $81A7 (0xAD, 0xA7, 0x81) - subroutine_bytes.extend([0xad, 0xa7, 0x81]) - # TAY (0xA8) - subroutine_bytes.append(0xa8) - # LDA $62C2,Y (0xB9, 0xC2, 0x62) - subroutine_bytes.extend([0xb9, 0xc2, 0x62]) - # BNE .is_imp (offset 0x30) - subroutine_bytes.extend([0xd0, 0x30]) - - # LDA $81A7 (0xAD, 0xA7, 0x81) - subroutine_bytes.extend([0xad, 0xa7, 0x81]) - # ASL A (0x0A) - subroutine_bytes.append(0x0a) - # TAX (0xAA) - subroutine_bytes.append(0xaa) - - # REP #$20 (0xC2, 0x20) - subroutine_bytes.extend([0xc2, 0x20]) - # LDA $2001,X (0xBD, 0x01, 0x20) - subroutine_bytes.extend([0xbd, 0x01, 0x20]) - # CMP #384 (0xC9, 0x80, 0x01) - subroutine_bytes.extend([0xc9, 0x80, 0x01]) - # BCS .not_imp_16 (offset 0x17) - subroutine_bytes.extend([0xb0, 0x17]) - # TAX (0xAA) - subroutine_bytes.append(0xaa) - # SEP #$20 (0xE2, 0x20) - subroutine_bytes.extend([0xe2, 0x20]) - - # LDA long who_there_flag (0xAF) - flag_addr = who_there_flag_space.start_address_snes - subroutine_bytes.extend([0xaf, flag_addr & 0xff, (flag_addr >> 8) & 0xff, (flag_addr >> 16) & 0xff]) - # BEQ .not_imp (offset 0x06) - subroutine_bytes.extend([0xf0, 0x06]) - - # LDA long boss_table,X (0xBF) - table_addr = boss_table_space.start_address_snes - subroutine_bytes.extend([0xbf, table_addr & 0xff, (table_addr >> 8) & 0xff, (table_addr >> 16) & 0xff]) - # BNE .is_imp (offset 0x12) - subroutine_bytes.extend([0xd0, 0x12]) - - # .not_imp: - # PLX (0xFA) - subroutine_bytes.append(0xfa) - # LDA $81A7 (0xAD, 0xA7, 0x81) - subroutine_bytes.extend([0xad, 0xa7, 0x81]) - # TAY (0xA8) - subroutine_bytes.append(0xa8) - # LDA #$00 (0xA9, 0x00) - subroutine_bytes.extend([0xa9, 0x00]) - # RTL (0x6B) - subroutine_bytes.append(0x6b) - - # .not_imp_16: - # SEP #$20 (0xE2, 0x20) - subroutine_bytes.extend([0xe2, 0x20]) - # PLX (0xFA) - subroutine_bytes.append(0xfa) - # LDA $81A7 (0xAD, 0xA7, 0x81) - subroutine_bytes.extend([0xad, 0xa7, 0x81]) - # TAY (0xA8) - subroutine_bytes.append(0xa8) - # LDA #$00 (0xA9, 0x00) - subroutine_bytes.extend([0xa9, 0x00]) - # RTL (0x6B) - subroutine_bytes.append(0x6b) - - # .is_imp: - # LDA $81A7 (0xAD, 0xA7, 0x81) - subroutine_bytes.extend([0xad, 0xa7, 0x81]) - # ASL A (0x0A) - subroutine_bytes.append(0x0a) - # TAX (0xAA) - subroutine_bytes.append(0xaa) - # REP #$20 (0xC2, 0x20) - subroutine_bytes.extend([0xc2, 0x20]) - # LDA #$0000 (0xA9, 0x00, 0x00) - subroutine_bytes.extend([0xa9, 0x00, 0x00]) - # STA $812F,X (0x9D, 0x2F, 0x81) - subroutine_bytes.extend([0x9d, 0x2f, 0x81]) - # SEP #$20 (0xE2, 0x20) - subroutine_bytes.extend([0xe2, 0x20]) - - # PLX (0xFA) - subroutine_bytes.append(0xfa) - # LDA $81A7 (0xAD, 0xA7, 0x81) - subroutine_bytes.extend([0xad, 0xa7, 0x81]) - # TAY (0xA8) - subroutine_bytes.append(0xa8) - # LDA #$01 (0xA9, 0x01) - subroutine_bytes.extend([0xa9, 0x01]) - # RTL (0x6B) - subroutine_bytes.append(0x6b) - - subroutine_space.write(subroutine_bytes) - # 4. Patch the original graphics loader at ROM offset 0x01207B (Bank C1) - # We replace 9 bytes from 0x01207B to 0x012083 with: - # JSL check_imp_graphics (4 bytes) - # BEQ $09 (2 bytes) - # NOP (3 bytes) - patch_space = Reserve(0x01207b, 0x012083, "who's there imp graphics loader hook", asm.NOP()) - - sub_addr = subroutine_space.start_address_snes - patch_bytes = [ - 0x22, sub_addr & 0xff, (sub_addr >> 8) & 0xff, (sub_addr >> 16) & 0xff, # JSL check_imp_graphics - 0xf0, 0x09, # BEQ $09 (to 0x01208A) - 0xea, 0xea, 0xea # NOPs + patch_src = [ + asm.JSL(sub_addr), + asm.BEQ(0x09), # Branch to 0x01208A + asm.NOP(), + asm.NOP(), + asm.NOP(), ] - patch_space.write(patch_bytes) + Write(0x01207b, patch_src, "who's there imp graphics loader hook") def get_event_boss(self, original_boss_name): return self.packs.get_event_boss_replacement(original_boss_name) From 12f0a4605c75aa629d723cfe1f67b9ed97b17dff Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Wed, 27 May 2026 14:39:40 -0400 Subject: [PATCH 09/21] fix for whos there --- agents.md | 4 ++-- data/enemies.py | 2 +- llms.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agents.md b/agents.md index 76e4e727..5a28effb 100644 --- a/agents.md +++ b/agents.md @@ -158,8 +158,8 @@ AssertionError **Cause**: Modifying registers (such as `X` or `Y`) or using incorrect data bank instruction opcodes inside custom assembly subroutines. 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. **Resolution**: - Always preserve and restore modified registers using `PHX`/`PLX` or `PHY`/`PLY` at the boundaries of your subroutine. - 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. diff --git a/data/enemies.py b/data/enemies.py index 8d874531..226c142f 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -475,7 +475,7 @@ def who_there_assembly(self): asm.TAX(), asm.A16(), - asm.LDA(0x812F, asm.ABS_X), # Use 812F as per documentation in agents.md + 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(), diff --git a/llms.md b/llms.md index c03eea55..c2992885 100644 --- a/llms.md +++ b/llms.md @@ -190,6 +190,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 `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 `$2001,X` (where `X` is 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`). If a match is found, it overwrites WRAM `$812F,X` with `0` (the Imp ID) to redirect 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. - **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)`. From 7cdeb1d9fe4f0ad06d5cc21cbcb6bba8d0b874d5 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Mon, 18 May 2026 15:17:11 -0400 Subject: [PATCH 10/21] Refactor whos-there assembly to use instruction/asm.py DSL and WRAM ,X --- data/enemies.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/data/enemies.py b/data/enemies.py index 226c142f..59645094 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -442,12 +442,15 @@ def who_there_mod(self): self.who_there_assembly() def who_there_assembly(self): + from memory.space import Bank, Write 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 + 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 @@ -455,9 +458,12 @@ def who_there_assembly(self): 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[282] = 1 # Include SrBehemoth (Undead) Phase 2 for excluded_id in range(343, 352): # Exclude Final Battle Tiers boss_table_bytes[excluded_id] = 0 + boss_table_space = Write(Bank.F0, boss_table_bytes, "who's there boss table") boss_table_space = Write(Bank.F0, boss_table_bytes, "who's there boss table") table_addr = boss_table_space.start_address_snes @@ -530,6 +536,16 @@ def who_there_assembly(self): ] Write(0x01207b, patch_src, "who's there imp graphics loader hook") + # 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(), + ] + Write(0x01207b, patch_src, "who's there imp graphics loader hook") + def get_event_boss(self, original_boss_name): return self.packs.get_event_boss_replacement(original_boss_name) From 09d2cf118c90054a37f075db4da5014342d22e46 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Mon, 1 Jun 2026 07:57:28 -0400 Subject: [PATCH 11/21] Add "Who's There?" section to output text/flags menu --- args/bosses.py | 1 + args/graphics.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/args/bosses.py b/args/bosses.py index 2854fc1c..3043acdf 100644 --- a/args/bosses.py +++ b/args/bosses.py @@ -100,6 +100,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"), ] def menu(args): diff --git a/args/graphics.py b/args/graphics.py index d23580c7..2553406f 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -174,10 +174,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" @@ -192,17 +192,21 @@ def _other_options_log(args): if args.alternate_healing_text_color: healing_text = "Blue" - who_there = "Original" - if args.who_there: - who_there = "Imps" - - entries = [ + return [ ("Remove Flashes", remove_flashes, "remove_flashes"), ("World Minimap", world_minimap, "world_minimap"), ("Healing Text", healing_text, "healing_text"), - ("Who's There?", who_there, "who_there"), ] +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)) From e6fee16f18775182ba471a188676ddaa495c4b66 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Wed, 3 Jun 2026 08:53:00 -0400 Subject: [PATCH 12/21] udpates --- agents.md | 7 +++++-- data/enemies.py | 21 +++++++-------------- llms.md | 2 +- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/agents.md b/agents.md index 5a28effb..1cab809b 100644 --- a/agents.md +++ b/agents.md @@ -154,12 +154,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 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 `$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 59645094..ad9745ab 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -469,8 +469,10 @@ 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(), asm.LDA(0x81A7, asm.ABS), asm.TAY(), asm.LDA(0x62C2, asm.ABS_Y), @@ -493,19 +495,12 @@ def who_there_assembly(self): asm.LDA(table_addr, asm.LNG_X), asm.BNE("IS_IMP"), - "NOT_IMP", - asm.PLX(), - asm.LDA(0x81A7, asm.ABS), - asm.TAY(), - asm.LDA(0x00, asm.IMM8), - asm.RTL(), - "NOT_IMP_16", asm.A8(), - asm.PLX(), - asm.LDA(0x81A7, asm.ABS), - asm.TAY(), + "NOT_IMP", asm.LDA(0x00, asm.IMM8), + asm.PLX(), + asm.PLP(), asm.RTL(), "IS_IMP", @@ -516,11 +511,9 @@ def who_there_assembly(self): asm.LDA(0x0000, asm.IMM16), asm.STA(0x812F, asm.ABS_X), asm.A8(), - - asm.PLX(), - asm.LDA(0x81A7, asm.ABS), - asm.TAY(), asm.LDA(0x01, asm.IMM8), + asm.PLX(), + asm.PLP(), asm.RTL(), ] subroutine_space = Write(Bank.C0, src, "who's there check imp graphics") diff --git a/llms.md b/llms.md index c2992885..006a7d65 100644 --- a/llms.md +++ b/llms.md @@ -190,6 +190,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 `$2001,X` (where `X` is 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`). If a match is found, it overwrites WRAM `$812F,X` with `0` (the Imp ID) to redirect 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)`. From b5666bcfabfb25f51b8d11a5883082a553fe80ee Mon Sep 17 00:00:00 2001 From: Will Jones Date: Thu, 4 Jun 2026 15:46:28 -0400 Subject: [PATCH 13/21] Steve (#9) * Bump version to 1.4.4d * STEVE! * changes based on PR comments * Fix graphics menu alignment and visual sprite loader hook Z flag propagation * Fix Steveify flags menu display of None and support dynamic Arguments propagation * Change default Steveify name override to all-caps STEVE --- args/arguments.py | 9 +++++++++ args/graphics.py | 28 +++++++++++++++++++++++++++- data/characters.py | 4 ++++ data/dances.py | 4 ++++ data/data.py | 12 ++++++++++++ data/enemies.py | 9 +++++++++ data/espers.py | 6 ++++++ data/items.py | 5 +++++ data/lores.py | 4 ++++ data/magiteks.py | 4 ++++ data/spells.py | 17 +++++++++++++++++ data/swdtechs.py | 4 ++++ version.py | 2 +- 13 files changed, 106 insertions(+), 2 deletions(-) diff --git a/args/arguments.py b/args/arguments.py index cc372d13..9bcce51a 100644 --- a/args/arguments.py +++ b/args/arguments.py @@ -67,6 +67,15 @@ def __init__(self): if self.debug: self.spoiler_log = True + # Update the global args module attributes to match this Arguments instance, + # which is helpful if a wrapper script instantiates Arguments dynamically + # instead of importing the args module directly. + import sys + if "args" in sys.modules: + module = sys.modules["args"] + for name, value in self.__dict__.items(): + setattr(module, name, value) + def _process_min_max(self, arg_name): values = getattr(self, arg_name) if values: diff --git a/args/graphics.py b/args/graphics.py index 7f3f7b13..76efd16a 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -22,11 +22,30 @@ 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("-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): + if args.steveify: + args.steveify = "STEVE" + else: + args.steveify = None + elif not args.steveify or args.steveify.isspace() or args.steveify.lower() in ("none", "false"): + if args.steveify.lower() in ("none", "false"): + args.steveify = None + else: + args.steveify = "STEVE" + + if args.steveify is not None: + if len(args.steveify) > 6: + args.steveify = args.steveify[:6] + if args.character_names is not None: args.names = args.character_names.split('.') if len(args.names) != Characters.CHARACTER_COUNT: @@ -40,6 +59,9 @@ def process(args): else: args.names = Characters.DEFAULT_NAME + if args.steveify is not None: + args.names = [args.steveify] * Characters.CHARACTER_COUNT + args.palettes = [] if args.character_palettes: args.palette_ids = [int(palette_id) for palette_id in args.character_palettes.split('.')] @@ -101,6 +123,8 @@ def flags(args): if args.character_names: flags += " -name " + args.character_names + if args.steveify: + flags += " -steve " + args.steveify if args.character_palettes: flags += " -cpal " + args.character_palettes if args.character_portraits: @@ -189,8 +213,10 @@ def _other_options_log(args): entries = [ ("Remove Flashes", remove_flashes, "remove_flashes"), - ("World Minimap", world_minimap, "world_minimap"), + ("Minimap", world_minimap, "world_minimap"), ("Healing Text", healing_text, "healing_text"), + ("Who's There?", who_there, "who_there"), + ("Steveify", args.steveify if args.steveify else "None", "steveify"), ] for entry in entries: diff --git a/data/characters.py b/data/characters.py index c5f258d5..761075b0 100644 --- a/data/characters.py +++ b/data/characters.py @@ -133,6 +133,10 @@ def mod(self): if self.args.character_names: self.mod_names() + if self.args.steveify: + for character in self.characters: + character.name = self.args.steveify + if self.args.original_name_display: characters_asm.show_original_names() diff --git a/data/dances.py b/data/dances.py index 843249ea..f26cfec3 100644 --- a/data/dances.py +++ b/data/dances.py @@ -164,6 +164,10 @@ def shuffle(self): dance.dances = abilities[ability_index : ability_index + self.DATA_SIZE] def mod(self): + if self.args.steveify: + for dance in self.dances: + dance.name = self.args.steveify + self.write_learners_table() self.write_is_learner() self.after_battle_check_mod() diff --git a/data/data.py b/data/data.py index 3624b986..372a6ac9 100644 --- a/data/data.py +++ b/data/data.py @@ -21,6 +21,8 @@ class Data: def __init__(self, rom, args): + self.rom = rom + self.args = args self.dialogs = dialogs self.spells = spells.Spells(rom, args) @@ -81,6 +83,16 @@ def __init__(self, rom, args): self.title_graphics.mod() def write(self): + if self.args.steveify: + import data.text as text + ability_name_bytes = bytearray() + for i in range(175): + name_bytes = bytearray(text.get_bytes(self.args.steveify, text.TEXT2)) + name_bytes = name_bytes[:10] + name_bytes.extend([0xff] * (10 - len(name_bytes))) + ability_name_bytes.extend(name_bytes) + self.rom.set_bytes(0x26f7b9, ability_name_bytes) + self.dialogs.write() self.characters.write() self.items.write() diff --git a/data/enemies.py b/data/enemies.py index 7e16dd83..b384cf45 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -75,6 +75,8 @@ def get_enemy(self, name): return enemy.id def get_name(self, enemy_id): + if self.args.steveify: + return self.args.steveify if enemy_id in bosses.enemy_name: return bosses.enemy_name[enemy_id] return self.enemies[enemy_id].name @@ -439,6 +441,13 @@ def print(self): enemy.print() def write(self): + if self.args.steveify: + for enemy in self.enemies: + if enemy.name: + enemy.name = self.args.steveify + if enemy.special_name: + enemy.special_name = self.args.steveify + for enemy_index in range(len(self.enemies)): self.enemy_data[enemy_index] = self.enemies[enemy_index].data() self.enemy_name_data[enemy_index] = self.enemies[enemy_index].name_data() diff --git a/data/espers.py b/data/espers.py index bf2c0957..b99fb44c 100644 --- a/data/espers.py +++ b/data/espers.py @@ -275,6 +275,10 @@ def multi_summon(self): space = Reserve(0x24da3, 0x24da5, "espers set used in battle bit", asm.NOP()) def mod(self, dialogs): + if self.args.steveify: + for esper in self.espers: + esper.name = self.args.steveify + self.receive_dialogs_mod(dialogs) if self.args.esper_spells_shuffle or self.args.esper_spells_shuffle_random_rates: @@ -353,6 +357,8 @@ def get_receive_esper_dialog(self, esper): return self.receive_dialogs[esper] def get_name(self, esper): + if self.args.steveify: + return self.args.steveify return self.esper_names[esper] def log(self): diff --git a/data/items.py b/data/items.py index f90bf5b1..06aaec5e 100644 --- a/data/items.py +++ b/data/items.py @@ -220,6 +220,11 @@ def moogle_starting_equipment(self): self.characters.characters[index].init_head = random.choice(tiers[Item.HELMET][1]) def mod(self): + if self.args.steveify: + for item in self.items: + if item.id != self.EMPTY: + item.name = self.args.steveify + not_relic_condition = lambda x: x != Item.RELIC if self.args.item_equipable_random: self.equipable_random(not_relic_condition, self.args.item_equipable_random_min, diff --git a/data/lores.py b/data/lores.py index c4cfa602..d0eff7ea 100644 --- a/data/lores.py +++ b/data/lores.py @@ -253,6 +253,10 @@ def show_mp_mod(self): ) def mod(self, dialogs): + if self.args.steveify: + for lore in self.lores: + lore.name = self.args.steveify + self.write_learners_table() self.write_is_learner() self.after_battle_check_mod() diff --git a/data/magiteks.py b/data/magiteks.py index 1dadccec..8cb93327 100644 --- a/data/magiteks.py +++ b/data/magiteks.py @@ -34,6 +34,10 @@ def fix_reflectable_beams(self): self.magiteks[self.ICE_BEAM].flags2 = 0x22 def mod(self): + if self.args.steveify: + for magitek in self.magiteks: + magitek.name = self.args.steveify + self.fix_reflectable_beams() pass diff --git a/data/spells.py b/data/spells.py index ad82b430..47374109 100644 --- a/data/spells.py +++ b/data/spells.py @@ -102,6 +102,23 @@ def alternate_healing_text_color(self): space.write(0x44, 0x7f) #default: F6 4B def mod(self): + if self.args.steveify: + for spell in self.spells: + icon = "" + for tag in ['', '', '']: + if spell.name.startswith(tag): + icon = tag + break + suffix = "" + for s in [" 2", " 3", "2"]: + if spell.name.endswith(s): + suffix = s + break + + max_steve_len = 6 - len(suffix) + steve_part = self.args.steveify[:max_steve_len] + spell.name = f"{icon}{steve_part}{suffix}" + if self.args.magic_mp_shuffle: self.shuffle_mp() elif self.args.magic_mp_random_value: diff --git a/data/swdtechs.py b/data/swdtechs.py index 847cbe85..866c2d5a 100644 --- a/data/swdtechs.py +++ b/data/swdtechs.py @@ -130,6 +130,10 @@ def enable_fast_swdtech(self): space.write(0x00) def mod(self): + if self.args.steveify: + for swdtech in self.swdtechs: + swdtech.name = self.args.steveify + self.write_learners_table() self.write_is_learner() diff --git a/version.py b/version.py index aa56ed40..652d004a 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -__version__ = "1.4.3" +__version__ = "1.4.4d" From 119e36cb2d4bc5c3455e9b02ade285a24982704d Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 15:56:14 -0400 Subject: [PATCH 14/21] Remove "who_there" reference --- args/graphics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/args/graphics.py b/args/graphics.py index 76efd16a..bd9506d9 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -215,7 +215,6 @@ def _other_options_log(args): ("Remove Flashes", remove_flashes, "remove_flashes"), ("Minimap", world_minimap, "world_minimap"), ("Healing Text", healing_text, "healing_text"), - ("Who's There?", who_there, "who_there"), ("Steveify", args.steveify if args.steveify else "None", "steveify"), ] From 17a5e0f4fb02438b8e7b3619b2c11ee527f9bbfa Mon Sep 17 00:00:00 2001 From: Will Jones Date: Thu, 4 Jun 2026 15:57:23 -0400 Subject: [PATCH 15/21] Update data/data.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- data/data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/data.py b/data/data.py index 372a6ac9..b79315b9 100644 --- a/data/data.py +++ b/data/data.py @@ -86,10 +86,10 @@ def write(self): if self.args.steveify: import data.text as text ability_name_bytes = bytearray() + name_bytes = bytearray(text.get_bytes(self.args.steveify, text.TEXT2)) + name_bytes = name_bytes[:10] + name_bytes.extend([0xff] * (10 - len(name_bytes))) for i in range(175): - name_bytes = bytearray(text.get_bytes(self.args.steveify, text.TEXT2)) - name_bytes = name_bytes[:10] - name_bytes.extend([0xff] * (10 - len(name_bytes))) ability_name_bytes.extend(name_bytes) self.rom.set_bytes(0x26f7b9, ability_name_bytes) From 152350018ad4617a2e5e66b939255fc8e7438f3b Mon Sep 17 00:00:00 2001 From: Will Jones Date: Thu, 4 Jun 2026 16:02:16 -0400 Subject: [PATCH 16/21] Update data/items.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- data/items.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/data/items.py b/data/items.py index 06aaec5e..0338b72e 100644 --- a/data/items.py +++ b/data/items.py @@ -223,8 +223,12 @@ def mod(self): if self.args.steveify: for item in self.items: if item.id != self.EMPTY: - item.name = self.args.steveify - + icon = "" + if item.name.startswith("<"): + end_tag = item.name.find(">") + if end_tag != -1: + icon = item.name[:end_tag + 1] + item.name = f"{icon}{self.args.steveify}" not_relic_condition = lambda x: x != Item.RELIC if self.args.item_equipable_random: self.equipable_random(not_relic_condition, self.args.item_equipable_random_min, From 4531ee7facff5dc80ca75b71e6eb99397315ebd4 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 16:05:49 -0400 Subject: [PATCH 17/21] de-steveification --- args/graphics.py | 40 +--------------------------------------- data/characters.py | 4 ---- data/dances.py | 4 ---- data/data.py | 10 ---------- data/enemies.py | 9 --------- data/espers.py | 6 ------ data/items.py | 5 ----- data/lores.py | 4 ---- data/magiteks.py | 4 ---- data/spells.py | 17 ----------------- data/swdtechs.py | 4 ---- 11 files changed, 1 insertion(+), 106 deletions(-) diff --git a/args/graphics.py b/args/graphics.py index f5934271..429bc040 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -24,46 +24,12 @@ def parse(parser): graphics.add_argument("-who", "--who-there", action = "store_true", help = "Who's There? Bosses look like Imps and have the name '??????'") - 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): - if args.steveify: - args.steveify = "STEVE" - else: - args.steveify = None - elif not args.steveify or args.steveify.isspace() or args.steveify.lower() in ("none", "false"): - if args.steveify.lower() in ("none", "false"): - args.steveify = None - else: - args.steveify = "STEVE" - - if args.steveify is not None: - if len(args.steveify) > 6: - args.steveify = args.steveify[:6] - - if args.character_names is not None: - args.names = args.character_names.split('.') - if len(args.names) != Characters.CHARACTER_COUNT: - raise ValueError(f"Invalid number of name arguments ({len(args.names)} should be {Characters.CHARACTER_COUNT})") - - for index in range(len(args.names)): - if args.names[index]: - args.names[index] = args.names[index][ : Characters.NAME_SIZE] - else: - args.names[index] = Characters.DEFAULT_NAME[index] - else: - args.names = Characters.DEFAULT_NAME - - if args.steveify is not None: - args.names = [args.steveify] * Characters.CHARACTER_COUNT - + args.palettes = [] if args.character_palettes: args.palette_ids = [int(palette_id) for palette_id in args.character_palettes.split('.')] @@ -125,8 +91,6 @@ def flags(args): if args.character_names: flags += " -name " + args.character_names - if args.steveify: - flags += " -steve " + args.steveify if args.character_palettes: flags += " -cpal " + args.character_palettes if args.character_portraits: @@ -219,8 +183,6 @@ def options(args): ("Remove Flashes", remove_flashes, "remove_flashes"), ("Minimap", world_minimap, "world_minimap"), ("Healing Text", healing_text, "healing_text"), - ("Who's There?", who_there, "who_there"), - ("Steveify", args.steveify if args.steveify else "None", "steveify"), ] def menu(args): diff --git a/data/characters.py b/data/characters.py index 761075b0..c5f258d5 100644 --- a/data/characters.py +++ b/data/characters.py @@ -133,10 +133,6 @@ def mod(self): if self.args.character_names: self.mod_names() - if self.args.steveify: - for character in self.characters: - character.name = self.args.steveify - if self.args.original_name_display: characters_asm.show_original_names() diff --git a/data/dances.py b/data/dances.py index f26cfec3..843249ea 100644 --- a/data/dances.py +++ b/data/dances.py @@ -164,10 +164,6 @@ def shuffle(self): dance.dances = abilities[ability_index : ability_index + self.DATA_SIZE] def mod(self): - if self.args.steveify: - for dance in self.dances: - dance.name = self.args.steveify - self.write_learners_table() self.write_is_learner() self.after_battle_check_mod() diff --git a/data/data.py b/data/data.py index 372a6ac9..3bc6111f 100644 --- a/data/data.py +++ b/data/data.py @@ -83,16 +83,6 @@ def __init__(self, rom, args): self.title_graphics.mod() def write(self): - if self.args.steveify: - import data.text as text - ability_name_bytes = bytearray() - for i in range(175): - name_bytes = bytearray(text.get_bytes(self.args.steveify, text.TEXT2)) - name_bytes = name_bytes[:10] - name_bytes.extend([0xff] * (10 - len(name_bytes))) - ability_name_bytes.extend(name_bytes) - self.rom.set_bytes(0x26f7b9, ability_name_bytes) - self.dialogs.write() self.characters.write() self.items.write() diff --git a/data/enemies.py b/data/enemies.py index 74e023b3..ad9745ab 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -75,8 +75,6 @@ def get_enemy(self, name): return enemy.id def get_name(self, enemy_id): - if self.args.steveify: - return self.args.steveify if enemy_id in bosses.enemy_name: return bosses.enemy_name[enemy_id] return self.enemies[enemy_id].name @@ -549,13 +547,6 @@ def print(self): enemy.print() def write(self): - if self.args.steveify: - for enemy in self.enemies: - if enemy.name: - enemy.name = self.args.steveify - if enemy.special_name: - enemy.special_name = self.args.steveify - for enemy_index in range(len(self.enemies)): self.enemy_data[enemy_index] = self.enemies[enemy_index].data() self.enemy_name_data[enemy_index] = self.enemies[enemy_index].name_data() diff --git a/data/espers.py b/data/espers.py index b99fb44c..bf2c0957 100644 --- a/data/espers.py +++ b/data/espers.py @@ -275,10 +275,6 @@ def multi_summon(self): space = Reserve(0x24da3, 0x24da5, "espers set used in battle bit", asm.NOP()) def mod(self, dialogs): - if self.args.steveify: - for esper in self.espers: - esper.name = self.args.steveify - self.receive_dialogs_mod(dialogs) if self.args.esper_spells_shuffle or self.args.esper_spells_shuffle_random_rates: @@ -357,8 +353,6 @@ def get_receive_esper_dialog(self, esper): return self.receive_dialogs[esper] def get_name(self, esper): - if self.args.steveify: - return self.args.steveify return self.esper_names[esper] def log(self): diff --git a/data/items.py b/data/items.py index 06aaec5e..f90bf5b1 100644 --- a/data/items.py +++ b/data/items.py @@ -220,11 +220,6 @@ def moogle_starting_equipment(self): self.characters.characters[index].init_head = random.choice(tiers[Item.HELMET][1]) def mod(self): - if self.args.steveify: - for item in self.items: - if item.id != self.EMPTY: - item.name = self.args.steveify - not_relic_condition = lambda x: x != Item.RELIC if self.args.item_equipable_random: self.equipable_random(not_relic_condition, self.args.item_equipable_random_min, diff --git a/data/lores.py b/data/lores.py index d0eff7ea..c4cfa602 100644 --- a/data/lores.py +++ b/data/lores.py @@ -253,10 +253,6 @@ def show_mp_mod(self): ) def mod(self, dialogs): - if self.args.steveify: - for lore in self.lores: - lore.name = self.args.steveify - self.write_learners_table() self.write_is_learner() self.after_battle_check_mod() diff --git a/data/magiteks.py b/data/magiteks.py index 8cb93327..1dadccec 100644 --- a/data/magiteks.py +++ b/data/magiteks.py @@ -34,10 +34,6 @@ def fix_reflectable_beams(self): self.magiteks[self.ICE_BEAM].flags2 = 0x22 def mod(self): - if self.args.steveify: - for magitek in self.magiteks: - magitek.name = self.args.steveify - self.fix_reflectable_beams() pass diff --git a/data/spells.py b/data/spells.py index 47374109..ad82b430 100644 --- a/data/spells.py +++ b/data/spells.py @@ -102,23 +102,6 @@ def alternate_healing_text_color(self): space.write(0x44, 0x7f) #default: F6 4B def mod(self): - if self.args.steveify: - for spell in self.spells: - icon = "" - for tag in ['', '', '']: - if spell.name.startswith(tag): - icon = tag - break - suffix = "" - for s in [" 2", " 3", "2"]: - if spell.name.endswith(s): - suffix = s - break - - max_steve_len = 6 - len(suffix) - steve_part = self.args.steveify[:max_steve_len] - spell.name = f"{icon}{steve_part}{suffix}" - if self.args.magic_mp_shuffle: self.shuffle_mp() elif self.args.magic_mp_random_value: diff --git a/data/swdtechs.py b/data/swdtechs.py index 866c2d5a..847cbe85 100644 --- a/data/swdtechs.py +++ b/data/swdtechs.py @@ -130,10 +130,6 @@ def enable_fast_swdtech(self): space.write(0x00) def mod(self): - if self.args.steveify: - for swdtech in self.swdtechs: - swdtech.name = self.args.steveify - self.write_learners_table() self.write_is_learner() From c4f3ad579d7a9d818d80c5e9e839b49574510f9e Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 16:22:13 -0400 Subject: [PATCH 18/21] fix steveify logic and update name handling in espers and lores - Improved validation of `steveify` argument. - Updated `Espers.get_name` to use internal data directly. - Enhanced `Lores.mod` method for dynamic name modification based on regex. --- args/graphics.py | 8 +++++--- data/espers.py | 4 +--- data/lores.py | 13 +++++++++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/args/graphics.py b/args/graphics.py index bd9506d9..6d612819 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -36,10 +36,12 @@ def process(args): args.steveify = "STEVE" else: args.steveify = None - elif not args.steveify or args.steveify.isspace() or args.steveify.lower() in ("none", "false"): - if args.steveify.lower() in ("none", "false"): + else: + args.steveify = args.steveify.strip() + steveify_upper = args.steveify.upper() + if steveify_upper in ("NONE", "FALSE"): args.steveify = None - else: + elif not args.steveify: args.steveify = "STEVE" if args.steveify is not None: diff --git a/data/espers.py b/data/espers.py index b99fb44c..c5064814 100644 --- a/data/espers.py +++ b/data/espers.py @@ -357,9 +357,7 @@ def get_receive_esper_dialog(self, esper): return self.receive_dialogs[esper] def get_name(self, esper): - if self.args.steveify: - return self.args.steveify - return self.esper_names[esper] + return self.espers[esper].name def log(self): from log import COLUMN_WIDTH, section_entries, format_option diff --git a/data/lores.py b/data/lores.py index d0eff7ea..0029d504 100644 --- a/data/lores.py +++ b/data/lores.py @@ -253,10 +253,6 @@ def show_mp_mod(self): ) def mod(self, dialogs): - if self.args.steveify: - for lore in self.lores: - lore.name = self.args.steveify - self.write_learners_table() self.write_is_learner() self.after_battle_check_mod() @@ -275,6 +271,15 @@ def mod(self, dialogs): if self.args.lores_level_randomize: self.random_lx_levels(dialogs) + if self.args.steveify: + import re + for lore in self.lores: + match = re.search('L.*[?1-9]', lore.name) + if match: + lore.name = f"{match.group()} {self.args.steveify}"[:self.NAME_SIZE] + else: + lore.name = self.args.steveify[:self.NAME_SIZE] + def write(self): if self.args.spoiler_log: self.log() From 0bc8615ef38bc23ff023106bc633f5ed9f6b46cc Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 16:27:19 -0400 Subject: [PATCH 19/21] fix and clarify logic in `graphics.py` and optimize assembly handling in `enemies.py` - Added validation for character names length in `graphics.py`. - Removed redundant code in `who_there_assembly` method of `enemies.py`. - Simplified the boss table exclusion logic. - Removed duplicate patching code for the graphics loader hook. --- args/graphics.py | 13 +++++++++++++ data/enemies.py | 16 ---------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/args/graphics.py b/args/graphics.py index 429bc040..573b4130 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -86,6 +86,19 @@ def process(args): else: args.sprite_palettes = DEFAULT_CHARACTER_SPRITE_PALETTES + if args.character_names is not None: + args.names = args.character_names.split('.') + if len(args.names) != Characters.CHARACTER_COUNT: + raise ValueError(f'Invalid number of name arguments ({len(args.names)} should be {Characters.CHARACTER_COUNT})') + + for index in range(len(args.names)): + if args.names[index]: + args.names[index] = args.names[index][ : Characters.NAME_SIZE] + else: + args.names[index] = Characters.DEFAULT_NAME[index] + else: + args.names = Characters.DEFAULT_NAME + def flags(args): flags = "" diff --git a/data/enemies.py b/data/enemies.py index ad9745ab..f748dfc4 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -442,15 +442,12 @@ def who_there_mod(self): self.who_there_assembly() def who_there_assembly(self): - from memory.space import Bank, Write 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 - 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 @@ -458,12 +455,9 @@ def who_there_assembly(self): 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[282] = 1 # Include SrBehemoth (Undead) Phase 2 for excluded_id in range(343, 352): # Exclude Final Battle Tiers boss_table_bytes[excluded_id] = 0 - boss_table_space = Write(Bank.F0, boss_table_bytes, "who's there boss table") boss_table_space = Write(Bank.F0, boss_table_bytes, "who's there boss table") table_addr = boss_table_space.start_address_snes @@ -529,16 +523,6 @@ def who_there_assembly(self): ] Write(0x01207b, patch_src, "who's there imp graphics loader hook") - # 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(), - ] - Write(0x01207b, patch_src, "who's there imp graphics loader hook") - def get_event_boss(self, original_boss_name): return self.packs.get_event_boss_replacement(original_boss_name) From 8e477cef31aec2e664a48597ebd4c5d48230ef1c Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Thu, 4 Jun 2026 16:27:19 -0400 Subject: [PATCH 20/21] fix and clarify logic in `graphics.py` and optimize assembly handling in `enemies.py` - Added validation for character names length in `graphics.py`. - Removed redundant code in `who_there_assembly` method of `enemies.py`. - Simplified the boss table exclusion logic. - Removed duplicate patching code for the graphics loader hook. --- args/graphics.py | 13 +++++++++++++ data/enemies.py | 16 ---------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/args/graphics.py b/args/graphics.py index 2553406f..93794ed4 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -99,6 +99,19 @@ def process(args): else: args.sprite_palettes = DEFAULT_CHARACTER_SPRITE_PALETTES + if args.character_names is not None: + args.names = args.character_names.split('.') + if len(args.names) != Characters.CHARACTER_COUNT: + raise ValueError(f'Invalid number of name arguments ({len(args.names)} should be {Characters.CHARACTER_COUNT})') + + for index in range(len(args.names)): + if args.names[index]: + args.names[index] = args.names[index][ : Characters.NAME_SIZE] + else: + args.names[index] = Characters.DEFAULT_NAME[index] + else: + args.names = Characters.DEFAULT_NAME + def flags(args): flags = "" diff --git a/data/enemies.py b/data/enemies.py index ad9745ab..f748dfc4 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -442,15 +442,12 @@ def who_there_mod(self): self.who_there_assembly() def who_there_assembly(self): - from memory.space import Bank, Write 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 - 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 @@ -458,12 +455,9 @@ def who_there_assembly(self): 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[282] = 1 # Include SrBehemoth (Undead) Phase 2 for excluded_id in range(343, 352): # Exclude Final Battle Tiers boss_table_bytes[excluded_id] = 0 - boss_table_space = Write(Bank.F0, boss_table_bytes, "who's there boss table") boss_table_space = Write(Bank.F0, boss_table_bytes, "who's there boss table") table_addr = boss_table_space.start_address_snes @@ -529,16 +523,6 @@ def who_there_assembly(self): ] Write(0x01207b, patch_src, "who's there imp graphics loader hook") - # 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(), - ] - Write(0x01207b, patch_src, "who's there imp graphics loader hook") - def get_event_boss(self, original_boss_name): return self.packs.get_event_boss_replacement(original_boss_name) From 231c443755770de28ee4f951f07af4c391e134a9 Mon Sep 17 00:00:00 2001 From: wrjones104 Date: Tue, 23 Jun 2026 14:06:35 -0400 Subject: [PATCH 21/21] Refactor name processing in graphics.py and add NOP in enemies.py - Removed redundant name processing logic from `graphics.py`. - Added an additional NOP instruction in the enemy graphics loading sequence in `enemies.py`. --- args/graphics.py | 13 +------------ data/enemies.py | 1 + 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/args/graphics.py b/args/graphics.py index 13aa2f1b..563c021f 100644 --- a/args/graphics.py +++ b/args/graphics.py @@ -86,18 +86,7 @@ def process(args): else: args.sprite_palettes = DEFAULT_CHARACTER_SPRITE_PALETTES - if args.character_names is not None: - args.names = args.character_names.split('.') - if len(args.names) != Characters.CHARACTER_COUNT: - raise ValueError(f'Invalid number of name arguments ({len(args.names)} should be {Characters.CHARACTER_COUNT})') - - for index in range(len(args.names)): - if args.names[index]: - args.names[index] = args.names[index][ : Characters.NAME_SIZE] - else: - args.names[index] = Characters.DEFAULT_NAME[index] - else: - args.names = Characters.DEFAULT_NAME + def flags(args): flags = "" diff --git a/data/enemies.py b/data/enemies.py index 3a57f03c..6ff321a8 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -522,6 +522,7 @@ def who_there_assembly(self): asm.NOP(), asm.NOP(), asm.NOP(), + asm.NOP(), ] Write(0x01207b, patch_src, "who's there imp graphics loader hook")