diff --git a/.gitignore b/.gitignore index f1cccbe..fbdc6fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ build/** *.smc +*.sfc oldies +# a816 build artifacts +a.out +a.out.adbg +*.o + # Generated build assets (keep intro source files) assets/* !assets/intro.col diff --git a/a816.toml b/a816.toml new file mode 100644 index 0000000..fcab65d --- /dev/null +++ b/a816.toml @@ -0,0 +1,3 @@ +entrypoint = "ff4.s" +module-paths = ["build/obj", "src"] +include-paths = ["src"] diff --git a/build.py b/build.py index 93816a5..2b77f0a 100755 --- a/build.py +++ b/build.py @@ -86,7 +86,7 @@ def assets_need_refresh(source, destination): def build_patch(input, output, lang): - from a816.module_builder import build_with_imports_direct + from a816.module_builder import build_with_imports obj_dir = Path("build/obj") if obj_dir.exists(): @@ -97,15 +97,15 @@ def build_patch(input, output, lang): if out_path.exists(): out_path.unlink() - result = build_with_imports_direct( + result = build_with_imports( main_source=Path(input), output_file=Path(output), output_format="ips", module_paths=[Path("build/obj"), Path("src")], output_dir=Path("build/obj"), symbols={"LANG": lang}, - include_paths=[Path("src")], - prelude_file=Path("config.i"), + include_paths=[Path("src"), Path(".")], + overlap_mode="warn", ) if result.exit_code != 0: @@ -165,7 +165,9 @@ def build_fixed_asset(table, input_file, binary_text_file): write_pointers_value_as_binary(pointers, binary_text_file) -def build_fixed_to_ptr_asset(table, input_file, binary_text_file, pointers_file, buffer_width=None): +def build_fixed_to_ptr_asset( + table, input_file, binary_text_file, pointers_file, buffer_width=None +): pointers = read_fixed_from_xml( input_file, table, formatter=lambda t: t.strip() + "[end]" ) @@ -184,7 +186,7 @@ def build_fixed_to_ptr_asset(table, input_file, binary_text_file, pointers_file, print(f"{text} is too long ({ptr_len}px , {math.ceil(ptr_len / 8)} tiles)") text = table.to_text(max_ptr[1].value) - print(f"{text} is the largest ({max_length}px ({math.ceil(max_length/8)})") + print(f"{text} is the largest ({max_length}px ({math.ceil(max_length / 8)})") write_pointers_value_as_binary(pointers, binary_text_file) write_pointers_addresses_as_binary( @@ -327,8 +329,7 @@ def add_custom_kernings(text: str, advance: int) -> None: known_pairs_to_kern = [ "Ya", "Pa", - "Po" - "Fa", + "PoFa", "Fe", "Fo", "Fu", @@ -489,6 +490,12 @@ def build_assets(assets): menu_table, ), ("fixed", menu_table, os.path.join(text_root, "items.xml"), "assets/items.dat"), + ( + "fixed", + menu_table, + os.path.join(text_root, "items_unleashed.xml"), + "assets/items_unleashed.dat", + ), ("fixed", menu_table, os.path.join(text_root, "magic.xml"), "assets/magic.dat"), ( "fixed", @@ -528,7 +535,7 @@ def build_assets(assets): os.path.join(text_root, "monsters_long.xml"), "assets/monsters_long.dat", "assets/monsters_long.ptr", - 80 + 80, ), ( "nullterminated", diff --git a/config.i b/config.i index a3e296a..a185474 100644 --- a/config.i +++ b/config.i @@ -13,6 +13,7 @@ ENABLE_BUTTON_DISPLAY := 1 BATTLE_CMD_VWF := 1 BATTLE_NAMES_VWF := 1 BATTLE_MONSTERS_VWF := 1 +BATTLE_ITEMS_VWF := 1 INVENTORY_ROLLING_BUFFER := 1 TREASURE_INVENTORY_ROLLING := 1 ; WIP, treasure rolling-buffer scaffolding only TREASURE_DEBUG_ALWAYS_DROP := 1 ; force every win to drop an item (testing) diff --git a/ff4.s b/ff4.s index 45b4942..7aa4c97 100644 --- a/ff4.s +++ b/ff4.s @@ -4,8 +4,35 @@ Final Fantasy IV the new hack. ---------------- """ -; Forward declaration - conditional_bg1_vofs is at start of relocated region ($208000) -conditional_bg1_vofs := 0x208000 +; Auto-prepended: imports must precede .include'd patches +.import "assets" +.import "battle/commands_reloc" +.import "battle/equip_window" +.import "battle/graphics" +.import "battle/inventory_rolling" +.import "battle/items_reloc" +.import "battle/magic_reloc" +.import "battle/math_reloc" +.import "battle/monsters_reloc" +.import "battle/redraw_gates" +.import "battle/sram" +.import "dakuten" +.import "dialog" +.import "ingame/init_bg_scroll_hdma" +.import "ingame/items_menu_vwf" +.import "ingame/places_names_window" +.import "intro" +.import "kerning" +.import "libmz" +.import "menus/in_game_text" +.import "menus/start_screen_text" +.import "menus/system_menus_text" +.import "menus/tools_shop_text" +.import "small_vwf/init" +.import "vwf" + +.include "config.i" + .include "src/libmz.i" .include "src/items.i" @@ -42,7 +69,7 @@ conditional_bg1_vofs := 0x208000 .include "src/ingame/items_menu.s" ; Relocated init_bg_scroll_hdma (was at $01:EBD2, frees 566 bytes in bank $01). -; Blob with internal absolute references — pinned to offset $EBD2 within an +; Blob with internal absolute references - pinned to offset $EBD2 within an ; expansion bank. Caller patch retargets the single JSL at $02:818A. .if INVENTORY_ROLLING_BUFFER { .include "src/ingame/init_bg_scroll_hdma_patches.s" @@ -53,42 +80,52 @@ conditional_bg1_vofs := 0x208000 dialog_bank_ptr_base = 0x218000 -*=0xFFC0 - ; patch snes cartridge type - ; original PCB: SHVC-1A3B - ; target PCB: SHVC-1A5B +.alloc at 0x00FFC0 { +; patch snes cartridge type +; original PCB: SHVC-1A3B ; target PCB: SHVC-1A5B .ascii "Final Fantasy IV " +} -;FFD5 20H / 30H Map Mode - -*=0xFFD6 +.alloc at 0x00FFD6 { +; FFD5 20H / 30H Map Mode .db 0x02 ; Cartridge Type .db 0x0B ; ~ 0BH ROM Size .db 0x07 ; RAM Size +} + .if ENABLE_BRK_HANDLER { - *=0x00FFE0 -; JML trampoline in vector-table padding ; native/emu BRK vectors point here. +; JML trampoline in vector-table padding; native/emu BRK vectors point here. + .alloc at 0x00FFE0 { jmp.l brk_handler - *=0x00FFE6 + } + + + .alloc at 0x00FFE6 { .dw 0xFFE0 - *=0x00FFFE + } + + + .alloc at 0x00FFFE { .dw 0xFFE0 + } } -*=0x008031 - ; déroutage pour ajouter le splash screen -.if ENABLE_INTRO { +; déroutage pour ajouter le splash screen +.alloc at 0x008031 { + .if ENABLE_INTRO { jsr.l start_splash_screen -} else { + } else { jsr.l clear_ram + } } -*=0x00B463 - ; déroutage pour utiliser la vwf dans les dialogues. +; déroutage pour utiliser la vwf dans les dialogues. +.alloc at 0x00B463 { jsr.l vwfstart rts +} ; ============================================================================ ; Bank-20 relocated region. @@ -113,7 +150,8 @@ dialog_bank_ptr_base = 0x218000 ; Address is pinned by `conditional_bg1_vofs := 0x208000` at the top of ; this file ; `strategy order` keeps it first in the pool. .if INVENTORY_ROLLING_BUFFER { - lda.l 0x7E0000 + menu_hdma_enable +conditional_bg1_vofs: + lda.l field_menu_rolling.hdma_enable bne _cond_skip_bg1vofs ; HDMA not active - do original BG1VOFS writes ; Menu context: D=$0100, so $93 reads from $0193 @@ -129,8 +167,14 @@ _cond_skip_bg1vofs: clear_ram: """ -Clear the dialog VWF tile buffer at $702000-$706FFF (16-bit zeroes) after letting the boot ROM init at -$15C9AA. +Clear the dialog VWF tile buffer + engine scratch at $702000-$7070FF +(includes VWF_CONFIG_BASE, VWF_CHR_DIRTY / DIRTY_B, VWF_CALLER_CTX, and +the secondary descriptor fields) after letting the boot ROM init at +$15C9AA. Range was $5000 bytes pre-secondary-descriptor ; bumped to +$5100 so the new dirty / vram_word / byte_count / src_offset bytes +land zero on cold boot instead of inheriting random SRAM and +triggering a bogus secondary flush on the very first NMI (which trashed +the save-selection sprite CHR). """ @@ -142,7 +186,7 @@ $15C9AA. _loop: sta.l 0x702000, x inx - cpx.w #0x5000 + cpx.w #0x5100 bne _loop } rtl @@ -167,6 +211,30 @@ Output: X = offset into ItemName table. rtl +multiply_item_index_17: +""" +Relocated multiply-by-ITEM_UNLEASHED_RECORD_SIZE for the items_unleashed +name offset. Called from $019023 via JSL when the field menu is +wired to the 17-byte assets_items_unleashed_dat table. +Input: $43 = item ID (16-bit mode active). +Output: X = offset into ItemName table. +""" + + +; ITEM_UNLEASHED_RECORD_SIZE = 17 = (id << 4) + id. + lda 0x43 + pha + asl + asl + asl + asl ; * 16 + clc + adc 0x01, s ; * 16 + id = * 17 + tax + pla ; balance stack + rtl + + multiply_by_12: """A: value to multiply ; returns A*12 in A.""" php @@ -184,6 +252,29 @@ multiply_by_12: rtl +multiply_by_17: +""" +A: value to multiply ; returns A*17 in A. Mirror of multiply_by_12 +sized for the 17-byte assets_items_unleashed_dat stride. +""" + + + php + rep #0x20 + and.w #0x00FF + pha + asl + asl + asl + asl + clc + adc 0x01, s ; * 16 + value = * 17 + sta 0x01, s + pla + plp + rtl + + brk_handler: """ BRK trap: mask interrupts, disable NMI, fetch the BRK signature byte @@ -223,8 +314,7 @@ signature byte sits at PB:(PC - 1). ; and matches the legacy `*=0x208000` chain so .import modules without ; their own `*=` directive land in bank-20 as expected. -*=0x208100 - ; --- Imported modules --------------------------------------------------- +; --- Imported modules --------------------------------------------------- .import "libmz" .import "dialog" @@ -253,6 +343,7 @@ signature byte sits at PB:(PC - 1). } .import "ingame/places_names_window" +.import "ingame/items_menu_vwf" .import "menus/system_menus_text" .import "dakuten" .import "menus/start_screen_text" @@ -265,6 +356,7 @@ signature byte sits at PB:(PC - 1). .if INVENTORY_ROLLING_BUFFER { .import "ingame/init_bg_scroll_hdma" .include "src/ingame/inventory_rolling.s" + .include "src/lib/rolling_inventory_engine.s" } .if TREASURE_INVENTORY_ROLLING { @@ -275,33 +367,23 @@ signature byte sits at PB:(PC - 1). ; --- Binary text assets ------------------------------------------------- -.incbin "assets/attack_names.ptr" -.incbin "assets/attack_names.dat" -.incbin "assets/monsters_long.ptr" -.incbin "assets/monsters_long.dat" -.incbin "assets/battle_commands_nul.ptr" -.incbin "assets/battle_commands_nul.dat" -.incbin "assets/magic.dat" -.incbin "assets/places_names.dat" -.incbin "assets/classes.ptr" -.incbin "assets/classes.dat" -.incbin "assets/items.dat" -.incbin "assets/item_descriptions.dat" + .if TREASURE_INVENTORY_ROLLING { .include "src/ingame/key_item_picker_patches.s" } .if TRIGGER_ENDING_CUTSCENE { ; all effects are the Ending cutscene - *=0xc436 + .alloc at 0xc436 { lda #0x39 nop + } } .if DEBUG_SHOW_ITEM_WINDOW { ; Hijack ExecEvent to always run F7 (select item) with Baron Key ; EventCmd_f7 at $00ED96 expects: X points to script, $09d5+X+1 = item ID - *=0x00E1EB + .alloc at 0x00E1EB { lda #0xD1 sta 0x09d6 lda #0xFF @@ -309,5 +391,15 @@ signature byte sits at PB:(PC - 1). ldx #0x0000 stx 0xb3 jmp.w 0xED96 + } } -;end + +; Park the 17-byte-stride items_unleashed.dat in an empty bank so the +; full 4352-byte table fits without crossing a LoROM bank boundary +; (which would otherwise leave the upper half of the table at +; $21:0xxx, an address LoROM does not map back to ROM data). + +.alloc at 0x238000 { + .incbin "assets/items_unleashed.dat" +} + diff --git a/plans/battle_items_vwf.md b/plans/battle_items_vwf.md new file mode 100644 index 0000000..ebe9bb3 --- /dev/null +++ b/plans/battle_items_vwf.md @@ -0,0 +1,117 @@ +# Battle items VWF + +## What + +Replace the fixed-width 12-character item-name rendering in the battle +inventory with the existing 8x8 VWF blitter (`battle_render.display_char`). +Lets proportionally-spaced French names fit the 15-tile slot budget that +the rolling inventory already reserves, removes hard truncation, frees +the per-glyph padding wasted on narrow characters. + +## Why + +Vanilla `DrawInventoryItemText` packs item names as 12 fixed tile IDs +into a 48-byte buffer. Our rolling inventory expanded to 60 bytes (15 +tiles per row × 2 bytes), but the renderer still emits one tile-id per +character, so a 12-character French name like `Médaille d'Or` either +overflows or truncates. VWF rendering pulls the same name through +`battle_render.display_char`, allocating CHR dynamically and packing +proportional glyphs into 15 tiles of horizontal pixels (~120 px). + +## Boundaries + +- In scope: battle inventory (the in-battle item menu). +- In scope: the rolling-buffer slot refresh hook (re-render on + scroll edge). +- Out of scope: equipped-items buffer ($9A00). Equip menu rendering + stays fixed-width for this PR; can follow up. +- Out of scope: item descriptions (already VWF via small_vwf). +- Out of scope: field inventory VWF. Same approach, different surface. + +## Approach + +1. **Reserve a battle_render region for inventory**. Current regions + (`src/battle/message.s:143-150`): + ``` + 0x00-0x3F messages + 0x40-0x7F monster names + 0x80-0xAF char names + 0xB0-0xEF commands + ``` + Add `0xF0-0xFF` (16 tiles = 512B CHR) for inventory item name. With + 6 ring slots and 15 tiles per name, the ring needs ~90 tiles. Either + extend the CHR window past $BDE0 (free runway up to roughly + $BFE0-ish per the audit) or reuse the commands region during the + item-menu mode (commands hide when inventory is open). +2. **Patch `DrawInventoryItemText`** at `$02:9FA4`. Replace the + per-character tile-id copy with a `battle_render.init_inventory` + + per-byte `display_char` loop, then write the resulting tile IDs + into the existing 60-byte slot at `$97A6 + slot * 60`. +3. **Quantity digits**. Stay fixed-width for now (the rightmost 2 + tiles per row in the slot layout). Avoids a special-case digit + path in display_char. +4. **Trash icon**. Existing 2x2 fixed glyph stays. Item id $FF skips + the VWF render path. +5. **Tilemap layout unchanged**. The slot still consumes 30 tiles + (15 × 2 rows), still gets DMA'd to VRAM tilemap by the existing + rolling-buffer transfer. Only the CHR pointed at by the tile IDs + changes. + +## Files + +- `src/battle/items_patches.s` — hook `DrawInventoryItemText` text + copy loop. +- `src/battle/message.s` — add inventory region constants + + `init_inventory` entry. +- `src/battle/inventory_rolling.s` — slot-refresh callsite. Already + pre-renders on swap / scroll; VWF render lands at the same site. +- `tests/test_battle_init.py` — extend with an "open battle + inventory" frame that captures the VWF rendered names. + +## Tests + +- Battle init smoke unchanged (no item menu open). +- New `tests/test_battle_inventory_vwf.py`: load a battle-with-items + kss, open the item menu, capture screen golden showing + VWF-rendered names. +- `tests/_profile/profile_battle_vwf.py` exercised with the inventory + open; confirm `_display_char` calls fit within budget on edge + frames (re-render is one slot, ~15 chars × 4.4k cy = 66k cy ≈ 18% + of frame). + +## Risks + +- **CHR overflow**: 90 tiles for the ring needs $A000-base allocation + past current region. Audit the $BDE0-? runway carefully before + expanding. +- **Tile-id collision with commands**: if commands region 0xB0-0xEF + is the chosen reuse target, items render must wait for cmd window + to be hidden. The existing battle inventory open path already + hides the cmd window via the slice-2 gates, but verify. +- **Edge-slot render time**: VWF blit of one item name on scroll + edge must fit in ~1 vblank. Item names are ~12-15 chars × 4.4k cy + = 53-66k cy ≈ 15-18% of frame budget. Headroom OK. +- **dakuten/special chars**: item names with accents flow through + `battle_display_dakuten_char`. Already handled by the existing VWF + path. Verify on `Médaille` etc. + +## Phasing + +| phase | scope | size | +|---|---|---| +| 1 | Region reservation + region-init helper | ~50 lines asm | +| 2 | `DrawInventoryItemText` VWF hook + slot write | ~150 lines asm | +| 3 | Rolling-buffer slot-refresh wiring | ~80 lines asm | +| 4 | Goldens + smoke test for battle inventory | tests | +| 5 | Polish: quantity-digit alignment, trash icon | small | + +Each phase commits independently. PR can land as either one chunk or +phase-by-phase depending on review cadence. + +## Crack + +The 90-tile CHR budget is the load-bearing assumption. If the +$A000-$BFFF window proves too tight, fall back to reusing commands +region 0xB0-0xEF during item-menu mode and round-trip the cmd window +on close. Adds a tilemap clear on transition but keeps the CHR +footprint where it is. diff --git a/plans/menu_rewrite.md b/plans/menu_rewrite.md new file mode 100644 index 0000000..baa4b31 --- /dev/null +++ b/plans/menu_rewrite.md @@ -0,0 +1,140 @@ +# Menu system rewrite + +## Why + +FF4's menu/UI layer accumulated technical debt that costs disproportionate +effort on every new feature: + +- 4 BG3 CHR bases that flip per context (menu `$4000`, town `$4000`-small, + dialog `$C000`, battle `$5000`). +- 4 staging buffers (`$0774` text, `$7E:D600` BG3, `$7E:B600` BG1, + `$7E:C600` BG4). +- Tilemap bases shift per BG mode. +- Vanilla `$EB=1` NMI dispatch interleaved with custom rolling-engine hooks. +- Tile-id semantics flip 8-bit / 9-bit by mode. +- Per-menu HDMA channels (5, 6, 4) each with their own header/footer/signal + hook trio just to anchor the window border while items scroll. +- Save/restore range depends on caller (`$1300` widened to `$2000` then + bumped again for the VWF leak). + +Every new menu feature is an archaeology dig. The current rolling engine +landed in 18 commits and still doesn't cover the key-item picker because +that runs as an overlay on the active map and the existing engine's +assumptions don't hold there. + +## Target architecture (FF6-derived) + +Both FF5 and FF6 simplify in two key ways FF4 didn't pursue: + +1. **Window borders and item text live on different BG layers.** FF6 puts + the item list on BG1 (HDMA-scrollable), window frames on BG2 (static), + cursor sprites on BG3. The "anchor the bottom border while items + scroll" gymnastics dissolve — borders don't move. +2. **HDMA scroll uses a mostly-static ROM table** with one runtime patch + for the scroll bias. FF6's `LoadItemBG1VScrollHDMATbl` + (`ff6decomp/src/menu/item.asm:270`) copies a ROM-resident table into + WRAM and `ADC`s the current scroll position into the variable slice. + No per-frame buffer_pos / buffer_slots modular math. + +### Proposed VRAM map + +``` +$0000-$2FFF BG1 + BG2 CHR (mode-1 4bpp, shared map tileset) +$3000-$3FFF BG1 tilemap (item text — HDMA-scrollable layer) +$4000-$4FFF ??? (consider relocating sprite CHR here once layout settles) +$5000-$5FFF BG2 tilemap (window borders — static) +$6000-$7FFF reserved +$8000-$BFFF sprite CHR +$C000-$DFFF BG3 CHR (dialog VWF + description font — shared by all + BG3-text contexts incl. picker) +$E000-$EFFF BG3 tilemap (description / overlay text) +``` + +Single CHR window for BG3-text across menu, dialog, picker. No flipping +BG34NBA per context. + +### HDMA strategy (FF6-mirrored) + +Per menu: +- Static ROM table `_HdmaTable` with pre-computed `count + scroll` + entries for every row of the list. Hand-tuned per menu's vertical layout. +- One runtime subroutine `LoadHdmaTable` copies ROM table → WRAM + shadow (e.g. `$7E:9800`), `ADC`s the variable slice with current + scroll_pos. ~10 instructions. +- HDMA channel 5 hardcoded to scroll BG1VOFS. Only one channel needed; + no `header_hook` / `footer_hook` / `signal_hook` dispatch. + +### Window draw + +Single `DrawWindow` writes border tiles to whichever BG staging buffer +is selected. Caller selects BG2 for window frames, BG1 for body content. +No animation, no HDMA window-position channels. Window appears +instantly on tilemap blit. + +## Function replacement list + +Replace wholesale (vanilla site becomes `JSR thunk; RTS` in bank-01, +thunk JSL's bank-20 reimpl). Old vanilla code becomes unreachable. + +| Vanilla | Address | Replaces | Blocks | +|---|---|---|---| +| `DrawWindow` | `$01:80D9` | Custom window-frame blitter writing to selected BG | BG layer split | +| `UpdateItemText` | `$00:B229` | Custom VWF render of N visible rows into BG1 tilemap | Picker integration | +| `DrawItemSlot` inner | `$01:A1ED` | Custom row writer matching new tilemap layout | Per-menu render hooks | +| `ShowItemWindow` | `$00:AF4D` | Custom picker driver routed through rolling engine | Standalone picker | +| `TfrBG1/2/3/4TilesVblank` | `$01:9420-9447` | Unified NMI DMA driver (one entry, dirty-mask dispatch) | 4-trampoline spaghetti | +| Main field-menu loop | `$01:9F7B+` | Custom dispatcher running rolling engine + new draw stack | Vanilla input FSM | + +Routines that stay (layer-agnostic, no need to touch): +- `InitItemList` (`$01:B2D3`) — list filter +- `DrawItemCursors` (`$01:A105`) — sprite hand +- `CheckCanUseItem` (`$01:A25D`) — palette decision + +## Migration order + +Each step is one commit, parking-lot tested via screenshot goldens. + +1. **`DrawWindow` reimpl in bank-20** (mirrors vanilla output exactly). + No call-site changes. Verify byte-for-byte identical render via test. +2. **Switch own trampolines** (`draw_window_trampoline`, + `_treasure_draw_inventory_window`, `_menu_draw_inventory_window`, + `_drops_draw_window`) to the new entry. Vanilla `$01:80D9` still alive + for vanilla callers (event dialogs, etc). +3. **Hijack `$01:80D9`** with `JSR _drawwindow_thunk; RTS`. Vanilla code + becomes dead. +4. **Per menu: redirect window draw to BG2 staging.** Wrap each menu's + draw-window call with `SelectBG2 → DrawWindow → SelectBG1`. Border + moves from BG1 to BG2. +5. **Per menu: delete HDMA header/footer hooks.** Now that border is on + BG2 (static), BG1 HDMA only needs per-row scroll entries. Collapse + `update_X_scroll_hdma` to FF6-style static-table + scroll-bias patch. +6. **`UpdateItemText` reimpl.** Replaces vanilla item-list rendering. + Routes through small-VWF directly. +7. **`DrawItemSlot` reimpl.** Same pattern; replaces per-slot vanilla + render. +8. **Picker wire-up.** Now trivial — picker is just `DrawWindow` (border + on BG2) + `UpdateItemText` (VWF on BG1 region) + rolling engine for + scroll. No more "town context vs menu context" branches. +9. **`TfrBG*TilesVblank` unification.** One vblank DMA driver dispatched + by a dirty mask. Replaces the 4 vanilla trampolines + each menu's + per-BG transfer-pending shadow. +10. **Main field-menu loop reimpl.** Last large vanilla function to + delete. Drives rolling engine + new draw stack from a single + dispatcher. + +## Acceptance gates + +- Every commit keeps the 143/19/1 baseline (no new fails). +- Screenshot goldens flag any pixel diff for review — re-record only + when the diff is intentional. +- After step 5, expect to record new field-menu screenshot goldens + (border-on-BG2 visual is identical but tilemap snapshot will differ). +- After step 10, delete `src/lib/rolling_buffer.s` macro stubs + + bank01_trampolines entries that point at unreachable vanilla addresses. + +## Out of scope + +- World map / airship / battle inventory (different BG modes; pattern + port-able later once the field-menu pipeline is proven). +- Mode-7 transitions. +- HDMA-driven palette/scroll effects outside the inventory list. diff --git a/plans/rolling_inventory_engine.md b/plans/rolling_inventory_engine.md new file mode 100644 index 0000000..a767591 --- /dev/null +++ b/plans/rolling_inventory_engine.md @@ -0,0 +1,276 @@ +# Rolling Inventory Engine — Unified Bank-20 Design + +Status: draft. Phase 1 = dynamic VWF render preserved. ROM pre-rendered slots +deferred to a later phase. + +## Goal + +One engine, many menus. Today `inventory_rolling.s`, `treasure_rolling.s`, +`drops_rolling.s`, `key_item_picker.s`, and `shop.s` each duplicate slot +math, scroll animation, HDMA setup, and VWF render orchestration. Drops + +treasure currently share a single VWF CHR region and clobber each other. + +Phase 1 collapses them onto a shared engine living in bank-20, driven by a +per-instance state struct. Each menu owns its own struct instance at a fixed +WRAM base. The engine never holds inventory-specific knowledge — render and +draw-window hooks are supplied per instance via the struct. + +## Non-goals (Phase 1) + +- Pre-rendering slot tiles into ROM and DMAing them in. Comes later. +- Touching the battle inventories. They reuse leaf helpers at most. +- Changing the visible layout, kerning, or tile budget per menu. + +## Architecture + +``` +bank-01 caller (e.g. items_menu, treasure, drops, key_items, shop) + | + | JSL with state struct pointer in (e.g.) X + v +bank-20 rolling_inventory engine + | + | reads state.* fields + | calls state.render_slot_hook / state.draw_window_hook + v +per-instance hooks back in bank-01 (small, menu-specific) +``` + +Engine = scroll state machine + slot index math + VWF region setup + +HDMA bookkeeping. Hooks = "how do I draw the window chrome" and "how do I +render slot N into the active VWF region for this menu". Hooks stay in +caller's bank. + +## Extended state struct + +`RollingBufferState` today is 12 bytes of engine scratch. Phase 1 extends it +with instance config + VWF region fields so the engine reads everything off +the struct pointer. No globals like `VWF_CONFIG_BASE` floating around. + +``` +.struct RollingBufferState { + ; --- engine scratch (12 bytes, unchanged) --- + byte top_row + byte buffer_pos + byte edge_row + byte slot_index + word base_scroll + byte hdma_enable + byte _pad + byte scroll_state + byte scroll_remaining + byte scroll_direction + byte transfer_pending + word scroll_anim_offset + byte hdma_copy_pending + + ; --- instance config --- + byte visible_rows ; e.g. 5 field items, 3 shop + byte slot_height_tiles ; rows of CHR per slot (usually 2) + long item_list_ptr ; far ptr to inventory array + byte item_count ; live count (decremented on use/drop) + byte hdma_channel ; statically assigned per instance + + ; --- VWF region (pointer to standalone VwfRegionConfig, see below) --- + long vwf_cfg_ptr + + ; --- hooks (far ptrs, bank-01 routines) --- + long fn_draw_window ; draw frame, headers, static chrome (init only) + long fn_ensure_hdma ; install instance HDMA channel + long fn_render_slot ; render slot at A=index into vwf_stage_ptr + + ; --- dirty tracking --- + byte dirty_mask ; bit per visible row, max 8 rows +} +``` + +Size: ~31 bytes per instance. Five instances = 155 bytes WRAM. Plus 11 +bytes per `VwfConfig` (trimmed existing struct from `src/vwf_state.i:74`, +one per rolling instance + any extras for battle). + +### Trim + reuse existing `VwfConfig` struct + +`src/vwf_state.i:74` already defines `.struct VwfConfig` (14 bytes) as a +forward-compat reservation. Nobody reads it yet ; `VWF_CONFIG_BASE` is one +SRAM slot at `$70:7080`. That single slot IS the clobber bug: coexisting +instances (drops + treasure) need disjoint configs. + +`VwfConfig` also carries dead weight: `font_ptr` / `font_bank` / +`kerning_ptr` / `kerning_bank`. We have one font globally and kerning is +already a `flags` bit. Drop those 6 bytes. + +Trimmed struct (11 bytes): + +``` +.struct VwfConfig { + word tile_id_base ; first tile_id this renderer owns + byte slot_budget ; tile-budget clamp ; $FF = none + word tilemap_base ; WRAM dest base (bank-$7E implied) + byte palette_byte ; default palette / attr + byte flags ; bit 0 = kerning, bit 1 = priority OR ; rest reserved + word chr_vram_word ; VRAM word addr for DMA dest + word chr_byte_count ; DMA byte count +} +``` + +Plan: stop inventing a parallel struct. Trim + consume `VwfConfig`: + +1. Trim dead font/kerning ptr fields out of `src/vwf_state.i`. +2. Allocate per-instance `VwfConfig` slots at fixed SRAM bases (`$70:7080`, + `$70:708C`, `$70:7098`, ...). One per rolling instance + any battle + consumer. +3. Migrate `src/vwf.s` / `src/small_vwf/render.s` to read `VwfConfig` fields + instead of hardcoded tile_id base + tilemap dest. Font ptr stays the + single global it always was. + +`RollingBufferState.vwf_cfg_ptr` (`long`) points at the instance's +`VwfConfig`. Battle reuses the same struct. + +Resolved design points: + +- **No `fn_format_qty`.** Render hook owns layout. Key items hook writes name + only; field items hook writes name + qty. Engine never branches on it. +- **No bank fields.** `item_list_ptr` and `vwf_stage_ptr` are `long`. Removes + "did you set the bank" footgun. +- **HDMA channel = static struct field, not free-list.** Coexistence map is + known at design time (only drops + treasure overlap), so allocation is a + static table: drops gets ch 3, everyone else ch 2. Verify against current + `src/ingame/init_bg_scroll_hdma.s` before locking numbers. +- **Shop qty overlay = caller's problem.** Shop calls + `rolling_engine_invalidate_slot` on qty change; its render hook draws the + overlay. Engine stays slot-agnostic. +- **`fn_draw_window` lives in struct** even though only called at init. Keeps + init signature clean (just `X = state ptr, Y = top_row`); 3 bytes / instance + is noise. +- **`dirty_mask` capped at 8 rows.** No current menu needs more. Locked. + +## WRAM instance bases (proposal) + +| Instance | Base | Notes | +|---------------|------------|------------------------------------------| +| field items | `$7E:1BA8` | already there | +| treasure inv | `$7E:1BD0` | already there | +| drops | `$7E:1C00` | new; owns disjoint VWF region | +| key items | `$7E:1C30` | already at 1BF0, may relocate | +| shop | `$7E:1C60` | new | + +Field menu + shop never coexist with treasure/drops, but drops + treasure DO +coexist on the same screen, so their `vwf_chr_base` MUST differ. That is the +bug we fix. + +## Engine entry points (bank-20) + +All entry points take `X = instance struct ptr` (16-bit, in WRAM, bank fixed +to `$7E`). JSL from bank-01 callers. + +``` +; Cold init. Zero engine scratch, latch instance config from caller-prefilled +; fields, draw window, render initial visible slots. +; in : X = state ptr +; Y = initial top_row (usually 0) +; out: VWF region populated, HDMA armed +rolling_engine_init: + +; Per-frame tick. Read pad, advance scroll state machine, schedule VWF +; renders for newly-revealed rows, update HDMA scratch. +; in : X = state ptr +; A.b = pad-down bits this frame +; out: state.dirty_mask reflects rows needing redraw +rolling_engine_tick: + +; NMI-time flush. Walk dirty_mask, DMA VWF stage -> VRAM, write tilemap. +; MUST be called inside force-blank or HDMA gap window. +; in : X = state ptr +rolling_engine_vblank_flush: + +; Cursor / selection helpers — thin wrappers over slot_index math. +rolling_engine_cursor_up: +rolling_engine_cursor_down: + +; Mutation API for callers that add / remove items mid-menu. +; in : X = state ptr +; A = slot index being mutated +; Marks affected rows dirty; engine re-renders next tick. +rolling_engine_invalidate_slot: +rolling_engine_invalidate_all: + +; Teardown. Tear down HDMA channel, leave state struct intact for re-entry. +rolling_engine_shutdown: +``` + +Internally the engine reuses the existing `engine_*` macros from +`src/lib/rolling_buffer.s` (scroll prepare / start / update / finish). Those +macros become the engine's private kernel; bank-01 callers stop including +them directly. + +## Caller skeleton (bank-01) + +Each menu shrinks to: declare instance, fill config fields, JSL init, +JSL tick from main loop, JSL flush from NMI, supply 3-4 hook routines. + +``` +items_menu_open: + ldx.w #field_items_state + lda.w #visible_rows_5 : sta.w 0,X RollingBufferState.visible_rows + ; ... fill remaining config + hooks once ... + ldy.w #0 + jsl rolling_engine_init + rts + +items_menu_render_slot: ; A = slot index, X = state ptr + ; existing VWF render code, writes to state.vwf_stage_ptr + rtl +``` + +Hooks are `.long` because engine lives in bank-20 and hooks in bank-01. +Engine does `jsl [state + fn_render_slot]` style indirection (via a small +trampoline since 65816 has no JSL-indirect). + +## Migration plan — single branch, multi-commit, squash at merge + +Single feature branch. Each commit green at HEAD. TDD per commit (failing +test first, commit when green). Squash to one commit at merge time per +`feedback_no_rebase_published_branches`. + +0. **Consume existing `VwfConfig`.** Wire `src/vwf.s` and + `src/small_vwf/render.s` to read from a `VwfConfig*` instead of + hardcoded font ptr + tile_id base. Allocate one `VwfConfig` slot per + coexisting instance (drops + treasure get distinct slots). Legacy + callers point at the original `$70:7080` slot ; un-migrated paths + unchanged. Clobber bug fixed at VWF layer. +1. **Engine skeleton in bank-20** + extended struct + smoke harness driving + a fake inventory. No callers migrated. Existing `engine_*` macros become + engine's private kernel. Engine reads `state.vwf_cfg_ptr` and passes it + straight through to VWF entry points. +2. **Port field items.** Drop duplicated scroll state machine from + `inventory_rolling.s`. Goldens: `tests/goldens/field_items/*` unchanged. +3. **Port shop.** Second consumer flushes single-instance assumptions. +4. **Port key items**, enable actual rolling (today it doesn't). New + goldens for rolling behaviour. +5. **Split drops + treasure VWF regions.** Each gets own `vwf_chr_base` and + `vwf_dst_tile_id`. New combined golden verifies no clobber. +6. **Delete duplication.** Remove dead code from `inventory_rolling.s` / + `drops_rolling.s` / `treasure_rolling.s` once all callers are on engine. + +## Battle (out of scope, future note) + +Battle inventory menus have a different tile budget, OAM-driven cursor, and +tighter NMI window. They can reuse: + +- the slot math helpers (which row is visible, where does it map in CHR), +- the VWF stage -> VRAM DMA helper, +- the dirty-mask flush. + +They cannot reuse the field scroll-animation state machine as-is. Punt to a +later design once the field side stabilises. + +## Verification gates before phase 6 + +- xdds disassembly of patched ROM at each engine entry point: confirm + bank-20 location, MX flags, JSL targets resolve. +- Goldens green for field items, shop, key items, drops+treasure combined. +- Mesen-S smoke run: open each menu, scroll past wrap on both directions, + add/remove items mid-menu, verify dirty_mask handling. +- WRAM check: instance bases do not overlap and stay inside the existing + `$7E:1Bxx` rolling region (extend allocation if needed; document in + `sram_management.s`). diff --git a/renderd.bin b/renderd.bin new file mode 100644 index 0000000..3afdb12 Binary files /dev/null and b/renderd.bin differ diff --git a/requirements.txt b/requirements.txt index 7fff9ba..ff40071 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -a816==1.1.0a22 +a816==1.1.0a30 kintsuki[visual]==0.0.0a13 diff --git a/src/assets.s b/src/assets.s index ad3f503..dd061bf 100644 --- a/src/assets.s +++ b/src/assets.s @@ -7,47 +7,40 @@ plus the `font_table` pointer table indexed by font id. ; Pinned binary blobs (fonts, scripts, tilemaps). ; ---------------------------------------------------------------- -*=0x0AF000 -.incbin "fonts/8x8.bin" - -*=0x0FA710 -.incbin "assets/characters_names.dat" - -*=0x0E9800 -.incbin "assets/monsters.dat" - -*=0x218000 -.incbin "assets/bank1_1.ptr" - -.incbin "assets/bank1_2.ptr" - -.incbin "assets/bank2.ptr" - -*=0x228000 -.incbin "assets/bank1_1.dat" - -*=0x24A000 -.incbin "assets/bank1_2.dat" - -*=0x25A000 -.incbin "assets/bank2.dat" - -*=0x27B000 -.incbin "assets/battle_statuses.dat" - -*=0x288000 -.incbin "assets/menu_font.dat" - -.incbin "assets/font.dat" - -.incbin "assets/wicked_font.dat" - -.incbin "assets/book_font.dat" - -.incbin "assets/bold_font.dat" - -.incbin "assets/battle_commands.dat" - +.include "config.i" +.alloc at 0x0AF000 { + .incbin "fonts/8x8.bin" +} +.alloc at 0x0FA710 { + .incbin "assets/characters_names.dat" +} +.alloc at 0x0E9800 { + .incbin "assets/monsters.dat" +} +.alloc at 0x218000 { + .incbin "assets/bank1_1.ptr" + .incbin "assets/bank1_2.ptr" + .incbin "assets/bank2.ptr" +} +.alloc at 0x228000 { + .incbin "assets/bank1_1.dat" +} +.alloc at 0x24A000 { + .incbin "assets/bank1_2.dat" +} +.alloc at 0x25A000 { + .incbin "assets/bank2.dat" +} +.alloc at 0x27B000 { + .incbin "assets/battle_statuses.dat" +} +.alloc at 0x288000 { + .incbin "assets/menu_font.dat" + .incbin "assets/font.dat" + .incbin "assets/wicked_font.dat" + .incbin "assets/book_font.dat" + .incbin "assets/bold_font.dat" + .incbin "assets/battle_commands.dat" font_table: """24-bit pointer table indexed by font id (0=dialog, 1=wicked, 2=book, 3=bold).""" .pointer assets_font_dat @@ -56,19 +49,44 @@ font_table: .pointer assets_bold_font_dat -.incbin "assets/credits_text.bin" + .incbin "assets/credits_text.bin" +} +.alloc at 0x298000 { + .incbin "assets/battle_messages.ptr" + .incbin "assets/battle_messages.dat" +} +.alloc at 0x299900 { + .incbin "assets/battle_text.ptr" + .incbin "assets/battle_text.dat" +} -*=0x298000 -.incbin "assets/battle_messages.ptr" +.pool assets { + range 0x2e8000 0x2effff + range 0x318000 0x31ffff + strategy pack +} -.incbin "assets/battle_messages.dat" +.alloc __assets_stupid_mandatory_symbol in assets { + .incbin "assets/attack_names.ptr" + .incbin "assets/attack_names.dat" + .incbin "assets/monsters_long.ptr" + .incbin "assets/monsters_long.dat" + .incbin "assets/battle_commands_nul.ptr" + .incbin "assets/battle_commands_nul.dat" + .incbin "assets/magic.dat" + .incbin "assets/places_names.dat" + .incbin "assets/classes.ptr" + .incbin "assets/classes.dat" + .incbin "assets/items.dat" + .incbin "assets/item_descriptions.dat" + .incbin "assets/dakuten.bin" +} -*=0x299900 -.incbin "assets/battle_text.ptr" -.incbin "assets/battle_text.dat" .if ENABLE_INTRO { - *=0x318000 + .alloc _intro_assets in assets { .incbin "assets/intro.map" .incbin "assets/intro.col" .incbin "assets/intro.set" + } } + diff --git a/src/bank20.i b/src/bank20.i new file mode 100644 index 0000000..0a917ea --- /dev/null +++ b/src/bank20.i @@ -0,0 +1,11 @@ +"""Shared bank-20 relocated-code pool declaration. + +Every module that wraps its body in `.alloc in bank20_reloc { ... }` +must include this header so the pool is visible at the module's +codegen pass. Pool decls are idempotent across modules — the +linker dedupes identical ranges via `_merge_one_pool_decl`.""" + +.pool bank20_reloc { + range 0x208000 0x20FFFF + strategy order +} diff --git a/src/battle/commands_patches.s b/src/battle/commands_patches.s index 39767d5..6d162b3 100644 --- a/src/battle/commands_patches.s +++ b/src/battle/commands_patches.s @@ -2,77 +2,85 @@ In-place patches that resize the battle command-window layout when `BATTLE_CMD_VWF` is enabled (shorter slot stride, command-id base, format-buffer pointer). """ +.include "config.i" command_buffer_ptr = 0x97a6 + 0x601 ; old spell lists buffers .if BATTLE_CMD_VWF { command_length = 6 ; Command window - *=0x16fe5a + 6 * 2 - .db 0x05, 0x00, command_length + 2, 0x0d - - *=0x2b990 - lda.b #0x18 + 8 +.alloc at 0x16fe5a + 6 * 2 { + .db 0x05, 0x00, command_length + 2, 0x0d +} +.alloc at 0x2b990 { + lda.b #0x18 + 8 +} } else { command_length = 10 ; move command cursor on the moved window - *=0x2b990 - lda.b #0x18 - *=0x029CD6 - lda.b #command_length - - *=0x029D15 - lda.b #command_length - - *=0x029D42 - cpy.w #command_length + 2 - - *=0x029D5A - cpy.w #command_length + 2 - - *=0x029D39 - lda.l assets_battle_commands_dat, x - - *=0x029CE0 - lda.b #command_length * 4 - -; patches source & length of battle commands used in display attack window. - *=0x02cb49 - lda.b #command_length +.alloc at 0x2b990 { + lda.b #0x18 +} +.alloc at 0x029CD6 { + lda.b #command_length +} +.alloc at 0x029D15 { + lda.b #command_length +} +.alloc at 0x029D42 { + cpy.w #command_length + 2 +} +.alloc at 0x029D5A { + cpy.w #command_length + 2 +} +.alloc at 0x029D39 { + lda.l assets_battle_commands_dat, x +} +.alloc at 0x029CE0 { + lda.b #command_length * 4 -; attack window kick for example ends up there - *=0x02cb54 - lda.l assets_battle_commands_dat, x + ; patches source & length of battle commands used in display attack window. +} +.alloc at 0x02cb49 { + lda.b #command_length - *=0x02cb5d - cpy.w #command_length + ; attack window kick for example ends up there +} +.alloc at 0x02cb54 { + lda.l assets_battle_commands_dat, x +} +.alloc at 0x02cb5d { + cpy.w #command_length -; Command window - *=0x16fe5a + 6 * 2 - .db 0x04, 0x00, command_length + 2, 0x0d + ; Command window +} +.alloc at 0x16fe5a + 6 * 2 { + .db 0x04, 0x00, command_length + 2, 0x0d +} } { ; ram position of the prebuilt battle windows - *=0x16FEAD -cmd_text_buf_ptrs: - battle_data_size = command_length * 4 * 5 - .dw command_buffer_ptr - .dw command_buffer_ptr + battle_data_size - .dw command_buffer_ptr + battle_data_size * 2 - .dw command_buffer_ptr + battle_data_size * 3 - .dw command_buffer_ptr + battle_data_size * 4 - - *=0x16FE54 - .db command_length * 2 - .db 0x0a - .dw command_buffer_ptr ; read address - .if BATTLE_CMD_VWF { - .dw 0xC1F4 - 2 ; write address - } else { - .dw 0xC1F4 - 4 ; write address - } - - *=0x02999F - ldx.w #battle_data_size +.alloc at 0x16FEAD { + cmd_text_buf_ptrs: + battle_data_size = command_length * 4 * 5 + .dw command_buffer_ptr + .dw command_buffer_ptr + battle_data_size + .dw command_buffer_ptr + battle_data_size * 2 + .dw command_buffer_ptr + battle_data_size * 3 + .dw command_buffer_ptr + battle_data_size * 4 +} +.alloc at 0x16FE54 { + .db command_length * 2 + .db 0x0a + .dw command_buffer_ptr ; read address + .if BATTLE_CMD_VWF { + .dw 0xC1F4 - 2 ; write address + } else { + .dw 0xC1F4 - 4 ; write address + } +} +.alloc at 0x02999F { + ldx.w #battle_data_size +} } diff --git a/src/battle/commands_reloc.s b/src/battle/commands_reloc.s index c4cda17..19f8d52 100644 --- a/src/battle/commands_reloc.s +++ b/src/battle/commands_reloc.s @@ -1,10 +1,13 @@ """ +.include "../bank20.i" + Relocated battle-command-list renderer (`draw_command_list_for_character`) plus its private command-build loop, called by the patched bank-$02 hook. """ +.include "config.i" .extern messages_vwf .extern messages_vwf.init_commands_list -.extern _draw_text_battle_far +.extern draw_text_battle_far .extern assets_battle_commands_nul_ptr .extern assets_battle_commands_nul_dat .extern command_buffer_ptr @@ -13,172 +16,177 @@ loop, called by the patched bank-$02 hook. mult8_far := 0x2855c -.if BATTLE_CMD_VWF { - command_length = 6 -} else { - command_length = 10 -} -draw_command_list_for_character: -""" -Public RTL entry: render the per-character battle command list for the active character. -Dirty-bit gating moved up to the bank-02 thunk -(`_draw_battle_command_window_relocated`) which clears CMD_DIRTY_BIT -before calling here ; gating twice would leave the inner buffer -filled with $00FF blanks. Inventory-open short-circuit stays. -""" -; Skip command rendering if inventory is active (bit 2 of $4A) -; This prevents format buffer ($74FD) conflicts with inventory code - lda.l 0x7E004A - and #0x04 - bne _skip_commands - - lda 0x1822 ; selected character slot - sta 0x1816 - phx - jsr.w _draw_command_list_for_character - plx - -_skip_commands: - rtl - -_draw_command_list_for_character: -{ - stz.w 0x1817 - ldy #0x74FD ; keep the pointer in Y this will allow to run _draw_single_command multiple times -; to build {tile_flag}cmd1\n{tile_flag}cmd2\n{tile_flag}cmd3\n{tile_flag}cmd4\n{tile_flag}cmd5\0 -; to issue a single draw text call to the command list region. - lda 0x1817 ; battle command slot - sta 0x26 - lda.b #command_length * 2 - sta 0x28 - jsr.l mult8_far ; command_id * (command_length * 2) - - - lda 0x1817 ; slot ID - asl - asl - tax - stx 0x00 - -;lda 0x1816 ; character id -;asl -;tax - -;rep #0x20 -;ldx.w #command_buffer_ptr - 10 -;lda.l 0x16fead,x ;cmd_text_buf_ptrs,x -;stx 0xef52 ; destination ptr ? - -_loop: - tdc - lda 0x1817 ; slot ID - asl - asl - tax - stx 0x00 - - lda 0x1816 ; character id - - rep #0x20 - asl - tax - lda.l 0x16feb7, x - clc - adc.b 0x00 - tax - sep #0x20 - - jsr.w _draw_single_command - inc 0x1817 - lda 0x1817 - cmp #5 - beq _exit - lda #1 - sta.w 0x0000, y - iny - bra _loop -_exit: - - tdc - sta.w 0x0000, y - - sta 0xef55 - - ldx.w #command_buffer_ptr - stx 0xef52 ; destination - - rep #0x20 - -; Fill the window with 0x00ff before rendering - lda.w #command_length * 4 * 5 - sta 0x00 - -_clear_buffer_loop: - lda.w #0x00ff - sta.l 0x7e0000 + command_length * 4 * 5, x - dex - dex - dec 0 - bne _clear_buffer_loop - - sep #0x20 - - ldx #0x74fd ; text buffer - stx 0xef50 - lda.b #command_length ; draw text line length used for newline - sta 0xef54 - - jsr.l messages_vwf.init_commands_list - jsr.l _draw_text_battle_far - rts -} +.include "../bank20.i" -_draw_single_command: -{ - tdc - sep #0x20 - lda.w 0x0001, x ; 0x3303 - - cmp #0xff - bne _continue - bra _exit -_continue: - asl - tax - tdc - lda.l assets_battle_commands_nul_ptr, x - tax - - lda #0x00 ; white text - and #0x80 - beq _active_command - lda #0x04 ; gray text - -_active_command: - sta.w 0x0001, y - lda #0x0e ; change tile flags - sta.w 0x0000, y - iny - iny - -_battle_command_loop: - { -_loop: - lda.l assets_battle_commands_nul_dat, x - cmp #0 - beq _exit_command_loop - sta.w 0x0000, y - - inx - iny - bra _loop +.alloc battle_commands_reloc_block in bank20_reloc { + .if BATTLE_CMD_VWF { + command_length = 6 + } else { + command_length = 10 } -_exit_command_loop: -_pad_loop: - + draw_command_list_for_character: + """ + Public RTL entry: render the per-character battle command list for the active character. + Dirty-bit gating moved up to the bank-02 thunk + (`draw_battle_command_window_relocated`) which clears CMD_DIRTY_BIT + before calling here ; gating twice would leave the inner buffer + filled with $00FF blanks. Inventory-open short-circuit stays. + """ + ; Skip command rendering if inventory is active (bit 2 of $4A) + ; This prevents format buffer ($74FD) conflicts with inventory code + lda.l 0x7E004A + and #0x04 + bne _skip_commands + + lda 0x1822 ; selected character slot + sta 0x1816 + phx + jsr.w _draw_command_list_for_character + plx + + _skip_commands: + rtl + + _draw_command_list_for_character: + { + stz.w 0x1817 + ldy #0x74FD ; keep the pointer in Y this will allow to run _draw_single_command multiple times + ; to build {tile_flag}cmd1\n{tile_flag}cmd2\n{tile_flag}cmd3\n{tile_flag}cmd4\n{tile_flag}cmd5\0 + ; to issue a single draw text call to the command list region. + lda 0x1817 ; battle command slot + sta 0x26 + lda.b #command_length * 2 + sta 0x28 + jsr.l mult8_far ; command_id * (command_length * 2) + + + lda 0x1817 ; slot ID + asl + asl + tax + stx 0x00 + + ;lda 0x1816 ; character id + ;asl + ;tax + + ;rep #0x20 + ;ldx.w #command_buffer_ptr - 10 + ;lda.l 0x16fead,x ;cmd_text_buf_ptrs,x + ;stx 0xef52 ; destination ptr ? + + _loop: + tdc + lda 0x1817 ; slot ID + asl + asl + tax + stx 0x00 + + lda 0x1816 ; character id + + rep #0x20 + asl + tax + lda.l 0x16feb7, x + clc + adc.b 0x00 + tax + sep #0x20 + + jsr.w _draw_single_command + inc 0x1817 + lda 0x1817 + cmp #5 + beq _exit + lda #1 + sta.w 0x0000, y + iny + bra _loop + _exit: + + tdc + sta.w 0x0000, y + + sta 0xef55 + + ldx.w #command_buffer_ptr + stx 0xef52 ; destination + + rep #0x20 + + ; Fill the window with 0x00ff before rendering + lda.w #command_length * 4 * 5 + sta 0x00 + + _clear_buffer_loop: + lda.w #0x00ff + sta.l 0x7e0000 + command_length * 4 * 5, x + dex + dex + dec 0 + bne _clear_buffer_loop + + sep #0x20 + + ldx #0x74fd ; text buffer + stx 0xef50 + lda.b #command_length ; draw text line length used for newline + sta 0xef54 + + jsr.l messages_vwf.init_commands_list + jsr.l draw_text_battle_far + rts + } -_exit: - rts + _draw_single_command: + { + tdc + sep #0x20 + lda.w 0x0001, x ; 0x3303 + + cmp #0xff + bne _continue + bra _exit + _continue: + asl + tax + tdc + lda.l assets_battle_commands_nul_ptr, x + tax + + lda #0x00 ; white text + and #0x80 + beq _active_command + lda #0x04 ; gray text + + _active_command: + sta.w 0x0001, y + lda #0x0e ; change tile flags + sta.w 0x0000, y + iny + iny + + _battle_command_loop: + { + _loop: + lda.l assets_battle_commands_nul_dat, x + cmp #0 + beq _exit_command_loop + sta.w 0x0000, y + + inx + iny + bra _loop + } + + _exit_command_loop: + _pad_loop: + + + _exit: + rts + } } diff --git a/src/battle/debug_always_drop.s b/src/battle/debug_always_drop.s index 5fbd990..52170e8 100644 --- a/src/battle/debug_always_drop.s +++ b/src/battle/debug_always_drop.s @@ -11,8 +11,10 @@ NOPing the random-roll gate at $03:ED0D. ; because forcing it pulls drop-table index 0 (empty) and produces an ; "no item" treasure prompt instead of an actual drop. +.include "config.i" .if TREASURE_DEBUG_ALWAYS_DROP { - *=0x03ED0D - nop - nop +.alloc at 0x03ED0D { + nop + nop +} } diff --git a/src/battle/equip_window.s b/src/battle/equip_window.s index 36f3ced..49e3150 100644 --- a/src/battle/equip_window.s +++ b/src/battle/equip_window.s @@ -1,4 +1,6 @@ """ +.include "../bank20.i" + Relocated battle equipped-items window transfer routine. Pivots the per-character equip popup from `R-label L-label / R-item L-item` (two columns of stacked @@ -23,102 +25,107 @@ Tilemap dest bases (set by hooks in items_patches.s): .extern load_menu_tfr_data_trampoline -tfr_equip_window_new: -""" -Relocated replacement for vanilla `TfrEquipWindow` at $02:97A6. - -Walks 6 per-block transfer passes: slot-1 / slot-2 labels into the -left column (rows 1 and 3), slot-1 / slot-2 item top+bot halves into -the right column (rows 0-1 and 2-3 respectively). Caller is the -bank-02 trampoline that JSLs here ; return via RTL. -""" - -{ - rep #0x10 ; X/Y 16-bit at runtime (caller may have X=1) - ldx.w #0xD1E8 - stx 0x00 - ldx.w #0xD204 - stx 0x02 - lda 0x1822 - asl - tax - rep #0x20 - lda.l 0x16FEC1, x ; EquipTextBufPtrs - pha - lda.l 0x16FF25, x ; RLHandTextBufPtrs - tax - sep #0x20 - -; --- Pass A: slot-1 label -> LEFT col row 1 (buf[0..$13]) --- - ldy.w #0x0040 -loop_a: - lda.w 0x0000, x - sta (0x00), y - inx - iny - cpy.w #0x0054 - bne loop_a - -; --- Pass B: slot-2 label -> LEFT col row 3 (buf[$14..$27]) --- - ldy.w #0x00C0 -loop_b: - lda.w 0x0000, x - sta (0x00), y - inx - iny - cpy.w #0x00D4 - bne loop_b - -; --- Switch to EquipBuf --- - plx - -; --- Pass C: slot-1 item top -> RIGHT col row 0 (buf[0..$1D]) --- - ldy.w #0x0000 -loop_c: - lda.w 0x0000, x - sta (0x02), y - inx - iny - cpy.w #0x001E - bne loop_c - -; --- Pass D: slot-1 item bot -> RIGHT col row 1 (buf[$1E..$3B]) --- - ldy.w #0x0040 -loop_d: - lda.w 0x0000, x - sta (0x02), y - inx - iny - cpy.w #0x005E - bne loop_d - -; --- Pass E: slot-2 item top -> RIGHT col row 2 (buf[$3C..$59]) --- - ldy.w #0x0080 -loop_e: - lda.w 0x0000, x - sta (0x02), y - inx - iny - cpy.w #0x009E - bne loop_e - -; --- Pass F: slot-2 item bot -> RIGHT col row 3 (buf[$5A..$77]) --- - ldy.w #0x00C0 -loop_f: - lda.w 0x0000, x - sta (0x02), y - inx - iny - cpy.w #0x00DE - bne loop_f - -; --- Trigger tilemap upload (window 7, 2 transfers) --- - lda #0x07 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline - lda #0x01 - sta 0x1825 - sta 0x1824 - rtl +.include "../bank20.i" + +.alloc battle_equip_window_block in bank20_reloc { + tfr_equip_window_new: + """ + Relocated replacement for vanilla `TfrEquipWindow` at $02:97A6. + + Walks 6 per-block transfer passes: slot-1 / slot-2 labels into the + left column (rows 1 and 3), slot-1 / slot-2 item top+bot halves into + the right column (rows 0-1 and 2-3 respectively). Caller is the + bank-02 trampoline that JSLs here ; return via RTL. + """ + + + { + rep #0x10 ; X/Y 16-bit at runtime (caller may have X=1) + ldx.w #0xD1E8 + stx 0x00 + ldx.w #0xD204 + stx 0x02 + lda 0x1822 + asl + tax + rep #0x20 + lda.l 0x16FEC1, x ; EquipTextBufPtrs + pha + lda.l 0x16FF25, x ; RLHandTextBufPtrs + tax + sep #0x20 + + ; --- Pass A: slot-1 label -> LEFT col row 1 (buf[0..$13]) --- + ldy.w #0x0040 + loop_a: + lda.w 0x0000, x + sta (0x00), y + inx + iny + cpy.w #0x0054 + bne loop_a + + ; --- Pass B: slot-2 label -> LEFT col row 3 (buf[$14..$27]) --- + ldy.w #0x00C0 + loop_b: + lda.w 0x0000, x + sta (0x00), y + inx + iny + cpy.w #0x00D4 + bne loop_b + + ; --- Switch to EquipBuf --- + plx + + ; --- Pass C: slot-1 item top -> RIGHT col row 0 (buf[0..$1D]) --- + ldy.w #0x0000 + loop_c: + lda.w 0x0000, x + sta (0x02), y + inx + iny + cpy.w #0x001E + bne loop_c + + ; --- Pass D: slot-1 item bot -> RIGHT col row 1 (buf[$1E..$3B]) --- + ldy.w #0x0040 + loop_d: + lda.w 0x0000, x + sta (0x02), y + inx + iny + cpy.w #0x005E + bne loop_d + + ; --- Pass E: slot-2 item top -> RIGHT col row 2 (buf[$3C..$59]) --- + ldy.w #0x0080 + loop_e: + lda.w 0x0000, x + sta (0x02), y + inx + iny + cpy.w #0x009E + bne loop_e + + ; --- Pass F: slot-2 item bot -> RIGHT col row 3 (buf[$5A..$77]) --- + ldy.w #0x00C0 + loop_f: + lda.w 0x0000, x + sta (0x02), y + inx + iny + cpy.w #0x00DE + bne loop_f + + ; --- Trigger tilemap upload (window 7, 2 transfers) --- + lda #0x07 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + lda #0x01 + sta 0x1825 + sta 0x1824 + rtl + } } diff --git a/src/battle/graphics.s b/src/battle/graphics.s index 11de48b..03f4c0e 100644 --- a/src/battle/graphics.s +++ b/src/battle/graphics.s @@ -1,14 +1,21 @@ """ +.include "../bank20.i" + Hard-coded battle command graphics: tile-id rows used when the row/defend command labels need to be redrawn from ROM. """ -.scope defend_row { - """Two glyph rows for the Defend / Row battle command labels.""" - defend_row_length = 6 -defend_text: - .db 0xff, 0xff, 0xff, 0xff, 0xff, 0xff - .db 222, 224, 231, 225, 226, 255 -row_text: - .db 0xff, 0xff, 0xff, 0xff, 0xff, 0xff - .db 223, 224, 229, 227, 255, 255 + +.include "../bank20.i" + +.alloc battle_graphics_block in bank20_reloc { + .scope defend_row { + """Two glyph rows for the Defend / Row battle command labels.""" + defend_row_length = 6 + defend_text: + .db 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + .db 222, 224, 231, 225, 226, 255 + row_text: + .db 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + .db 223, 224, 229, 227, 255, 255 + } } diff --git a/src/battle/graphics_patches.s b/src/battle/graphics_patches.s index 31dc3bf..46b41ca 100644 --- a/src/battle/graphics_patches.s +++ b/src/battle/graphics_patches.s @@ -6,98 +6,95 @@ piggyback on `defend_row` data. ; MISS sprite graphics -*=0x0cfc60 -.incbin "fonts/miss.bin" - -*=0x16fb87 -{ -; chars to copy from the font to the 4bpp tileset in battle - - - .table "text/ff4_menus.tbl" - .db 0x76, 0x77 - .text "/" - .text "GRadeginqrsu" - .db 0x8c, 0x90, 0x7f - .db 0x80, 0x81, 0x82 - .db 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x16, 0x17, 0x18, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff +.alloc at 0x0cfc60 { + .incbin "fonts/miss.bin" } +.alloc at 0x16fb87 { + { + ; chars to copy from the font to the 4bpp tileset in battle + + + .table "text/ff4_menus.tbl" + .db 0x76, 0x77 + .text "/" + .text "GRadeginqrsu" + .db 0x8c, 0x90, 0x7f + .db 0x80, 0x81, 0x82 + .db 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x16, 0x17, 0x18, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff + } +} +.alloc at 0x02975d { + ; PM Needed + .db 223, 226, 230, 233, 228, 232, 0xff - -*=0x02975d - ; PM Needed - .db 223, 226, 230, 233, 228, 232, 0xff - -; Defend / Row window content -; moves the row destination back a few bytes - -*=0x29a90 - ldx.w #0xd618 - -*=0x29a8b - ldx.w #0xd5e8 - -*=0x029a98 - lda.l defend_row.defend_text, x - -*=0x029a9e - lda.l defend_row.row_text, x - -*=0x029aa7 - cpx.w #defend_row.defend_row_length * 2 - -*=0x029aac - cpx.w #defend_row.defend_row_length - -*=0x02b96a ; moves row pointer back 8 pixels. - lda.b #0x0c - - -*=0x16fe5a + 6 * 8 ; moves row pointer back 8 pixels. - .db 0x00, 0x09, 0x08, 0x04 - - -*=0x16fe5a + 6 * 9 - ; row window - .db 0x18, 0x09, 0x08, 0x04 - -; switches around small mp to pm in the MP Needed battle window. - -*=0x02a1e3 - lda #0xdc - sta 0xba94, y - dec - sta 0xba96, y - - -; Hands text -; (Layout handled by relocated tfr_equip_window_new in src/battle/equip_window.s) - -*=0x16fed5 -{ - .table "text/ff4_menus.tbl" -hand_text: - .text "Droite " - .text "Gauche " - .text "Droite " - .text "Principale" - .text "Principale" - .text "Gauche " - .text "Principale" - .text "Principale" + ; Defend / Row window content + ; moves the row destination back a few bytes +} +.alloc at 0x29a90 { + ldx.w #0xd618 +} +.alloc at 0x29a8b { + ldx.w #0xd5e8 +} +.alloc at 0x029a98 { + lda.l defend_row.defend_text, x +} +.alloc at 0x029a9e { + lda.l defend_row.row_text, x +} +.alloc at 0x029aa7 { + cpx.w #defend_row.defend_row_length * 2 } +.alloc at 0x029aac { + cpx.w #defend_row.defend_row_length +} +.alloc at 0x02b96a { + lda.b #0x0c +} +.alloc at 0x16fe5a + 6 * 8 { + .db 0x00, 0x09, 0x08, 0x04 +} +.alloc at 0x16fe5a + 6 * 9 { + ; row window + .db 0x18, 0x09, 0x08, 0x04 + ; switches around small mp to pm in the MP Needed battle window. +} +.alloc at 0x02a1e3 { + lda #0xdc + sta 0xba94, y + dec + sta 0xba96, y -*=0x02A51C - lda.l assets_battle_statuses_dat, x - sta.b 0x00 - lda.l assets_battle_statuses_dat + 1, x - sta.b 0x01 - lda.b #assets_battle_statuses_dat >> 16 -*=0x02A32A - lda.l assets_battle_statuses_dat, x - sta.b 0x00 - lda.l assets_battle_statuses_dat + 1, x - sta.b 0x01 - lda.b #assets_battle_statuses_dat >> 16 + ; Hands text + ; (Layout handled by relocated tfr_equip_window_new in src/battle/equip_window.s) +} +.alloc at 0x16fed5 { + { + .table "text/ff4_menus.tbl" + hand_text: + .text "Droite " + .text "Gauche " + .text "Droite " + .text "Principale" + .text "Principale" + .text "Gauche " + .text "Principale" + .text "Principale" + } +} +.alloc at 0x02A51C { + lda.l assets_battle_statuses_dat, x + sta.b 0x00 + lda.l assets_battle_statuses_dat + 1, x + sta.b 0x01 + lda.b #assets_battle_statuses_dat >> 16 +} +.alloc at 0x02A32A { + lda.l assets_battle_statuses_dat, x + sta.b 0x00 + lda.l assets_battle_statuses_dat + 1, x + sta.b 0x01 + lda.b #assets_battle_statuses_dat >> 16 +} diff --git a/src/battle/inventory_budget.i b/src/battle/inventory_budget.i new file mode 100644 index 0000000..748881e --- /dev/null +++ b/src/battle/inventory_budget.i @@ -0,0 +1,21 @@ +""" +Shared inventory VWF tile-budget constants. Included by both +`src/battle/inventory_rolling.s` (slot init / clear loops / allocator +clamp) and `src/battle/message.s` (NMI partial-DMA path tables). + +The runtime budget is validated against the items XML assets by +`tests/test_vwf_asset_widths.py` ; bumping ITEM_VWF_TILE_BUDGET also +requires updating the per-slot tile_id tables to stay disjoint. +""" + + +; CHR tile_ids reserved per inventory slot (each tile_id = 16 CHR bytes). +ITEM_VWF_TILE_BUDGET := 10 + +; First slot's tile_id low byte. Slot N occupies +; ITEM_VWF_TILE_BASE + N * ITEM_VWF_TILE_BUDGET .. + budget-1. +ITEM_VWF_TILE_BASE := 0xC0 + +; Derived sizes. +ITEM_VWF_CHR_BYTES := ITEM_VWF_TILE_BUDGET * 16 ; CHR slice per slot +ITEM_VWF_CHR_WORDS := ITEM_VWF_TILE_BUDGET * 8 ; word-stride clear-loop count diff --git a/src/battle/inventory_rolling.s b/src/battle/inventory_rolling.s index 30eab21..e2048a6 100644 --- a/src/battle/inventory_rolling.s +++ b/src/battle/inventory_rolling.s @@ -3,153 +3,190 @@ Battle inventory rolling-buffer engine (single column, 5 visible rows + 1 prefet `InitInventoryTextBuf` / `TfrInventoryList`, hooks scroll up/down, rebuilds the wrapped HDMA scroll table and runs the field-menu NMI DMA check. """ +.include "config.i" +.include "../items.i" .extern assets_items_dat +.extern assets_items_unleashed_dat .extern mult8_trampoline .extern load_menu_tfr_data_trampoline .extern hex_to_dec_trampoline .extern normalize_num_trampoline .extern draw_text_rolling_trampoline .extern return_to_bank02 +.extern render.flush_chr_to_vram + +; externs live at root scope: a816 registers `.extern` only in the scope it is +; declared in, and an `.alloc` body opens its own scope, so an extern declared +; inside never resolves at the use site. +.if BATTLE_ITEMS_VWF { + .extern messages_vwf.init_inventory + .extern messages_vwf.deinit +} -; ============================================================================ -; Rolling Buffer Implementation for Battle Inventory (Single Column) -; ============================================================================ -; -; Single-column layout with circular buffer, matching FF6's approach. -; Only 5 visible rows are rendered, with edge rows pre-rendered on scroll. -; -; Key insight: Render BEFORE scroll animation starts (not after). -; -; See docs/ff6_rolling_inventory_analysis.md for design rationale. -; -; ============================================================================ - -; ============================================================================ -; CONSTANTS -; ============================================================================ - -; Layout (single column) -VISIBLE_ROWS := 5 ; Rows visible on screen -BUFFER_SLOTS := 6 ; 6 slots for 5 visible (1 off-screen for pre-render) -TOTAL_ITEMS := 48 ; Total inventory items -TOTAL_ROWS := 48 ; One item per row now - -; Buffer sizes -TEXT_BYTES_PER_ITEM := 60 ; 30 tiles x 2 bytes (dakuten + main rows) -TILEMAP_BYTES_PER_ROW := 128 ; 2 tilemap rows x 64 bytes ($80) - -; Memory addresses - using freed spell list buffers -; Spell list buffers freed by magic direct rendering: $97A6, $9E66, $A526, $ABE6, $B2A6 -text_buffer_base := 0x97A6 ; Ring buffer (6 slots × 60 = 360 bytes, uses freed spell buffer 1) -inv_format_buffer := 0x9E66 ; Format buffer for draw_text (uses freed spell buffer 2) -tilemap_buffer_base := 0xC4E6 ; Tilemap buffer -tilemap_content_offset := 0x44 ; Offset to content area ($C52A - $C4E6) - -; ============================================================================ -; RAM VARIABLES (Using unused battle RAM) -; ============================================================================ -; $EF97-$EF99 are explicitly marked "unused" in RAM map -; $EF82 is marked "-" (unused) - -rolling_top_row := 0xEF97 ; Top visible row index (0-43) -rolling_buffer_pos := 0xEF98 ; Circular buffer position (0-4) -rolling_edge_row := 0xEF99 ; Row index to render (0-47) -rolling_slot_index := 0xEF82 ; Current slot index for rendering (0-5) -; NOTE: Using $EF82 instead of $1817 to avoid conflicts with NMI/battle commands - -; ============================================================================ -; VRAM CIRCULAR BUFFER SLOTS (FF6-style) -; ============================================================================ -; 5 fixed slots for visible rows, each 128 bytes (2 tilemap rows) -; Total VRAM usage: 640 bytes (vs 3072+ for linear) -; -; The HDMA scroll offset rotates which slot appears at which screen row. -; When scrolling down: slot[circular_pos] becomes new bottom, circular_pos++ -; When scrolling up: circular_pos--, slot[circular_pos] becomes new top - -VRAM_SLOT_SIZE := 0x80 ; 128 bytes per slot (2 tilemap rows) -VRAM_SLOT_BASE := 0x7400 ; Base VRAM address for slots - -; VRAM slot addresses - MUST match actual content positions after transfer! -; VRAM uses WORD addresses (2 bytes per word). -; Content goes to WRAM tilemap_buffer_base + tilemap_content_offset ($C4E6 + $44 = $C52A) -; Entry 3 transfers from $C4E6 to VRAM $7400, size $0400 bytes = $200 words -; WRAM byte offset $44 = VRAM word offset $22 (divide by 2) -; So $C52A maps to VRAM $7400 + $22 = $7422 -; Each WRAM slot is $80 bytes = $40 VRAM words (64 words = 2 tilemap rows = 16 pixels) - -_vram_slot_table: - .dw 0x7422 ; Slot 0 (content at $7400 + $22 words) - .dw 0x7462 ; Slot 1 (+$40 words) - .dw 0x74A2 ; Slot 2 (+$40 words) - .dw 0x74E2 ; Slot 3 (+$40 words) - .dw 0x7522 ; Slot 4 (+$40 words) - .dw 0x7562 ; Slot 5 (off-screen pre-render slot) - -; Tilemap buffer slots (mirror of VRAM slots in WRAM) -; Each slot is 128 bytes at tilemap_buffer_base + slot * 128 -TILEMAP_SLOT_BASE := tilemap_buffer_base + tilemap_content_offset - -; ============================================================================ -; init_inventory_text_buf_rolling -; ============================================================================ -; Replacement for $029E9C - renders only 5 visible rows -; Called when inventory window opens -; -; Input: None (reads $EF71 for current scroll position) -; Output: Text buffer populated with 5 visible items -; Clobbers: A, X, Y, $00-$06, $26-$2A - -init_inventory_text_buf_rolling: -""" -Replacement for original `$029E9C`: render the 5 visible inventory rows + 1 off-screen prefetch slot when the -in-battle inventory window opens, resetting the rolling-buffer state. -""" -; Set data bank to $7E for WRAM access - phb - lda #0x7E - pha - plb - -; Initialize rolling buffer state -; At init time, always start at row 0 (top of inventory) -; NOTE: EF65 and EF67 are initialized by ResetListScrollHDMA when inventory opens - stz.w 0xEF71 ; Game's row index = 0 - stz.w 0xEF86 ; Scroll offset = 0 (top item visible) - stz.b 0x60 ; Cursor row = 0 (top visible row) - stz.w rolling_top_row - stz.w rolling_buffer_pos - ; Note: Game's $4A flag (bit 2) already indicates inventory is active - -; Render 6 rows to circular slots (5 visible + 1 off-screen) -; At init, item index = slot index (both 0-5) +.include "../bank20.i" + +.alloc battle_inventory_rolling_block in bank20_reloc { + + ; ============================================================================ + ; Rolling Buffer Implementation for Battle Inventory (Single Column) + ; ============================================================================ + ; + ; Single-column layout with circular buffer, matching FF6's approach. + ; Only 5 visible rows are rendered, with edge rows pre-rendered on scroll. + ; + ; Key insight: Render BEFORE scroll animation starts (not after). + ; + ; See docs/ff6_rolling_inventory_analysis.md for design rationale. + ; + ; ============================================================================ + + ; ============================================================================ + ; CONSTANTS + ; ============================================================================ + + ; Layout (single column) + VISIBLE_ROWS := 5 ; Rows visible on screen + BUFFER_SLOTS := 6 ; 6 slots for 5 visible (1 off-screen for pre-render) + TOTAL_ITEMS := 48 ; Total inventory items + .include "src/battle/inventory_budget.i" +.include "../bank20.i" + TOTAL_ROWS := 48 ; One item per row now + + ; Buffer sizes + TEXT_BYTES_PER_ITEM := 60 ; 30 tiles x 2 bytes (dakuten + main rows) + TILEMAP_BYTES_PER_ROW := 128 ; 2 tilemap rows x 64 bytes ($80) + + ; Memory addresses - using freed spell list buffers + ; Spell list buffers freed by magic direct rendering: $97A6, $9E66, $A526, $ABE6, $B2A6 + text_buffer_base := 0x97A6 ; Ring buffer (6 slots × 60 = 360 bytes, uses freed spell buffer 1) + inv_format_buffer := 0x9E66 ; Format buffer for draw_text (uses freed spell buffer 2) + tilemap_buffer_base := 0xC4E6 ; Tilemap buffer + tilemap_content_offset := 0x46 ; Was $44 ($C52A, col 2). +2 = 1 tiles right ($C52E, col 2). + + ; ============================================================================ + ; RAM VARIABLES (Using unused battle RAM) + ; ============================================================================ + ; $EF97-$EF99 are explicitly marked "unused" in RAM map + ; $EF82 is marked "-" (unused) + + rolling_top_row := 0xEF97 ; Top visible row index (0-43) + rolling_buffer_pos := 0xEF98 ; Circular buffer position (0-4) + rolling_edge_row := 0xEF99 ; Row index to render (0-47) + rolling_slot_index := 0xEF82 ; Current slot index for rendering (0-5) + inventory_needs_full_refresh := 0xEF9D ; Non-zero = re-render all 5 visible slots this frame. + ; Set on init / item swap. Scroll edges render the + ; new hidden slot directly via _render_*_edge_row and + ; do NOT need a full refresh. + ; NOTE: Using $EF82 instead of $1817 to avoid conflicts with NMI/battle commands + + ; ============================================================================ + ; VRAM CIRCULAR BUFFER SLOTS (FF6-style) + ; ============================================================================ + ; 5 fixed slots for visible rows, each 128 bytes (2 tilemap rows) + ; Total VRAM usage: 640 bytes (vs 3072+ for linear) + ; + ; The HDMA scroll offset rotates which slot appears at which screen row. + ; When scrolling down: slot[circular_pos] becomes new bottom, circular_pos++ + ; When scrolling up: circular_pos--, slot[circular_pos] becomes new top + + VRAM_SLOT_SIZE := 0x80 ; 128 bytes per slot (2 tilemap rows) + VRAM_SLOT_BASE := 0x7400 ; Base VRAM address for slots + + ; VRAM slot addresses - MUST match actual content positions after transfer! + ; VRAM uses WORD addresses (2 bytes per word). + ; Content goes to WRAM tilemap_buffer_base + tilemap_content_offset ($C4E6 + $44 = $C52A) + ; Entry 3 transfers from $C4E6 to VRAM $7400, size $0400 bytes = $200 words + ; WRAM byte offset $44 = VRAM word offset $22 (divide by 2) + ; So $C52A maps to VRAM $7400 + $22 = $7422 + ; Each WRAM slot is $80 bytes = $40 VRAM words (64 words = 2 tilemap rows = 16 pixels) + + _vram_slot_table: + .dw 0x7422 ; Slot 0 (content at $7400 + $22 words) + .dw 0x7462 ; Slot 1 (+$40 words) + .dw 0x74A2 ; Slot 2 (+$40 words) + .dw 0x74E2 ; Slot 3 (+$40 words) + .dw 0x7522 ; Slot 4 (+$40 words) + .dw 0x7562 ; Slot 5 (off-screen pre-render slot) + + ; Tilemap buffer slots (mirror of VRAM slots in WRAM) + ; Each slot is 128 bytes at tilemap_buffer_base + slot * 128 + TILEMAP_SLOT_BASE := tilemap_buffer_base + tilemap_content_offset + + ; ============================================================================ + ; init_inventory_text_buf_rolling + ; ============================================================================ + ; Replacement for $029E9C - renders only 5 visible rows + ; Called when inventory window opens + ; + ; Input: None (reads $EF71 for current scroll position) + ; Output: Text buffer populated with 5 visible items + ; Clobbers: A, X, Y, $00-$06, $26-$2A + + init_inventory_text_buf_rolling: + """ + Replacement for original `$029E9C`: render the 5 visible inventory rows + 1 off-screen prefetch slot when the + in-battle inventory window opens, resetting the rolling-buffer state. + """ + ; Set data bank to $7E for WRAM access + phb + lda #0x7E + pha + plb + + ; Initialize rolling buffer state + ; At init time, always start at row 0 (top of inventory) + ; NOTE: EF65 and EF67 are initialized by ResetListScrollHDMA when inventory opens + stz.w 0xEF71 ; Game's row index = 0 + stz.w 0xEF86 ; Scroll offset = 0 (top item visible) + lda #1 + sta.w inventory_needs_full_refresh ; First frame after open: paint all visible. + stz.b 0x60 ; Cursor row = 0 (top visible row) + stz.w rolling_top_row + stz.w rolling_buffer_pos + ; Note: Game's $4A flag (bit 2) already indicates inventory is active + + .if BATTLE_ITEMS_VWF { + ; Reset the VWF allocator to tile_id 0xC0 once for the whole pass. + ; The 6-slot render loop below increments the allocator naturally so + ; each item owns a distinct tile range (item N at 0xC0 + N * ~9 tiles). + jsr.l messages_vwf.init_inventory + } + + ; Render 6 rows to circular slots (5 visible + 1 off-screen) + ; At init, item index = slot index (both 0-5) lda #0 sta.b 0x06 ; Slot index (0-5) -_init_row_loop: - ; For init: item index = slot index - lda.b 0x06 - sta.w rolling_edge_row ; Item index for data lookup - sta.w rolling_slot_index ; Slot index for buffer position + _init_row_loop: + ; For init: item index = slot index + lda.b 0x06 + sta.w rolling_edge_row ; Item index for data lookup + sta.w rolling_slot_index ; Slot index for buffer position -; Save slot index - lda.b 0x06 - pha + ; Save slot index + lda.b 0x06 + pha -; Render item to circular slot - jsr.w _render_item_to_circular_slot + ; Render item to circular slot + jsr.w _render_item_to_circular_slot -; Restore slot index - pla - sta.b 0x06 + ; Restore slot index + pla + sta.b 0x06 -; Next slot - inc.b 0x06 - lda.b 0x06 - cmp #BUFFER_SLOTS ; 6 slots total - bne _init_row_loop + ; Next slot + inc.b 0x06 + lda.b 0x06 + cmp #BUFFER_SLOTS ; 6 slots total + bne _init_row_loop -; Queue VRAM transfer for initial render + .if BATTLE_ITEMS_VWF { + ; End of inventory rolling pass: clear the VWF battle flag and signal + ; DMA so the inventory tile slice flushes to VRAM on the next NMI. + jsr.l messages_vwf.deinit + } + + ; Queue VRAM transfer for initial render lda #0x03 ldy.w #0x0002 jsr.l load_menu_tfr_data_trampoline @@ -160,280 +197,286 @@ _init_row_loop: plb ; Restore data bank rtl -; ============================================================================ -; _render_inventory_item -; ============================================================================ -; Renders a single inventory item to text buffer -; -; Input: rolling_slot_index = item slot index (0-47) -; Output: Item rendered to text buffer -; Clobbers: A, X, Y, $00-$02, $26-$2A - -_render_inventory_item: - ; Set data bank to $7E for WRAM access - phb - lda #0x7E - pha - plb - -; Calculate text buffer destination -; text_addr = text_buffer_base + (slot x TEXT_BYTES_PER_ITEM) - lda.w rolling_slot_index - sta.b 0x26 - lda #TEXT_BYTES_PER_ITEM - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - clc - adc #text_buffer_base - sta.w 0xEF52 ; draw_text output destination - tdc - sep #0x20 - -; Set up format buffer pointer - ldx.w #inv_format_buffer - stx.w 0xEF50 - -; Set tile count for draw_text - lda #15 ; 15 tiles per line - sta.w 0xEF54 - -; Get item data from game inventory -; Inventory structure: 4 bytes per slot at $321A -; $321A+0: flags (bit 7 = disabled) -; $321A+1: item ID -; $321A+2: quantity -; $321A+3: ??? - tdc ; Clear B before TAX - lda.w rolling_slot_index - asl - asl ; x 4 bytes per item - tax - - lda.l 0x7E321B, x ; Item ID (WRAM) - sta.b 0x02 ; Save for later - sta.b 0x26 ; For name lookup - lda.l 0x7E321C, x ; Quantity (WRAM) - pha ; Save quantity - -; Determine palette (white=enabled, gray=disabled) - lda #0x00 - sta.b 0x00 ; Palette for name - sta.b 0x01 ; Palette for symbol - lda.l 0x7E321A, x ; Flags (WRAM) - and #0x80 - beq _not_disabled - lda #0x04 ; Gray palette - sta.b 0x00 - sta.b 0x01 - -_not_disabled: - -; Calculate item name address: assets_items_dat + (id x 13) -; Must use 16-bit math since id x 13 can exceed 255 - rep #0x20 ; 16-bit A - lda.b 0x02 ; Load (will get $02-$03) - and.w #0x00FF ; Mask to item ID only - sta.b 0x08 ; Save original ID - asl ; x2 - clc - adc.b 0x08 ; x3 - asl - asl ; x12 - tax - sep #0x20 ; Back to 8-bit - -; Build format string in $74FD (protected by inventory-active skip checks) - ldy.w #0x0000 - -; Format code: change tile flags - lda #0x0E - sta.w inv_format_buffer, y - iny - lda.b 0x01 ; Symbol palette - sta.w inv_format_buffer, y - iny - -; Format code: set tile (symbol) - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.l assets_items_dat, x ; Item symbol - sta.w inv_format_buffer, y - iny + ; ============================================================================ + ; _render_inventory_item + ; ============================================================================ + ; Renders a single inventory item to text buffer + ; + ; Input: rolling_slot_index = item slot index (0-47) + ; Output: Item rendered to text buffer + ; Clobbers: A, X, Y, $00-$02, $26-$2A + + _render_inventory_item: + ; Set data bank to $7E for WRAM access + phb + lda #0x7E + pha + plb + + ; Calculate text buffer destination + ; text_addr = text_buffer_base + (slot x TEXT_BYTES_PER_ITEM) + lda.w rolling_slot_index + sta.b 0x26 + lda #TEXT_BYTES_PER_ITEM + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + clc + adc #text_buffer_base + sta.w 0xEF52 ; draw_text output destination + tdc + sep #0x20 + + ; Set up format buffer pointer + ldx.w #inv_format_buffer + stx.w 0xEF50 + + ; Set tile count for draw_text + lda #15 ; 15 tiles per line + sta.w 0xEF54 + + ; Get item data from game inventory + ; Inventory structure: 4 bytes per slot at $321A + ; $321A+0: flags (bit 7 = disabled) + ; $321A+1: item ID + ; $321A+2: quantity + ; $321A+3: ??? + tdc ; Clear B before TAX + lda.w rolling_slot_index + asl + asl ; x 4 bytes per item + tax + + lda.l 0x7E321B, x ; Item ID (WRAM) + sta.b 0x02 ; Save for later + sta.b 0x26 ; For name lookup + lda.l 0x7E321C, x ; Quantity (WRAM) + pha ; Save quantity + + ; Determine palette (white=enabled, gray=disabled) + lda #0x00 + sta.b 0x00 ; Palette for name + sta.b 0x01 ; Palette for symbol + lda.l 0x7E321A, x ; Flags (WRAM) + and #0x80 + beq _not_disabled + lda #0x04 ; Gray palette + sta.b 0x00 + sta.b 0x01 + + _not_disabled: + + ; Calculate item name address: assets_items_dat + (id x 13) + ; Must use 16-bit math since id x 13 can exceed 255 + rep #0x20 ; 16-bit A + lda.b 0x02 ; Load (will get $02-$03) + and.w #0x00FF ; Mask to item ID only + sta.b 0x08 ; Save original ID + asl ; x2 + clc + adc.b 0x08 ; x3 + asl + asl ; x12 + tax + sep #0x20 ; Back to 8-bit + + ; Build format string. Starts in fixed mode ; 0x0F toggles fixed <-> + ; VWF so only the name portion routes through the VWF blitter. + ldy.w #0x0000 + + lda #0x0E + sta.w inv_format_buffer, y + iny + lda.b 0x01 + sta.w inv_format_buffer, y + iny + + lda.l assets_items_dat, x + sta.w inv_format_buffer, y + iny + + .if BATTLE_ITEMS_VWF { + lda #0xFE + sta.w inv_format_buffer, y + iny + } -; Format code: change tile flags (for name) lda #0x0E sta.w inv_format_buffer, y iny - lda.b 0x00 ; Name palette + lda.b 0x00 sta.w inv_format_buffer, y iny -; Copy 11 character item name lda #11 - sta.b 0x00 ; Loop counter + sta.b 0x00 -_name_copy_loop: - inx - lda.l assets_items_dat, x - sta.w inv_format_buffer, y - iny - dec.b 0x00 - bne _name_copy_loop + _name_copy_loop: + inx + lda.l assets_items_dat, x + sta.w inv_format_buffer, y + iny + dec.b 0x00 + bne _name_copy_loop + + .if BATTLE_ITEMS_VWF { + lda #0xFE + sta.w inv_format_buffer, y + iny + lda #0xFC + sta.w inv_format_buffer, y + iny + lda #12 + sta.w inv_format_buffer, y + iny + } -; Handle quantity or empty slot - lda.b 0x02 ; Item ID + lda.b 0x02 bne _has_item -; Empty slot - just finish with line break - pla ; Discard quantity - lda #0x05 ; Line break + pla + lda #0x05 sta.w inv_format_buffer, y iny lda #0x03 bra _finish_format -_has_item: - ; Add colon and quantity - lda #0xC8 ; Colon character ":" - sta.w inv_format_buffer, y - iny + _has_item: + lda #0xC8 + sta.w inv_format_buffer, y + iny - pla ; Get quantity - tax - jsr.l hex_to_dec_trampoline ; Convert to decimal - jsr.l normalize_num_trampoline ; Format digits + pla + tax + jsr.l hex_to_dec_trampoline + jsr.l normalize_num_trampoline -; Add tens digit - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180E ; Tens digit - sta.w inv_format_buffer, y - iny + lda.w 0x180E + sta.w inv_format_buffer, y + iny -; Add ones digit - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180F ; Ones digit + lda.w 0x180F -_finish_format: - sta.w inv_format_buffer, y - iny + _finish_format: + sta.w inv_format_buffer, y + iny -; Terminator - lda #0x00 - sta.w inv_format_buffer, y + lda #0x00 + sta.w inv_format_buffer, y -; Call draw_text to render formatted text to text buffer - jsr.l draw_text_rolling_trampoline + ; Call draw_text to render formatted text to text buffer + jsr.l draw_text_rolling_trampoline - plb ; Restore data bank - rts - -; ============================================================================ -; _render_inventory_item_circular -; ============================================================================ -; Renders item to circular buffer slot -; -; Input: rolling_edge_row = item index (0-47) for data lookup -; rolling_slot_index = buffer slot (0-4) for destination -; Output: Item rendered to text buffer at slot position -; Clobbers: A, X, Y, $00-$02, $26-$2A - -_render_inventory_item_circular: - phb - lda #0x7E - pha - plb - -; Calculate text buffer destination using SLOT (not item index) -; text_addr = text_buffer_base + (slot x TEXT_BYTES_PER_ITEM) - lda.w rolling_slot_index ; Buffer slot (0-4) - sta.b 0x26 - lda #TEXT_BYTES_PER_ITEM - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - clc - adc #text_buffer_base - sta.w 0xEF52 ; draw_text output destination - tdc - sep #0x20 - -; Set up format buffer pointer - ldx.w #inv_format_buffer - stx.w 0xEF50 - -; Set tile count for draw_text - lda #15 ; 15 tiles per line - sta.w 0xEF54 - -; Get item data using ITEM INDEX (rolling_edge_row), not slot - tdc - lda.w rolling_edge_row ; Item index (0-47) - asl - asl ; x 4 bytes per item - tax - - lda.l 0x7E321B, x ; Item ID - sta.b 0x02 - sta.b 0x26 - lda.l 0x7E321C, x ; Quantity - pha - -; Determine palette - lda #0x00 - sta.b 0x00 - sta.b 0x01 - lda.l 0x7E321A, x ; Flags - and #0x80 - beq _circ_not_disabled - lda #0x04 - sta.b 0x00 - sta.b 0x01 + plb ; Restore data bank + rts -_circ_not_disabled: - -; Calculate item name address: assets_items_dat + (id x 12) - rep #0x20 - lda.b 0x02 - and.w #0x00FF - sta.b 0x08 - asl - clc - adc.b 0x08 - asl - asl - tax - sep #0x20 - -; Build format string in $74FD (protected by inventory-active skip checks) - ldy.w #0x0000 - -; Tile flags for symbol - lda #0x0E - sta.w inv_format_buffer, y - iny - lda.b 0x01 - sta.w inv_format_buffer, y - iny - -; Symbol tile - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.l assets_items_dat, x - sta.w inv_format_buffer, y - iny - -; Tile flags for name + ; ============================================================================ + ; _render_inventory_item_circular + ; ============================================================================ + ; Renders item to circular buffer slot + ; + ; Input: rolling_edge_row = item index (0-47) for data lookup + ; rolling_slot_index = buffer slot (0-4) for destination + ; Output: Item rendered to text buffer at slot position + ; Clobbers: A, X, Y, $00-$02, $26-$2A + + _render_inventory_item_circular: + phb + lda #0x7E + pha + plb + + ; Calculate text buffer destination using SLOT (not item index) + ; text_addr = text_buffer_base + (slot x TEXT_BYTES_PER_ITEM) + lda.w rolling_slot_index ; Buffer slot (0-4) + sta.b 0x26 + lda #TEXT_BYTES_PER_ITEM + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + clc + adc #text_buffer_base + sta.w 0xEF52 ; draw_text output destination + tdc + sep #0x20 + + ; Set up format buffer pointer + ldx.w #inv_format_buffer + stx.w 0xEF50 + + ; Set tile count for draw_text + lda #15 ; 15 tiles per line + sta.w 0xEF54 + + ; Get item data using ITEM INDEX (rolling_edge_row), not slot + tdc + lda.w rolling_edge_row ; Item index (0-47) + asl + asl ; x 4 bytes per item + tax + + lda.l 0x7E321B, x ; Item ID + sta.b 0x02 + sta.b 0x26 + lda.l 0x7E321C, x ; Quantity + pha + + ; Determine palette + lda #0x00 + sta.b 0x00 + sta.b 0x01 + lda.l 0x7E321A, x ; Flags + and #0x80 + beq _circ_not_disabled + lda #0x04 + sta.b 0x00 + sta.b 0x01 + + _circ_not_disabled: + + ; Calculate item name address: assets_items_dat + (id x 12) + rep #0x20 + lda.b 0x02 + and.w #0x00FF + sta.b 0x08 + asl + clc + adc.b 0x08 + asl + asl + tax + sep #0x20 + + ; Build format string. The custom inventory renderer starts in fixed + ; mode and uses 0x0F as a fixed<->VWF toggle so only the name routes + ; through the VWF blitter. + ldy.w #0x0000 + + ; Tile flags for symbol (fixed mode default) + lda #0x0E + sta.w inv_format_buffer, y + iny + lda.b 0x01 + sta.w inv_format_buffer, y + iny + + ; Symbol tile (raw byte ; default mode is fixed) + lda.l assets_items_dat, x + sta.w inv_format_buffer, y + iny + + ; Toggle to VWF for the name + .if BATTLE_ITEMS_VWF { + lda #0xFE + sta.w inv_format_buffer, y + iny + } + + ; Tile flags for name lda #0x0E sta.w inv_format_buffer, y iny @@ -441,19 +484,32 @@ _circ_not_disabled: sta.w inv_format_buffer, y iny -; 11-char name + ; 11-char name (raw chars dispatched through VWF blitter) lda #11 sta.b 0x00 -_circ_name_loop: - inx - lda.l assets_items_dat, x - sta.w inv_format_buffer, y - iny - dec.b 0x00 - bne _circ_name_loop - -; Quantity handling + _circ_name_loop: + inx + lda.l assets_items_dat, x + sta.w inv_format_buffer, y + iny + dec.b 0x00 + bne _circ_name_loop + + ; Toggle back to fixed for colon + digits + .if BATTLE_ITEMS_VWF { + lda #0xFE + sta.w inv_format_buffer, y + iny + lda #0xFC + sta.w inv_format_buffer, y + iny + lda #12 + sta.w inv_format_buffer, y + iny + } + + ; Quantity handling lda.b 0x02 bne _circ_has_item @@ -464,472 +520,478 @@ _circ_name_loop: lda #0x03 bra _circ_finish -_circ_has_item: - lda #0xC8 ; Colon - sta.w inv_format_buffer, y - iny + _circ_has_item: + lda #0xC8 + sta.w inv_format_buffer, y + iny - pla - tax - jsr.l hex_to_dec_trampoline - jsr.l normalize_num_trampoline + pla + tax + jsr.l hex_to_dec_trampoline + jsr.l normalize_num_trampoline - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180E - sta.w inv_format_buffer, y - iny - - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180F - -_circ_finish: - sta.w inv_format_buffer, y - iny - - lda #0x00 - sta.w inv_format_buffer, y + lda.w 0x180E + sta.w inv_format_buffer, y + iny - jsr.l draw_text_rolling_trampoline + lda.w 0x180F - plb - rts + _circ_finish: + sta.w inv_format_buffer, y + iny -; ============================================================================ -; _copy_item_to_tilemap_circular -; ============================================================================ -; Copies item from text buffer slot to tilemap slot (circular buffer) -; -; Input: rolling_slot_index = buffer slot (0-4) -; Output: Item copied to tilemap buffer at slot position -; Clobbers: A, X, Y, $00-$06 + lda #0x00 + sta.w inv_format_buffer, y -_copy_item_to_tilemap_circular: - ; Save DBR - mult8_trampoline may change it - phb - lda #0x7E - pha - plb ; Ensure DBR is $7E for WRAM access - -; Calculate tilemap buffer address for this SLOT (not item index) -; tilemap_addr = tilemap_buffer_base + (slot x TILEMAP_BYTES_PER_ROW) -; NO content_offset - write to full 128-byte slot - lda.w rolling_slot_index ; Buffer slot (0-4) - sta.b 0x26 - lda #TILEMAP_BYTES_PER_ROW - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - clc - adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! - sta.b 0x00 ; Tilemap destination - sep #0x20 - -; Calculate text buffer source using SLOT - lda.w rolling_slot_index - sta.b 0x26 - lda #TEXT_BYTES_PER_ITEM - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - tax - sep #0x20 - -; Copy first tilemap row (30 bytes) - ldy.w #0x0000 - -_circ_copy_row1: - lda.w text_buffer_base, x - sta (0x00), y - inx - iny - cpy.w #30 - bne _circ_copy_row1 - -; Clear remaining bytes of first row (30-63) to prevent stale border tiles -; Fill with $FF (blank tile) and $00 (palette 0) - -_circ_clear_row1: - lda #0xFF ; Blank tile - sta (0x00), y - iny - lda #0x00 ; Palette 0 - sta (0x00), y - iny - cpy.w #0x0040 - bne _circ_clear_row1 + jsr.l draw_text_rolling_trampoline -; Copy second tilemap row (+$40 offset) - ldy.w #0x0040 + plb + rts -_circ_copy_row2: - lda.w text_buffer_base, x - sta (0x00), y - inx - iny - cpy.w #0x005E - bne _circ_copy_row2 - -; Clear remaining bytes of second row (0x5E-0x7F) - -_circ_clear_row2: - lda #0xFF ; Blank tile - sta (0x00), y - iny - lda #0x00 ; Palette 0 - sta (0x00), y - iny - cpy.w #0x0080 - bne _circ_clear_row2 - -; Restore DBR - plb - rts - -; ============================================================================ -; _copy_item_to_tilemap -; ============================================================================ -; Copies a single item from text buffer to the correct tilemap position -; (Used for initial rendering where slot = item index) -; -; Input: rolling_slot_index = item slot index (0-47) -; Output: Item copied to tilemap buffer -; Clobbers: A, X, Y, $00-$06 - -_copy_item_to_tilemap: - ; Calculate row index from slot (same as slot for single column) - lda.w rolling_slot_index - sta.b 0x04 ; Save row index - -; Calculate tilemap buffer address for this row -; tilemap_addr = tilemap_buffer_base + (row x TILEMAP_BYTES_PER_ROW) - sta.b 0x26 - lda #TILEMAP_BYTES_PER_ROW - sta.b 0x28 - jsr.l mult8_trampoline - -; Add content offset (left column position) - rep #0x20 - lda.b 0x2A - clc - adc #tilemap_buffer_base + tilemap_content_offset - sta.b 0x00 ; Tilemap destination - sep #0x20 - -; Calculate text buffer source offset - lda.w rolling_slot_index - sta.b 0x26 - lda #TEXT_BYTES_PER_ITEM - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A ; Get offset (slot x 60) - tax ; X = offset into text buffer - sep #0x20 - -; Copy first tilemap row (30 bytes = 15 tiles) - ldy.w #0x0000 - -_copy_row1: - lda.w text_buffer_base, x ; Read from $8EA6 + offset - sta (0x00), y ; Write to tilemap buffer - inx - iny - cpy.w #30 - bne _copy_row1 - -; Copy second tilemap row (30 bytes) -; Tilemap row 2 is at +$40 (64 bytes) from row 1 - ldy.w #0x0040 - -_copy_row2: - lda.w text_buffer_base, x ; Read from $8EA6 + offset - sta (0x00), y - inx - iny - cpy.w #0x005E ; $40 + 30 = $5E - bne _copy_row2 - - rts - -; ============================================================================ -; _render_bottom_edge_row -; ============================================================================ -; Pre-renders the new bottom row BEFORE scroll down animation starts -; Uses circular buffer - writes to the slot that's scrolling OUT (top slot) -; After scroll, that slot will appear at the bottom -; -; Called via JSL from bank 02 - -_render_bottom_edge_row: - ; Disable interrupts to prevent NMI from corrupting rolling_slot_index - sei - -; Set data bank to $7E for WRAM access - phb - lda #0x7E - pha - plb - -; Calculate the new bottom item index -; new_bottom = current_top + BUFFER_SLOTS (the item that will be at bottom after scroll) -; With 6 slots and 5 visible, this is the item we need to pre-render - lda.w 0xEF71 - clc - adc #BUFFER_SLOTS - -; Check bounds (max item is 47) - cmp #TOTAL_ITEMS - bcs _render_bottom_done ; Past end, nothing to render - - sta.w rolling_edge_row ; Save item index for data lookup - -; CIRCULAR BUFFER: Write to OFF-SCREEN slot (below bottom) -; Off-screen slot = (rolling_buffer_pos + 5) % 6 -; This slot is currently invisible and will scroll into view at bottom - lda.w rolling_buffer_pos - clc - adc #VISIBLE_ROWS ; +5 to get to off-screen slot - cmp #BUFFER_SLOTS - bcc _bottom_slot_ok - sec - sbc #BUFFER_SLOTS ; Wrap if >= 6 - -_bottom_slot_ok: - sta.w rolling_slot_index ; Slot index (0-5) for buffer positioning - -; Render item data to this circular slot - jsr.w _render_item_to_circular_slot - -; Queue full tilemap transfer (game's VBlank will handle it) - lda #0x03 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline - lda #0x01 - sta.w 0x1825 - sta.w 0x1824 - -; Advance circular position (top slot advances as we scroll down) - lda.w rolling_buffer_pos - inc - cmp #BUFFER_SLOTS - bcc _store_pos_down - lda #0 - -_store_pos_down: - sta.w rolling_buffer_pos - -_render_bottom_done: - plb ; Restore data bank - cli ; re-enable interrupts - rts ; Called from within bank $20 now - -; ============================================================================ -; _render_top_edge_row -; ============================================================================ -; Pre-renders the new top row BEFORE scroll up animation starts -; Uses circular buffer - writes to the slot that's scrolling OUT (bottom slot) -; After scroll, that slot will appear at the top -; -; Called via JSL from bank 02 - -_render_top_edge_row: - ; Disable interrupts to prevent NMI from corrupting rolling_slot_index - sei - -; Set data bank to $7E for WRAM access - phb - lda #0x7E - pha - plb - -; Calculate the new top item index -; new_top = current_top - 1 - lda.w 0xEF71 - dec ; New top row after scroll - -; Check bounds - bmi _render_top_done ; Negative = invalid - - sta.w rolling_edge_row ; Save item index for data lookup - -; CIRCULAR BUFFER: Write to slot BEFORE current circular_pos -; This slot will become the new TOP after scroll -; First decrement circular_pos, then write to that slot - lda.w rolling_buffer_pos - dec - bpl _store_pos_up - lda #BUFFER_SLOTS - 1 ; Wrap 0→5 - -_store_pos_up: - sta.w rolling_buffer_pos ; Update position FIRST for scroll up - sta.w rolling_slot_index ; Slot index (0-5) for buffer positioning + ; ============================================================================ + ; _copy_item_to_tilemap_circular + ; ============================================================================ + ; Copies item from text buffer slot to tilemap slot (circular buffer) + ; + ; Input: rolling_slot_index = buffer slot (0-4) + ; Output: Item copied to tilemap buffer at slot position + ; Clobbers: A, X, Y, $00-$06 + + _copy_item_to_tilemap_circular: + ; Save DBR - mult8_trampoline may change it + phb + lda #0x7E + pha + plb ; Ensure DBR is $7E for WRAM access + + ; Calculate tilemap buffer address for this SLOT (not item index) + ; tilemap_addr = tilemap_buffer_base + (slot x TILEMAP_BYTES_PER_ROW) + ; NO content_offset - write to full 128-byte slot + lda.w rolling_slot_index ; Buffer slot (0-4) + sta.b 0x26 + lda #TILEMAP_BYTES_PER_ROW + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + clc + adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! + sta.b 0x00 ; Tilemap destination + sep #0x20 + + ; Calculate text buffer source using SLOT + lda.w rolling_slot_index + sta.b 0x26 + lda #TEXT_BYTES_PER_ITEM + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + tax + sep #0x20 + + ; Copy first tilemap row (30 bytes) + ldy.w #0x0000 + + _circ_copy_row1: + lda.w text_buffer_base, x + sta (0x00), y + inx + iny + cpy.w #30 + bne _circ_copy_row1 + + ; Clear remaining bytes of first row (30-63) to prevent stale border tiles + ; Fill with $FF (blank tile) and $00 (palette 0) + + _circ_clear_row1: + lda #0xFF ; Blank tile + sta (0x00), y + iny + lda #0x00 ; Palette 0 + sta (0x00), y + iny + cpy.w #0x0040 + bne _circ_clear_row1 + + ; Copy second tilemap row (+$40 offset) + ldy.w #0x0040 + + _circ_copy_row2: + lda.w text_buffer_base, x + sta (0x00), y + inx + iny + cpy.w #0x005E + bne _circ_copy_row2 + + ; Clear remaining bytes of second row (0x5E-0x7F) + + _circ_clear_row2: + lda #0xFF ; Blank tile + sta (0x00), y + iny + lda #0x00 ; Palette 0 + sta (0x00), y + iny + cpy.w #0x0080 + bne _circ_clear_row2 + + ; Restore DBR + plb + rts + + ; ============================================================================ + ; _copy_item_to_tilemap + ; ============================================================================ + ; Copies a single item from text buffer to the correct tilemap position + ; (Used for initial rendering where slot = item index) + ; + ; Input: rolling_slot_index = item slot index (0-47) + ; Output: Item copied to tilemap buffer + ; Clobbers: A, X, Y, $00-$06 + + _copy_item_to_tilemap: + ; Calculate row index from slot (same as slot for single column) + lda.w rolling_slot_index + sta.b 0x04 ; Save row index + + ; Calculate tilemap buffer address for this row + ; tilemap_addr = tilemap_buffer_base + (row x TILEMAP_BYTES_PER_ROW) + sta.b 0x26 + lda #TILEMAP_BYTES_PER_ROW + sta.b 0x28 + jsr.l mult8_trampoline + + ; Add content offset (left column position) + rep #0x20 + lda.b 0x2A + clc + adc #tilemap_buffer_base + tilemap_content_offset + sta.b 0x00 ; Tilemap destination + sep #0x20 + + ; Calculate text buffer source offset + lda.w rolling_slot_index + sta.b 0x26 + lda #TEXT_BYTES_PER_ITEM + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A ; Get offset (slot x 60) + tax ; X = offset into text buffer + sep #0x20 + + ; Copy first tilemap row (30 bytes = 15 tiles) + ldy.w #0x0000 + + _copy_row1: + lda.w text_buffer_base, x ; Read from $8EA6 + offset + sta (0x00), y ; Write to tilemap buffer + inx + iny + cpy.w #30 + bne _copy_row1 + + ; Copy second tilemap row (30 bytes) + ; Tilemap row 2 is at +$40 (64 bytes) from row 1 + ldy.w #0x0040 + + _copy_row2: + lda.w text_buffer_base, x ; Read from $8EA6 + offset + sta (0x00), y + inx + iny + cpy.w #0x005E ; $40 + 30 = $5E + bne _copy_row2 + + rts + + ; ============================================================================ + ; _render_bottom_edge_row + ; ============================================================================ + ; Pre-renders the new bottom row BEFORE scroll down animation starts + ; Uses circular buffer - writes to the slot that's scrolling OUT (top slot) + ; After scroll, that slot will appear at the bottom + ; + ; Called via JSL from bank 02 -; Render item data to this circular slot - jsr.w _render_item_to_circular_slot + _render_bottom_edge_row: + ; Disable interrupts to prevent NMI from corrupting rolling_slot_index + sei -; Queue full tilemap transfer (game's VBlank will handle it) - lda #0x03 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline - lda #0x01 - sta.w 0x1825 - sta.w 0x1824 + ; Set data bank to $7E for WRAM access + phb + lda #0x7E + pha + plb + + ; Calculate the new bottom item index + ; new_bottom = current_top + BUFFER_SLOTS (the item that will be at bottom after scroll) + ; With 6 slots and 5 visible, this is the item we need to pre-render + lda.w 0xEF71 + clc + adc #BUFFER_SLOTS + + ; Check bounds (max item is 47) + cmp #TOTAL_ITEMS + bcs _render_bottom_done ; Past end, nothing to render + + sta.w rolling_edge_row ; Save item index for data lookup + + ; CIRCULAR BUFFER: Write to OFF-SCREEN slot (below bottom) + ; Off-screen slot = (rolling_buffer_pos + 5) % 6 + ; This slot is currently invisible and will scroll into view at bottom + lda.w rolling_buffer_pos + clc + adc #VISIBLE_ROWS ; +5 to get to off-screen slot + cmp #BUFFER_SLOTS + bcc _bottom_slot_ok + sec + sbc #BUFFER_SLOTS ; Wrap if >= 6 + + _bottom_slot_ok: + sta.w rolling_slot_index ; Slot index (0-5) for buffer positioning + + ; Render item data to this circular slot + jsr.w _render_item_to_circular_slot + + ; Queue full tilemap transfer (game's VBlank will handle it) + lda #0x03 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + lda #0x01 + sta.w 0x1825 + sta.w 0x1824 + + ; Advance circular position (top slot advances as we scroll down) + lda.w rolling_buffer_pos + inc + cmp #BUFFER_SLOTS + bcc _store_pos_down + lda #0 + + _store_pos_down: + sta.w rolling_buffer_pos + + _render_bottom_done: + plb ; Restore data bank + cli ; re-enable interrupts + rts ; Called from within bank $20 now + + ; ============================================================================ + ; _render_top_edge_row + ; ============================================================================ + ; Pre-renders the new top row BEFORE scroll up animation starts + ; Uses circular buffer - writes to the slot that's scrolling OUT (bottom slot) + ; After scroll, that slot will appear at the top + ; + ; Called via JSL from bank 02 -_render_top_done: - plb ; Restore data bank - cli ; re-enable interrupts - rts ; Called from within bank $20 now - -; ============================================================================ -; _render_item_to_circular_slot -; ============================================================================ -; Renders item to a circular buffer slot -; -; Input: rolling_edge_row = item index (0-47) for data lookup -; rolling_slot_index = slot index (0-4) for destination -; Output: Item rendered to text buffer slot, copied to tilemap slot - -_render_item_to_circular_slot: - ; Save processor status and disable interrupts - ; Using PHP/PLP instead of SEI/CLI to handle nested calls correctly - php - sei - -; Save DBR - draw_text may change it and we need it for tilemap copy - phb - -; Save D and set to $0000 for direct page operations - rep #0x20 - tdc - pha ; Save original D on stack - lda.w #0x0000 - tcd - sep #0x20 - -; CRITICAL: Save zero page variables on stack -; This prevents corruption if NMI fires during game's block copy -; Save $00-$0B (block copy uses $00-$06) and $26-$2B (Mult8 uses these) - lda.b 0x00 - pha - lda.b 0x01 - pha - lda.b 0x02 - pha - lda.b 0x03 - pha - lda.b 0x04 - pha - lda.b 0x05 - pha - lda.b 0x06 - pha - lda.b 0x07 - pha - lda.b 0x08 - pha - lda.b 0x09 - pha - lda.b 0x0A - pha - lda.b 0x0B - pha - ; Also save $26-$2B used by mult8_trampoline - lda.b 0x26 - pha - lda.b 0x27 - pha - lda.b 0x28 - pha - lda.b 0x29 - pha - lda.b 0x2A - pha - lda.b 0x2B - pha - -; Calculate text buffer destination for this SLOT -; text_addr = text_buffer_base + (slot × TEXT_BYTES_PER_ITEM) - lda.w rolling_slot_index ; Slot index (0-4) - sta.b 0x26 - lda #TEXT_BYTES_PER_ITEM ; 60 bytes per slot - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - clc - adc #text_buffer_base - sta.w 0xEF52 ; draw_text output destination - tdc - sep #0x20 - -; Set up format buffer pointer - ldx.w #inv_format_buffer - stx.w 0xEF50 - lda #15 - sta.w 0xEF54 - -; Get item data using rolling_edge_row (the actual item index) - tdc - lda.w rolling_edge_row - asl - asl - tax - - lda.l 0x7E321B, x ; Item ID - sta.b 0x02 - lda.l 0x7E321C, x ; Quantity - pha - -; Palette selection - lda #0x00 - sta.b 0x00 - sta.b 0x01 - lda.l 0x7E321A, x - and #0x80 - beq _slot_not_disabled - lda #0x04 - sta.b 0x00 - sta.b 0x01 + _render_top_edge_row: + ; Disable interrupts to prevent NMI from corrupting rolling_slot_index + sei -_slot_not_disabled: + ; Set data bank to $7E for WRAM access + phb + lda #0x7E + pha + plb + + ; Calculate the new top item index + ; new_top = current_top - 1 + lda.w 0xEF71 + dec ; New top row after scroll + + ; Check bounds + bmi _render_top_done ; Negative = invalid + + sta.w rolling_edge_row ; Save item index for data lookup + + ; CIRCULAR BUFFER: Write to slot BEFORE current circular_pos + ; This slot will become the new TOP after scroll + ; First decrement circular_pos, then write to that slot + lda.w rolling_buffer_pos + dec + bpl _store_pos_up + lda #BUFFER_SLOTS - 1 ; Wrap 0→5 + + _store_pos_up: + sta.w rolling_buffer_pos ; Update position FIRST for scroll up + sta.w rolling_slot_index ; Slot index (0-5) for buffer positioning + + ; Render item data to this circular slot + jsr.w _render_item_to_circular_slot + + ; Queue full tilemap transfer (game's VBlank will handle it) + lda #0x03 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + lda #0x01 + sta.w 0x1825 + sta.w 0x1824 + + _render_top_done: + plb ; Restore data bank + cli ; re-enable interrupts + rts ; Called from within bank $20 now + + ; ============================================================================ + ; _render_item_to_circular_slot + ; ============================================================================ + ; Renders item to a circular buffer slot + ; + ; Input: rolling_edge_row = item index (0-47) for data lookup + ; rolling_slot_index = slot index (0-4) for destination + ; Output: Item rendered to text buffer slot, copied to tilemap slot + + _render_item_to_circular_slot: + ; Save processor status and disable interrupts + ; Using PHP/PLP instead of SEI/CLI to handle nested calls correctly + php + sei + + ; Save DBR - draw_text may change it and we need it for tilemap copy + phb + + ; Save D and set to $0000 for direct page operations + rep #0x20 + tdc + pha ; Save original D on stack + lda.w #0x0000 + tcd + sep #0x20 + + ; CRITICAL: Save zero page variables on stack + ; This prevents corruption if NMI fires during game's block copy + ; Save $00-$0B (block copy uses $00-$06) and $26-$2B (Mult8 uses these) + lda.b 0x00 + pha + lda.b 0x01 + pha + lda.b 0x02 + pha + lda.b 0x03 + pha + lda.b 0x04 + pha + lda.b 0x05 + pha + lda.b 0x06 + pha + lda.b 0x07 + pha + lda.b 0x08 + pha + lda.b 0x09 + pha + lda.b 0x0A + pha + lda.b 0x0B + pha + ; Also save $26-$2B used by mult8_trampoline + lda.b 0x26 + pha + lda.b 0x27 + pha + lda.b 0x28 + pha + lda.b 0x29 + pha + lda.b 0x2A + pha + lda.b 0x2B + pha + + ; Calculate text buffer destination for this SLOT + ; text_addr = text_buffer_base + (slot × TEXT_BYTES_PER_ITEM) + lda.w rolling_slot_index ; Slot index (0-4) + sta.b 0x26 + lda #TEXT_BYTES_PER_ITEM ; 60 bytes per slot + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + clc + adc #text_buffer_base + sta.w 0xEF52 ; draw_text output destination + tdc + sep #0x20 + + ; Set up format buffer pointer + ldx.w #inv_format_buffer + stx.w 0xEF50 + lda #15 + sta.w 0xEF54 + + ; Get item data using rolling_edge_row (the actual item index) + tdc + lda.w rolling_edge_row + asl + asl + tax + + lda.l 0x7E321B, x ; Item ID + sta.b 0x02 + bne _slot_id_nonzero + jmp.w _empty_slot_fast ; ID == 0 -> empty entry, skip VWF + + _slot_id_nonzero: + lda.l 0x7E321C, x ; Quantity + pha + + ; Palette selection + lda #0x00 + sta.b 0x00 + sta.b 0x01 + lda.l 0x7E321A, x + and #0x80 + beq _slot_not_disabled + lda #0x04 + sta.b 0x00 + sta.b 0x01 + + _slot_not_disabled: + + ; Calculate item name offset into the 17-byte-per-record + ; assets_items_unleashed_dat table: id * 17 = (id << 4) + id. + rep #0x20 + lda.b 0x02 + and.w #0x00FF + sta.b 0x08 + asl + asl + asl + asl + clc + adc.b 0x08 + tax + sep #0x20 + + ; Build format string. Same fixed-default + 0x0F-toggle pattern the + ; other two format builders in this file use. + ldy.w #0x0000 + lda #0x0E + sta.w inv_format_buffer, y + iny + lda.b 0x01 + sta.w inv_format_buffer, y + iny + + lda.l assets_items_unleashed_dat, x + sta.w inv_format_buffer, y + iny + + .if BATTLE_ITEMS_VWF { + lda #0xFE + sta.w inv_format_buffer, y + iny + } -; Calculate item name offset - rep #0x20 - lda.b 0x02 - and.w #0x00FF - sta.b 0x08 - asl - clc - adc.b 0x08 - asl - asl - tax - sep #0x20 - -; Build format string - ldy.w #0x0000 - lda #0x0E - sta.w inv_format_buffer, y - iny - lda.b 0x01 - sta.w inv_format_buffer, y - iny - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.l assets_items_dat, x - sta.w inv_format_buffer, y - iny lda #0x0E sta.w inv_format_buffer, y iny @@ -937,16 +999,28 @@ _slot_not_disabled: sta.w inv_format_buffer, y iny - lda #11 + lda #16 sta.b 0x00 -_slot_name_loop: - inx - lda.l assets_items_dat, x - sta.w inv_format_buffer, y - iny - dec.b 0x00 - bne _slot_name_loop + _slot_name_loop: + inx + lda.l assets_items_unleashed_dat, x + sta.w inv_format_buffer, y + iny + dec.b 0x00 + bne _slot_name_loop + + .if BATTLE_ITEMS_VWF { + lda #0xFE + sta.w inv_format_buffer, y + iny + lda #0xFC + sta.w inv_format_buffer, y + iny + lda #12 + sta.w inv_format_buffer, y + iny + } lda.b 0x02 bne _slot_has_item @@ -957,1538 +1031,1592 @@ _slot_name_loop: lda #0x03 bra _slot_finish -_slot_has_item: - lda #0xC8 - sta.w inv_format_buffer, y - iny - pla - tax - jsr.l hex_to_dec_trampoline - jsr.l normalize_num_trampoline - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180E - sta.w inv_format_buffer, y - iny - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180F - -_slot_finish: - sta.w inv_format_buffer, y - iny - lda #0x00 - sta.w inv_format_buffer, y - - jsr.l draw_text_rolling_trampoline - -; draw_text fills text buffer - tilemap copy is done separately by: -; - _copy_all_slots_to_tilemap in tfr_inventory_list_rolling (for init) -; - _copy_slot_to_tilemap in scroll hooks (for scrolling) - -; CRITICAL: Restore zero page variables from stack (reverse order) -; First restore $26-$2B (last pushed) - pla - sta.b 0x2B - pla - sta.b 0x2A - pla - sta.b 0x29 - pla - sta.b 0x28 - pla - sta.b 0x27 - pla - sta.b 0x26 - ; Then restore $00-$0B - pla - sta.b 0x0B - pla - sta.b 0x0A - pla - sta.b 0x09 - pla - sta.b 0x08 - pla - sta.b 0x07 - pla - sta.b 0x06 - pla - sta.b 0x05 - pla - sta.b 0x04 - pla - sta.b 0x03 - pla - sta.b 0x02 - pla - sta.b 0x01 - pla - sta.b 0x00 - -; Restore D before returning - rep #0x20 - pla - tcd - sep #0x20 - -; Restore DBR (draw_text may have changed it) - plb - -; Restore processor status (including interrupt flag) - plp - rts - -; ============================================================================ -; _transfer_circular_slot -; ============================================================================ -; Transfers a single circular slot to its fixed VRAM address -; Uses direct DMA instead of menu transfer system for precise control -; -; Input: rolling_slot_index = slot index (0-4) -; Output: 128 bytes transferred to VRAM - -_transfer_circular_slot: - ; Save processor status and disable interrupts - php - sei - -; Save DBR - mult8_trampoline may change it - phb - lda #0x7E - pha - plb ; Ensure DBR is $7E for WRAM access - -; CRITICAL: Save zero page variables on stack -; This prevents corruption if NMI fires during game's block copy -; Save $00-$0B (block copy uses $00-$06) and $26-$2B (Mult8 uses these) - lda.b 0x00 - pha - lda.b 0x01 - pha - lda.b 0x02 - pha - lda.b 0x03 - pha - lda.b 0x04 - pha - lda.b 0x05 - pha - lda.b 0x06 - pha - lda.b 0x07 - pha - lda.b 0x08 - pha - lda.b 0x09 - pha - lda.b 0x0A - pha - lda.b 0x0B - pha - ; Also save $26-$2B used by mult8_trampoline - lda.b 0x26 - pha - lda.b 0x27 - pha - lda.b 0x28 - pha - lda.b 0x29 - pha - lda.b 0x2A - pha - lda.b 0x2B - pha - -; Look up VRAM destination from slot table - lda.w rolling_slot_index - asl ; ×2 for word lookup - tax - rep #0x20 - lda.l _vram_slot_table, x ; Get VRAM address ($7400, $7480, etc.) - sta.b 0x04 ; Save VRAM dest - sep #0x20 - -; Calculate tilemap buffer source -; source = tilemap_buffer_base + (slot × 128) - NO content_offset! - lda.w rolling_slot_index - sta.b 0x26 - lda #TILEMAP_BYTES_PER_ROW - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - clc - adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! - sta.b 0x00 ; Source address (low word) - sep #0x20 - -; Queue using menu transfer system (entry 3 covers our slots) -; The transfer will include this slot since it's at the right offset - lda #0x03 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline - - lda #0x01 - sta.w 0x1825 - sta.w 0x1824 - -; CRITICAL: Restore zero page variables from stack (reverse order) -; First restore $26-$2B (last pushed) - pla - sta.b 0x2B - pla - sta.b 0x2A - pla - sta.b 0x29 - pla - sta.b 0x28 - pla - sta.b 0x27 - pla - sta.b 0x26 - ; Then restore $00-$0B - pla - sta.b 0x0B - pla - sta.b 0x0A - pla - sta.b 0x09 - pla - sta.b 0x08 - pla - sta.b 0x07 - pla - sta.b 0x06 - pla - sta.b 0x05 - pla - sta.b 0x04 - pla - sta.b 0x03 - pla - sta.b 0x02 - pla - sta.b 0x01 - pla - sta.b 0x00 - -; Restore DBR - plb - -; Restore processor status (including interrupt flag) - plp - rts - -; ============================================================================ -; _copy_slot_to_tilemap -; ============================================================================ -; Copies a single slot from text buffer to tilemap buffer -; Input: rolling_slot_index = slot index (0-5) -; Uses $00-$01, $26, $28, $2A, X, Y - -_copy_slot_to_tilemap: - ; Save processor status and disable interrupts - ; Using PHP/PLP instead of SEI/CLI to handle nested calls correctly - php - sei - -; Save DBR - mult8_trampoline may change it - phb - -; Save and set D to 0 for direct page operations - rep #0x20 - tdc - pha - lda.w #0x0000 - tcd - sep #0x20 - -; CRITICAL: Save zero page variables on stack -; This prevents corruption if NMI fires during game's block copy -; Save $00-$0B (block copy uses $00-$06) and $26-$2B (Mult8 uses these) - lda.b 0x00 - pha - lda.b 0x01 - pha - lda.b 0x02 - pha - lda.b 0x03 - pha - lda.b 0x04 - pha - lda.b 0x05 - pha - lda.b 0x06 - pha - lda.b 0x07 - pha - lda.b 0x08 - pha - lda.b 0x09 - pha - lda.b 0x0A - pha - lda.b 0x0B - pha - ; Also save $26-$2B used by mult8_trampoline - lda.b 0x26 - pha - lda.b 0x27 - pha - lda.b 0x28 - pha - lda.b 0x29 - pha - lda.b 0x2A - pha - lda.b 0x2B - pha - -; Calculate tilemap destination (NO content_offset - write to full slot) - lda.w rolling_slot_index - sta.b 0x26 - lda #TILEMAP_BYTES_PER_ROW - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - clc - adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! - sta.b 0x00 - sep #0x20 - -; Calculate text buffer source - lda.w rolling_slot_index - sta.b 0x26 - lda #TEXT_BYTES_PER_ITEM - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - tax - sep #0x20 - -; Copy row 1 (30 bytes) - content only, preserve borders - ldy.w #0x0000 - -_copy_slot_row1: - lda.w text_buffer_base, x - sta (0x00), y - inx - iny - cpy.w #30 - bne _copy_slot_row1 - -; Copy row 2 (+$40 offset) - content only, preserve borders - ldy.w #0x0040 - -_copy_slot_row2: - lda.w text_buffer_base, x - sta (0x00), y - inx - iny - cpy.w #0x005E - bne _copy_slot_row2 - -; CRITICAL: Restore zero page variables $26-$2B from stack (reverse order - pushed last, pop first) - pla - sta.b 0x2B - pla - sta.b 0x2A - pla - sta.b 0x29 - pla - sta.b 0x28 - pla - sta.b 0x27 - pla - sta.b 0x26 - -; CRITICAL: Restore zero page variables $00-$0B from stack (reverse order) - pla - sta.b 0x0B - pla - sta.b 0x0A - pla - sta.b 0x09 - pla - sta.b 0x08 - pla - sta.b 0x07 - pla - sta.b 0x06 - pla - sta.b 0x05 - pla - sta.b 0x04 - pla - sta.b 0x03 - pla - sta.b 0x02 - pla - sta.b 0x01 - pla - sta.b 0x00 - -; Restore D register - rep #0x20 - pla - tcd - sep #0x20 - -; Restore DBR (pushed after php/sei) - plb - -; Restore processor status (including interrupt flag) - plp - rts + _slot_has_item: + lda #0xC8 + sta.w inv_format_buffer, y + iny + pla + ; M=8 pla loads A.lo only; A.hi retains a leaked byte from the + ; preceding 16-bit arithmetic. With X=16 the next `tax` would copy + ; that garbage into X.hi and hex_to_dec would emit BCD for the + ; wider value (qty 1 + leaked $06 -> X=$0601 -> "37"). Zero-extend + ; A explicitly before transfer. + rep #0x20 + and.w #0x00ff + tax + sep #0x20 + jsr.l hex_to_dec_trampoline + jsr.l normalize_num_trampoline + lda.w 0x180E + sta.w inv_format_buffer, y + iny + lda.w 0x180F + + _slot_finish: + sta.w inv_format_buffer, y + iny + lda #0x00 + sta.w inv_format_buffer, y + + jsr.l draw_text_rolling_trampoline + jmp.w _slot_render_done + + _empty_slot_fast: + """ + Empty inventory entry (item ID = 0). Skip the format-build + VWF + blit and fill the slot's text buffer with 30 blank tile entries + (2 bytes each = `$FF $00`). Saves the ~80K cycles draw_text would + otherwise burn rendering an empty palette + name string. + """ + rep #0x20 + lda.w 0xEF52 + sta.b 0x04 ; ptr-to-slot in DP scratch + sep #0x20 + ldy.w #0 + + _empty_fill_loop: + lda.b #0xFF + sta (0x04), y + iny + lda.b #0x00 + sta (0x04), y + iny + cpy.w #60 + bne _empty_fill_loop + + _slot_render_done: + + ; draw_text fills text buffer - tilemap copy is done separately by: + ; - _copy_all_slots_to_tilemap in tfr_inventory_list_rolling (for init) + ; - _copy_slot_to_tilemap in scroll hooks (for scrolling) + + ; CRITICAL: Restore zero page variables from stack (reverse order) + ; First restore $26-$2B (last pushed) + pla + sta.b 0x2B + pla + sta.b 0x2A + pla + sta.b 0x29 + pla + sta.b 0x28 + pla + sta.b 0x27 + pla + sta.b 0x26 + ; Then restore $00-$0B + pla + sta.b 0x0B + pla + sta.b 0x0A + pla + sta.b 0x09 + pla + sta.b 0x08 + pla + sta.b 0x07 + pla + sta.b 0x06 + pla + sta.b 0x05 + pla + sta.b 0x04 + pla + sta.b 0x03 + pla + sta.b 0x02 + pla + sta.b 0x01 + pla + sta.b 0x00 + + ; Restore D before returning + rep #0x20 + pla + tcd + sep #0x20 + + ; Restore DBR (draw_text may have changed it) + plb + + ; Restore processor status (including interrupt flag) + plp + rts + + ; ============================================================================ + ; _transfer_circular_slot + ; ============================================================================ + ; Transfers a single circular slot to its fixed VRAM address + ; Uses direct DMA instead of menu transfer system for precise control + ; + ; Input: rolling_slot_index = slot index (0-4) + ; Output: 128 bytes transferred to VRAM -; ============================================================================ -; _copy_all_slots_to_tilemap -; ============================================================================ -; Copies all 6 slots from text buffer to tilemap buffer -; Called from tfr_inventory_list_rolling AFTER game clears window buffers + _transfer_circular_slot: + ; Save processor status and disable interrupts + php + sei -_copy_all_slots_to_tilemap: ; Save DBR - mult8_trampoline may change it - phb - lda #0x7E - pha - plb ; Ensure DBR is $7E for WRAM access - - lda #0 - sta.b 0x06 ; Slot counter (0-5) - -_copy_slots_loop: - ; Calculate tilemap destination: tilemap_buffer_base + (slot × 128) - ; NO content_offset - write to full 128-byte slot - lda.b 0x06 - sta.b 0x26 - lda #TILEMAP_BYTES_PER_ROW ; 128 - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - clc - adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! - sta.b 0x00 ; Tilemap dest pointer - sep #0x20 - -; Calculate text buffer source: text_buffer_base + (slot × 60) - lda.b 0x06 - sta.b 0x26 - lda #TEXT_BYTES_PER_ITEM ; 60 - sta.b 0x28 - jsr.l mult8_trampoline - - rep #0x20 - lda.b 0x2A - tax ; X = text buffer offset - sep #0x20 - -; Copy row 1 (30 bytes) - content only, preserve borders - ldy.w #0x0000 - -_copy_all_row1: - lda.w text_buffer_base, x - sta (0x00), y - inx - iny - cpy.w #30 - bne _copy_all_row1 - -; Copy row 2 (+$40 offset in tilemap) - content only, preserve borders - ldy.w #0x0040 - -_copy_all_row2: - lda.w text_buffer_base, x - sta (0x00), y - inx - iny - cpy.w #0x005E - bne _copy_all_row2 - -; Next slot - inc.b 0x06 - lda.b 0x06 - cmp #BUFFER_SLOTS ; 6 slots - bne _copy_slots_loop - -; Restore DBR - plb - rts - -; ============================================================================ -; tfr_inventory_list_rolling -; ============================================================================ -; Replacement for TfrInventoryList ($0298FA) -; Transfers the visible portion of tilemap buffer to VRAM -; -; Called via JSL from bank 02 - -tfr_inventory_list_rolling: -""" -Replacement for original `TfrInventoryList` ($0298FA): re-render the visible inventory rows into our buffer and DMA -the slice to VRAM. Reached via JSL from bank 02. -""" -; Set data bank to $7E for WRAM access - phb - lda #0x7E - pha - plb - -; RE-RENDER all visible items to our rolling buffer -; This is necessary because DrawInventoryItemText (called after item swap) -; writes to the OLD text buffer at $8EA6, not our buffer at $97A6. -; By always re-rendering here, swapped items display correctly. - jsr.w _refresh_visible_items_internal - -; Copy all 6 slots from text buffer to tilemap buffer -; This runs AFTER the game's window clearing at $9AF4 - jsr.w _copy_all_slots_to_tilemap - -; Queue VRAM transfer (entry 3 covers $7400-$77FF) - lda #0x03 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline + phb + lda #0x7E + pha + plb ; Ensure DBR is $7E for WRAM access + + ; CRITICAL: Save zero page variables on stack + ; This prevents corruption if NMI fires during game's block copy + ; Save $00-$0B (block copy uses $00-$06) and $26-$2B (Mult8 uses these) + lda.b 0x00 + pha + lda.b 0x01 + pha + lda.b 0x02 + pha + lda.b 0x03 + pha + lda.b 0x04 + pha + lda.b 0x05 + pha + lda.b 0x06 + pha + lda.b 0x07 + pha + lda.b 0x08 + pha + lda.b 0x09 + pha + lda.b 0x0A + pha + lda.b 0x0B + pha + ; Also save $26-$2B used by mult8_trampoline + lda.b 0x26 + pha + lda.b 0x27 + pha + lda.b 0x28 + pha + lda.b 0x29 + pha + lda.b 0x2A + pha + lda.b 0x2B + pha + + ; Look up VRAM destination from slot table + lda.w rolling_slot_index + asl ; ×2 for word lookup + tax + rep #0x20 + lda.l _vram_slot_table, x ; Get VRAM address ($7400, $7480, etc.) + sta.b 0x04 ; Save VRAM dest + sep #0x20 + + ; Calculate tilemap buffer source + ; source = tilemap_buffer_base + (slot × 128) - NO content_offset! + lda.w rolling_slot_index + sta.b 0x26 + lda #TILEMAP_BYTES_PER_ROW + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + clc + adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! + sta.b 0x00 ; Source address (low word) + sep #0x20 + + ; Queue using menu transfer system (entry 3 covers our slots) + ; The transfer will include this slot since it's at the right offset + lda #0x03 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + + lda #0x01 + sta.w 0x1825 + sta.w 0x1824 + + ; CRITICAL: Restore zero page variables from stack (reverse order) + ; First restore $26-$2B (last pushed) + pla + sta.b 0x2B + pla + sta.b 0x2A + pla + sta.b 0x29 + pla + sta.b 0x28 + pla + sta.b 0x27 + pla + sta.b 0x26 + ; Then restore $00-$0B + pla + sta.b 0x0B + pla + sta.b 0x0A + pla + sta.b 0x09 + pla + sta.b 0x08 + pla + sta.b 0x07 + pla + sta.b 0x06 + pla + sta.b 0x05 + pla + sta.b 0x04 + pla + sta.b 0x03 + pla + sta.b 0x02 + pla + sta.b 0x01 + pla + sta.b 0x00 + + ; Restore DBR + plb + + ; Restore processor status (including interrupt flag) + plp + rts + + ; ============================================================================ + ; _copy_slot_to_tilemap + ; ============================================================================ + ; Copies a single slot from text buffer to tilemap buffer + ; Input: rolling_slot_index = slot index (0-5) + ; Uses $00-$01, $26, $28, $2A, X, Y + + _copy_slot_to_tilemap: + ; Save processor status and disable interrupts + ; Using PHP/PLP instead of SEI/CLI to handle nested calls correctly + php + sei - lda #0x01 - sta.w 0x1825 ; 1 transfer only - sta.w 0x1824 ; Enable transfer - - plb ; Restore data bank - rtl - -; ============================================================================ -; _refresh_visible_items_internal -; ============================================================================ -; Re-render all 5 visible items to our rolling buffer slots. -; Uses current EF71 as the top item index. -; Does NOT queue VRAM transfer (caller handles that). - -_refresh_visible_items_internal: - ; Render 5 visible items to their CORRECT circular buffer slots - ; The visible slots depend on rolling_buffer_pos due to circular rotation! + ; Save DBR - mult8_trampoline may change it + phb + + ; Save and set D to 0 for direct page operations + rep #0x20 + tdc + pha + lda.w #0x0000 + tcd + sep #0x20 + + ; CRITICAL: Save zero page variables on stack + ; This prevents corruption if NMI fires during game's block copy + ; Save $00-$0B (block copy uses $00-$06) and $26-$2B (Mult8 uses these) + lda.b 0x00 + pha + lda.b 0x01 + pha + lda.b 0x02 + pha + lda.b 0x03 + pha + lda.b 0x04 + pha + lda.b 0x05 + pha + lda.b 0x06 + pha + lda.b 0x07 + pha + lda.b 0x08 + pha + lda.b 0x09 + pha + lda.b 0x0A + pha + lda.b 0x0B + pha + ; Also save $26-$2B used by mult8_trampoline + lda.b 0x26 + pha + lda.b 0x27 + pha + lda.b 0x28 + pha + lda.b 0x29 + pha + lda.b 0x2A + pha + lda.b 0x2B + pha + + ; Calculate tilemap destination (NO content_offset - write to full slot) + lda.w rolling_slot_index + sta.b 0x26 + lda #TILEMAP_BYTES_PER_ROW + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + clc + adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! + sta.b 0x00 + sep #0x20 + + ; Calculate text buffer source + lda.w rolling_slot_index + sta.b 0x26 + lda #TEXT_BYTES_PER_ITEM + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + tax + sep #0x20 + + ; Copy row 1 (30 bytes) - content only, preserve borders + ldy.w #0x0000 + + _copy_slot_row1: + lda.w text_buffer_base, x + sta (0x00), y + inx + iny + cpy.w #30 + bne _copy_slot_row1 + + ; Copy row 2 (+$40 offset) - content only, preserve borders + ldy.w #0x0040 + + _copy_slot_row2: + lda.w text_buffer_base, x + sta (0x00), y + inx + iny + cpy.w #0x005E + bne _copy_slot_row2 + + ; CRITICAL: Restore zero page variables $26-$2B from stack (reverse order - pushed last, pop first) + pla + sta.b 0x2B + pla + sta.b 0x2A + pla + sta.b 0x29 + pla + sta.b 0x28 + pla + sta.b 0x27 + pla + sta.b 0x26 + + ; CRITICAL: Restore zero page variables $00-$0B from stack (reverse order) + pla + sta.b 0x0B + pla + sta.b 0x0A + pla + sta.b 0x09 + pla + sta.b 0x08 + pla + sta.b 0x07 + pla + sta.b 0x06 + pla + sta.b 0x05 + pla + sta.b 0x04 + pla + sta.b 0x03 + pla + sta.b 0x02 + pla + sta.b 0x01 + pla + sta.b 0x00 + + ; Restore D register + rep #0x20 + pla + tcd + sep #0x20 + + ; Restore DBR (pushed after php/sei) + plb + + ; Restore processor status (including interrupt flag) + plp + rts + + ; ============================================================================ + ; _copy_all_slots_to_tilemap + ; ============================================================================ + ; Copies all 6 slots from text buffer to tilemap buffer + ; Called from tfr_inventory_list_rolling AFTER game clears window buffers + + _copy_all_slots_to_tilemap: + ; Save DBR - mult8_trampoline may change it + phb + lda #0x7E + pha + plb ; Ensure DBR is $7E for WRAM access + + lda #0 + sta.b 0x06 ; Slot counter (0-5) + + _copy_slots_loop: + ; Calculate tilemap destination: tilemap_buffer_base + (slot × 128) + ; NO content_offset - write to full 128-byte slot + lda.b 0x06 + sta.b 0x26 + lda #TILEMAP_BYTES_PER_ROW ; 128 + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + clc + adc #tilemap_buffer_base + tilemap_content_offset ; NO +tilemap_content_offset! + sta.b 0x00 ; Tilemap dest pointer + sep #0x20 + + ; Calculate text buffer source: text_buffer_base + (slot × 60) + lda.b 0x06 + sta.b 0x26 + lda #TEXT_BYTES_PER_ITEM ; 60 + sta.b 0x28 + jsr.l mult8_trampoline + + rep #0x20 + lda.b 0x2A + tax ; X = text buffer offset + sep #0x20 + + ; Copy row 1 (30 bytes) - content only, preserve borders + ldy.w #0x0000 + + _copy_all_row1: + lda.w text_buffer_base, x + sta (0x00), y + inx + iny + cpy.w #30 + bne _copy_all_row1 + + ; Copy row 2 (+$40 offset in tilemap) - content only, preserve borders + ldy.w #0x0040 + + _copy_all_row2: + lda.w text_buffer_base, x + sta (0x00), y + inx + iny + cpy.w #0x005E + bne _copy_all_row2 + + ; Next slot + inc.b 0x06 + lda.b 0x06 + cmp #BUFFER_SLOTS ; 6 slots + bne _copy_slots_loop + + ; Restore DBR + plb + rts + + ; ============================================================================ + ; tfr_inventory_list_rolling + ; ============================================================================ + ; Replacement for TfrInventoryList ($0298FA) + ; Transfers the visible portion of tilemap buffer to VRAM ; + ; Called via JSL from bank 02 -; After seam crossing, slots are rotated. For example if rolling_buffer_pos=3: -; - Slot 3 shows item at rolling_top_row + 0 -; - Slot 4 shows item at rolling_top_row + 1 -; - Slot 5 shows item at rolling_top_row + 2 -; - Slot 0 shows item at rolling_top_row + 3 (wrapped) -; - Slot 1 shows item at rolling_top_row + 4 - - lda #0 - sta.b 0x06 ; Visible row index (0-4) - -_refresh_int_loop: - ; Calculate item index = rolling_top_row + visible_row - lda.w rolling_top_row - clc - adc.b 0x06 - sta.w rolling_edge_row ; Item index for data lookup - -; Calculate actual slot = (rolling_buffer_pos + visible_row) % BUFFER_SLOTS - lda.w rolling_buffer_pos - clc - adc.b 0x06 ; pos + visible_row - cmp #BUFFER_SLOTS - bcc _refresh_slot_ok - sec - sbc #BUFFER_SLOTS ; Wrap if >= 6 - -_refresh_slot_ok: - sta.w rolling_slot_index ; Actual circular buffer slot - -; Save visible row index - lda.b 0x06 - pha - -; Render item to this slot - jsr.w _render_item_to_circular_slot - -; Restore visible row index - pla - sta.b 0x06 - -; Next visible row - inc.b 0x06 - lda.b 0x06 - cmp #VISIBLE_ROWS ; 5 visible items - bne _refresh_int_loop - - rts - -; ============================================================================ -; _refresh_visible_items - Re-render all visible items after scroll -; ============================================================================ -; Simpler approach: instead of circular buffer tricks, just redraw the 5 -; visible items to fixed slot positions (0-4) after each scroll. -; EF65 is reset to 0 by the caller. -; -; Called via JSL from wrap_and_clear_trampoline - -_refresh_visible_items: - phb - lda #0x7E - pha - plb - -; Render 5 visible items to slots 0-4 -; Item index = EF71 + slot - lda #0 - sta.b 0x06 ; Slot index (0-4) - -_refresh_loop: - ; Calculate item index = EF71 + slot - lda.w 0xEF71 - clc - adc.b 0x06 - sta.w rolling_edge_row ; Item index for data lookup - -; Use slot index for buffer position - lda.b 0x06 - sta.w rolling_slot_index ; Slot index (0-4) - -; Save slot index - lda.b 0x06 - pha - -; Render item to this slot - jsr.w _render_item_to_circular_slot - -; Restore slot index - pla - sta.b 0x06 - -; Next slot - inc.b 0x06 - lda.b 0x06 - cmp #VISIBLE_ROWS ; 5 visible items - bne _refresh_loop - -; Queue VRAM transfer - lda #0x03 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline - lda #0x01 - sta.w 0x1825 - sta.w 0x1824 - - plb - rtl - -; ============================================================================ -; _post_render_up - Called after scroll UP animation completes -; ============================================================================ -; Renders the next item (for potential next scroll UP) to the off-screen slot -; Off-screen slot after scroll UP = (pos - 1) mod 6 (above top) -; -; Called via JSL from wrap_and_clear_trampoline - -_post_render_up: - phb - lda #0x7E - pha - plb - -; Calculate item index = EF71 + VISIBLE_ROWS (the next item down the list) - lda.w 0xEF71 - clc - adc #VISIBLE_ROWS - cmp #TOTAL_ITEMS - bcs _post_up_done ; Past end, nothing to render - sta.w rolling_edge_row - -; Off-screen slot = (pos - 1) mod 6 - lda.w rolling_buffer_pos - dec - bpl _post_up_slot_ok - lda #BUFFER_SLOTS - 1 - -_post_up_slot_ok: - sta.w rolling_slot_index - - jsr.w _render_item_to_circular_slot - -; Queue VRAM transfer - lda #0x03 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline - lda #0x01 - sta.w 0x1825 - sta.w 0x1824 - -_post_up_done: - plb - rtl - -; ============================================================================ -; _post_render_down - Called after scroll DOWN animation completes -; ============================================================================ -; Renders the previous item (for potential scroll UP back) to the off-screen slot -; Off-screen slot after scroll DOWN = (pos + 5) mod 6 (below bottom) -; -; Called via JSL from wrap_and_clear_trampoline - -_post_render_down: - phb - lda #0x7E - pha - plb - -; Calculate item index = EF71 + VISIBLE_ROWS (the item just past visible bottom) -; This prepares for scroll UP (which would show this item at bottom) - lda.w 0xEF71 - clc - adc #VISIBLE_ROWS - cmp #TOTAL_ITEMS - bcs _post_down_done ; Past end, nothing to render - sta.w rolling_edge_row - -; Off-screen slot = (pos + 5) mod 6 - lda.w rolling_buffer_pos - clc - adc #VISIBLE_ROWS - cmp #BUFFER_SLOTS - bcc _post_down_slot_ok - sec - sbc #BUFFER_SLOTS - -_post_down_slot_ok: - sta.w rolling_slot_index - - jsr.w _render_item_to_circular_slot - -; Queue VRAM transfer - lda #0x03 - ldy.w #0x0002 - jsr.l load_menu_tfr_data_trampoline - lda #0x01 - sta.w 0x1825 - sta.w 0x1824 - -_post_down_done: - plb - rtl - -; ============================================================================ -; post_scroll_down_render -; ============================================================================ -; Called via JSL from wrap_and_clear_trampoline after scroll animation completes. -; -; For scroll DOWN: Pre-render happened BEFORE animation (in scroll_list_down_hook) -; so the newly visible slot already has correct content. -; -; We check if it was a scroll DOWN and optionally prepare the NEXT off-screen -; slot for future scroll UP operations. - -post_scroll_down_render: -"""Tail of `wrap_and_clear_trampoline` (JSL): post-animation hook for scroll-down.""" - ; Check if this was a scroll DOWN animation (type 2) - ; $1820 still contains the animation type at this point - lda.w 0x1820 - cmp #0x02 - bne _psd_done - -; For scroll DOWN, the pre-render already happened in scroll_list_down_hook. -; The slot that scrolled into view (at bottom) has correct content. -; -; Optionally, we could prepare the slot that's now off-screen (at top) -; for a potential scroll UP. But since scroll_list_up_hook does pre-render, -; this isn't strictly necessary. - -_psd_done: - rtl - -; ============================================================================ -; scroll_list_down_hook -; ============================================================================ -; Called via JMP.L from $02A8B8 -; Starts scroll animation. Pre-rendering happens AFTER animation in post_scroll_down_render. -; Returns via JMP.L to return_to_bank02 (which has RTS) -; - -; Original code at $02A8B8: -; ldx $ef71, dex, stx $ef71, lda #$0c, sta $ef64, lda #$02, sta $1820, rts - -;MENU_FLAG_INVENTORY := 0x04 ; Bit 2 of $4A = inventory menu active - -scroll_list_down_hook: -""" -Replacement for the scroll-down stub at `$02A8B8`: kicks off the down-scroll animation, pre-renders the new bottom -slot for the rolling buffer. -""" -; === GUARD: Only use rolling buffer for inventory menu === -; Check bit 2 of $4A (inventory flag). If not set, use original magic behavior. - lda.b 0x4A - and #0x04 - bne _sd_is_inventory - -; --- ORIGINAL MAGIC MENU BEHAVIOR --- -; Original code: ldx $ef71, dex, stx $ef71, lda #$0c, sta $ef64, lda #$02, sta $1820, rts - ldx.w 0xEF71 - dex - stx.w 0xEF71 - lda #0x0C - sta.w 0xEF64 - lda #0x02 - sta.w 0x1820 - jmp.l return_to_bank02 - -_sd_is_inventory: - ; NOTE: For inventory in single-column mode, we INCREMENT EF71 to show later items + tfr_inventory_list_rolling: + """ + Replacement for original `TfrInventoryList` ($0298FA): re-render the visible inventory rows into our buffer and DMA + the slice to VRAM. Reached via JSL from bank 02. + """ + ; Set data bank to $7E for WRAM access + phb + lda #0x7E + pha + plb + + ; RE-RENDER all visible items only when something invalidated them + ; (open / item swap). Scroll-edge hooks paint the new hidden slot + ; before animation, so steady-state scroll requires zero re-render + ; here. The flag was the per-frame full rebuild that was clobbering + ; the pre-rendered hidden slot. + lda.w inventory_needs_full_refresh + beq _tfr_skip_refresh + jsr.w _refresh_visible_items_internal + stz.w inventory_needs_full_refresh + + _tfr_skip_refresh: + + ; Copy all 6 slots from text buffer to tilemap buffer + ; This runs AFTER the game's window clearing at $9AF4 + jsr.w _copy_all_slots_to_tilemap + + ; Queue VRAM transfer (entry 3 covers $7400-$77FF) + lda #0x03 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + + lda #0x01 + sta.w 0x1825 ; 1 transfer only + sta.w 0x1824 ; Enable transfer + + plb ; Restore data bank + rtl + + ; ============================================================================ + ; _refresh_visible_items_internal + ; ============================================================================ + ; Re-render all 5 visible items to our rolling buffer slots. + ; Uses current EF71 as the top item index. + ; Does NOT queue VRAM transfer (caller handles that). + + _refresh_visible_items_internal: + ; Render 5 visible items to their CORRECT circular buffer slots + ; The visible slots depend on rolling_buffer_pos due to circular rotation! + ; + + ; After seam crossing, slots are rotated. For example if rolling_buffer_pos=3: + ; - Slot 3 shows item at rolling_top_row + 0 + ; - Slot 4 shows item at rolling_top_row + 1 + ; - Slot 5 shows item at rolling_top_row + 2 + ; - Slot 0 shows item at rolling_top_row + 3 (wrapped) + ; - Slot 1 shows item at rolling_top_row + 4 + + lda #0 + sta.b 0x06 ; Visible row index (0-4) + + _refresh_int_loop: + ; Calculate item index = rolling_top_row + visible_row + lda.w rolling_top_row + clc + adc.b 0x06 + sta.w rolling_edge_row ; Item index for data lookup + + ; Calculate actual slot = (rolling_buffer_pos + visible_row) % BUFFER_SLOTS + lda.w rolling_buffer_pos + clc + adc.b 0x06 ; pos + visible_row + cmp #BUFFER_SLOTS + bcc _refresh_slot_ok + sec + sbc #BUFFER_SLOTS ; Wrap if >= 6 + + _refresh_slot_ok: + sta.w rolling_slot_index ; Actual circular buffer slot + + ; Save visible row index + lda.b 0x06 + pha + + ; Render item to this slot + jsr.w _render_item_to_circular_slot + + ; Restore visible row index + pla + sta.b 0x06 + + ; Next visible row + inc.b 0x06 + lda.b 0x06 + cmp #VISIBLE_ROWS ; 5 visible items + bne _refresh_int_loop + + rts + + ; ============================================================================ + ; _refresh_visible_items - Re-render all visible items after scroll + ; ============================================================================ + ; Simpler approach: instead of circular buffer tricks, just redraw the 5 + ; visible items to fixed slot positions (0-4) after each scroll. + ; EF65 is reset to 0 by the caller. ; - ; FF6-STYLE: Pre-render the new bottom item BEFORE starting the animation! - ; This ensures the slot has correct content when it scrolls into view. - -; Disable interrupts to prevent NMI from corrupting state - sei - -; Set data bank to $7E for RAM access - phb - lda #0x7E - pha - plb - -; Check if we can scroll further: rolling_top_row + VISIBLE_ROWS must be < TOTAL_ITEMS -; Use rolling_top_row, NOT EF71 which has different meaning in battle! - lda.w rolling_top_row - cmp #( TOTAL_ITEMS - VISIBLE_ROWS ) - bcs _sd_abort ; Already at max, can't scroll down - -; === FF6-STYLE PRE-RENDER === -; Before animation, render the item that will appear at the NEW bottom. -; The "off-screen" slot below the visible area will scroll into view. -; Off-screen slot = (rolling_buffer_pos + VISIBLE_ROWS) % BUFFER_SLOTS - -; Calculate item index = rolling_top_row + VISIBLE_ROWS (the item appearing at new bottom) -; After increment, rolling_top_row will be current+1, so bottom item = (current+1) + 4 = current + 5 - lda.w rolling_top_row - clc - adc #VISIBLE_ROWS ; item = rolling_top_row + 5 - cmp #TOTAL_ITEMS - bcs _sd_skip_prerender ; Past end, nothing to render - sta.w rolling_edge_row ; Save item index - -; Calculate the off-screen slot that will become visible -; This is the slot 5 positions ahead of current top (wrapping) - lda.w rolling_buffer_pos - clc - adc #VISIBLE_ROWS ; pos + 5 - cmp #BUFFER_SLOTS - bcc _sd_slot_ok - sec - sbc #BUFFER_SLOTS ; Wrap if >= 6 - -_sd_slot_ok: - sta.w rolling_slot_index - -; Render item to the off-screen slot - jsr.w _render_item_to_circular_slot - ; Copy to tilemap buffer - jsr.w _copy_slot_to_tilemap - ; Queue VRAM transfer - jsr.w _transfer_circular_slot - -_sd_skip_prerender: - ; INCREMENT EF71 to show later items (animation loop is NOPed out) - lda.w 0xEF71 - inc - sta.w 0xEF71 - -; INCREMENT rolling_top_row to track which item is at top of visible area -; This is critical for _refresh_visible_items_internal to work correctly! - lda.w rolling_top_row - inc - sta.w rolling_top_row - -; Set up scroll animation - lda #0x0C - sta.w 0xEF64 - lda #0x02 ; Animation 2 (scroll down) - sta.w 0x1820 - -; Advance circular buffer position (top slot moves up, becomes off-screen) - lda.w rolling_buffer_pos - inc - cmp #BUFFER_SLOTS - bcc _sd_pos_ok - lda #0 + ; Called via JSL from wrap_and_clear_trampoline + + _refresh_visible_items: + phb + lda #0x7E + pha + plb + + ; Render 5 visible items to slots 0-4 + ; Item index = EF71 + slot + lda #0 + sta.b 0x06 ; Slot index (0-4) + + _refresh_loop: + ; Calculate item index = EF71 + slot + lda.w 0xEF71 + clc + adc.b 0x06 + sta.w rolling_edge_row ; Item index for data lookup + + ; Use slot index for buffer position + lda.b 0x06 + sta.w rolling_slot_index ; Slot index (0-4) + + ; Save slot index + lda.b 0x06 + pha + + ; Render item to this slot + jsr.w _render_item_to_circular_slot + + ; Restore slot index + pla + sta.b 0x06 + + ; Next slot + inc.b 0x06 + lda.b 0x06 + cmp #VISIBLE_ROWS ; 5 visible items + bne _refresh_loop -_sd_pos_ok: - sta.w rolling_buffer_pos + ; Queue VRAM transfer + lda #0x03 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + lda #0x01 + sta.w 0x1825 + sta.w 0x1824 + + plb + rtl + + ; ============================================================================ + ; _post_render_up - Called after scroll UP animation completes + ; ============================================================================ + ; Renders the next item (for potential next scroll UP) to the off-screen slot + ; Off-screen slot after scroll UP = (pos - 1) mod 6 (above top) + ; + ; Called via JSL from wrap_and_clear_trampoline -_sd_abort: - ; Ensure cursor 1 stays visible during scroll animation - stz.w 0xEF69 ; Clear hide cursor 1 flag - stz.w 0xEF6E ; Clear alternate hide cursor 1 flag + _post_render_up: + phb + lda #0x7E + pha + plb - plb ; Restore data bank - cli ; re-enable interrupts - jmp.l return_to_bank02 + ; Calculate item index = EF71 + VISIBLE_ROWS (the next item down the list) + lda.w 0xEF71 + clc + adc #VISIBLE_ROWS + cmp #TOTAL_ITEMS + bcs _post_up_done ; Past end, nothing to render + sta.w rolling_edge_row -; ============================================================================ -; scroll_list_up_hook -; ============================================================================ -; Called via JMP.L from $02A8CA -; Pre-renders new top row, then does original scroll setup -; Returns via JMP.L to return_to_bank02 (which has RTS) -; + ; Off-screen slot = (pos - 1) mod 6 + lda.w rolling_buffer_pos + dec + bpl _post_up_slot_ok + lda #BUFFER_SLOTS - 1 -; Original code at $02A8CA: -; ldx $ef71, inx, stx $ef71, lda #$0c, sta $ef64, lda #$03, sta $1820, rts + _post_up_slot_ok: + sta.w rolling_slot_index -scroll_list_up_hook: -""" -Replacement for the scroll-up stub at `$02A8CA`: pre-renders the new top row before kicking off the scroll-up -animation. -""" -; === GUARD: Only use rolling buffer for inventory menu === -; Check bit 2 of $4A (inventory flag). If not set, use original magic behavior. - lda.b 0x4A - and #0x04 - bne _su_is_inventory - -; --- ORIGINAL MAGIC MENU BEHAVIOR --- -; Original code: ldx $ef71, inx, stx $ef71, lda #$0c, sta $ef64, lda #$03, sta $1820, rts - ldx.w 0xEF71 - inx - stx.w 0xEF71 - lda #0x0C - sta.w 0xEF64 - lda #0x03 - sta.w 0x1820 - jmp.l return_to_bank02 + jsr.w _render_item_to_circular_slot -_su_is_inventory: - ; NOTE: For inventory in single-column mode, we DECREMENT rolling_top_row to show earlier items + ; Queue VRAM transfer + lda #0x03 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + lda #0x01 + sta.w 0x1825 + sta.w 0x1824 + + _post_up_done: + plb + rtl + + ; ============================================================================ + ; _post_render_down - Called after scroll DOWN animation completes + ; ============================================================================ + ; Renders the previous item (for potential scroll UP back) to the off-screen slot + ; Off-screen slot after scroll DOWN = (pos + 5) mod 6 (below bottom) ; + ; Called via JSL from wrap_and_clear_trampoline + + _post_render_down: + phb + lda #0x7E + pha + plb + + ; Calculate item index = EF71 + VISIBLE_ROWS (the item just past visible bottom) + ; This prepares for scroll UP (which would show this item at bottom) + lda.w 0xEF71 + clc + adc #VISIBLE_ROWS + cmp #TOTAL_ITEMS + bcs _post_down_done ; Past end, nothing to render + sta.w rolling_edge_row + + ; Off-screen slot = (pos + 5) mod 6 + lda.w rolling_buffer_pos + clc + adc #VISIBLE_ROWS + cmp #BUFFER_SLOTS + bcc _post_down_slot_ok + sec + sbc #BUFFER_SLOTS + + _post_down_slot_ok: + sta.w rolling_slot_index + + jsr.w _render_item_to_circular_slot -; For scroll UP, we DO need to pre-render BEFORE animation because: -; - The slot that will scroll INTO view might have stale data from a previous scroll down -; - We need to put the correct item there before it becomes visible - -; Disable interrupts to prevent NMI from corrupting rolling_slot_index - sei - -; Set data bank to $7E for RAM access - phb - lda #0x7E - pha - plb - -; Check if we can scroll: rolling_top_row must be > 0 -; (Use our custom variable, NOT EF71 which has different meaning in battle!) - lda.w rolling_top_row - beq _su_abort ; Already at item 0, can't scroll up - -; First decrement buffer position (new top slot) - lda.w rolling_buffer_pos - dec - bpl _su_pos_ok - lda #BUFFER_SLOTS - 1 ; Wrap 0→5 - -_su_pos_ok: - sta.w rolling_buffer_pos - sta.w rolling_slot_index ; This slot will be the new top (currently off-screen) - -; Calculate previous item index = rolling_top_row - 1 (the item that will appear at top) -; Use rolling_top_row, NOT EF71 which has different meaning in battle! - lda.w rolling_top_row - dec - sta.w rolling_edge_row - -; Pre-render to new top slot (currently off-screen, about to scroll in) - jsr.w _render_item_to_circular_slot - ; Copy to tilemap buffer - jsr.w _copy_slot_to_tilemap - ; Queue VRAM transfer for this slot - jsr.w _transfer_circular_slot - -; DECREMENT rolling_top_row to track which item is at top of visible area -; This is the authoritative source for which items are visible! - lda.w rolling_top_row - dec - sta.w rolling_top_row - -; Set up scroll animation - lda #0x0C - sta.w 0xEF64 - lda #0x03 ; Animation 3 (scroll up) - sta.w 0x1820 - -_su_abort: - ; Ensure cursor 1 stays visible during scroll animation - stz.w 0xEF69 ; Clear hide cursor 1 flag - stz.w 0xEF6E ; Clear alternate hide cursor 1 flag - - plb ; Restore data bank - cli ; re-enable interrupts - jmp.l return_to_bank02 - -; ============================================================================ -; update_list_scroll_hdma_wrapped -; ============================================================================ -; Replacement for $02A7F1 - builds HDMA table with scroll value WRAPPING -; This is the key to the circular buffer - scroll values wrap at 96 pixels -; -; Called via JMP.L from $02A7F1 -; Input: X = current scroll offset (from $EF65) -; Y = scanlines per row (12) -; Output: HDMA table at $7F74 filled with wrapped scroll values - -HDMA_TABLE := 0x7E7F74 ; Full 24-bit address: bank $7E, offset $7F74 (V scroll entries) -HDMA_TABLE_SIZE := 0x00F0 ; 240 bytes (same as HDMA_Y_SIZE) -SCROLL_WRAP := 0x0060 ; 96 = 6 slots × 16 pixels (tilemap buffer size) -SCROLL_WRAP_LIMIT := 0x01D3 ; 467 = 371 + 96 (wrap when >= this value) -OUR_BASE_SCROLL := 0x0173 ; 371 - same as original game - -update_list_scroll_hdma_wrapped: -""" -Replacement for the HDMA-build stub at `$02A7F1`: rebuild the per-scanline V-scroll table for the inventory rolling -buffer. -""" -; Check if we're in inventory mode (bit 2 of $4A) -; If not, use original behavior for other windows - lda.b 0x4A - and #0x04 - bne _use_circular_buffer - -; Original behavior for non-inventory windows -; Input: X = scroll offset, Y = scanlines per row (12) - rep #0x20 ; 16-bit A - txa ; A = scroll offset - ldx.w #0x0000 ; Table index - -_orig_loop: - sta.w 0x7F74, x ; Store scroll value - dey - bne _orig_skip_add - clc - adc.w #0x0004 ; Add 4 every 12 scanlines - ldy.w #0x000C ; Reset scanline counter - -_orig_skip_add: - inx - inx - inx - inx - cpx.w #0x00F0 ; 240 bytes - bne _orig_loop - .db 0x7B ; TDC (clear A) - sep #0x20 ; 8-bit A - jmp.l return_to_bank02 ; Return via bank $02 trampoline - -_use_circular_buffer: - ; FF6-STYLE CIRCULAR BUFFER HDMA + ; Queue VRAM transfer + lda #0x03 + ldy.w #0x0002 + jsr.l load_menu_tfr_data_trampoline + lda #0x01 + sta.w 0x1825 + sta.w 0x1824 + + _post_down_done: + plb + rtl + + ; ============================================================================ + ; post_scroll_down_render + ; ============================================================================ + ; Called via JSL from wrap_and_clear_trampoline after scroll animation completes. + ; + ; For scroll DOWN: Pre-render happened BEFORE animation (in scroll_list_down_hook) + ; so the newly visible slot already has correct content. + ; + ; We check if it was a scroll DOWN and optionally prepare the NEXT off-screen + ; slot for future scroll UP operations. + + post_scroll_down_render: + """Tail of `wrap_and_clear_trampoline` (JSL): post-animation hook for scroll-down.""" + ; Check if this was a scroll DOWN animation (type 2) + ; $1820 still contains the animation type at this point + lda.w 0x1820 + cmp #0x02 + bne _psd_done + + ; For scroll DOWN, the pre-render already happened in scroll_list_down_hook. + ; The slot that scrolled into view (at bottom) has correct content. + ; + ; Optionally, we could prepare the slot that's now off-screen (at top) + ; for a potential scroll UP. But since scroll_list_up_hook does pre-render, + ; this isn't strictly necessary. + + _psd_done: + rtl + + ; ============================================================================ + ; scroll_list_down_hook + ; ============================================================================ + ; Called via JMP.L from $02A8B8 + ; Starts scroll animation. Pre-rendering happens AFTER animation in post_scroll_down_render. + ; Returns via JMP.L to return_to_bank02 (which has RTS) ; -; Key insight from FF6's LoadItemBG1VScrollHDMATbl: -; - At screen scanline S with scroll V, displayed VRAM row = V + S - -; - To show VRAM slot N at screen row R (scanline R*12): -; scroll + R*12 = N*16, therefore scroll = N*16 - R*12 -; - -; Algorithm for each visible row (0-4): -; vram_slot = (rolling_buffer_pos + row) % 6 -; scroll = BASE + (vram_slot * 16) - (row * 12) -; -; This ensures proper alignment AND seamless wraparound. -; - sei ; Disable interrupts during HDMA table update - -; CRITICAL: Save direct page variables that the caller depends on - lda.b 0x00 - pha - lda.b 0x01 - pha - lda.b 0x02 - pha - lda.b 0x03 - pha - lda.b 0x04 - pha - lda.b 0x05 - pha - - rep #0x30 ; 16-bit A, X, Y - - ldx.w #0x0000 ; HDMA table index - stz.b 0x02 ; Row counter (clears $02 and $03 in 16-bit mode) - -_row_loop: - ; Calculate vram_slot = (rolling_buffer_pos + row) % 6 - lda.w rolling_buffer_pos - and.w #0x00FF - clc - adc.b 0x02 ; + row number - -_mod6: - cmp.w #BUFFER_SLOTS ; >= 6? - bcc _mod6_done - sec - sbc.w #BUFFER_SLOTS - bra _mod6 - -_mod6_done: - ; A = vram_slot (0-5) - -; Calculate vram_slot * 16 - asl - asl - asl - asl ; A = vram_slot * 16 - sta.b 0x00 ; Save vram_offset - -; Calculate row * 12 = row * 8 + row * 4 -; Use Y register for temp to avoid clobbering direct page - lda.b 0x02 ; row (only low byte matters, high is 0) - and.w #0x00FF ; Ensure only low byte - asl - asl - asl ; row * 8 - tay ; Y = row * 8 - lda.b 0x02 - and.w #0x00FF - asl - asl ; row * 4 - sta.b 0x04 ; Use $04 for temp (not overlapping) - tya ; A = row * 8 - clc - adc.b 0x04 ; row * 8 + row * 4 = row * 12 - ; A = scanline_offset (row * 12) - -; scroll = BASE + vram_offset - scanline_offset -; eor.w #0xFFFF ; Negate: -scanline_offset -; a816 does not support yet the w for eor. - .db 0x49 - .dw 0xffff - - inc - clc - adc.b 0x00 ; + vram_offset - clc - adc.w #OUR_BASE_SCROLL ; + BASE - sta.b 0x00 ; $00 = scroll value for this row - -; Store same scroll value for all 12 scanlines of this row - ldy.w #0x000C ; 12 scanlines - -_scanline_loop: - lda.b 0x00 - sta.l HDMA_TABLE, x - inx - inx - inx - inx ; X += 4 (next HDMA entry) - dey - bne _scanline_loop - -; Next row - inc.b 0x02 - lda.b 0x02 - cmp.w #VISIBLE_ROWS ; 5 rows total - bne _row_loop + ; Original code at $02A8B8: + ; ldx $ef71, dex, stx $ef71, lda #$0c, sta $ef64, lda #$02, sta $1820, rts + + ;MENU_FLAG_INVENTORY := 0x04 ; Bit 2 of $4A = inventory menu active + + scroll_list_down_hook: + """ + Replacement for the scroll-down stub at `$02A8B8`: kicks off the down-scroll animation, pre-renders the new bottom + slot for the rolling buffer. + """ + ; === GUARD: Only use rolling buffer for inventory menu === + ; Check bit 2 of $4A (inventory flag). If not set, use original magic behavior. + lda.b 0x4A + and #0x04 + bne _sd_is_inventory + + ; --- ORIGINAL MAGIC MENU BEHAVIOR --- + ; Original code: ldx $ef71, dex, stx $ef71, lda #$0c, sta $ef64, lda #$02, sta $1820, rts + ldx.w 0xEF71 + dex + stx.w 0xEF71 + lda #0x0C + sta.w 0xEF64 + lda #0x02 + sta.w 0x1820 + jmp.l return_to_bank02 + + _sd_is_inventory: + ; NOTE: For inventory in single-column mode, we INCREMENT EF71 to show later items + ; + ; FF6-STYLE: Pre-render the new bottom item BEFORE starting the animation! + ; This ensures the slot has correct content when it scrolls into view. + + ; Disable interrupts to prevent NMI from corrupting state + sei + + ; Set data bank to $7E for RAM access + phb + lda #0x7E + pha + plb + + ; Check if we can scroll further: rolling_top_row + VISIBLE_ROWS must be < TOTAL_ITEMS + ; Use rolling_top_row, NOT EF71 which has different meaning in battle! + lda.w rolling_top_row + cmp #( TOTAL_ITEMS - VISIBLE_ROWS ) + bcs _sd_abort ; Already at max, can't scroll down + + ; === FF6-STYLE PRE-RENDER === + ; Before animation, render the item that will appear at the NEW bottom. + ; The "off-screen" slot below the visible area will scroll into view. + ; Off-screen slot = (rolling_buffer_pos + VISIBLE_ROWS) % BUFFER_SLOTS + + ; Calculate item index = rolling_top_row + VISIBLE_ROWS (the item appearing at new bottom) + ; After increment, rolling_top_row will be current+1, so bottom item = (current+1) + 4 = current + 5 + lda.w rolling_top_row + clc + adc #VISIBLE_ROWS ; item = rolling_top_row + 5 + cmp #TOTAL_ITEMS + bcs _sd_skip_prerender ; Past end, nothing to render + sta.w rolling_edge_row ; Save item index + + ; Calculate the off-screen slot that will become visible + ; This is the slot 5 positions ahead of current top (wrapping) + lda.w rolling_buffer_pos + clc + adc #VISIBLE_ROWS ; pos + 5 + cmp #BUFFER_SLOTS + bcc _sd_slot_ok + sec + sbc #BUFFER_SLOTS ; Wrap if >= 6 + + _sd_slot_ok: + sta.w rolling_slot_index + + ; Render item to the off-screen slot + jsr.w _render_item_to_circular_slot + ; Copy to tilemap buffer + jsr.w _copy_slot_to_tilemap + ; Queue VRAM transfer + jsr.w _transfer_circular_slot + + _sd_skip_prerender: + ; INCREMENT EF71 to show later items (animation loop is NOPed out) + lda.w 0xEF71 + inc + sta.w 0xEF71 + + ; INCREMENT rolling_top_row to track which item is at top of visible area + ; This is critical for _refresh_visible_items_internal to work correctly! + lda.w rolling_top_row + inc + sta.w rolling_top_row + + ; Set up scroll animation + lda #0x0C + sta.w 0xEF64 + lda #0x02 ; Animation 2 (scroll down) + sta.w 0x1820 + + ; Advance circular buffer position (top slot moves up, becomes off-screen) + lda.w rolling_buffer_pos + inc + cmp #BUFFER_SLOTS + bcc _sd_pos_ok + lda #0 + + _sd_pos_ok: + sta.w rolling_buffer_pos + + _sd_abort: + ; Ensure cursor 1 stays visible during scroll animation + stz.w 0xEF69 ; Clear hide cursor 1 flag + stz.w 0xEF6E ; Clear alternate hide cursor 1 flag + + plb ; Restore data bank + cli ; re-enable interrupts + jmp.l return_to_bank02 + + ; ============================================================================ + ; scroll_list_up_hook + ; ============================================================================ + ; Called via JMP.L from $02A8CA + ; Pre-renders new top row, then does original scroll setup + ; Returns via JMP.L to return_to_bank02 (which has RTS) + ; -; CRITICAL: Restore direct page variables before returning - sep #0x20 ; 8-bit A for PLA - pla - sta.b 0x05 - pla - sta.b 0x04 - pla - sta.b 0x03 - pla - sta.b 0x02 - pla - sta.b 0x01 - pla - sta.b 0x00 + ; Original code at $02A8CA: + ; ldx $ef71, inx, stx $ef71, lda #$0c, sta $ef64, lda #$03, sta $1820, rts + + scroll_list_up_hook: + """ + Replacement for the scroll-up stub at `$02A8CA`: pre-renders the new top row before kicking off the scroll-up + animation. + """ + ; === GUARD: Only use rolling buffer for inventory menu === + ; Check bit 2 of $4A (inventory flag). If not set, use original magic behavior. + lda.b 0x4A + and #0x04 + bne _su_is_inventory + + ; --- ORIGINAL MAGIC MENU BEHAVIOR --- + ; Original code: ldx $ef71, inx, stx $ef71, lda #$0c, sta $ef64, lda #$03, sta $1820, rts + ldx.w 0xEF71 + inx + stx.w 0xEF71 + lda #0x0C + sta.w 0xEF64 + lda #0x03 + sta.w 0x1820 + jmp.l return_to_bank02 + + _su_is_inventory: + ; NOTE: For inventory in single-column mode, we DECREMENT rolling_top_row to show earlier items + ; + + ; For scroll UP, we DO need to pre-render BEFORE animation because: + ; - The slot that will scroll INTO view might have stale data from a previous scroll down + ; - We need to put the correct item there before it becomes visible -; Check if scroll animation is active before forcing cursor position -; Only force during animation ($1820 = 2 or 3), otherwise let normal code handle it - lda.l 0x7E1820 ; Animation type - beq _cursor_skip_force ; If 0, no animation - skip forcing - -; Animation is active - ensure cursor stays visible -; Must use long addressing since data bank may be $02 (ROM), not $7E (WRAM) - pha ; Save animation type - lda #0x00 - sta.l 0x7EEF69 ; Clear hide cursor 1 flag - sta.l 0x7EEF6E ; Clear alternate hide cursor 1 flag - -; X position is always $0C for single-column mode - lda #0x0C - sta.l 0x7EEF6B ; Set cursor 1 X position - -; Set Y based on animation direction -; Scroll down ($1820=2): cursor at bottom row, Y = $CC -; Scroll up ($1820=3): cursor at top row, Y = $9C - pla ; Restore animation type - cmp #0x02 ; Scroll down? - bne _cursor_scroll_up - lda #0xCC ; Bottom row Y position - bra _cursor_set_y - -_cursor_scroll_up: - lda #0x9C ; Top row Y position - -_cursor_set_y: - sta.l 0x7EEF6D ; Set cursor 1 Y position - -_cursor_skip_force: - - cli - jmp.l return_to_bank02 - -; HDMA table addresses - Y scroll portion only! -; Original ResetListScrollHDMA writes to $81F4 (Y scroll in swap table) -; Animation swaps this with $7F74 (Y scroll in active table) -; X scroll values at $81D2/$7F52 are left alone. -HDMA_SWAP_Y := 0x7E81F4 ; Y scroll in swap table -HDMA_ACTIVE_Y := 0x7E7F74 ; Y scroll in active table -HDMA_Y_SIZE := 0x00F0 ; 240 bytes (same as original) - -; ============================================================================ -; reset_list_scroll_hdma_rolling -; ============================================================================ -; Replacement for $02AAB8 - fills Y scroll in BOTH HDMA tables -; X scroll values stay as original game initialized them. -; -; Only difference from original: uses 132-based values instead of 371-based. - -reset_list_scroll_hdma_rolling: -"""Replacement for `$02AAB8`: fill the Y-scroll bytes of both HDMA tables when the inventory window opens.""" + ; Disable interrupts to prevent NMI from corrupting rolling_slot_index + sei + + ; Set data bank to $7E for RAM access + phb + lda #0x7E + pha + plb + + ; Check if we can scroll: rolling_top_row must be > 0 + ; (Use our custom variable, NOT EF71 which has different meaning in battle!) + lda.w rolling_top_row + beq _su_abort ; Already at item 0, can't scroll up + + ; First decrement buffer position (new top slot) + lda.w rolling_buffer_pos + dec + bpl _su_pos_ok + lda #BUFFER_SLOTS - 1 ; Wrap 0→5 + + _su_pos_ok: + sta.w rolling_buffer_pos + sta.w rolling_slot_index ; This slot will be the new top (currently off-screen) + + ; Calculate previous item index = rolling_top_row - 1 (the item that will appear at top) + ; Use rolling_top_row, NOT EF71 which has different meaning in battle! + lda.w rolling_top_row + dec + sta.w rolling_edge_row + + ; Pre-render to new top slot (currently off-screen, about to scroll in) + jsr.w _render_item_to_circular_slot + ; Copy to tilemap buffer + jsr.w _copy_slot_to_tilemap + ; Queue VRAM transfer for this slot + jsr.w _transfer_circular_slot + + ; DECREMENT rolling_top_row to track which item is at top of visible area + ; This is the authoritative source for which items are visible! + lda.w rolling_top_row + dec + sta.w rolling_top_row + + ; Set up scroll animation + lda #0x0C + sta.w 0xEF64 + lda #0x03 ; Animation 3 (scroll up) + sta.w 0x1820 + bra _su_exit + + _su_abort: + ; Pre-increment EF85, EF86, $63 so the caller's unconditional + ; `dec EF86 / dec EF85 / dec $63` at $02:B4FA-B500 lands back on + ; the original values when our scroll-up aborts at the top of + ; the list. Without this, $63 (cursor item index) underflows + ; from 0 to 0xFF and the row 0 cursor item points at junk, + ; reproducing the "one extra UP past item 0" visual. + inc.w 0xEF85 + inc.w 0xEF86 + inc.b 0x63 + + _su_exit: + ; Ensure cursor 1 stays visible during scroll animation + stz.w 0xEF69 ; Clear hide cursor 1 flag + stz.w 0xEF6E ; Clear alternate hide cursor 1 flag + + plb ; Restore data bank + cli ; re-enable interrupts + jmp.l return_to_bank02 + + ; ============================================================================ + ; update_list_scroll_hdma_wrapped + ; ============================================================================ + ; Replacement for $02A7F1 - builds HDMA table with scroll value WRAPPING + ; This is the key to the circular buffer - scroll values wrap at 96 pixels + ; + ; Called via JMP.L from $02A7F1 + ; Input: X = current scroll offset (from $EF65) + ; Y = scanlines per row (12) + ; Output: HDMA table at $7F74 filled with wrapped scroll values + + HDMA_TABLE := 0x7E7F74 ; Full 24-bit address: bank $7E, offset $7F74 (V scroll entries) + HDMA_TABLE_SIZE := 0x00F0 ; 240 bytes (same as HDMA_Y_SIZE) + SCROLL_WRAP := 0x0060 ; 96 = 6 slots × 16 pixels (tilemap buffer size) + SCROLL_WRAP_LIMIT := 0x01D3 ; 467 = 371 + 96 (wrap when >= this value) + OUR_BASE_SCROLL := 0x0173 ; 371 - same as original game + + update_list_scroll_hdma_wrapped: + """ + Replacement for the HDMA-build stub at `$02A7F1`: rebuild the per-scanline V-scroll table for the inventory rolling + buffer. + """ ; Check if we're in inventory mode (bit 2 of $4A) - lda.b 0x4A - and #0x04 - bne _reset_use_circular - -; Original behavior for non-inventory windows - ldx.w #0x0173 ; 371 - base scroll - stx.w 0xEF65 - ldy.w #0x000C ; 12 scanlines - sty.w 0xEF67 - rep #0x20 ; 16-bit A - txa ; A = 371 - ldx.w #0x0000 - -_reset_orig_loop: - sta.w 0x81F4, x ; Swap table only (like original) - dey - bne _reset_orig_skip - clc - adc.w #0x0004 ; Add 4 every 12 scanlines - ldy.w #0x000C - -_reset_orig_skip: - inx - inx - inx - inx - cpx.w #0x00F0 - bne _reset_orig_loop - .db 0x7B ; TDC - sep #0x20 - rtl - -_reset_use_circular: - ; Circular buffer setup for inventory - ; Save $00-$03 to prevent corrupting caller's state - lda.b 0x00 - pha - lda.b 0x01 - pha - lda.b 0x02 - pha - lda.b 0x03 - pha - - ldx.w #0x0173 ; 371 - game's expected base scroll - stx.w 0xEF65 - ldy.w #0x000C ; 12 scanlines per item row - sty.w 0xEF67 - -; Re-initialize circular buffer contents when inventory (re)opens -; This ensures VRAM has correct items even after closing/reopening - jsr.l init_inventory_text_buf_rolling - -; rolling_buffer_pos is now set by init_inventory_text_buf_rolling - -; Fill Y scroll in both tables using circular buffer formula - stz.b 0x02 ; Row counter low byte (8-bit mode) - stz.b 0x03 ; Row counter high byte - rep #0x30 ; 16-bit A and X/Y - ldx.w #0x0000 ; HDMA table index - -_reset_row_loop: - ; At init: vram_slot = row, so scroll = BASE + row * 4 - lda.b 0x02 ; row - and.w #0x00FF - asl - asl ; row * 4 - clc - adc.w #OUR_BASE_SCROLL ; + BASE - sta.b 0x00 ; Save scroll value - -; Store same value for all 12 scanlines of this row - ldy.w #0x000C ; 12 scanlines - -_reset_scanline_loop: - lda.b 0x00 - sta.l HDMA_SWAP_Y, x ; $81F4 + X - sta.l HDMA_ACTIVE_Y, x ; $7F74 + X - inx - inx - inx - inx ; X += 4 - dey - bne _reset_scanline_loop - -; Next row - inc.b 0x02 - lda.b 0x02 - cmp.w #VISIBLE_ROWS ; 5 rows - bne _reset_row_loop - - sep #0x20 ; 8-bit A (game expects this) - ; Restore $00-$03 before returning - pla - sta.b 0x03 - pla - sta.b 0x02 - pla - sta.b 0x01 - pla - sta.b 0x00 - rtl - -; ============================================================================ -; check_cursor2_visibility_rolling -; ============================================================================ -; Checks if cursor 2 (the first selected item in swap mode) should be visible. -; Called after scroll animation completes. -; - -; Logic: -; - If swap mode NOT active ($EF94 == 0), return immediately -; - Get first selected item index from $EF95 (mask out bit 7) + ; If not, use original behavior for other windows + lda.b 0x4A + and #0x04 + bne _use_circular_buffer + + ; Original behavior for non-inventory windows + ; Input: X = scroll offset, Y = scanlines per row (12) + rep #0x20 ; 16-bit A + txa ; A = scroll offset + ldx.w #0x0000 ; Table index + + _orig_loop: + sta.w 0x7F74, x ; Store scroll value + dey + bne _orig_skip_add + clc + adc.w #0x0004 ; Add 4 every 12 scanlines + ldy.w #0x000C ; Reset scanline counter + + _orig_skip_add: + inx + inx + inx + inx + cpx.w #0x00F0 ; 240 bytes + bne _orig_loop + .db 0x7B ; TDC (clear A) + sep #0x20 ; 8-bit A + jmp.l return_to_bank02 ; Return via bank $02 trampoline + + _use_circular_buffer: + ; FF6-STYLE CIRCULAR BUFFER HDMA + ; + + ; Key insight from FF6's LoadItemBG1VScrollHDMATbl: + ; - At screen scanline S with scroll V, displayed VRAM row = V + S + + ; - To show VRAM slot N at screen row R (scanline R*12): + ; scroll + R*12 = N*16, therefore scroll = N*16 - R*12 + ; -; - If rolling_top_row <= selected < rolling_top_row + VISIBLE_ROWS: -; Show cursor 2 ($EF6A = 0) + ; Algorithm for each visible row (0-4): + ; vram_slot = (rolling_buffer_pos + row) % 6 + ; scroll = BASE + (vram_slot * 16) - (row * 12) + ; + ; This ensures proper alignment AND seamless wraparound. + ; + sei ; Disable interrupts during HDMA table update + + ; CRITICAL: Save direct page variables that the caller depends on + lda.b 0x00 + pha + lda.b 0x01 + pha + lda.b 0x02 + pha + lda.b 0x03 + pha + lda.b 0x04 + pha + lda.b 0x05 + pha + + rep #0x30 ; 16-bit A, X, Y + + ldx.w #0x0000 ; HDMA table index + stz.b 0x02 ; Row counter (clears $02 and $03 in 16-bit mode) + + _row_loop: + ; Calculate vram_slot = (rolling_buffer_pos + row) % 6 + lda.w rolling_buffer_pos + and.w #0x00FF + clc + adc.b 0x02 ; + row number + + _mod6: + cmp.w #BUFFER_SLOTS ; >= 6? + bcc _mod6_done + sec + sbc.w #BUFFER_SLOTS + bra _mod6 + + _mod6_done: + ; A = vram_slot (0-5) + + ; Calculate vram_slot * 16 + asl + asl + asl + asl ; A = vram_slot * 16 + sta.b 0x00 ; Save vram_offset + + ; Calculate row * 12 = row * 8 + row * 4 + ; Use Y register for temp to avoid clobbering direct page + lda.b 0x02 ; row (only low byte matters, high is 0) + and.w #0x00FF ; Ensure only low byte + asl + asl + asl ; row * 8 + tay ; Y = row * 8 + lda.b 0x02 + and.w #0x00FF + asl + asl ; row * 4 + sta.b 0x04 ; Use $04 for temp (not overlapping) + tya ; A = row * 8 + clc + adc.b 0x04 ; row * 8 + row * 4 = row * 12 + ; A = scanline_offset (row * 12) + + ; scroll = BASE + vram_offset - scanline_offset + ; eor.w #0xFFFF ; Negate: -scanline_offset + ; a816 does not support yet the w for eor. + .db 0x49 + .dw 0xffff + + inc + clc + adc.b 0x00 ; + vram_offset + clc + adc.w #OUR_BASE_SCROLL ; + BASE + sta.b 0x00 ; $00 = scroll value for this row + + ; Store same scroll value for all 12 scanlines of this row + ldy.w #0x000C ; 12 scanlines + + _scanline_loop: + lda.b 0x00 + sta.l HDMA_TABLE, x + inx + inx + inx + inx ; X += 4 (next HDMA entry) + dey + bne _scanline_loop + + ; Next row + inc.b 0x02 + lda.b 0x02 + cmp.w #VISIBLE_ROWS ; 5 rows total + bne _row_loop + + ; CRITICAL: Restore direct page variables before returning + sep #0x20 ; 8-bit A for PLA + pla + sta.b 0x05 + pla + sta.b 0x04 + pla + sta.b 0x03 + pla + sta.b 0x02 + pla + sta.b 0x01 + pla + sta.b 0x00 + + ; Check if scroll animation is active before forcing cursor position + ; Only force during animation ($1820 = 2 or 3), otherwise let normal code handle it + lda.l 0x7E1820 ; Animation type + beq _cursor_skip_force ; If 0, no animation - skip forcing + + ; Animation is active - ensure cursor stays visible + ; Must use long addressing since data bank may be $02 (ROM), not $7E (WRAM) + pha ; Save animation type + lda #0x00 + sta.l 0x7EEF69 ; Clear hide cursor 1 flag + sta.l 0x7EEF6E ; Clear alternate hide cursor 1 flag + + ; X position is always $0C for single-column mode + lda #0x0C + sta.l 0x7EEF6B ; Set cursor 1 X position + + ; Set Y based on animation direction + ; Scroll down ($1820=2): cursor at bottom row, Y = $CC + ; Scroll up ($1820=3): cursor at top row, Y = $9C + pla ; Restore animation type + cmp #0x02 ; Scroll down? + bne _cursor_scroll_up + lda #0xCC ; Bottom row Y position + bra _cursor_set_y + + _cursor_scroll_up: + lda #0x9C ; Top row Y position + + _cursor_set_y: + sta.l 0x7EEF6D ; Set cursor 1 Y position + + _cursor_skip_force: + + cli + jmp.l return_to_bank02 + + ; HDMA table addresses - Y scroll portion only! + ; Original ResetListScrollHDMA writes to $81F4 (Y scroll in swap table) + ; Animation swaps this with $7F74 (Y scroll in active table) + ; X scroll values at $81D2/$7F52 are left alone. + HDMA_SWAP_Y := 0x7E81F4 ; Y scroll in swap table + HDMA_ACTIVE_Y := 0x7E7F74 ; Y scroll in active table + HDMA_Y_SIZE := 0x00F0 ; 240 bytes (same as original) + + ; ============================================================================ + ; reset_list_scroll_hdma_rolling + ; ============================================================================ + ; Replacement for $02AAB8 - fills Y scroll in BOTH HDMA tables + ; X scroll values stay as original game initialized them. + ; + ; Only difference from original: uses 132-based values instead of 371-based. + + reset_list_scroll_hdma_rolling: + """Replacement for `$02AAB8`: fill the Y-scroll bytes of both HDMA tables when the inventory window opens.""" + ; Check if we're in inventory mode (bit 2 of $4A) + lda.b 0x4A + and #0x04 + bne _reset_use_circular + + ; Original behavior for non-inventory windows + ldx.w #0x0173 ; 371 - base scroll + stx.w 0xEF65 + ldy.w #0x000C ; 12 scanlines + sty.w 0xEF67 + rep #0x20 ; 16-bit A + txa ; A = 371 + ldx.w #0x0000 + + _reset_orig_loop: + sta.w 0x81F4, x ; Swap table only (like original) + dey + bne _reset_orig_skip + clc + adc.w #0x0004 ; Add 4 every 12 scanlines + ldy.w #0x000C + + _reset_orig_skip: + inx + inx + inx + inx + cpx.w #0x00F0 + bne _reset_orig_loop + .db 0x7B ; TDC + sep #0x20 + rtl + + _reset_use_circular: + ; Circular buffer setup for inventory + ; Save $00-$03 to prevent corrupting caller's state + lda.b 0x00 + pha + lda.b 0x01 + pha + lda.b 0x02 + pha + lda.b 0x03 + pha + + ldx.w #0x0173 ; 371 - game's expected base scroll + stx.w 0xEF65 + ldy.w #0x000C ; 12 scanlines per item row + sty.w 0xEF67 + + ; Re-initialize circular buffer contents when inventory (re)opens + ; This ensures VRAM has correct items even after closing/reopening + jsr.l init_inventory_text_buf_rolling + + ; rolling_buffer_pos is now set by init_inventory_text_buf_rolling + + ; Fill Y scroll in both tables using circular buffer formula + stz.b 0x02 ; Row counter low byte (8-bit mode) + stz.b 0x03 ; Row counter high byte + rep #0x30 ; 16-bit A and X/Y + ldx.w #0x0000 ; HDMA table index + + _reset_row_loop: + ; At init: vram_slot = row, so scroll = BASE + row * 4 + lda.b 0x02 ; row + and.w #0x00FF + asl + asl ; row * 4 + clc + adc.w #OUR_BASE_SCROLL ; + BASE + sta.b 0x00 ; Save scroll value + + ; Store same value for all 12 scanlines of this row + ldy.w #0x000C ; 12 scanlines + + _reset_scanline_loop: + lda.b 0x00 + sta.l HDMA_SWAP_Y, x ; $81F4 + X + sta.l HDMA_ACTIVE_Y, x ; $7F74 + X + inx + inx + inx + inx ; X += 4 + dey + bne _reset_scanline_loop + + ; Next row + inc.b 0x02 + lda.b 0x02 + cmp.w #VISIBLE_ROWS ; 5 rows + bne _reset_row_loop + + sep #0x20 ; 8-bit A (game expects this) + ; Restore $00-$03 before returning + pla + sta.b 0x03 + pla + sta.b 0x02 + pla + sta.b 0x01 + pla + sta.b 0x00 + rtl + + ; ============================================================================ + ; check_cursor2_visibility_rolling + ; ============================================================================ + ; Checks if cursor 2 (the first selected item in swap mode) should be visible. + ; Called after scroll animation completes. + ; -; - Else: -; Hide cursor 2 ($EF6A = 1) -; -; Called via JSR from wrap_and_clear_trampoline (bank $02) + ; Logic: + ; - If swap mode NOT active ($EF94 == 0), return immediately + ; - Get first selected item index from $EF95 (mask out bit 7) -swap_mode_flag := 0xEF94 ; Non-zero = swap mode active -first_selected_item := 0xEF95 ; First selected item index (bit 7 may be set) -hide_cursor_2 := 0xEF6A ; Non-zero = hide cursor 2 + ; - If rolling_top_row <= selected < rolling_top_row + VISIBLE_ROWS: + ; Show cursor 2 ($EF6A = 0) -check_cursor2_visibility_rolling: -""" -Toggle cursor-2 visibility while swap mode is active based on whether the first-selected item is in the visible -window. -""" -; Check if we're in inventory mode (bit 2 of $4A) -; If not, don't touch cursor 2 state - lda.b 0x4A - and #0x04 - beq _cursor2_done - -; Check if swap mode is active - lda.w swap_mode_flag - beq _cursor2_done ; Not in swap mode, nothing to check - -; Get first selected item index (mask out bit 7) - lda.w first_selected_item - and #0x7F ; Clear bit 7 - sta.b 0x00 ; Save selected item index - -; Check if selected item is in visible range: -; visible if: rolling_top_row <= selected < rolling_top_row + VISIBLE_ROWS - -; First check: selected >= rolling_top_row - lda.b 0x00 ; Selected item - cmp.w rolling_top_row - bcc _cursor2_hide ; selected < rolling_top_row, hide cursor - -; Second check: selected < rolling_top_row + VISIBLE_ROWS - lda.w rolling_top_row - clc - adc #VISIBLE_ROWS ; rolling_top_row + 5 - sta.b 0x01 ; Save upper bound - - lda.b 0x00 ; Selected item - cmp.b 0x01 ; Compare to upper bound - bcs _cursor2_hide ; selected >= upper bound, hide cursor - -; Item is visible - show cursor 2 - stz.w hide_cursor_2 ; $EF6A = 0 (show) - bra _cursor2_done - -_cursor2_hide: - ; Item is not visible - hide cursor 2 - lda #0x01 - sta.w hide_cursor_2 ; $EF6A = 1 (hide) - -_cursor2_done: - rtl ; Called via JSL from bank $02 - -; ============================================================================ -; Field Menu NMI Handler (relocated from bank $01 to save space) -; ============================================================================ -; Constants for field menu HDMA (duplicated here for bank $20 access) -field_menu_hdma_enable := 0x1BAE -field_menu_hdma_copy_pending := 0x1BB6 -field_menu_transfer_pending := 0x1BB3 -FIELD_HDMA_TABLE := 0x7E9800 -FIELD_HDMA_SHADOW := 0x7E9840 -FIELD_HDMA_TABLE_SIZE := 40 - -; Drops profile HDMA constants (mirror src/ingame/drops_rolling.s -; definitions; duplicated here so the NMI handler can reference them -; without crossing the bank-$01 / bank-$20 import boundary). -DROPS_HDMA_TABLE := 0x7E9880 -DROPS_HDMA_SHADOW := 0x7E98C0 -DROPS_HDMA_TABLE_SIZE := 40 -drops_hdma_copy_pending := 0x1BEE - -; Called via JSL from bank $01 nmi_dma_transfer_check - -field_menu_nmi_dma_transfer_check_impl: -""" -NMI hook (JSL from bank $01) relocated to bank $20: transfer the field rolling-buffer tilemap to VRAM and update -the live HDMA scroll table. -""" - php - sep #0x20 ; 8-bit A - -; === GUARD: Only run if menu HDMA is enabled === -; This prevents field menu DMA from corrupting battle VRAM - lda.l 0x7E0000 + field_menu_hdma_enable - bne _field_nmi_active - jmp.w _field_nmi_done - -_field_nmi_active: - -; === HDMA table copy: shadow -> active === - lda.l 0x7E0000 + field_menu_hdma_copy_pending - beq _field_nmi_hdma_copy_done - lda #0x00 - sta.l 0x7E0000 + field_menu_hdma_copy_pending - -; Copy 40 bytes from shadow ($9840) to active ($9800) - rep #0x30 ; 16-bit A, X, Y - ldx.w #0x0000 - -_field_nmi_hdma_copy_loop: - lda.l FIELD_HDMA_SHADOW, x - sta.l FIELD_HDMA_TABLE, x - inx - inx - cpx.w #FIELD_HDMA_TABLE_SIZE - bcc _field_nmi_hdma_copy_loop - sep #0x20 ; Back to 8-bit A - -_field_nmi_hdma_copy_done: - ; === Drops HDMA table copy: $7E:98C0 shadow -> $7E:9880 active === - ; Drops uses ch4 driving BG4VOFS with its own 64-byte table, so the - ; shadow→active copy needs a parallel block keyed off - ; drops_hdma_copy_pending. Skip if drops HDMA is disabled in - ; $1BAE bit 4. - sep #0x20 - lda.l 0x7E1BAE - and #0x10 - beq _drops_nmi_hdma_copy_done - lda.l 0x7E0000 + drops_hdma_copy_pending - beq _drops_nmi_hdma_copy_done - lda #0x00 - sta.l 0x7E0000 + drops_hdma_copy_pending - rep #0x30 - ldx.w #0x0000 - -_drops_nmi_hdma_copy_loop: - lda.l DROPS_HDMA_SHADOW, x - sta.l DROPS_HDMA_TABLE, x - inx - inx - cpx.w #DROPS_HDMA_TABLE_SIZE - bcc _drops_nmi_hdma_copy_loop - sep #0x20 - -_drops_nmi_hdma_copy_done: - -; === Tilemap DMA transfer (field menu = BG1) === -; Skip when treasure menu owns the screen: $1BB3 is then the -; original drops cursor row, not field_menu_transfer_pending, -; and clearing it would snap the drops cursor back to row 0. - lda.l 0x7E1BC6 - bne _field_nmi_check_treasure - lda.l 0x7E0000 + field_menu_transfer_pending - beq _field_nmi_check_treasure - lda #0x00 - sta.l 0x7E0000 + field_menu_transfer_pending - - sep #0x20 - lda #0x01 - sta.w 0x4300 - lda #0x18 - sta.w 0x4301 - rep #0x20 - lda.w #0xB600 - sta.w 0x4302 - sep #0x20 - lda #0x7E - sta.w 0x4304 - rep #0x20 - lda.w #0x0800 - sta.w 0x4305 - lda.w #0x6000 - sta.w 0x2116 - sep #0x20 - lda #0x01 - sta.w 0x420B + ; - Else: + ; Hide cursor 2 ($EF6A = 1) + ; + ; Called via JSR from wrap_and_clear_trampoline (bank $02) -_field_nmi_check_treasure: -.if TREASURE_INVENTORY_ROLLING { -; === Tilemap DMA transfer (treasure menu = BG3) === - lda.l 0x7E0000 + 0x1BDB ; treasure_transfer_pending - beq _field_nmi_done - lda #0x00 - sta.l 0x7E0000 + 0x1BDB + swap_mode_flag := 0xEF94 ; Non-zero = swap mode active + first_selected_item := 0xEF95 ; First selected item index (bit 7 may be set) + hide_cursor_2 := 0xEF6A ; Non-zero = hide cursor 2 - sep #0x20 - lda #0x01 - sta.w 0x4300 - lda #0x18 - sta.w 0x4301 - rep #0x20 - lda.w #0xD600 ; BG3 screen buffer source ($7ED600) - sta.w 0x4302 - sep #0x20 - lda #0x7E - sta.w 0x4304 - rep #0x20 - lda.w #0x0800 ; 2 KB tilemap - sta.w 0x4305 - lda.w #0x7000 ; BG3 tilemap VRAM target - sta.w 0x2116 - sep #0x20 - lda #0x01 - sta.w 0x420B + check_cursor2_visibility_rolling: + """ + Toggle cursor-2 visibility while swap mode is active based on whether the first-selected item is in the visible + window. + """ + ; Check if we're in inventory mode (bit 2 of $4A) + ; If not, don't touch cursor 2 state + lda.b 0x4A + and #0x04 + beq _cursor2_done + + ; Check if swap mode is active + lda.w swap_mode_flag + beq _cursor2_done ; Not in swap mode, nothing to check + + ; Get first selected item index (mask out bit 7) + lda.w first_selected_item + and #0x7F ; Clear bit 7 + sta.b 0x00 ; Save selected item index + + ; Check if selected item is in visible range: + ; visible if: rolling_top_row <= selected < rolling_top_row + VISIBLE_ROWS + + ; First check: selected >= rolling_top_row + lda.b 0x00 ; Selected item + cmp.w rolling_top_row + bcc _cursor2_hide ; selected < rolling_top_row, hide cursor + + ; Second check: selected < rolling_top_row + VISIBLE_ROWS + lda.w rolling_top_row + clc + adc #VISIBLE_ROWS ; rolling_top_row + 5 + sta.b 0x01 ; Save upper bound + + lda.b 0x00 ; Selected item + cmp.b 0x01 ; Compare to upper bound + bcs _cursor2_hide ; selected >= upper bound, hide cursor + + ; Item is visible - show cursor 2 + stz.w hide_cursor_2 ; $EF6A = 0 (show) + bra _cursor2_done + + _cursor2_hide: + ; Item is not visible - hide cursor 2 + lda #0x01 + sta.w hide_cursor_2 ; $EF6A = 1 (hide) + + _cursor2_done: + rtl ; Called via JSL from bank $02 + + ; ============================================================================ + ; Field Menu NMI Handler (relocated from bank $01 to save space) + ; ============================================================================ + ; Field + drops state aliases - use the cast'd struct views from + ; items.i / src/ingame/drops_rolling.s. Field is `field_menu_rolling` + ; (defined in items.i, visible at module scope). Drops state at + ; $7E:9C30 isn't exported from its defining module, so re-cast it + ; locally to get drops_rolling.hdma_copy_pending et al. + drops_rolling := (0x7E9C30 as RollingBufferState) + FIELD_HDMA_TABLE := 0x7E9800 + FIELD_HDMA_SHADOW := 0x7E9840 + FIELD_HDMA_TABLE_SIZE := 40 + DROPS_HDMA_TABLE := 0x7E9880 + DROPS_HDMA_SHADOW := 0x7E98C0 + DROPS_HDMA_TABLE_SIZE := 40 + + ; Called via JSL from bank $01 nmi_dma_transfer_check + + field_menu_nmi_dma_transfer_check_impl: + """ + NMI hook (JSL from bank $01) relocated to bank $20: transfer the field rolling-buffer tilemap to VRAM and update + the live HDMA scroll table. + """ + php + sep #0x20 ; 8-bit A + + ; === GUARD: Only run if menu HDMA is enabled === + ; This prevents field menu DMA from corrupting battle VRAM + lda.l field_menu_rolling.hdma_enable + bne _field_nmi_active + jmp.w _field_nmi_done + + _field_nmi_active: + + ; === HDMA table copy: shadow -> active === + lda.l field_menu_rolling.hdma_copy_pending + beq _field_nmi_hdma_copy_done + lda #0x00 + sta.l field_menu_rolling.hdma_copy_pending + + ; Copy 40 bytes from shadow ($9840) to active ($9800) + rep #0x30 ; 16-bit A, X, Y + ldx.w #0x0000 + + _field_nmi_hdma_copy_loop: + lda.l FIELD_HDMA_SHADOW, x + sta.l FIELD_HDMA_TABLE, x + inx + inx + cpx.w #FIELD_HDMA_TABLE_SIZE + bcc _field_nmi_hdma_copy_loop + sep #0x20 ; Back to 8-bit A + + _field_nmi_hdma_copy_done: + ; === Drops HDMA table copy: $7E:98C0 shadow -> $7E:9880 active === + ; Drops uses ch4 driving BG4VOFS with its own 64-byte table, so the + ; shadow→active copy needs a parallel block keyed off + ; drops_rolling.hdma_copy_pending. Skip if drops HDMA is disabled in + ; $1BAE bit 4. + sep #0x20 + lda.l field_menu_rolling.hdma_enable + and #0x10 + beq _drops_nmi_hdma_copy_done + lda.l drops_rolling.hdma_copy_pending + beq _drops_nmi_hdma_copy_done + lda #0x00 + sta.l drops_rolling.hdma_copy_pending + rep #0x30 + ldx.w #0x0000 + + _drops_nmi_hdma_copy_loop: + lda.l DROPS_HDMA_SHADOW, x + sta.l DROPS_HDMA_TABLE, x + inx + inx + cpx.w #DROPS_HDMA_TABLE_SIZE + bcc _drops_nmi_hdma_copy_loop + sep #0x20 + + _drops_nmi_hdma_copy_done: + + ; === Tilemap DMA transfer (field menu = BG1) === + ; Skip when treasure menu owns the screen: $1BB3 is then the + ; original drops cursor row, not field_menu_transfer_pending, + ; and clearing it would snap the drops cursor back to row 0. + lda.l 0x7E1BC6 + bne _field_nmi_check_treasure + lda.l field_menu_rolling.transfer_pending + beq _field_nmi_check_treasure + lda #0x00 + sta.l field_menu_rolling.transfer_pending + + sep #0x20 + lda #0x01 + sta.w 0x4300 + lda #0x18 + sta.w 0x4301 + rep #0x20 + lda.w #0xB600 + sta.w 0x4302 + sep #0x20 + lda #0x7E + sta.w 0x4304 + rep #0x20 + lda.w #0x0800 + sta.w 0x4305 + lda.w #0x6000 + sta.w 0x2116 + sep #0x20 + lda #0x01 + sta.w 0x420B + + _field_nmi_check_treasure: + .if TREASURE_INVENTORY_ROLLING { + ; === Tilemap DMA transfer (treasure menu = BG3) === + lda.l 0x7E0000 + 0x9C0B ; treasure_rolling.transfer_pending + beq _field_nmi_done + lda #0x00 + sta.l 0x7E0000 + 0x9C0B + + sep #0x20 + lda #0x01 + sta.w 0x4300 + lda #0x18 + sta.w 0x4301 + rep #0x20 + lda.w #0xD600 ; BG3 screen buffer source ($7ED600) + sta.w 0x4302 + sep #0x20 + lda #0x7E + sta.w 0x4304 + rep #0x20 + lda.w #0x0800 ; 2 KB tilemap + sta.w 0x4305 + lda.w #0x7000 ; BG3 tilemap VRAM target + sta.w 0x2116 + sep #0x20 + lda #0x01 + sta.w 0x420B + } + + _field_nmi_done: + ; --- VWF CHR flush (DMA channel 6) --- + ; Hands off to `render.flush_chr_to_vram`. Gates on `VWF_CHR_DIRTY` + ; and reads VRAM dest + size from `VwfConfig`. Drives ch6, which + ; the FF4 DMA audit shows untouched by vanilla btlgfx / menu and + ; by every engine we ship (ch7 is the shared battle / libmz / + ; field-NMI-tilemap channel, ch3 was the WRAM mirror experiment). + ; `flush_chr_to_vram` lives in the same bank-20 reloc region as + ; this impl ; use a short JSR so the return stays balanced with + ; the RTS the engine routine ends with. + jsr.w render.flush_chr_to_vram + plp + rtl } - -_field_nmi_done: - plp - rtl diff --git a/src/battle/inventory_rolling_patches.s b/src/battle/inventory_rolling_patches.s index c275220..590f113 100644 --- a/src/battle/inventory_rolling_patches.s +++ b/src/battle/inventory_rolling_patches.s @@ -2,6 +2,7 @@ ROM patches that wire the battle inventory rolling-buffer engine into bank $02 (JSL trampolines for cross-bank calls, JML hooks for the scroll animation, surgical NOPs / RTS overrides). """ +.include "config.i" .extern init_inventory_text_buf_rolling .extern tfr_inventory_list_rolling .extern scroll_list_down_hook @@ -16,6 +17,13 @@ calls, JML hooks for the scroll animation, surgical NOPs / RTS overrides). .extern battle_render.TILEMAP_PENDING_COMMANDS .extern battle_menu_dirty .extern CMD_DIRTY_BIT +.if BATTLE_ITEMS_VWF { + .extern messages_vwf.init_inventory + .extern messages_vwf.init_inventory_for_current_slot + .extern messages_vwf.mirror_main_to_cmd + .extern messages_vwf.draw_inventory_text + .extern messages_vwf.deinit +} ; ============================================================================ ; Rolling Inventory Buffer - ROM Patches (Single Column) @@ -35,53 +43,62 @@ calls, JML hooks for the scroll animation, surgical NOPs / RTS overrides). ; PATCH: InitInventoryTextBuf ($029E9C) ; ============================================================================ -*=0x029E9C - jsr.l init_inventory_text_buf_rolling - .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 - .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 - .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 - rts - -; ============================================================================ -; PATCH: TfrInventoryList ($0298FA) -; ============================================================================ -; Original function is $98FA-$9982 (136 bytes). We replace with JSL+RTS (5 bytes) -; This frees $98FF-$9982 (131 bytes) for our trampolines. - -*=0x0298FA - jsr.l tfr_inventory_list_rolling - rts - -; ============================================================================ -; TRAMPOLINES (Bank $02 free space: $98FF-$9982) -; ============================================================================ -; These are called via JSL from bank $20, return via RTL -.pool bank02_trampolines { - range 0x0298ff 0x029982 - strategy order +.alloc at 0x029E9C { + jsr.l init_inventory_text_buf_rolling + .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 + .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 + .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 + rts + + ; ============================================================================ + ; PATCH: TfrInventoryList ($0298FA) + ; ============================================================================ + ; Original function is $98FA-$9982 (136 bytes). We replace with JSL+RTS (5 bytes) + ; This frees $98FF-$9982 (131 bytes) for our trampolines. +} +.alloc at 0x0298FA { + jsr.l tfr_inventory_list_rolling + rts + + ; ============================================================================ + ; TRAMPOLINES (Bank $02 free space: $98FF-$9982) + ; ============================================================================ + ; These are called via JSL from bank $20, return via RTL + .pool bank02_trampolines { + range 0x0298ff 0x029982 + strategy order + } } - .alloc bank02_trampolines_block in bank02_trampolines { - draw_text_rolling_trampoline: -"""Bank-$02 trampoline: forces VWF battle-flags off for the duration of original draw_text call.""" - ; Save current battle flags to prevent VWF mode interference +""" +Bank-$02 trampoline around draw_text for inventory rendering. With +BATTLE_ITEMS_VWF on, sets battle_flags = 0x02 so battle_display_char +routes the put_char dispatch through messages_vwf.put_fixed_char_* +for proportional rendering. Wraps the call with init_names / deinit +so the VWF tile allocator and pending-DMA mask stay in sync. +""" + + + .if BATTLE_ITEMS_VWF { +; Custom draw_inventory_text owns the format walk end-to-end (escape +; codes 0x00 / 0x03 / 0x0E plus VWF blits for raw chars). No need to +; toggle battle_flags here ; it manages its own VWF state. + jsr.l messages_vwf.draw_inventory_text + rtl + } else { lda.l 0x704F00 pha - ; Clear battle flags (force WRAM mode for inventory) - lda #0x00 + lda.b #0x00 sta.l 0x704F00 - -; Original draw_text call xba - lda #0x00 + lda.b #0x00 xba - jsr 0xA455 ; draw_text at $02A455 - -; Restore battle flags + jsr 0xA455 pla sta.l 0x704F00 rtl + } mult8_trampoline: """Bank-$02 RTL trampoline around original Mult8 ($028560).""" @@ -112,27 +129,24 @@ _update_enabled_items_trampoline: ; ============================================================================ ; Original function was overwritten. This must fit in bank $02 free space. -_draw_battle_command_window_relocated: - ; Thunk-level gate on CMD_DIRTY_BIT. Set TILEMAP_PENDING_COMMANDS - ; so the NMI dma_transfer picks up the cmd-window tilemap DMA - ; in sync with the tile-data DMA. Skip on clean ; VRAM retains - ; last upload and no DMA fires. - lda.l battle_menu_dirty - bit.b #CMD_DIRTY_BIT - beq _dbcw_skip - and.b #( ~ CMD_DIRTY_BIT ) & 0xFF - sta.l battle_menu_dirty +draw_battle_command_window_relocated: +; Drop the CMD_DIRTY_BIT gate. The cmd-window tilemap region at +; $C1A5+ is a mirror of the main view ($BE65+) overlaid with cmd +; tiles. ATB rotation / monster death / HP ticks etc. only write to +; main, never re-mirror, so gating left the cmd region frozen at +; battle-init state (and we kept seeing empty char-name + monster +; rows behind the cmd window). Mirror is now a single ch3 WRAM DMA +; (~400 cycles), small enough to run every frame; total cost is in +; the same ballpark as vanilla's per-frame DrawCmdWindow. lda.l battle_render.tilemap_pending_mask ora.b #battle_render.TILEMAP_PENDING_COMMANDS sta.l battle_render.tilemap_pending_mask - jsr.w draw_window_render_hook ; Draw command list + sets LDX #$0340 + jsr.w draw_window_render_hook ; Draw command list (X side-effect unused now) -_dbcw_loop: - lda.w 0xBE65, x ; Copy main menu window tilemap - sta.w 0xC1A5, x - dex - bne _dbcw_loop +; Mirror main-view tilemap $BE65..$C1A4 -> $C1A5..$C4E4 via WRAM DMA +; ch3 (replaces a $340-iter lda/sta loop ; ~10K cycles -> ~400). + jsr.l messages_vwf.mirror_main_to_cmd lda #0x02 jsr 0x9B59 ; Load menu window data jsr 0x9BC7 ; Draw window @@ -140,9 +154,6 @@ _dbcw_loop: ldx.w #0x0064 jmp 0x99F1 ; tail-call DrawCmdListText (rts via that function) -_dbcw_skip: - rts - ; ============================================================================ ; WRAP/CLEAR TRAMPOLINE (small, stays in bank $02) ; ============================================================================ @@ -153,12 +164,12 @@ _dbcw_skip: wrap_and_clear_trampoline: """Bank-$02 tail of the scroll animation: post-render and cursor visibility check.""" - ; FF6-style circular scroll - no reset needed! - ; The wrap function in update_list_scroll_hdma_wrapped handles coordinate conversion. - ; - ; After scroll down, we need to post-render the next item to prepare for - ; future scrolls. The off-screen slot that just scrolled off should be - ; updated with the next item in the list. +; FF6-style circular scroll - no reset needed! +; The wrap function in update_list_scroll_hdma_wrapped handles coordinate conversion. +; +; After scroll down, we need to post-render the next item to prepare for +; future scrolls. The off-screen slot that just scrolled off should be +; updated with the next item in the list. jsr.l post_scroll_down_render ; Post-render if scroll down ; Check cursor 2 visibility for swap mode @@ -174,8 +185,9 @@ wrap_and_clear_trampoline: return_to_bank02: """Trailing RTS used as a JML target by bank-$20 hooks to return to bank-$02.""" rts +} -} ; end .alloc bank02_trampolines_block +; end .alloc bank02_trampolines_block ; ============================================================================ ; ROM PATCHES @@ -184,110 +196,111 @@ return_to_bank02: ; Redirect callers of $9989 to relocated function ; Must use JSR (not JSL) since function ends with JMP, not RTL -*=0x0296CB - jsr.w _draw_battle_command_window_relocated - -*=0x029983 - jsr.w _draw_battle_command_window_relocated - -; ============================================================================ -; PATCHES in ascending address order (assembler requires this) -; ============================================================================ - -; Scroll animation end - wrap $EF65 (4 bytes each) -; Must use JMP (not JMP.L) - 3 bytes + 1 NOP = 4 bytes - -*=0x02A86E - jmp.w wrap_and_clear_trampoline - nop - -; Animation loop DECrement path ($02A872-$02A87B) - 10 bytes -; Original: LDX $EF71 / DEX / STX $EF71 / JMP CheckListCursorVisible -; NOP the LDX/DEX/STX, replace JMP with RTS (skips CheckListCursorVisible) -; CheckListCursorVisible can incorrectly hide cursor 2 with our circular buffer scroll values - -*=0x02A872 -_nop_patch_dec: - nop - nop - nop - nop - nop - nop - nop - rts ; Skip CheckListCursorVisible (was JMP $A82D) - nop ; Fill remaining 2 bytes of JMP - nop - -*=0x02A8AA - jmp.w wrap_and_clear_trampoline - nop - -; Animation loop INCrement path ($02A8AE-$02A8B7) - 10 bytes -; Original: LDX $EF71 / INX / STX $EF71 / JMP CheckListCursorVisible -; NOP the LDX/INX/STX, replace JMP with RTS (skips CheckListCursorVisible) -; CheckListCursorVisible can incorrectly hide cursor 2 with our circular buffer scroll values - -*=0x02A8AE -_nop_patch_inc: - nop - nop - nop - nop - nop - nop - nop - rts ; Skip CheckListCursorVisible (was JMP $A82D) - nop ; Fill remaining 2 bytes of JMP - nop - -; Scroll hooks - use JMP.L to bank $20 functions - -*=0x02A8B8 - jmp.l scroll_list_down_hook - -*=0x02A8CA - jmp.l scroll_list_up_hook - -; ============================================================================ -; PATCH: UpdateListScrollHDMA ($02A7F1) -; ============================================================================ -; This is the KEY patch for true FF6-style circular buffer. -; Original code builds HDMA table with unbounded scroll values. -; Our replacement wraps scroll values at 96 pixels (6 rows). -; -; Original function is 32 bytes ($02A7F1-$02A810). -; We replace with JMP.L (4 bytes) + NOPs. - -*=0x02A7F1 - jmp.l update_list_scroll_hdma_wrapped - ; Fill remaining bytes with NOPs (32 - 4 = 28 bytes) - .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 - .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 - .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 - .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 - -; ============================================================================ -; PATCH: Cursor scroll limit for single-column mode -; ============================================================================ - -; Original code at $02B517 checks if at end of list: -; lda $ef85 / cmp #$17 / bne @b521 -; The #$17 (23) is for 2-column mode (24 items per column). -; For single-column (48 items), change to #$2B (43 = 48-5). - -*=0x02B519 - .db 0x2B ; CMP #$2B instead of CMP #$17 +.alloc at 0x0296CB { + jsr.w draw_battle_command_window_relocated +} +.alloc at 0x029983 { + jsr.w draw_battle_command_window_relocated -; ============================================================================ -; PATCH: ResetListScrollHDMA ($02AAB8) -; ============================================================================ -; Fill BOTH $7F74 (active table) AND $81F4 (swap table) with our converted -; scroll values. This prevents the menu animation from swapping in bad values. -; -; Original fills only $81F4 with 371-based values. -; We fill both with our 132-based values so animation swap is a no-op. + ; ============================================================================ + ; PATCHES in ascending address order (assembler requires this) + ; ============================================================================ -*=0x02AAB8 - jsr.l reset_list_scroll_hdma_rolling - rts + ; Scroll animation end - wrap $EF65 (4 bytes each) + ; Must use JMP (not JMP.L) - 3 bytes + 1 NOP = 4 bytes +} +.alloc at 0x02A86E { + jmp.w wrap_and_clear_trampoline + nop + + ; Animation loop DECrement path ($02A872-$02A87B) - 10 bytes + ; Original: LDX $EF71 / DEX / STX $EF71 / JMP CheckListCursorVisible + ; NOP the LDX/DEX/STX, replace JMP with RTS (skips CheckListCursorVisible) + ; CheckListCursorVisible can incorrectly hide cursor 2 with our circular buffer scroll values +} +.alloc at 0x02A872 { + _nop_patch_dec: + nop + nop + nop + nop + nop + nop + nop + rts ; Skip CheckListCursorVisible (was JMP $A82D) + nop ; Fill remaining 2 bytes of JMP + nop +} +.alloc at 0x02A8AA { + jmp.w wrap_and_clear_trampoline + nop + + ; Animation loop INCrement path ($02A8AE-$02A8B7) - 10 bytes + ; Original: LDX $EF71 / INX / STX $EF71 / JMP CheckListCursorVisible + ; NOP the LDX/INX/STX, replace JMP with RTS (skips CheckListCursorVisible) + ; CheckListCursorVisible can incorrectly hide cursor 2 with our circular buffer scroll values +} +.alloc at 0x02A8AE { + _nop_patch_inc: + nop + nop + nop + nop + nop + nop + nop + rts ; Skip CheckListCursorVisible (was JMP $A82D) + nop ; Fill remaining 2 bytes of JMP + nop + + ; Scroll hooks - use JMP.L to bank $20 functions +} +.alloc at 0x02A8B8 { + jmp.l scroll_list_down_hook +} +.alloc at 0x02A8CA { + jmp.l scroll_list_up_hook + + ; ============================================================================ + ; PATCH: UpdateListScrollHDMA ($02A7F1) + ; ============================================================================ + ; This is the KEY patch for true FF6-style circular buffer. + ; Original code builds HDMA table with unbounded scroll values. + ; Our replacement wraps scroll values at 96 pixels (6 rows). + ; + ; Original function is 32 bytes ($02A7F1-$02A810). + ; We replace with JMP.L (4 bytes) + NOPs. +} +.alloc at 0x02A7F1 { + jmp.l update_list_scroll_hdma_wrapped + ; Fill remaining bytes with NOPs (32 - 4 = 28 bytes) + .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 + .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 + .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 + .db 0xEA, 0xEA, 0xEA, 0xEA ; nop x 4 + + ; ============================================================================ + ; PATCH: Cursor scroll limit for single-column mode + ; ============================================================================ + + ; Original code at $02B517 checks if at end of list: + ; lda $ef85 / cmp #$17 / bne @b521 + ; The #$17 (23) is for 2-column mode (24 items per column). + ; For single-column (48 items), change to #$2B (43 = 48-5). +} +.alloc at 0x02B519 { + .db 0x2B ; CMP #$2B instead of CMP #$17 + + ; ============================================================================ + ; PATCH: ResetListScrollHDMA ($02AAB8) + ; ============================================================================ + ; Fill BOTH $7F74 (active table) AND $81F4 (swap table) with our converted + ; scroll values. This prevents the menu animation from swapping in bad values. + ; + ; Original fills only $81F4 with 371-based values. + ; We fill both with our 132-based values so animation swap is a no-op. +} +.alloc at 0x02AAB8 { + jsr.l reset_list_scroll_hdma_rolling + rts +} diff --git a/src/battle/items_patches.s b/src/battle/items_patches.s index 8eb7eb7..28a6e8a 100644 --- a/src/battle/items_patches.s +++ b/src/battle/items_patches.s @@ -11,111 +11,117 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; --- multiplier --- ; Original: 02/9E1A: A9 09 LDA #0x09 -*=0x029E1A +.include "config.i" + +; root-scope extern: `.alloc` bodies open their own scope, so an extern +; declared inside one never resolves at the use site. +.extern tfr_equip_window_new + +.alloc at 0x029E1A { lda #ITEM_NAME_RECORD_SIZE ; --- item symbol --- ; Original: 02/9E44: BF 00 80 0F LDA 0x0F8000,X - -*=0x029E44 +} +.alloc at 0x029E44 { lda.l assets_items_dat, x ; --- loop counter --- ; Original: 02/9E58: A9 08 LDA #0x08 - -*=0x029E58 +} +.alloc at 0x029E58 { lda #ITEM_NAME_TEXT_SIZE ; --- item name (+1 skip symbol) --- ; Original: 02/9E5C: BF 01 80 0F LDA 0x0F8001,X - -*=0x029E5C +} +.alloc at 0x029E5C { lda.l assets_items_dat + 1, x ; ===== BTLGFX/MENU: LOCATION 2 ===== ; --- multiplier --- ; Original: 02/9FEF: A9 09 LDA #0x09 - -*=0x029FEF +} +.alloc at 0x029FEF { lda #ITEM_NAME_RECORD_SIZE ; --- item symbol --- ; Original: 02/A00C: BF 00 80 0F LDA 0x0F8000,X - -*=0x02A00C +} +.alloc at 0x02A00C { lda.l assets_items_dat, x ; --- loop counter --- ; Original: 02/A020: A9 08 LDA #0x08 - -*=0x02A020 +} +.alloc at 0x02A020 { lda #ITEM_NAME_TEXT_SIZE ; --- item name (+1 skip symbol) --- ; Original: 02/A024: BF 01 80 0F LDA 0x0F8001,X - -*=0x02A024 +} +.alloc at 0x02A024 { lda.l assets_items_dat + 1, x ; ===== TEXTVAR_03: BATTLE MESSAGE ITEM NAME ===== ; --- multiplier --- ; Original: 02/A594: A9 09 LDA #0x09 - -*=0x02A594 +} +.alloc at 0x02A594 { lda #ITEM_NAME_RECORD_SIZE ; --- loop counter --- ; Original: 02/A5A1: A9 08 LDA #0x08 - -*=0x02A5A1 +} +.alloc at 0x02A5A1 { lda #ITEM_NAME_TEXT_SIZE ; --- item name --- ; Original: 02/A5A5: BF 00 80 0F LDA 0x0F8000,X - -*=0x02A5A5 +} +.alloc at 0x02A5A5 { lda.l assets_items_dat, x ; ===== BTLGFX/BTLGFX: ITEM DISPLAY ===== ; --- multiplier --- ; Original: 02/CB77: A9 09 LDA #0x09 - -*=0x02CB77 +} +.alloc at 0x02CB77 { lda #ITEM_NAME_RECORD_SIZE ; --- item name --- ; Original: 02/CB83: BF 00 80 0F LDA 0x0F8000,X - -*=0x02CB83 +} +.alloc at 0x02CB83 { lda.l assets_items_dat, x ; --- loop counter --- ; Original: 02/CB8C: C0 08 00 CPY #0x0008 - -*=0x02CB8C +} +.alloc at 0x02CB8C { cpy #0x000b ; ===== EQUIPPED ITEMS DISPLAY WIDTH ===== ; --- equipped items width 1 --- ; Original: 02/AB47: A9 09 LDA #0x09 - -*=0x02AB47 +} +.alloc at 0x02AB47 { lda #ITEM_NAME_RECORD_SIZE ; --- equipped items width 2 --- ; Original: 02/B442: A9 09 LDA #0x09 - -*=0x02B442 +} +.alloc at 0x02B442 { lda #ITEM_NAME_RECORD_SIZE ; --- equipped items width 3 --- ; Original: 02/B698: A9 09 LDA #0x09 - -*=0x02B698 +} +.alloc at 0x02B698 { lda #ITEM_NAME_RECORD_SIZE ; ===== INVENTORY BUFFER STRIDE EXPANSION ===== @@ -124,43 +130,56 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; --- DrawInventoryItemText: buffer stride --- ; Original: 02/9FAF: A9 30 LDA #0x30 - -*=0x029FAF +} +.alloc at 0x029FAF { lda #0x3c ; 60 bytes per item instead of 48 ; --- DrawInventoryItemText: line length --- ; Original: 02/9FC4: A9 0C LDA #0x0C - -*=0x029FC4 +} +.alloc at 0x029FC4 { lda #0x0f ; 15 tiles per line instead of 12 - +} ; ===== TfrInventoryList PATCHES ===== ; Skip when rolling buffer enabled - it has its own transfer routine .if INVENTORY_ROLLING_BUFFER == 0 { ; --- right column buffer offset --- ; Original: 02/9923: BD D6 8E LDA 0x8ED6,X - *=0x029923 + .alloc at 0x029923 { lda.w 0x8EE2, x ; 0x8EA6 + 0x3C = 0x8EE2 ; --- loop 1 end (first tilemap row) --- ; Original: 02/992A: C0 1A 00 CPY #0x001A - *=0x02992A + } + + + .alloc at 0x02992A { cpy #0x0020 ; copy 30 bytes (Y: 2 to 32) instead of 24 ; --- right column buffer offset (line 2) --- ; Original: 02/9937: BD D6 8E LDA 0x8ED6,X - *=0x029937 + } + + + .alloc at 0x029937 { lda.w 0x8EE2, x ; --- loop 2 end (second tilemap row) --- ; Original: 02/993E: C0 5A 00 CPY #0x005A - *=0x02993E + } + + + .alloc at 0x02993E { cpy #0x0060 ; copy 30 bytes (Y: 0x42 to 0x60) instead of 24 ; --- X stride after each row --- ; Original: 02/9947: 69 30 00 ADC #0x0030 - *=0x029947 + } + + + .alloc at 0x029947 { adc #0x003c ; advance 60 bytes instead of 48 + } } ; end !INVENTORY_ROLLING_BUFFER @@ -169,54 +188,54 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; --- tile count (using item) --- ; Original: 02/9F2B: A9 0C LDA #0x0C - -*=0x029F2B +.alloc at 0x029F2B { lda #0x0f ; 15 tiles instead of 12 ; --- row 2 attribute offset (using item) --- ; Original: 02/9F47: 99 BF 8E STA 0x8EBF,Y - -*=0x029F47 +} +.alloc at 0x029F47 { sta.w 0x8EC5, y ; 0x8EA7 + 0x1E = 0x8EC5 ; --- Y stride (using item) --- ; Original: 02/9F54: 69 18 00 ADC #0x0018 - -*=0x029F54 +} +.alloc at 0x029F54 { adc #0x001e ; advance 30 bytes instead of 24 ; ===== UpdateEnabledItems PATCHES (throw mode) ===== ; --- tile count (throw) --- ; Original: 02/9F68: A9 0C LDA #0x0C - -*=0x029F68 +} +.alloc at 0x029F68 { lda #0x0f ; 15 tiles instead of 12 ; --- row 2 attribute offset (throw) --- ; Original: 02/9F86: 99 BF 8E STA 0x8EBF,Y - -*=0x029F86 +} +.alloc at 0x029F86 { sta.w 0x8EC5, y ; --- Y stride (throw) --- ; Original: 02/9F93: 69 18 00 ADC #0x0018 - -*=0x029F93 +} +.alloc at 0x029F93 { adc #0x001e ; advance 30 bytes instead of 24 - +} ; ===== INVENTORY WINDOW SIZE ===== ; MenuWindowTbl entry 5 (inventory) at 0x16FE5A + 5*6 = 0x16FE78 ; Format: x, y, width, height ; Original: 0x01, 0x00, 0x1E, 0x33 .if INVENTORY_ROLLING_BUFFER { - *=0x16FE78 + .alloc at 0x16FE78 { .db 0x00, 0x00, 0x20, 0x0F ; x=0, y=0, width=32, height=15 (6 items × 2 + 3 border) + } } else { - *=0x16FE78 + .alloc at 0x16FE78 { .db 0x00, 0x00, 0x20, 0x33 ; x=0 (edge), y=0, width=32, height=51 + } } - ; ===== TILEMAP BASE POINTERS ===== ; Skip when rolling buffer enabled - these addresses are used for hooks/trampoline .if INVENTORY_ROLLING_BUFFER == 0 { @@ -225,24 +244,34 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; --- TfrInventoryList: left column base pointer --- ; Original: 02/98FD: A2 2A C5 LDX #0xC52A - *=0x0298FD + .alloc at 0x0298FD { ldx #0xC526 ; 2 tiles left ; --- NOP out 0xFF border writes that now overwrite content --- ; Original: 02/9910: 91 00 STA ($00),Y (left col Y=0) - *=0x029910 + } + + + .alloc at 0x029910 { nop nop ; Original: 02/9917: 91 00 STA ($00),Y (left col Y=$40) - *=0x029917 + } + + + .alloc at 0x029917 { nop nop ; --- TfrInventoryList: right column base pointer --- ; Original: 02/9902: A2 46 C5 LDX #0xC546 - *=0x029902 + } + + + .alloc at 0x029902 { ldx #0xC544 ; 1 tile left + } } ; end !INVENTORY_ROLLING_BUFFER @@ -251,25 +280,25 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; --- Hand cursor X positions --- ; Original: 16/FC59: 0x0C, 0x7C (left=12px, right=124px) -*=0x16FC59 +.alloc at 0x16FC59 { .db 0x0C, 0x7C ; Keep original positions (left=12px, right=124px) ; --- Arrow sprite positions (X, Y, tile, attr) --- ; Original: 16/FC3C - -*=0x16FC3C +} +.alloc at 0x16FC3C { .db 0xec + 10, 0x90, 0x4f, 0xb1 ; up arrow 1 (X was 0xec) .db 0xec + 10, 0x98, 0x4e, 0xb1 ; up arrow 2 .db 0xec + 10, 0xcc, 0x4e, 0x31 ; down arrow 1 .db 0xec + 10, 0xd4, 0x4f, 0x31 ; down arrow 2 - +} ; ===== 1D CURSOR NAVIGATION (ROLLING BUFFER ONLY) ===== .if INVENTORY_ROLLING_BUFFER { ; Disable left/right button handling for single-column scrolling mode ; Original at $02B541: CMP #$02 / BNE $B55A (left button check) ; Replace with BRA to common exit at $B579 ; BRA opcode=$80, offset=$36 ($B579-$B543=54) - *=0x02B541 + .alloc at 0x02B541 { .db 0x80, 0x36 ; bra $B579 (skip left/right handlers) nop ; Fill remaining bytes nop @@ -280,22 +309,34 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; NOP the second INC/DEC $63 in each up/down handler. ; UP button - scroll case: $02B500-B503 has DEC $63 / DEC $63 - *=0x02B502 + } +} + +.alloc at 0x02B502 { nop ; NOP second DEC $63 nop ; UP button - non-scroll case: $02B508-B50B has DEC $63 / DEC $63 - *=0x02B50A +} + + +.alloc at 0x02B50A { nop ; NOP second DEC $63 nop ; DOWN button - scroll case: $02B530-B533 has INC $63 / INC $63 - *=0x02B532 +} + + +.alloc at 0x02B532 { nop ; NOP second INC $63 nop ; DOWN button - non-scroll case: $02B538-B53B has INC $63 / INC $63 - *=0x02B53A +} + + +.alloc at 0x02B53A { nop ; NOP second INC $63 nop } @@ -305,14 +346,13 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; --- Window 1 left edge: 8 → 0 --- ; Original: 02/8A1D: A9 08 LDA #$08 - -*=0x028A1D +.alloc at 0x028A1D { lda #0x00 ; left edge at pixel 0 ; --- Window 1 right edge: 248 → 255 --- ; Original: 02/8A22: A9 F8 LDA #$F8 - -*=0x028A22 +} +.alloc at 0x028A22 { lda #0xff ; right edge at pixel 255 ; ===== EQUIPPED ITEMS BUFFER EXPANSION ===== @@ -321,32 +361,31 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; --- DrawEquipItemText: tile count --- ; Original: 02/9DD3: A9 0C LDA #$0C - -*=0x029DD3 +} +.alloc at 0x029DD3 { lda #0x0f ; 15 tiles instead of 12 ; --- TfrEquipWindow: replace entire body with JSL to relocated routine --- ; Vanilla body $0297A6..$029824 (126 bytes) hard-coded a side-by-side dual-write ; pattern. New routine in bank $20 walks label/item per hand row-by-row. ; Trampoline overwrites the entry; obsolete in-body patches removed. - .extern tfr_equip_window_new - -*=0x0297A6 +} +.alloc at 0x0297A6 { jsr.l tfr_equip_window_new rts ; --- EquipHandPtrs: item offset within buffer --- ; Original: 02/9D67: 00 30 (item 1 at 0, item 2 at 48) - -*=0x029D67 +} +.alloc at 0x029D67 { .db 0x00, 0x3c ; item 1 at 0, item 2 at 60 ; --- EquipTextBufPtrs: relocate to freed magic buffer space --- ; Original buffer at $BC86 is too small (96 bytes per char) ; New: 120 bytes per char (2 items × 60 bytes) at $9A00 ; Original: 16/FEC1: BC86, BCE6, BD46, BDA6, BE06 (stride $60) - -*=0x16FEC1 +} +.alloc at 0x16FEC1 { .dw 0x9A00 ; char 0 .dw 0x9A00 + 0x78 ; char 1 ($9A78) .dw 0x9A00 + 0x78 * 2 ; char 2 ($9AF0) @@ -359,6 +398,8 @@ every $0F8000 item-data reference to land on `assets_items_dat`. ; MenuWindowTbl entry 6 (equipped items) at 0x16FE5A + 6*6 = 0x16FE7E ; Format: x, y, width, height ; Original: 0x01, 0x00, 0x1E, 0x07 - -*=0x16FE7E +} +.alloc at 0x16FE7E { .db 0x00, 0x00, 0x20, 0x07 ; x=0 (edge), y=0, width=32, height=7 +} + diff --git a/src/battle/magic/patches.s b/src/battle/magic/patches.s index 822a6f6..61325bf 100644 --- a/src/battle/magic/patches.s +++ b/src/battle/magic/patches.s @@ -5,178 +5,178 @@ long-form attack-name copier. .extern draw_magic_list_direct .extern magic_list_ptrs -*=0x029A69 ; do not initalize the magic text buffers - ; 029A69 20 70 A0 JSR $A070 - nop - nop - nop - -*=0x029834 ; Black magic - ldx.w #24 * 4 - -*=0x02982F ; summon - ldx.w #24 * 4 * 2 - -*=0x16fe1c ; patches the transfer size - .dw 0x600 ; 0x400 - -*=0x029839 -_transfer_white_magic: - ldx.w #0x0000 ; white magic - phx - stx 0x06 - lda 0x1822 - sta 0x00 - jsr.l draw_magic_list_direct - lda #0x02 ; spell list - ldy.w #0x0002 - jsr.w 0x9738 ; LoadMenuTfrData - lda #0x01 - sta 0x1825 ; 1 transfer - sta 0x1824 ; enable menu tilemap vram transfer - plx - rts - -*=0x029ead -_draw_magic_list: - lda 0x00 ; character slot - asl - tax - rep #0x20 - lda.l magic_list_ptrs, x ; pointers to spell lists - clc - adc 0x06 ; add magic type offset - sta 0x00 - tdc - sep #0x20 - rts - -draw_letter_far: -"""Far-callable wrapper around original draw_letter ($02A497).""" - pha - tdc - sta.l 0x7FFFFF - pla - jsr.w 0xa497 ; Original draw_letter - rtl - -; attack name window - -*=0x02cbcc ; patches for display attack name - lda.b #battle_magic_length - -*=0x02cbdd - lda.l assets_magic_dat, x - -*=0x02cbe6 - cpy.w #battle_magic_length + 1 - -*=0x02cba2 - sec - sbc #0x48 - - rep #0x20 - and.w #0x00ff - asl - tax - ; force the longest one. - ; ldx.w #51* 2 - lda.l assets_attack_names_ptr, x - tax - sep #0x20 - - tdc - tay - -loop: -"""Inner loop of the attack-name copier: stream attack-name bytes into the format buffer.""" - lda.l assets_attack_names_dat, x - sta 0x74fd, y - beq exit - iny - inx - bra loop - -exit: -"""Tail of `loop`: jump to original attack-name window display.""" - jmp.w 0xbca2 ; display monster? attack name window - -; attack window position - -*=0x029369 - ldx.w #0x0009 - -; attack window size - -*=0x02936f - ldx.w #0x040e - -*=0x029382 - ldx.w #0xdb50 - 4 - 2 - 16 - - -; cursor and scrolling - -*=0x16fe1c ; patches the spell menu vram transfer - ;destination_buffer - .dw 0x600 ; 0x400 - -*=0x02B72B ; number of lines to scroll ? - cmp #11 - -*=0x02B781 ; items per line for cursor dpad right - cmp #1 - - -*=0x02B712 ; dpad up - nop - nop - -*=0x02B71C - nop - nop - -*=0x02B751 ; dpad down - ; INC D,$63 - nop - nop - -*=0x02B742 - nop - nop - -*=0x16FC56 ; magic list cursor x position - .db 8 - 8 - .db 0x3C + 8 * 3 + 4 - 8 - -*=0x02B764 ; up and down should only inc /dec once ? - inc 0x5F - ;inc 0x5F - nop - nop - inc 0x63 - ;inc 0x63 - nop - nop - -*=0x02B785 - dec 0x5F - ;dec 0x5F - nop - nop - dec 0x63 - ;dec 0x63 - nop - nop - -*=0x02A567 ; display magic name in battle messages. - lda #9 - -*=0x02A573 - lda.l assets_magic_dat, x - -*=0x02A57E - lda.l assets_magic_dat + 1, x - -*=0x02A57A - lda #9 - 1 +.alloc at 0x029A69 { + ; 029A69 20 70 A0 JSR $A070 + nop + nop + nop +} +.alloc at 0x029834 { + ldx.w #24 * 4 +} +.alloc at 0x02982F { + ldx.w #24 * 4 * 2 +} +.alloc at 0x16fe1c { + .dw 0x600 ; 0x400 +} +.alloc at 0x029839 { + _transfer_white_magic: + ldx.w #0x0000 ; white magic + phx + stx 0x06 + lda 0x1822 + sta 0x00 + jsr.l draw_magic_list_direct + lda #0x02 ; spell list + ldy.w #0x0002 + jsr.w 0x9738 ; LoadMenuTfrData + lda #0x01 + sta 0x1825 ; 1 transfer + sta 0x1824 ; enable menu tilemap vram transfer + plx + rts +} +.alloc at 0x029ead { + _draw_magic_list: + lda 0x00 ; character slot + asl + tax + rep #0x20 + lda.l magic_list_ptrs, x ; pointers to spell lists + clc + adc 0x06 ; add magic type offset + sta 0x00 + tdc + sep #0x20 + rts + + draw_letter_far: + """Far-callable wrapper around original draw_letter ($02A497).""" + pha + tdc + sta.l 0x7FFFFF + pla + jsr.w 0xa497 ; Original draw_letter + rtl + + ; attack name window +} +.alloc at 0x02cbcc { + lda.b #battle_magic_length +} +.alloc at 0x02cbdd { + lda.l assets_magic_dat, x +} +.alloc at 0x02cbe6 { + cpy.w #battle_magic_length + 1 +} +.alloc at 0x02cba2 { + sec + sbc #0x48 + + rep #0x20 + and.w #0x00ff + asl + tax + ; force the longest one. + ; ldx.w #51* 2 + lda.l assets_attack_names_ptr, x + tax + sep #0x20 + + tdc + tay + + loop: + """Inner loop of the attack-name copier: stream attack-name bytes into the format buffer.""" + lda.l assets_attack_names_dat, x + sta 0x74fd, y + beq exit + iny + inx + bra loop + + exit: + """Tail of `loop`: jump to original attack-name window display.""" + jmp.w 0xbca2 ; display monster? attack name window + + ; attack window position +} +.alloc at 0x029369 { + ldx.w #0x0009 + + ; attack window size +} +.alloc at 0x02936f { + ldx.w #0x040e +} +.alloc at 0x029382 { + ldx.w #0xdb50 - 4 - 2 - 16 + + + ; cursor and scrolling +} +.alloc at 0x16fe1c { + ;destination_buffer + .dw 0x600 ; 0x400 +} +.alloc at 0x02B72B { + cmp #11 +} +.alloc at 0x02B781 { + cmp #1 +} +.alloc at 0x02B712 { + nop + nop +} +.alloc at 0x02B71C { + nop + nop +} +.alloc at 0x02B751 { + ; INC D,$63 + nop + nop +} +.alloc at 0x02B742 { + nop + nop +} +.alloc at 0x16FC56 { + .db 8 - 8 + .db 0x3C + 8 * 3 + 4 - 8 +} +.alloc at 0x02B764 { + inc 0x5F + ;inc 0x5F + nop + nop + inc 0x63 + ;inc 0x63 + nop + nop +} +.alloc at 0x02B785 { + dec 0x5F + ;dec 0x5F + nop + nop + dec 0x63 + ;dec 0x63 + nop + nop +} +.alloc at 0x02A567 { + lda #9 +} +.alloc at 0x02A573 { + lda.l assets_magic_dat, x +} +.alloc at 0x02A57E { + lda.l assets_magic_dat + 1, x +} +.alloc at 0x02A57A { + lda #9 - 1 +} diff --git a/src/battle/magic_patches.s b/src/battle/magic_patches.s index 1d9ce49..f719f10 100644 --- a/src/battle/magic_patches.s +++ b/src/battle/magic_patches.s @@ -4,249 +4,252 @@ In-place patches for the battle magic-list code (slot stride / spell-id width ch """ battle_magic_length = 8 -*=0x02984D - ; where originally it was 3. clear related - - ldx.w #0x0018 - -; used for the copy for the dakuten offset - -*=0x02A094 - ; _ _ _ _ _ _ - ; A B B B B B - lda.b #battle_magic_length - -*=0x02A0E2 - lda.b #battle_magic_length - -*=0x02a113 - lda.b #battle_magic_length - 1 - -*=0x02A0FF - lda.l assets_magic_dat, x - -*=0x02A117 - lda.l assets_magic_dat + 1, x - -*=0x02A09E - lda.b #battle_magic_length * 4 - -*=0x02986C - ldx #0xC52A - stx 0x00 - ldx.w #0xC52A + 2 + battle_magic_length * 2 - stx 0x02 - ldx #0xC546 - stx 0x04 - -;original - magic_data_base = 0x909A + 0x60 - -;magic_data_base = 0x703720 - -*=0x029893 - lda.w magic_data_base, x - sta (0x00), y - lda.w magic_data_base + battle_magic_length * 4, x - sta (0x02), y - nop - nop - nop - ;LDA 0x97D6,X - nop - nop - ;STA (0x4),Y - iny - inx - cpy.w #battle_magic_length * 2 - - -*=0x0298AC - lda.w magic_data_base, x - sta (0x00), y - lda.w magic_data_base + battle_magic_length * 4, x - sta (0x02), y - ;LDA $97D6,X - nop - nop - nop - ;STA (D,4),Y - nop - nop - iny - inx - cpy.w #0x40 + battle_magic_length * 2 - -*=0x0298C6 - adc.w #battle_magic_length * 2 * 2 - -*=0x02988D - lda.b #12 - - -; 0x240 * 3 = 0x6C0 +.alloc at 0x02984D { + ; where originally it was 3. clear related + + ldx.w #0x0018 + + ; used for the copy for the dakuten offset +} +.alloc at 0x02A094 { + ; _ _ _ _ _ _ + ; A B B B B B + lda.b #battle_magic_length +} +.alloc at 0x02A0E2 { + lda.b #battle_magic_length +} +.alloc at 0x02a113 { + lda.b #battle_magic_length - 1 +} +.alloc at 0x02A0FF { + lda.l assets_magic_dat, x +} +.alloc at 0x02A117 { + lda.l assets_magic_dat + 1, x +} +.alloc at 0x02A09E { + lda.b #battle_magic_length * 4 +} +.alloc at 0x02986C { + ldx #0xC52A + stx 0x00 + ldx.w #0xC52A + 2 + battle_magic_length * 2 + stx 0x02 + ldx #0xC546 + stx 0x04 + + ;original + magic_data_base = 0x909A + 0x60 + + ;magic_data_base = 0x703720 +} +.alloc at 0x029893 { + lda.w magic_data_base, x + sta (0x00), y + lda.w magic_data_base + battle_magic_length * 4, x + sta (0x02), y + nop + nop + nop + ;LDA 0x97D6,X + nop + nop + ;STA (0x4),Y + iny + inx + cpy.w #battle_magic_length * 2 +} +.alloc at 0x0298AC { + lda.w magic_data_base, x + sta (0x00), y + lda.w magic_data_base + battle_magic_length * 4, x + sta (0x02), y + ;LDA $97D6,X + nop + nop + nop + ;STA (D,4),Y + nop + nop + iny + inx + cpy.w #0x40 + battle_magic_length * 2 +} +.alloc at 0x0298C6 { + adc.w #battle_magic_length * 2 * 2 +} +.alloc at 0x02988D { + lda.b #12 + + + ; 0x240 * 3 = 0x6C0 +} { size = 0x120 ;fsize = size * 4 ;fsize = 0x6C0 ; original fsize = 0x900 - *=0x029825 - .dw 0x0000 - .dw fsize - .dw fsize * 2 - .dw fsize * 3 - .dw fsize * 4 - - *=0x16FF2F - .dw magic_data_base & 0xFFFF - .dw ( magic_data_base + fsize ) & 0xFFFF - .dw ( magic_data_base + fsize * 2 ) & 0xFFFF - .dw ( magic_data_base + fsize * 3 ) & 0xFFFF - .dw ( magic_data_base + fsize * 4 ) & 0xFFFF - -; Black magic - *=0x029834 - ldx.w #0x300 ; 0x240 - -; summon - *=0x02982F - ldx.w #0x600 ; 0x480 - -; patches the transfer size - *=0x16fe1c - .dw 0x600 ; 0x400 +.alloc at 0x029825 { + .dw 0x0000 + .dw fsize + .dw fsize * 2 + .dw fsize * 3 + .dw fsize * 4 +} +.alloc at 0x16FF2F { + .dw magic_data_base & 0xFFFF + .dw ( magic_data_base + fsize ) & 0xFFFF + .dw ( magic_data_base + fsize * 2 ) & 0xFFFF + .dw ( magic_data_base + fsize * 3 ) & 0xFFFF + .dw ( magic_data_base + fsize * 4 ) & 0xFFFF + + ; Black magic +} +.alloc at 0x029834 { + ldx.w #0x300 ; 0x240 + + ; summon +} +.alloc at 0x02982F { + ldx.w #0x600 ; 0x480 + + ; patches the transfer size +} +.alloc at 0x16fe1c { + .dw 0x600 ; 0x400 +} } ; number of lines to scroll ? -*=0x02B72B - cmp #11 +.alloc at 0x02B72B { + cmp #11 + + ; items per line for cursor dpad right +} +.alloc at 0x02B781 { + cmp #1 + -; items per line for cursor dpad right + ; dpad up +} +.alloc at 0x02B712 { + nop + nop +} +.alloc at 0x02B71C { + nop + nop -*=0x02B781 - cmp #1 - - -; dpad up - -*=0x02B712 - nop - nop - -*=0x02B71C - nop - nop - -; dpad down - -*=0x02B751 - ; INC D,$63 - nop - nop - -*=0x02B742 - nop - nop - - -*=0x16FC56 - .db 2 - .db 0x3C + 14 - .db 0x74 - -*=0x16FC5B - .db 0x9C - .db 0xA8 - .db 0xB4 - - -*=0x02B764 - inc 0x5F - ;inc 0x5F - nop - nop - inc 0x63 - ;inc 0x63 - nop - nop - -*=0x02B785 - dec 0x5F - ;dec 0x5F - nop - nop - dec 0x63 - ;dec 0x63 - nop - nop - -*=0x02A567 - lda.b #battle_magic_length - -*=0x02A573 - lda.l assets_magic_dat, x - -*=0x02A57E - lda.l assets_magic_dat + 1, x - -; sets the palette for disabled spells (Not enough MP) - -*=0x029EC8 - lda #0x48 - sta 7 - -_loc_29ecc: - ldy.w #1 - lda (0) - bmi _loc_29edb - lda #0 - sta 6 - lda #0 - bra _loc_29ee3 - -_loc_29edb: - lda #4 - sta 6 - bra _loc_29ee3 - -_loc_29ee1: - lda 6 - -_loc_29ee3: - - sta (2), y - sta (4), y - iny - iny - cpy.w #battle_magic_length * 2 + 1 ; len * 2 + 1 : 0x0D - bne _loc_29ee1 - rep #0x20 ; ' ' - ;.A16 - lda 2 - clc - adc.w #battle_magic_length * 4 ; next word len * 4 : 0x18 - sta 2 - clc - adc.w #battle_magic_length * 2 ; len * 2 0x0c - sta 4 - lda 0 - clc - adc.w #4 ; next magic struct - sta 0 - tdc - sep #0x20 - ;.A8 - dec 7 ; loop on all magic - bne _loc_29ecc - rts - -; patches for display attack name - -*=0x02cbcc - lda.b #battle_magic_length - -*=0x02cbdd - lda.l assets_magic_dat, x - -*=0x02cbe6 - cpy.w #battle_magic_length + 1 + ; dpad down +} +.alloc at 0x02B751 { + ; INC D,$63 + nop + nop +} +.alloc at 0x02B742 { + nop + nop +} +.alloc at 0x16FC56 { + .db 2 + .db 0x3C + 14 + .db 0x74 +} +.alloc at 0x16FC5B { + .db 0x9C + .db 0xA8 + .db 0xB4 +} +.alloc at 0x02B764 { + inc 0x5F + ;inc 0x5F + nop + nop + inc 0x63 + ;inc 0x63 + nop + nop +} +.alloc at 0x02B785 { + dec 0x5F + ;dec 0x5F + nop + nop + dec 0x63 + ;dec 0x63 + nop + nop +} +.alloc at 0x02A567 { + lda.b #battle_magic_length +} +.alloc at 0x02A573 { + lda.l assets_magic_dat, x +} +.alloc at 0x02A57E { + lda.l assets_magic_dat + 1, x + + ; sets the palette for disabled spells (Not enough MP) +} +.alloc at 0x029EC8 { + lda #0x48 + sta 7 + + _loc_29ecc: + ldy.w #1 + lda (0) + bmi _loc_29edb + lda #0 + sta 6 + lda #0 + bra _loc_29ee3 + + _loc_29edb: + lda #4 + sta 6 + bra _loc_29ee3 + + _loc_29ee1: + lda 6 + + _loc_29ee3: + + sta (2), y + sta (4), y + iny + iny + cpy.w #battle_magic_length * 2 + 1 ; len * 2 + 1 : 0x0D + bne _loc_29ee1 + rep #0x20 ; ' ' + ;.A16 + lda 2 + clc + adc.w #battle_magic_length * 4 ; next word len * 4 : 0x18 + sta 2 + clc + adc.w #battle_magic_length * 2 ; len * 2 0x0c + sta 4 + lda 0 + clc + adc.w #4 ; next magic struct + sta 0 + tdc + sep #0x20 + ;.A8 + dec 7 ; loop on all magic + bne _loc_29ecc + rts + + ; patches for display attack name +} +.alloc at 0x02cbcc { + lda.b #battle_magic_length +} +.alloc at 0x02cbdd { + lda.l assets_magic_dat, x +} +.alloc at 0x02cbe6 { + cpy.w #battle_magic_length + 1 +} diff --git a/src/battle/magic_reloc.s b/src/battle/magic_reloc.s index 5d2f6dd..f28cc36 100644 --- a/src/battle/magic_reloc.s +++ b/src/battle/magic_reloc.s @@ -1,4 +1,6 @@ """ +.include "../bank20.i" + Relocated battle spell-list renderer (`draw_magic_list_direct`) and per-magic-type pointer table (`magic_list_ptrs`). """ @@ -10,165 +12,170 @@ destination_buffer = 0xc530 - 4 left_column_base = destination_buffer - 4 right_column_base = destination_buffer + 18 - 2 -draw_magic_list_direct: -"""Relocated battle spell-list renderer: walks the per-character spell-list pointer table.""" -{ - spell_id = 0x03 - spell_enabled_flag = 0x02 - current_row_offset = 0x08 - spell_counter = 0x06 - current_spell_index = 0x0a - lda 0x00 -; character slot - asl - tax - rep #0x20 - lda.l magic_list_ptrs, x -; pointers to spell lists - clc - adc 0x06 -; add magic type offset - sta 0x00 - stz.b current_row_offset -; start at row 0 (0x0000) - sep #0x20 - lda #0x18 -; 24 spells total (12 rows x 2 columns) - sta.b spell_counter -; spell counter - lda #0x00 -; current spell index (0-23) - sta.b current_spell_index - -spell_loop: - phx - pha - rep #0x20 - lda 0x00 - tax - sep #0x20 - lda.w 0x0000, x - and #0x80 - sta.b spell_enabled_flag - lda.w 0x0001, x - sta.b spell_id - pla - plx -; Fast column selection using precomputed addresses - lda.b current_spell_index -; spell index - and #0x01 -; check if odd - beq left_column -; Right column - rep #0x20 - lda.w #right_column_base - clc - adc.b current_row_offset -; add current row offset - sta 0x32 - bra set_second_addr - -left_column: -; Left column - rep #0x20 - lda.w #left_column_base - clc - adc.b current_row_offset -; add current row offset - sta 0x32 - -set_second_addr: - adc.w #0x0040 - sta 0x34 - sep #0x20 - ldy.w #0x00 -; Y offset - lda #0x00 -; tile flags - sta 0x36 - lda.b spell_enabled_flag - beq enabled_spell - lda #0x04 - sta 0x36 - -enabled_spell: -; Get spell ID and load spell name - lda.b spell_id -; read spell ID - rep #0x20 - and.w #0x007f -; clear disabled bit - sep #0x20 - sta.l 0x004202 - lda.b #battle_magic_length - sta.l 0x004203 - nop - nop - nop - nop - rep #0x20 - lda.l 0x004216 -; asl ; spell ID * 8 (8 bytes per name) -; asl -; asl - tax - sep #0x20 - lda.b #battle_magic_length -; 8 characters to write - sta 0x02 -; draw a space before rendering the magic name to clear left overs from the items. - lda #0xff - jsr.l draw_letter_far - - -letter_loop: - lda.l assets_magic_dat, x - jsr.l draw_letter_far - inx - dec 0x02 - bne letter_loop - lda #0x0A - sta 0x02 - -clear_loop: - lda.b #0xff - jsr.l draw_letter_far - dec 0x02 - bne clear_loop - -next_spell: -; Advance pointer by 4 bytes like original - rep #0x20 - lda 0x00 - clc - adc.w #0x0004 - sta 0x00 -; Increment row offset after right column (odd spell index) - lda.b current_spell_index -; current spell index - and.w #0x0001 -; check if odd (right column) - beq same_row -; Just finished right column - move to next row - lda.b current_row_offset - clc - adc.w #0x0080 - sta.b current_row_offset - -same_row: - sep #0x20 - inc 0x0a -; next spell index - dec.b spell_counter -; decrement spell counter - beq exit - jmp.w spell_loop - -exit: - rtl -} -magic_list_ptrs: -"""Per-magic-type spell-list base pointers (white, black, summon, ninja, kokan).""" - .dw 0x2c7a, 0x2d9a, 0x2eba, 0x2fda, 0x30fa +.include "../bank20.i" + +.alloc battle_magic_reloc_block in bank20_reloc { + draw_magic_list_direct: + """Relocated battle spell-list renderer: walks the per-character spell-list pointer table.""" + { + spell_id = 0x03 + spell_enabled_flag = 0x02 + current_row_offset = 0x08 + spell_counter = 0x06 + current_spell_index = 0x0a + lda 0x00 + ; character slot + asl + tax + rep #0x20 + lda.l magic_list_ptrs, x + ; pointers to spell lists + clc + adc 0x06 + ; add magic type offset + sta 0x00 + stz.b current_row_offset + ; start at row 0 (0x0000) + sep #0x20 + lda #0x18 + ; 24 spells total (12 rows x 2 columns) + sta.b spell_counter + ; spell counter + lda #0x00 + ; current spell index (0-23) + sta.b current_spell_index + + spell_loop: + phx + pha + rep #0x20 + lda 0x00 + tax + sep #0x20 + lda.w 0x0000, x + and #0x80 + sta.b spell_enabled_flag + lda.w 0x0001, x + sta.b spell_id + pla + plx + ; Fast column selection using precomputed addresses + lda.b current_spell_index + ; spell index + and #0x01 + ; check if odd + beq left_column + ; Right column + rep #0x20 + lda.w #right_column_base + clc + adc.b current_row_offset + ; add current row offset + sta 0x32 + bra set_second_addr + + left_column: + ; Left column + rep #0x20 + lda.w #left_column_base + clc + adc.b current_row_offset + ; add current row offset + sta 0x32 + + set_second_addr: + adc.w #0x0040 + sta 0x34 + sep #0x20 + ldy.w #0x00 + ; Y offset + lda #0x00 + ; tile flags + sta 0x36 + lda.b spell_enabled_flag + beq enabled_spell + lda #0x04 + sta 0x36 + + enabled_spell: + ; Get spell ID and load spell name + lda.b spell_id + ; read spell ID + rep #0x20 + and.w #0x007f + ; clear disabled bit + sep #0x20 + sta.l 0x004202 + lda.b #battle_magic_length + sta.l 0x004203 + nop + nop + nop + nop + rep #0x20 + lda.l 0x004216 + ; asl ; spell ID * 8 (8 bytes per name) + ; asl + ; asl + tax + sep #0x20 + lda.b #battle_magic_length + ; 8 characters to write + sta 0x02 + ; draw a space before rendering the magic name to clear left overs from the items. + lda #0xff + jsr.l draw_letter_far + + + letter_loop: + lda.l assets_magic_dat, x + jsr.l draw_letter_far + inx + dec 0x02 + bne letter_loop + lda #0x0A + sta 0x02 + + clear_loop: + lda.b #0xff + jsr.l draw_letter_far + dec 0x02 + bne clear_loop + + next_spell: + ; Advance pointer by 4 bytes like original + rep #0x20 + lda 0x00 + clc + adc.w #0x0004 + sta 0x00 + ; Increment row offset after right column (odd spell index) + lda.b current_spell_index + ; current spell index + and.w #0x0001 + ; check if odd (right column) + beq same_row + ; Just finished right column - move to next row + lda.b current_row_offset + clc + adc.w #0x0080 + sta.b current_row_offset + + same_row: + sep #0x20 + inc 0x0a + ; next spell index + dec.b spell_counter + ; decrement spell counter + beq exit + jmp.w spell_loop + + exit: + rtl + } + + magic_list_ptrs: + """Per-magic-type spell-list base pointers (white, black, summon, ninja, kokan).""" + .dw 0x2c7a, 0x2d9a, 0x2eba, 0x2fda, 0x30fa +} diff --git a/src/battle/math_patches.s b/src/battle/math_patches.s index a28a037..1e6e9e3 100644 --- a/src/battle/math_patches.s +++ b/src/battle/math_patches.s @@ -9,19 +9,20 @@ trampoline at $83B9 jumping into `_hw_mult16`. ; Uses same pattern as existing MultHW at $85D2 (26 bytes, fits in 28) ; =========================================================================== -*=0x028560 - phx ; Preserve X (original does this) - lda 0x26 - sta.l 0x004202 ; Multiplicand - lda 0x28 - sta.l 0x004203 ; Multiplier (triggers multiply) - ; Wait using bank switch (same as MultHW) - phb ; 3 cycles - lda #0x00 ; 2 cycles - pha ; 3 cycles - plb ; 4 cycles (DB=0 now, 12 cycles waited) - ldx 0x4216 ; 16-bit X reads RDMPYL/H (X is 16-bit) - stx 0x2a ; Store 16-bit result to $2a/$2b - plb ; Restore data bank - plx - rts +.alloc at 0x028560 { + phx ; Preserve X (original does this) + lda 0x26 + sta.l 0x004202 ; Multiplicand + lda 0x28 + sta.l 0x004203 ; Multiplier (triggers multiply) + ; Wait using bank switch (same as MultHW) + phb ; 3 cycles + lda #0x00 ; 2 cycles + pha ; 3 cycles + plb ; 4 cycles (DB=0 now, 12 cycles waited) + ldx 0x4216 ; 16-bit X reads RDMPYL/H (X is 16-bit) + stx 0x2a ; Store 16-bit result to $2a/$2b + plb ; Restore data bank + plx + rts +} diff --git a/src/battle/math_reloc.s b/src/battle/math_reloc.s index ca15d01..dbf3fc6 100644 --- a/src/battle/math_reloc.s +++ b/src/battle/math_reloc.s @@ -1,86 +1,93 @@ """ +.include "../bank20.i" + Relocated 16x16 -> 32 multiply (`_hw_mult16`) using the SNES hardware multiplier, plus shorta / shorta0 size-toggle helpers. Called from JMP trampoline at $83B9 (in `math_patches.s`). Input: $393D (16-bit) * $393F (16-bit). Output: $3941 (low 16-bit), $3943 (high 16-bit). """ -.macro shorta() { - """Switch A to 8-bit (`SEP #$20`).""" - sep #0x20 -} -.macro shorta0() { - """Clear A and switch to 8-bit (TDC then `SEP #$20`).""" - tdc - shorta() -} +.include "../bank20.i" -_hw_mult16: - php - rep #0x30 ; 16-bit A, X, Y - stz 0x3941 ; Clear result low - stz 0x3943 ; Clear result high - sep #0x20 ; 8-bit A for hardware multiply registers +.alloc battle_math_reloc_block in bank20_reloc { + .macro shorta() { + """Switch A to 8-bit (`SEP #$20`).""" + sep #0x20 + } -; The SNES hardware multiplier only does 8x8->16 -; For 16x16->32, we need: (AH*256 + AL) * (BH*256 + BL) -; = AH*BH*65536 + AH*BL*256 + AL*BH*256 + AL*BL -; We compute 4 partial products and add them with proper alignment. + .macro shorta0() { + """Clear A and switch to 8-bit (TDC then `SEP #$20`).""" + tdc + shorta() + } -; 1. Low(A) * Low(B) -> result bytes 0-1 - lda 0x393d ; A low byte - sta 0x4202 ; WRMPYA - multiplicand - lda 0x393f ; B low byte - sta 0x4203 ; WRMPYB - multiplier (triggers multiply) - nop - nop ; Wait 8 cycles for result - rep #0x20 - lda 0x4216 ; RDMPYL/H - 16-bit result - sta 0x3941 ; Store in result bytes 0-1 + _hw_mult16: + php + rep #0x30 ; 16-bit A, X, Y + stz 0x3941 ; Clear result low + stz 0x3943 ; Clear result high + sep #0x20 ; 8-bit A for hardware multiply registers -; 2. Low(A) * High(B) -> add to result bytes 1-2 - sep #0x20 - lda 0x393d ; A low byte - sta 0x4202 - lda 0x3940 ; B high byte ($393F + 1) - sta 0x4203 - nop - nop - rep #0x20 - lda 0x4216 - clc - adc 0x3942 ; Add to result bytes 1-2 (with carry to byte 3) - sta 0x3942 + ; The SNES hardware multiplier only does 8x8->16 + ; For 16x16->32, we need: (AH*256 + AL) * (BH*256 + BL) + ; = AH*BH*65536 + AH*BL*256 + AL*BH*256 + AL*BL + ; We compute 4 partial products and add them with proper alignment. -; 3. High(A) * Low(B) -> add to result bytes 1-2 - sep #0x20 - lda 0x393e ; A high byte ($393D + 1) - sta 0x4202 - lda 0x393f ; B low byte - sta 0x4203 - nop - nop - rep #0x20 - lda 0x4216 - clc - adc 0x3942 ; Add to result bytes 1-2 - sta 0x3942 + ; 1. Low(A) * Low(B) -> result bytes 0-1 + lda 0x393d ; A low byte + sta 0x4202 ; WRMPYA - multiplicand + lda 0x393f ; B low byte + sta 0x4203 ; WRMPYB - multiplier (triggers multiply) + nop + nop ; Wait 8 cycles for result + rep #0x20 + lda 0x4216 ; RDMPYL/H - 16-bit result + sta 0x3941 ; Store in result bytes 0-1 -; 4. High(A) * High(B) -> add to result bytes 2-3 - sep #0x20 - lda 0x393e ; A high byte - sta 0x4202 - lda 0x3940 ; B high byte - sta 0x4203 - nop - nop - rep #0x20 - lda 0x4216 - clc - adc 0x3943 ; Add to result bytes 2-3 - sta 0x3943 + ; 2. Low(A) * High(B) -> add to result bytes 1-2 + sep #0x20 + lda 0x393d ; A low byte + sta 0x4202 + lda 0x3940 ; B high byte ($393F + 1) + sta 0x4203 + nop + nop + rep #0x20 + lda 0x4216 + clc + adc 0x3942 ; Add to result bytes 1-2 (with carry to byte 3) + sta 0x3942 - shorta0() ; TDC + SEP #$20 - clear high byte of A, 8-bit mode - plp - rtl + ; 3. High(A) * Low(B) -> add to result bytes 1-2 + sep #0x20 + lda 0x393e ; A high byte ($393D + 1) + sta 0x4202 + lda 0x393f ; B low byte + sta 0x4203 + nop + nop + rep #0x20 + lda 0x4216 + clc + adc 0x3942 ; Add to result bytes 1-2 + sta 0x3942 + + ; 4. High(A) * High(B) -> add to result bytes 2-3 + sep #0x20 + lda 0x393e ; A high byte + sta 0x4202 + lda 0x3940 ; B high byte + sta 0x4203 + nop + nop + rep #0x20 + lda 0x4216 + clc + adc 0x3943 ; Add to result bytes 2-3 + sta 0x3943 + + shorta0() ; TDC + SEP #$20 - clear high byte of A, 8-bit mode + plp + rtl +} diff --git a/src/battle/message.s b/src/battle/message.s index 3eb6003..1e216ba 100644 --- a/src/battle/message.s +++ b/src/battle/message.s @@ -2,6 +2,9 @@ Battle-message tile renderer + VWF parser scopes (`battle_render` low-level blitter, `messages_vwf` high-level dialog-stream consumer). """ +.include "config.i" +.include "src/battle/inventory_budget.i" +.include "src/vwf_state.i" .if 0 { .scope _vwf_tile_ring { ; Ring buffer for VWF tile allocation @@ -148,31 +151,22 @@ _not_found: 0x80 -> 0xB0 char names 0xB0 -> 0xF0 commands ? this one is untested. """ - buffer_ptr = 0x703000 + buffer_ptr = VWF_CHR_BUFFER buffer_size = 8 * ( 128 + 32 ) * 2 region_size = 48 - pending_transfer_mask = 0x703c00 -; --- Per-region dirty bits (normal sense: 1 = dirty, 0 = clean) --- -; Sits next to the DMA queue byte; writers SET bits on state change. - region_dirty_bits = 0x703c01 - REGION_DIRTY_MESSAGES = 0x01 - REGION_DIRTY_MONSTERS = 0x02 - REGION_DIRTY_NAMES = 0x04 - REGION_DIRTY_COMMANDS = 0x08 -; Transient marker: $FF if `init_*_with_gate` short-circuited -; because the region was clean; $00 if it ran the full init. -; Used by deinit_with_gate to decide whether to signal DMA, and -; by the gated trampoline to decide whether to skip DrawText. - render_skipped = 0x703c02 -; Per-region tilemap-DMA pending bitmask. Set by `init_*_gated` -; on the render path ; consumed by `dma_transfer` in NMI to fire -; a per-region tilemap DMA (WRAM tilemap -> BG VRAM). Decouples -; tilemap upload from the vanilla `TfrCmdWindow` / `TfrMainMenu` -; periodic queue so the tile-data + tilemap transfers stay in -; sync on the same NMI as the render. - tilemap_pending_mask = 0x703c03 - TILEMAP_PENDING_COMMANDS = 0x01 - TILEMAP_PENDING_MAIN = 0x02 +; Gate state moved past the inventory tile slice ($703C00..$703EF0) +; so the rolling pre-render does not stomp these bytes. + pending_transfer_mask = 0x703f00 +; Per-slot CHR dirty bitmask for the inventory rolling buffer. Bit N +; set when slot N's CHR slice at $703000 + (slot_base + N*10)*16 has +; been touched and needs a VRAM flush. NMI `dma_transfer` consumes +; one or more bits per frame and DMAs only the dirty slot's 160-byte +; slice instead of the full 4KB CHR region, fits in vblank without +; forced blank. + dma_dirty_slots = 0x703f10 + ; Dirty-bit / render-skipped / tilemap-pending interface: shared with + ; redraw_gates.s and the writer-site shims via a compile-time include. + .include "render_defs.i" bits_left_on_tile = 0xA9 tilemap_offset = bits_left_on_tile + 2 temp = bits_left_on_tile + 4 @@ -191,6 +185,37 @@ init_names: init_commands_list: """Initialize the renderer targeting the commands list region.""" lda.b #region_size * 3 + bra _init +init_inventory_region: +""" +Reset the allocator to the inventory tile_id base (0xC0) once per +rolling render pass. Subsequent per-item DrawText calls let the +allocator increment naturally, so each item owns its own tile range +(item N uses 0xC0 + N * width_in_tiles). Skips clear_buffer for the +reasons noted on the other inventory entry - tile_id 0xC0+ would +overrun the shared buffer into the state words at $703C00+. +""" + + + lda.b #region_size * 4 + sta.l pending_transfer_mask + jsr.w render_allocator.init_with_tile_id + .if ENABLE_KERNING_MENU { + stz.b prev_char + } + lda.b #0x08 + sta.b bits_left_on_tile + stz.b temp + stz.b counter + rts +render_allocator_init_with_tile_id_thunk: +""" +In-scope thunk so messages_vwf can reach the allocator via jsr.w +across scope boundaries. A on entry = tile_id base. +""" + + + jmp.w render_allocator.init_with_tile_id _init: sta.l pending_transfer_mask jsr.w render_allocator.init_with_tile_id @@ -692,7 +717,7 @@ found_pair_cleanup: lda.w assets_menu_font_dat, y ; Load adjustment value (8-bit) - matches original and.w #0x00ff ; Ensure high byte is clear -; Clean up stack — use ply so A (adjustment) is preserved. +; Clean up stack - use ply so A (adjustment) is preserved. ply ; Remove target_char ply ; Remove low bound ply ; Remove high bound @@ -752,15 +777,28 @@ battle_msg_kerning_binary_ext: rtl } tilemap_write_no_inc: - lda.l render_allocator.allocated_tile_id +""" +Write a 10-bit tile_id reference at tilemap_offset. + +Low 8 bits of tile_id go in the entry's low byte ; bits 8-9 ride +the entry's high byte alongside the palette / flip / priority bits. +With the 16-bit allocator, the entry's high byte is just +(palette | allocator_high_byte) - no hardcoded +0x100 shift, so +inventory tile_ids past 0xFF reach the upper half of BG3 CHR +cleanly instead of wrapping back into the messages region. +""" + + phy ldy.b tilemap_offset + lda.l render_allocator.allocated_tile_id sta (0x34), y lda #0xff sta (0x32), y iny lda 0x36 sta (0x32), y + ora.l render_allocator.allocated_tile_id + 1 ora.b #0x01 sta (0x34), y ply @@ -778,7 +816,8 @@ tilemap_write: rts } -.extern flying_hdma_trampoline +; (flying_hdma_trampoline extern lives at sram.s root: message.s is .include'd +; inside an .alloc body, whose scope can't host an extern.) .scope messages_vwf { """High-level battle-message VWF parser: consumes the dialog stream and feeds glyphs into battle_render.""" @@ -829,6 +868,391 @@ init_names: jsr.l battle_flags.set_vwf_render jsr.w battle_render.init_names rtl +init_inventory_for_current_slot: +""" +Compute tile_id base from the live rolling_slot_index (0..5), +allocate 9 tile_ids per slot starting at 0xC0, and reset allocator +state. Saves the bank-02 trampoline ~20 bytes by keeping the math +here. +""" + + + php + rep #0x10 + rep #0x20 + lda.l 0x7EEF82 + and.w #0x00FF + sta.b 0x00 + +; Tile_id base = slot_index * ITEM_VWF_TILE_BUDGET + ITEM_VWF_TILE_BASE. +; With K=10 and BUFFER_SLOTS=6 that's 60 tiles in $C0..$FB, leaving 4 +; tiles of slack before the 8-bit wrap point. Math below assumes K=10 +; (asl x3 = *8, then +slot twice = *10) ; bump together if K changes. + asl + asl + asl + clc + adc.b 0x00 + clc + adc.b 0x00 + clc + adc.w #ITEM_VWF_TILE_BASE + sta.b 0x02 + +; Clear the slot's CHR slice (ITEM_VWF_CHR_BYTES bytes) at +; buffer_ptr + tile_id_base * 16. Stale bits would OR into the new +; render via the VWF blitter's ora-then-store path. Pattern $FF $00 +; alternates the 2 bitplanes (empty tile in 4bpp). Loop count = +; ITEM_VWF_CHR_WORDS = K * 8. + lda.b 0x02 + asl + asl + asl + asl + clc + adc.w #battle_render.buffer_ptr & 0xffff + tax + ldy.w #ITEM_VWF_CHR_WORDS +_chr_clear_loop: + lda.w #0x00ff + sta.l 0x700000, x + inx + inx + dey + bne _chr_clear_loop + +; Restore tile_id base into A for the allocator init. + lda.b 0x02 + sep #0x20 + pha + jsr.l battle_flags.set_vwf_render + pla + sta.l battle_render.pending_transfer_mask + jsr.w battle_render.render_allocator_init_with_tile_id_thunk +; Slot owns ITEM_VWF_TILE_BUDGET tile_ids. Set the allocator clamp at +; slot_base + (K-1) AFTER init_with_tile_id (which resets slot_limit_low +; to 0xFF). Overflow freezes at the last tile instead of bleeding into +; the next slot's CHR / tile_id range. + lda.b 0x02 + clc + adc.b #( ITEM_VWF_TILE_BUDGET - 1 ) + sta.l render_allocator.slot_limit_low + .if ENABLE_KERNING_MENU { + stz.b battle_render.prev_char + } + lda.b #0x08 + sta.b battle_render.bits_left_on_tile + stz.b battle_render.temp + stz.b battle_render.counter + plp + rtl +init_inventory: +""" +Wrap battle_render.init_inventory_region with set_vwf_render so the +message renderer routes char dispatch through the VWF put_char path. +""" + + + jsr.l battle_flags.set_vwf_render + jsr.w battle_render.init_inventory_region + rtl +mirror_main_to_cmd: +""" +Block-mirror the main-view BG3 tilemap region $7E:BE65..$C1A4 +($340 bytes covering entry-0 + window frame) onto the cmd-window +region $7E:C1A5..$C4E4 via the 65816 `MVN` block move. ~7 cycles +per byte ; ~5800 cycles total, roughly half the cost of the +original $340-iter lda/sta loop that this replaces. + +First pass tried WRAM->WRAM GP-DMA via $2180 (WMDATA). It writes +zeros: the A-bus read of source WRAM contends with the B-bus +write into WRAM through $2180, the WRAM controller can not serve +both halves of the cycle, and the destination ends up cleared. +MVN routes bytes through the CPU one at a time so the bus stays +single-master and the move actually lands. + +Caller assumed P with M=8 X=16 on entry, restored on exit. +""" + + + php + rep #0x30 + ldx.w #0xBE65 + ldy.w #0xC1A5 + lda.w #0x033F +; A = byte count - 1 ($340 bytes) + .db 0x54 + .db 0x7E + .db 0x7E +; mvn #$7E, #$7E (dst_bank, src_bank) + plp + rtl +draw_inventory_text: +""" +Custom inventory text renderer that walks a format buffer ourselves +instead of going through the vanilla draw_text dispatch. Owns the +escape interpretation end-to-end so we can mix VWF-rendered name +chars with fixed-width symbol / colon / digit / type tiles without +toggling battle_flags mid-stream. + +Caller setup mirrors the vanilla draw_text contract: + $EF50: 16-bit format buffer ptr (source bytes) + $EF52: 16-bit destination tilemap-buffer ptr (in bank \$7E) + $EF54: line length (tiles per row, currently 15) + $EF55: palette + $7EEF82: rolling_slot_index (0..5) for VWF allocator base + +Escape codes handled: + 0x00: terminator -> return + 0x03 BB: emit fixed tile_id BB at the next tilemap slot + 0x0E PP: set tile-attribute byte to PP for subsequent writes + any other byte: VWF blit through battle_render.display_char +""" + + + php + rep #0x10 + sep #0x20 + +; Set DBR to $7E so 16-bit absolute,Y / abs,X writes target WRAM. + phb + lda.b #0x7E + pha + plb + +; Init VWF state via the slot helper. It resets the allocator to +; (0xC0 + slot * 10), bits_left_on_tile = 8, etc. + jsr.w init_inventory_for_current_slot_local + +; Vanilla draw_text prologue: stash src ptr / dest row-1 ptr / dest +; row-2 ptr / palette into DP $30 / $32 / $34 / $36. tilemap_write +; relies on these for the indirect ($32),y and ($34),y writes. + lda.w 0xef55 + sta.b 0x36 + ldx.w 0xef50 + stx.b 0x30 + ldx.w 0xef52 + stx.b 0x32 +; Row 2 ptr = row 1 ptr + (line_length * 2). DON'T modify $ef54 in +; place ; the caller hands us the live line_length each call, and a +; persistent asl would double it every render. + lda.w 0xef54 + asl + clc + adc.b 0x32 + sta.b 0x34 + lda.b 0x33 + adc #0x00 + sta.b 0x35 + +; Pre-clear both row buffers in the slot with space tiles ($FF) + +; palette so an `0xFC NN` goto can skip ahead without leaving stale +; tile_ids on the way. 15 tiles per row, 2 bytes per entry = 30 byte +; pairs per row. + ldy.w #0x0000 +_di_clear: + lda #0xff + sta (0x32), y + sta (0x34), y + iny + lda.b 0x36 + sta (0x32), y + sta (0x34), y + iny + cpy.w #30 + bne _di_clear + +; X = src (format buffer ptr), Y = dest offset (0-relative into the +; tilemap buffer pointed to by \$32 / \$34). + ldx.w 0xEF50 + ldy.w #0x0000 + +; Control codes: +; 0x00 -> terminate +; 0x03 BB -> fixed tile_id BB at current pos +; 0x0E PP -> set palette / tile-flag byte +; 0xFC NN -> goto tile slot NN in the slot tilemap (no fill ; +; init pre-clears the slot to space tiles) +; 0xFE -> toggle fixed <-> VWF dispatch for subsequent chars +; Default mode at entry = fixed so the leading symbol byte lands as a +; literal tile_id without an explicit escape. Stash the mode flag in +; scratch DP $1F: $37/$38/$39/$3A are controller-1/2 button shadows +; and writing $80 to $37 set the A-bit, which cursor.asm @b5a6 then +; latched as a phantom A press, toggling swap mode every scroll. + lda.b #0x80 + sta.b 0x1F + +_di_loop: + lda.w 0x0000, x + beq _di_done + cmp #0x03 + beq _di_fixed + cmp #0x0E + beq _di_pal + cmp #0xFC + beq _di_pad + cmp #0xFE + beq _di_vwf_toggle + bit.b 0x1F + bmi _di_fixed_char +; VWF char -> battle_render.display_char (uses Y as tilemap_offset, +; advances both source X and dest Y as it allocates). + inx + sty.b battle_render.tilemap_offset + jsr.w battle_render.display_char + ldy.b battle_render.tilemap_offset + jmp.w _di_loop + +_di_fixed_char: +; Fixed-mode raw char: write current byte as tile_id at (\$34),y. No +; ora #0x01 on the attr -- the +0x100 high bit is only for VWF tiles +; in the 0x1xx range, fixed font tiles live in 0x00..0xFF. + sta (0x34), y + lda #0xff + sta (0x32), y + iny + lda.b 0x36 + sta (0x32), y + sta (0x34), y + iny + inx + jmp.w _di_loop + + +_di_fixed: +; 0x03 BB -> write fixed tile_id BB at (\$34),y. Same shape as +; _di_fixed_char (mirror of wram.put_char), no +0x100 bit set. + inx + lda.w 0x0000, x + sta (0x34), y + lda #0xff + sta (0x32), y + iny + lda.b 0x36 + sta (0x32), y + sta (0x34), y + iny + inx + jmp.w _di_loop + +_di_pal: +; 0x0E PP -> stash PP as the tile flags byte. Vanilla draw_text +; stores it at $36 ; mirror that so subsequent fixed / VWF writes +; pick up the right palette. + inx + lda.w 0x0000, x + sta.b 0x36 + inx + jmp.w _di_loop + +_di_done: +; Clear the VWF battle_flag so later non-inventory renders (monster HP +; refresh, status text, etc.) go back to the WRAM put_char path. + jsr.l battle_flags.clear_vwf_render +; Mark this slot's CHR slice dirty in dma_dirty_slots. NMI +; dma_transfer picks it up and DMAs the 160-byte slice to VRAM. +; Replaces the old full-4KB DMA trigger ; partial DMA fits in +; vblank without forced blank. + php + sep #0x30 + lda.l 0x7EEF82 ; slot index (low byte only ; M=8) + and.b #0x07 ; clamp to 0..7 (valid range 0..5) + tax + lda.l _dma_slot_bit_lut, x + ora.l battle_render.dma_dirty_slots + sta.l battle_render.dma_dirty_slots + plp + plb + plp + rtl + +_di_skip: + inx + jmp.w _di_loop + +_di_vwf_toggle: + inx + lda.b 0x1F + eor.b #0x80 + sta.b 0x1F + jmp.w _di_loop + +_di_pad: +; 0xFC NN -> goto tile slot NN (jump Y to NN * 2). The slot tilemap is +; pre-cleared to space tiles in init, so intermediate slots already +; show as blank without an explicit fill. Also resets bits_left so the +; next VWF char starts on a clean tile column. + inx + lda.w 0x0000, x + inx + asl + tay + lda #0x08 + sta.b battle_render.bits_left_on_tile + jmp.w _di_loop + +init_inventory_for_current_slot_local: +""" +Local jsr.w-reachable copy of init_inventory_for_current_slot that +returns via rts (the public entry returns rtl). +""" + + + php + rep #0x10 + rep #0x20 + lda.l 0x7EEF82 + and.w #0x00FF + sta.b 0x00 + asl + asl + asl + clc + adc.b 0x00 + clc + adc.b 0x00 + clc + adc.w #ITEM_VWF_TILE_BASE + sta.b 0x02 + asl + asl + asl + asl + clc + adc.w #battle_render.buffer_ptr & 0xffff + tax + ldy.w #ITEM_VWF_CHR_WORDS +_dis_chr_clear: + lda.w #0x00ff + sta.l 0x700000, x + inx + inx + dey + bne _dis_chr_clear + lda.b 0x02 + sep #0x20 + pha + jsr.l battle_flags.set_vwf_render + pla + sta.l battle_render.pending_transfer_mask + jsr.w battle_render.render_allocator_init_with_tile_id_thunk +; Slot owns ITEM_VWF_TILE_BUDGET tile_ids. Set the allocator clamp at +; slot_base + (K-1) AFTER init_with_tile_id (which resets slot_limit_low +; to 0xFF). Overflow freezes at the last tile instead of bleeding into +; the next slot's CHR / tile_id range. + lda.b 0x02 + clc + adc.b #( ITEM_VWF_TILE_BUDGET - 1 ) + sta.l render_allocator.slot_limit_low + .if ENABLE_KERNING_MENU { + stz.b battle_render.prev_char + } + lda.b #0x08 + sta.b battle_render.bits_left_on_tile + stz.b battle_render.temp + stz.b battle_render.counter + plp + rts init_monsters_gated: """ Gated counterpart to `init_monsters`. Always flips the VWF flag @@ -907,6 +1331,120 @@ normal length, no visible black strip. pha phx phy + .if BATTLE_ITEMS_VWF { +; Per-NMI BG3 V-scroll footer override. +; +; Channel 2 (BG3 V-scroll, indirect, $7E:760B -> $7E:7ED2 chunk) is +; multiplexed across the BG3 layer (cmd / status / inventory share). +; Vanilla 0x02BB at $7E:8068..$8081 leaves 2 rows of black below +; inv body where the window's bottom border should be. +; +; While inv is open: write border-row scrolls every NMI (vanilla +; state-1 swap at $02:A8FE would otherwise overwrite within a frame). +; On the close edge: one-shot restore 0x02BB so cmd/status menus +; that share ch2 stop rendering inventory's border tiles. +; +; Open-state shadow at $703F04 (next free past tilemap_pending_mask). + php + sep #0x30 + lda.l 0x7E004A + and.b #0x04 + beq _inv_footer_closed +; OPEN: pin the two bottom-border tile rows past body. $8068+ is +; past ch2's HDMA chunk (240-byte count terminates at $7FC2) ; +; these writes don't drive HDMA but mirror to the swap table via +; the vanilla state-1 routine which the inventory code reads. Real +; mid-scroll leak fix needs to patch INSIDE chunk 2 ($7F74-$7FBF) +; where vanilla rewrites body-row scrolls per rolling_buffer_pos. + rep #0x30 + lda.w #0x0193 + sta.l 0x7E8068 + sta.l 0x7E806C + sta.l 0x7E8070 + lda.w #0x019B + sta.l 0x7E8074 + sta.l 0x7E8078 + sta.l 0x7E807C + sta.l 0x7E8080 + bra _inv_footer_done +_inv_footer_closed: +; CLOSED: write vanilla idle pattern every frame so state-1 swap +; can't bring our open values back. Trace at battle-settle: +; $8064 = 0x0187 (vanilla "slot 5" transition entry, static) +; $8068/6C/70 = 0x01F7 +; $8074..$80 = 0x0026, 0x0025, 0x0024, 0x0023 (decreasing) + rep #0x30 + lda.w #0x01F7 + sta.l 0x7E8068 + sta.l 0x7E806C + sta.l 0x7E8070 + lda.w #0x0026 + sta.l 0x7E8074 + lda.w #0x0025 + sta.l 0x7E8078 + lda.w #0x0024 + sta.l 0x7E807C + lda.w #0x0023 + sta.l 0x7E8080 +_inv_footer_done: + plp +; --- Inventory CHR partial DMA --- +; Pop one bit from dma_dirty_slots and DMA that slot's 160-byte CHR +; slice ($703000 + (0xC0 + N*10)*16 -> VRAM $BC00 + N*$A0). Replaces +; the old full-4KB DMA path for inventory. Non-inv VWF regions still +; flow through the legacy pending_transfer_mask check below. + php + sep #0x20 + rep #0x10 + lda.l battle_render.dma_dirty_slots + and.b #0x3F + bne _inv_dma_have + jmp.w _no_inv_dma +_inv_dma_have: + ldx.w #0xFFFF +_inv_dma_find: + inx + lsr + bcc _inv_dma_find + cpx.w #0x0006 ; safety: out-of-range index -> no DMA + bcs _inv_dma_done +; X = slot index 0..5. Clear its bit before DMA so re-entry is safe. + phx + lda.l _dma_slot_bit_lut, x + eor.b #0xFF + and.l battle_render.dma_dirty_slots + sta.l battle_render.dma_dirty_slots + plx +; Build X = slot*2 with X.hi clean (we're in M=8 X=16, plain tax leaks A.hi). + rep #0x20 + txa + and.w #0x000F + asl + tax + sep #0x20 +; LUT reads use `.l` (24-bit long) since DBR in NMI isn't bank-20. + lda.l _dma_slot_vram_lut, x + sta.b 0x0c + lda.l _dma_slot_vram_lut + 1, x + sta.b 0x0d + lda.l _dma_slot_src_lut, x + sta.b 0x0a + lda.l _dma_slot_src_lut + 1, x + sta.b 0x0b + rep #0x20 + lda.b 0x0c + tay + lda.b 0x0a + tax + lda.w #0x00A0 + sta.b 0x0e + sep #0x20 + lda.b #0x70 + jsr.w _sram_dma_transfer_7 +_inv_dma_done: +_no_inv_dma: + plp + } lda.l battle_render.pending_transfer_mask bit #1 beq _no_transfer @@ -934,8 +1472,18 @@ normal length, no visible black strip. .if SMART { ldx.w #0x400 } else { + .if BATTLE_ITEMS_VWF { +; Inventory region extends past the legacy 0xC00 window. Bump to +; 0x1000 so the DMA covers the full BG3 CHR upper half ($B000..$BFFF +; = tile_ids 0x100..0x1FF). Gains 16 tile_ids for the inventory slot +; budget at zero risk -- the trailing 0x100 bytes were unused. +; TODO: don't transfer the whole thing every frame ; only dirty +; regions need flushing. + ldx.w #0x1000 + } else { ldx.w #0xc00 } + } stx 0x0e plx lda #0x70 @@ -1066,4 +1614,22 @@ bits_left_on_tile to 8, and advance the tilemap offset by one row (16 tiles). ;02/A64A: A8 TAY ;02/A64B: E2 20 SEP #$20 ;02/A64D: 60 RTS + +; Per-slot inventory CHR DMA tables. Auto-generated via .for. +; - bit_lut[N] = 1 << N (slot N's dirty-mask bit) +; - vram_lut[N] = ($BC00 + N*$A0) >> 1 (word-address into BG3 CHR upper half) +; - src_lut[N] = $3000 + (0xC0 + N*10)*16 = $3C00 + N*$A0 (bank-70 offset) +; Slot N owns 10 tile_ids starting at low byte (0xC0 + N*10), so 160-byte slice. +_dma_slot_bit_lut: + .for k := 0, 6 { + .db ( 1 << k ) + } +_dma_slot_vram_lut: + .for k := 0, 6 { + .dw ( 0xBC00 + k * 0xA0 ) >> 1 + } +_dma_slot_src_lut: + .for k := 0, 6 { + .dw 0x3C00 + k * 0xA0 + } } diff --git a/src/battle/message_patches.s b/src/battle/message_patches.s index e184047..cd16512 100644 --- a/src/battle/message_patches.s +++ b/src/battle/message_patches.s @@ -7,92 +7,107 @@ translated-string tables. .scope message_patches { """Pinned-address overlay scope rewriting original battle-message pointer loads to relocated tables.""" ; pointers to battle dialog - *=0x02c909 - lda.l assets_battle_messages_ptr, x - sta 0x00 - lda.l assets_battle_messages_ptr + 1, x - sta 0x01 - lda.w #assets_battle_messages_ptr >> 16 - sta 0x02 -; pointers to battle messages - *=0x02cc07 - lda.l assets_battle_text_ptr, x - sta 0x00 - tdc - sep #0x20 - lda.b #assets_battle_text_ptr >> 16 - sta 0x02 -; |tileset 0xb000 -> 0xbfff bg3 tiles -; |------------------- -; |tilemap bg2 64*32 -; |0xb000, 0x1000 -; |------------------- -; |tile map bg1 64*64 -; |0xc0000, 0x2000 -; |------------------- -; |tilemap bg3 64*64 -; |0xe0000, 0x2000 -; |------------------- -; resize bg1 to 32x64 and moves it to v:0xd000 - *=0x0382f5 -; bg 1 - lda.b #( 0xd000 >> 9 | 0b01 ) - sta 0x2107 - lda.b #( 0xc000 >> 9 | 0b01 ) - sta 0x2108 -; Fix teleport resizing bg 1 & 2, that reverted them to their original addresses. - *=0x02f11a -; bg 1 - lda.b #( 0xd000 >> 9 | 0b01 ) - sta.l 0x002107 - lda.b #( 0xc000 >> 9 | 0b01 ) - sta.l 0x002108 -; bg1 move - *=0x028B3B - ldy.w #0x6000 + 0x800 - *=0x02bcad - ldy.w #0x6000 + 0x800 - *=0x02BCB2 - ldy.w #0x6400 + 0x800 -; 029429 A0 39 5F LDY #$5F39 -; MP cost transfer destination - *=0x029429 - ldy.w #0x5f39 + 0x800 -; kick animation -; 02/C6A8: A0 40 61 LDY #$6140 - *=0x02c6a8 - ldy.w #0x6140 + 0x800 -; bg1 move -;02/91FE: A0 00 58 LDY #$5800 - *=0x0291FE - ldy.w #0x6000 -;02/921F: A0 00 5C LDY #$5C00 - *=0x02921F - ldy.w #0x6400 -; Menu defend row mp cost init - *=0x16fdd6 + 6 * 6 -; wram vram size - .dw 0xd366, 0x6400 + 0x260, 0x0340 - *=0x16fe0c + 6 * 6 - .dw 0xd366, 0x6400 + 0x260, 0x0140 - *=0x0292F8 -; move the text buffer pointer back 3 entries - ldx.w #0xDB2E - 3 * 2 - stx 0xEF52 - jsr.w msg_window_draw_text_trampoline +.alloc at 0x02c909 { + lda.l assets_battle_messages_ptr, x + sta 0x00 + lda.l assets_battle_messages_ptr + 1, x + sta 0x01 + lda.w #assets_battle_messages_ptr >> 16 + sta 0x02 + ; pointers to battle messages +} +.alloc at 0x02cc07 { + lda.l assets_battle_text_ptr, x + sta 0x00 + tdc + sep #0x20 + lda.b #assets_battle_text_ptr >> 16 + sta 0x02 + ; |tileset 0xb000 -> 0xbfff bg3 tiles + ; |------------------- + ; |tilemap bg2 64*32 + ; |0xb000, 0x1000 + ; |------------------- + ; |tile map bg1 64*64 + ; |0xc0000, 0x2000 + ; |------------------- + ; |tilemap bg3 64*64 + ; |0xe0000, 0x2000 + ; |------------------- + ; resize bg1 to 32x64 and moves it to v:0xd000 +} +.alloc at 0x0382f5 { + ; bg 1 + lda.b #( 0xd000 >> 9 | 0b01 ) + sta 0x2107 + lda.b #( 0xc000 >> 9 | 0b01 ) + sta 0x2108 + ; Fix teleport resizing bg 1 & 2, that reverted them to their original addresses. +} +.alloc at 0x02f11a { + ; bg 1 + lda.b #( 0xd000 >> 9 | 0b01 ) + sta.l 0x002107 + lda.b #( 0xc000 >> 9 | 0b01 ) + sta.l 0x002108 + ; bg1 move +} +.alloc at 0x028B3B { + ldy.w #0x6000 + 0x800 +} +.alloc at 0x02bcad { + ldy.w #0x6000 + 0x800 +} +.alloc at 0x02BCB2 { + ldy.w #0x6400 + 0x800 + ; 029429 A0 39 5F LDY #$5F39 + ; MP cost transfer destination +} +.alloc at 0x029429 { + ldy.w #0x5f39 + 0x800 + ; kick animation + ; 02/C6A8: A0 40 61 LDY #$6140 +} +.alloc at 0x02c6a8 { + ldy.w #0x6140 + 0x800 + ; bg1 move + ;02/91FE: A0 00 58 LDY #$5800 +} +.alloc at 0x0291FE { + ldy.w #0x6000 + ;02/921F: A0 00 5C LDY #$5C00 +} +.alloc at 0x02921F { + ldy.w #0x6400 + ; Menu defend row mp cost init +} +.alloc at 0x16fdd6 + 6 * 6 { + ; wram vram size + .dw 0xd366, 0x6400 + 0x260, 0x0340 +} +.alloc at 0x16fe0c + 6 * 6 { + .dw 0xd366, 0x6400 + 0x260, 0x0140 +} +.alloc at 0x0292F8 { + ; move the text buffer pointer back 3 entries + ldx.w #0xDB2E - 3 * 2 + stx 0xEF52 + jsr.w msg_window_draw_text_trampoline +} } ; patch the battle nmi routine to transfer the battle render buffer. -*=0x02836e - jsr.l messages_vwf.dma_transfer - -; make the message window bigger +.alloc at 0x02836e { + jsr.l messages_vwf.dma_transfer -*=0x0292DF - ; (0, 3) (26, 4) - ldx.w #0x0000 - stx.w 0xEF56 - ldx.w #0x041A + 6 - stx.w 0xEF58 + ; make the message window bigger +} +.alloc at 0x0292DF { + ; (0, 3) (26, 4) + ldx.w #0x0000 + stx.w 0xEF56 + ldx.w #0x041A + 6 + stx.w 0xEF58 +} diff --git a/src/battle/monsters_patches.s b/src/battle/monsters_patches.s index cc5f7bb..4a4994c 100644 --- a/src/battle/monsters_patches.s +++ b/src/battle/monsters_patches.s @@ -8,40 +8,40 @@ names) and forward to `load_monster_pointer`. ; transform the monster names loading routine from fixed size to pointed. -*=0x02a7d7 -{ - jsr.l load_monster_pointer - -_loop: - lda.l assets_monsters_long_dat, x - beq _exit - jsr.w 0xA497 -; draw text -; jsr.w msg_monster_window_trampoline - inx - bra _loop - -_exit: - rts - -_end: - .debug '{_end} < 0x02A7F0 ?' +.alloc at 0x02a7d7 { + { + jsr.l load_monster_pointer + + _loop: + lda.l assets_monsters_long_dat, x + beq _exit + jsr.w 0xA497 + ; draw text + ; jsr.w msg_monster_window_trampoline + inx + bra _loop + + _exit: + rts + + _end: + .debug '{_end} < 0x02A7F0 ?' + } } +.alloc at 0x02a7c2 { + jsr.l initialize_monster_slot + rts - -*=0x02a7c2 - jsr.l initialize_monster_slot - rts - -; escape code 0x05 tab followed by a number of chars - -*=0x02A6B3 - jsr.l tab_escape_code - nop - -; dec 0 - nop - nop - ; bne a6b3 - nop - nop + ; escape code 0x05 tab followed by a number of chars +} +.alloc at 0x02A6B3 { + jsr.l tab_escape_code + nop + + ; dec 0 + nop + nop + ; bne a6b3 + nop + nop +} diff --git a/src/battle/monsters_reloc.s b/src/battle/monsters_reloc.s index 348ae25..7396c3b 100644 --- a/src/battle/monsters_reloc.s +++ b/src/battle/monsters_reloc.s @@ -1,50 +1,57 @@ """ +.include "../bank20.i" + Relocated battle monster-name pointer resolver: indexes `assets_monsters_long_ptr[A*2]` and renders the resulting string into the active slot. """ .extern assets_monsters_long_ptr -load_monster_pointer: -"""Resolve a long-form monster name pointer from `assets_monsters_long_ptr[A*2]` and render it into the current slot.""" - rep #0x20 - ;lda.w #127 - 3 - asl - tax - lda.l assets_monsters_long_ptr, x - tax - tdc - sep #0x20 - phy - jsr.w _initialize_monster_slot_near - ply - rtl - -initialize_monster_slot: -"""Cross-bank (RTL) entry that clears a monster's display slot to spaces.""" - jsr.w _initialize_monster_slot_near - rtl - -tab_escape_code: -"""Text-stream escape that draws a column of spaces to advance the cursor.""" - jsr.w _draw_spaces - rtl - -_initialize_monster_slot_near: - -; clear monster slot with spaces - lda #11 - sta 0x00 - -_draw_spaces: - lda.b #0xff - sta (0x32), y - sta (0x34), y - iny - lda 0x36 - sta (0x32), y - sta (0x34), y - iny - dec 0x00 - bne _draw_spaces - - rts + +.include "../bank20.i" + +.alloc battle_monsters_reloc_block in bank20_reloc { + load_monster_pointer: + """Resolve a long-form monster name pointer from `assets_monsters_long_ptr[A*2]` and render it into the current slot.""" + rep #0x20 + ;lda.w #127 - 3 + asl + tax + lda.l assets_monsters_long_ptr, x + tax + tdc + sep #0x20 + phy + jsr.w _initialize_monster_slot_near + ply + rtl + + initialize_monster_slot: + """Cross-bank (RTL) entry that clears a monster's display slot to spaces.""" + jsr.w _initialize_monster_slot_near + rtl + + tab_escape_code: + """Text-stream escape that draws a column of spaces to advance the cursor.""" + jsr.w _draw_spaces + rtl + + _initialize_monster_slot_near: + + ; clear monster slot with spaces + lda #11 + sta 0x00 + + _draw_spaces: + lda.b #0xff + sta (0x32), y + sta (0x34), y + iny + lda 0x36 + sta (0x32), y + sta (0x34), y + iny + dec 0x00 + bne _draw_spaces + + rts +} diff --git a/src/battle/redraw_gates.s b/src/battle/redraw_gates.s index de728a5..ca3da54 100644 --- a/src/battle/redraw_gates.s +++ b/src/battle/redraw_gates.s @@ -1,4 +1,6 @@ """ +.include "../bank20.i" + Battle redraw-gate state + writer helpers. FF6-style dirty-bit gating for the battle redraw chain. One byte at @@ -23,290 +25,318 @@ battle_menu_dirty := 0x7EEF9A ; bit 5 = cmd window, bit 6 = status strip, battle_monster_dirty := 0x7EEF9B ; bits 0-7 = per-monster-slot name redraw scp_active_slot := 0x7EEF9C ; scratch for set_active_char_palette scp_pal_byte := 0x7EEF9D ; current palette byte being applied -status_hash := 0x7EEF9E ; hash of char-status bytes; gates DrawStatusText -obj_names_hash := 0x7EEF9F ; hash of monster slots + $1822; gates DrawObjNames -char_hp_hash := 0x7EEFA0 ; hash of char HP bytes; gates DrawCharHP -CMD_DIRTY_BIT := 0x20 ; bit 5 of battle_menu_dirty -NAMES_DIRTY_BIT := 0x10 ; bit 4 of battle_menu_dirty (char names region) -MONSTER_DIRTY_BIT := 0x01 ; bit 0 of battle_monster_dirty (any monster name) +; Cross-module LABEL: extern at root scope (`.extern` only registers in its own +; scope, and an `.alloc` body opens its own). +.extern clear_names_window_buffer -mark_cmd_dirty: -""" -Set the cmd-window dirty bit. Callable from any bank via JSL/RTL. -65816 `tsb` has no long-addressing form ; emulate via lda/ora/sta. -""" - lda.l battle_menu_dirty - ora.b #CMD_DIRTY_BIT - sta.l battle_menu_dirty - rtl +; Cross-module CONSTANTS are compile-time, not link symbols: share the same +; definitions message.s uses via the include, under the same scope name. +.scope battle_render { + .include "render_defs.i" +} -walker_rtl: -""" -RTL wrapper around `set_active_char_palette` so bank-02 callers -can JSL into it with matching pop. Reads $1822 into A first so -caller doesn't have to set it up. -""" - lda.l 0x7E1822 - jsr.w set_active_char_palette - rtl +.include "../bank20.i" -gate_status_check: -""" -Bank-20 body for the DrawStatusText hash gate. XOR of char-slot -status-1 bytes ($2003+slot*$40). Sets carry on dirty, clears on -clean. Caller (bank-02 trampoline at $02:97F8) tail-jumps to -$A2A1 on dirty, rts on clean. -""" - lda.l 0x7E2003 - eor.l 0x7E2043 - eor.l 0x7E2083 - eor.l 0x7E20C3 - eor.l 0x7E2103 - cmp.l status_hash - beq _gsc_clean - sta.l status_hash - sec - rtl - -_gsc_clean: - clc - rtl - -gate_obj_names_check: -""" -Bank-20 body for the DrawObjNames hash gate. XOR of monster slot -type bytes ($29B5..$29B8) + active-char index ($1822). Sets -carry on dirty (re-render needed), clears carry on clean. -Caller (bank-02 trampoline at $02:97C2) tail-jumps to $99D3 on -dirty, rts on clean. -""" - lda.l 0x7E29B5 - eor.l 0x7E29B6 - eor.l 0x7E29B7 - eor.l 0x7E29B8 - eor.l 0x7E1822 - cmp.l obj_names_hash - beq _goc_clean - sta.l obj_names_hash - sec - rtl - -_goc_clean: - clc - rtl - -mark_monsters_dirty_and_init: -""" -Hook shim for `UpdateDead` entry at $03:B1A0. The original 4 bytes -(`tdc ; tax; stx $a9`) get replaced by a JSL here ; this helper -sets the monsters-region dirty bit, then replicates the clobbered -prelude so execution can fall through to the original loop at -$03:B1A4. UpdateDead is the engine path that marks a monster -dead (`sta $29b5,x` with $FF after status apply) ; flagging -monsters dirty here lets the gated monster-name trampoline -re-render once the dead slot is wiped. -""" - lda.l battle_render.region_dirty_bits - ora.b #battle_render.REGION_DIRTY_MONSTERS - sta.l battle_render.region_dirty_bits - tdc - tax - stx.b 0xa9 - rtl - -mark_all_dirty: -""" -Reset both dirty bytes to $FF so the next frame renders everything. -Called once at battle init. -""" - lda.b #0xFF - sta.l battle_menu_dirty - sta.l battle_monster_dirty - rtl - - .extern clear_names_window_buffer - .extern battle_render.REGION_DIRTY_MONSTERS - .extern battle_render.REGION_DIRTY_NAMES - .extern battle_render.region_dirty_bits - .extern battle_render.render_skipped - .extern battle_render.tilemap_pending_mask - .extern battle_render.TILEMAP_PENDING_MAIN - -reset_queue_dirty_bits: -""" -Seed all redraw-gate state for a fresh battle. Called once per -battle from the InitMenuWindows hook ($02:9A63). Normal sense -everywhere: 1 = dirty, 0 = clean. Seed all bytes to $FF so the -first frame renders everything ; the per-region gates clear their -own bits after rendering, and writer sites re-arm on state change. -""" - lda.b #0x00 - sta.l battle_render.render_skipped - lda.b #0xFF - sta.l battle_render.region_dirty_bits - sta.l battle_menu_dirty - sta.l battle_monster_dirty - rtl - -gated_clear_names_window_buffer: -""" -Wrap `clear_names_window_buffer` ($02:A299 call site) with the -names-region dirty-bit check. When names is clean (bit clear), -skip the tilemap wipe so the gated `init_names_gated` + skipped -DrawText leaves the VRAM tilemap untouched. Without this the -tilemap wipe still fires every frame and the gated render skip -leaves blank tiles on screen. -""" - lda.l battle_render.region_dirty_bits - bit.b #battle_render.REGION_DIRTY_NAMES - beq _gcnwb_skip - jsr.l clear_names_window_buffer +.alloc battle_redraw_gates_block in bank20_reloc { + status_hash := 0x7EEF9E ; hash of char-status bytes; gates DrawStatusText + obj_names_hash := 0x7EEF9F ; hash of monster slots + $1822; gates DrawObjNames + char_hp_hash := 0x7EEFA0 ; hash of char HP bytes; gates DrawCharHP -_gcnwb_skip: - rtl + CMD_DIRTY_BIT := 0x20 ; bit 5 of battle_menu_dirty + NAMES_DIRTY_BIT := 0x10 ; bit 4 of battle_menu_dirty (char names region) + MONSTER_DIRTY_BIT := 0x01 ; bit 0 of battle_monster_dirty (any monster name) -set_active_char_palette: -""" -Walk all 5 char-name slots in the `$7E:B966` tilemap. Active slot -gets palette $04 (bit 2 of tilemap-entry hi byte = palette 1) ; -others get $00 (palette 0). - -Each char-name slot occupies row at `$B966 + slot * $40`. Names -are max 6 tiles wide in the FR translation. Each tilemap entry -is 2 bytes (low = tile id, hi = flags+palette). Two pointer -mirrors get written by the VWF dispatcher (`tilemap_write` -in message.s) at offsets +0 and +$0C of the row base, so 12 hi -bytes total per slot (6 on each mirror). - -A = active slot index (0..4) on entry. M=8, X=8. -""" + mark_cmd_dirty: + """ + Set the cmd-window dirty bit. Callable from any bank via JSL/RTL. + 65816 `tsb` has no long-addressing form ; emulate via lda/ora/sta. + """ + lda.l battle_menu_dirty + ora.b #CMD_DIRTY_BIT + sta.l battle_menu_dirty + rtl + walker_rtl: + """ + RTL wrapper around `set_active_char_palette` so bank-02 callers + can JSL into it with matching pop. Reads $1822 into A first so + caller doesn't have to set it up. + """ + lda.l 0x7E1822 + jsr.w set_active_char_palette + rtl - php - sep #0x20 - rep #0x10 - phb - pha - lda.b #0x7E ; force DBR = $7E so `(0x32),y` writes to WRAM - pha - plb - pla - sta.l scp_active_slot - ; Save $32/$33 ; walker reuses as scratch indirect-ptr ; NMI - ; caller's BG / DMA state needs them preserved. - rep #0x20 - lda.b 0x32 - pha - sep #0x20 - ldx.w #0 - -_scp_slot_loop: - cpx.w #5 - bcs _scp_done - txa - cmp.l scp_active_slot - beq _scp_is_active - lda #0x00 - bra _scp_have_pal - -_scp_is_active: - lda #0x08 ; palette 2 (vanilla `lda #$08` in UpdateCharNames @a24c) - -_scp_have_pal: - sta.l scp_pal_byte - -; base = $B966 + slot * 24 (per-slot stride confirmed via trace: -; $32 mirror at slot*24+0 (6 tiles padding), $34 mirror at -; slot*24+12 (6 tiles real name content)). - rep #0x20 - txa - and.w #0x000F - asl - asl - asl ; *8 - pha - asl ; *16 - clc - adc 1, s ; *16 + *8 = *24 - clc - adc.w #0xB966 - sta.b 0x32 - pla ; balance stack - sep #0x20 - ; Patch 6 entries on the ($32) mirror at offsets +1, +3, +5, +7, +9, +B - ldy.w #1 - jsr.w _scp_patch_six - ; Now patch the ($34) mirror at base + $0C - rep #0x20 - lda.b 0x32 - clc - adc.w #0x000C - sta.b 0x32 - sep #0x20 - ldy.w #1 - jsr.w _scp_patch_six - inx - bra _scp_slot_loop - -_scp_done: - rep #0x20 - pla - sta.b 0x32 - sep #0x20 - plb - plp - rts - -_scp_patch_six: - ; ($32) = row base in bank $7E. Y = first hi-byte offset. - ; Walks Y, Y+2, ..., Y+10. Masks `$E3`, ORs in `scp_pal_byte`. - phx - ldx.w #6 - -_scp_loop_six: - lda (0x32), y - and #0xE3 - ora.l scp_pal_byte - sta (0x32), y - iny - iny - dex - bne _scp_loop_six - plx - rts - -set_active_char_and_dirty: -""" -Writer-site shim for the active-char store (`sta $1822` at $03:A482, -followed by `sta $d0`). Original 5 bytes replaced by JSL + NOP. Shim -performs both stores then unconditionally marks cmd + names + -monsters dirty. The vanilla site fires only when the battle-menu -queue pops a new char (`check if menu needs to open` path in -`battle/char.asm:464`), so re-arming on every call is correct ; -a compare-against-current would mis-skip when the popped char -matches a stale value in $1822, leaving the prior cmd-window -tilemap on screen. -Caller has M=8, A holds the char index, DBR may differ from $7E. -""" - sep #0x20 - sta.l 0x7E1822 - sta.l 0x7E00D0 - pha - lda.l battle_menu_dirty - ora.b #CMD_DIRTY_BIT - sta.l battle_menu_dirty - ; Char-name palette patch (replaces the full names VWF re-render - ; that used to fire via REGION_DIRTY_NAMES on every rotation). - ; Walks the `$7E:B966` tilemap, rewrites the palette field of - ; each char's 6 max tiles on both pointer mirrors ; ~300 cycles - ; vs ~3M for the VWF re-render. Also flip names tilemap dirty - ; so the unified-DMA path uploads the patched tilemap to VRAM. - pla - pha - jsr.w set_active_char_palette - lda.l battle_render.tilemap_pending_mask - ora.b #battle_render.TILEMAP_PENDING_MAIN - sta.l battle_render.tilemap_pending_mask - pla - rtl + gate_status_check: + """ + Bank-20 body for the DrawStatusText hash gate. XOR of char-slot + status-1 bytes ($2003+slot*$40). Sets carry on dirty, clears on + clean. Caller (bank-02 trampoline at $02:97F8) tail-jumps to + $A2A1 on dirty, rts on clean. + """ + lda.l 0x7E2003 + eor.l 0x7E2043 + eor.l 0x7E2083 + eor.l 0x7E20C3 + eor.l 0x7E2103 + cmp.l status_hash + beq _gsc_clean + sta.l status_hash + sec + rtl + + _gsc_clean: + clc + rtl + + gate_obj_names_check: + """ + Bank-20 body for the DrawObjNames hash gate. XOR of monster slot + type bytes ($29B5..$29B8) + active-char index ($1822). Sets + carry on dirty (re-render needed), clears carry on clean. + Caller (bank-02 trampoline at $02:97C2) tail-jumps to $99D3 on + dirty, rts on clean. + """ + lda.l 0x7E29B5 + eor.l 0x7E29B6 + eor.l 0x7E29B7 + eor.l 0x7E29B8 + eor.l 0x7E1822 + cmp.l obj_names_hash + beq _goc_clean + sta.l obj_names_hash + sec + rtl + + _goc_clean: + clc + rtl + + mark_monsters_dirty_and_init: + """ + Hook shim for `UpdateDead` entry at $03:B1A0. The original 4 bytes + (`tdc ; tax; stx $a9`) get replaced by a JSL here ; this helper + sets the monsters-region dirty bit, then replicates the clobbered + prelude so execution can fall through to the original loop at + $03:B1A4. UpdateDead is the engine path that marks a monster + dead (`sta $29b5,x` with $FF after status apply) ; flagging + monsters dirty here lets the gated monster-name trampoline + re-render once the dead slot is wiped. + """ + lda.l battle_render.region_dirty_bits + ora.b #battle_render.REGION_DIRTY_MONSTERS + sta.l battle_render.region_dirty_bits + ; Propagate to cmd-window region: the cmd-window tilemap at $C1A5+ is + ; a mirror of the main view ($BE65+) overlaid with cmd tiles. If we + ; refresh monsters in the main view, the cmd mirror is stale, so set + ; CMD_DIRTY_BIT here too. The relocated DrawCmdWindow path picks this + ; up next frame and re-runs the WRAM mirror via `mirror_main_to_cmd`. + lda.l battle_menu_dirty + ora.b #CMD_DIRTY_BIT + sta.l battle_menu_dirty + tdc + tax + stx.b 0xa9 + rtl + + mark_all_dirty: + """ + Reset both dirty bytes to $FF so the next frame renders everything. + Called once at battle init. + """ + lda.b #0xFF + sta.l battle_menu_dirty + sta.l battle_monster_dirty + rtl + + reset_queue_dirty_bits: + """ + Seed all redraw-gate state for a fresh battle. Called once per + battle from the InitMenuWindows hook ($02:9A63). Normal sense + everywhere: 1 = dirty, 0 = clean. Seed all bytes to $FF so the + first frame renders everything ; the per-region gates clear their + own bits after rendering, and writer sites re-arm on state change. + """ + lda.b #0x00 + sta.l battle_render.render_skipped + lda.b #0xFF + sta.l battle_render.region_dirty_bits + sta.l battle_menu_dirty + sta.l battle_monster_dirty + rtl + + gated_clear_names_window_buffer: + """ + Wrap `clear_names_window_buffer` ($02:A299 call site) with the + names-region dirty-bit check. When names is clean (bit clear), + skip the tilemap wipe so the gated `init_names_gated` + skipped + DrawText leaves the VRAM tilemap untouched. Without this the + tilemap wipe still fires every frame and the gated render skip + leaves blank tiles on screen. + """ + lda.l battle_render.region_dirty_bits + bit.b #battle_render.REGION_DIRTY_NAMES + beq _gcnwb_skip + jsr.l clear_names_window_buffer + + _gcnwb_skip: + rtl + + set_active_char_palette: + """ + Walk all 5 char-name slots in the `$7E:B966` tilemap. Active slot + gets palette $04 (bit 2 of tilemap-entry hi byte = palette 1) ; + others get $00 (palette 0). + + Each char-name slot occupies row at `$B966 + slot * $40`. Names + are max 6 tiles wide in the FR translation. Each tilemap entry + is 2 bytes (low = tile id, hi = flags+palette). Two pointer + mirrors get written by the VWF dispatcher (`tilemap_write` + in message.s) at offsets +0 and +$0C of the row base, so 12 hi + bytes total per slot (6 on each mirror). + + A = active character SLOT on entry (vanilla `$1822` semantics). + + The on-screen tilemap is ordered by display position, NOT slot: + row R of `$B966` shows whoever vanilla's `CharOrderTbl` + (`$02:A1C8` = .byte 1,3,0,4,2) maps R to. The per-row loop below + already iterates display rows 0..4, so we compare each row's + CharOrderTbl entry against $1822 instead of comparing the row + index itself. Without this indirection, Cecil acting (slot 0) + used to light up row 0 of the tilemap, which displays slot 1 + (= Palom in the default order). + + M=8, X=8. + """ + + + php + sep #0x20 + rep #0x10 + phb + pha + lda.b #0x7E ; force DBR = $7E so `(0x32),y` writes to WRAM + pha + plb + pla + sta.l scp_active_slot + ; Save $32/$33 ; walker reuses as scratch indirect-ptr ; NMI + ; caller's BG / DMA state needs them preserved. + rep #0x20 + lda.b 0x32 + pha + sep #0x20 + ldx.w #0 + + _scp_slot_loop: + cpx.w #5 + bcs _scp_done + ; CharOrderTbl[row] = char slot displayed at this row. Compare to + ; the active slot; match -> highlight palette, miss -> palette 0. + lda.l 0x02A1C8, x + cmp.l scp_active_slot + beq _scp_is_active + lda #0x00 + bra _scp_have_pal + + _scp_is_active: + lda #0x08 ; palette 2 (vanilla `lda #$08` in UpdateCharNames @a24c) + + _scp_have_pal: + sta.l scp_pal_byte + + ; base = $B966 + slot * 24 (per-slot stride confirmed via trace: + ; $32 mirror at slot*24+0 (6 tiles padding), $34 mirror at + ; slot*24+12 (6 tiles real name content)). + rep #0x20 + txa + and.w #0x000F + asl + asl + asl ; *8 + pha + asl ; *16 + clc + adc 1, s ; *16 + *8 = *24 + clc + adc.w #0xB966 + sta.b 0x32 + pla ; balance stack + sep #0x20 + ; Patch 6 entries on the ($32) mirror at offsets +1, +3, +5, +7, +9, +B + ldy.w #1 + jsr.w _scp_patch_six + ; Now patch the ($34) mirror at base + $0C + rep #0x20 + lda.b 0x32 + clc + adc.w #0x000C + sta.b 0x32 + sep #0x20 + ldy.w #1 + jsr.w _scp_patch_six + inx + bra _scp_slot_loop + + _scp_done: + rep #0x20 + pla + sta.b 0x32 + sep #0x20 + plb + plp + rts + + _scp_patch_six: + ; ($32) = row base in bank $7E. Y = first hi-byte offset. + ; Walks Y, Y+2, ..., Y+10. Masks `$E3`, ORs in `scp_pal_byte`. + phx + ldx.w #6 + + _scp_loop_six: + lda (0x32), y + and #0xE3 + ora.l scp_pal_byte + sta (0x32), y + iny + iny + dex + bne _scp_loop_six + plx + rts + + set_active_char_and_dirty: + """ + Writer-site shim for the active-char store (`sta $1822` at $03:A482, + followed by `sta $d0`). Original 5 bytes replaced by JSL + NOP. Shim + performs both stores then unconditionally marks cmd + names + + monsters dirty. The vanilla site fires only when the battle-menu + queue pops a new char (`check if menu needs to open` path in + `battle/char.asm:464`), so re-arming on every call is correct ; + a compare-against-current would mis-skip when the popped char + matches a stale value in $1822, leaving the prior cmd-window + tilemap on screen. + Caller has M=8, A holds the char index, DBR may differ from $7E. + """ + sep #0x20 + sta.l 0x7E1822 + sta.l 0x7E00D0 + pha + lda.l battle_menu_dirty + ora.b #CMD_DIRTY_BIT + sta.l battle_menu_dirty + ; Char-name palette patch (replaces the full names VWF re-render + ; that used to fire via REGION_DIRTY_NAMES on every rotation). + ; Walks the `$7E:B966` tilemap, rewrites the palette field of + ; each char's 6 max tiles on both pointer mirrors ; ~300 cycles + ; vs ~3M for the VWF re-render. Also flip names tilemap dirty + ; so the unified-DMA path uploads the patched tilemap to VRAM. + pla + pha + jsr.w set_active_char_palette + lda.l battle_render.tilemap_pending_mask + ora.b #battle_render.TILEMAP_PENDING_MAIN + sta.l battle_render.tilemap_pending_mask + pla + rtl +} diff --git a/src/battle/redraw_writer_patches.s b/src/battle/redraw_writer_patches.s index a06e1da..6d09c66 100644 --- a/src/battle/redraw_writer_patches.s +++ b/src/battle/redraw_writer_patches.s @@ -40,164 +40,177 @@ follow-up patches. ; monsters at $703C01 so the queue-gated init paths re-render after ; an ATB rotation. -*=0x03A482 - jsr.l set_active_char_and_dirty - .db 0xEA ; nop padding so $03:A487 still aligns to `jsr ValidateArrows` - -; --- Monster-death dirty hook: `UpdateDead` entry at $03:B1A0 --- -; Replaces the 4-byte prelude (`tdc; tax; stx $a9`) with a JSL to -; the bank-20 shim that ORs in REGION_DIRTY_MONSTERS, replays the -; prelude, and RTLs. Engine reaches B1A4 with identical state to -; vanilla. Fires every time the engine applies dead-status to a -; battle slot (post-attack, regen tick, etc.) ; the gated monster- -; name trampoline picks up the dirty bit on the next frame. - -*=0x03B1A0 - jsr.l mark_monsters_dirty_and_init - -; --- Phase 2: NMI-safe UpdateFlyingHDMA --- -; Vanilla `UpdateFlyingHDMA` ($02:82E1) spin-waits on the IRQ flag -; `$f353` at $02:82E8 (5 bytes: `lda $f353; beq -5`) to avoid HDMA -; mid-fetch tearing in main-loop context. In NMI/vblank the HDMA -; engine is idle, so the wait is safe to skip and required to -; avoid hanging when called from NMI (the IRQ won't fire while we're -; in vblank). NOP out the 5 bytes ; main-loop callers still work -; (just take the wait-loop hit one less time per frame). - -*=0x0282E8 - nop - nop - nop - nop - nop - -; --- Phase 5: deduplicate UpdateFlyingHDMA --- -; Main-loop `UpdateObjPos` ($02:82B9) calls UpdateFlyingHDMA at -; $02:82BC ; our NMI hook in `messages_vwf.dma_transfer` already -; fires it every vblank, so the main-loop call is redundant. -; NOP the 3-byte JSR to reclaim ~5K cycles/NMI. - -*=0x0282BC - nop - nop - nop - -; --- Bank-02 free-space pool: vanilla TfrEquipWindow body --- -; TfrEquipWindow was relocated to bank-20 (see items_patches.s) -; leaving $02:97AB..$02:9824 free. Pool-allocate our helpers here -; instead of hand-placing each via `*=` ; `strategy order` keeps -; symbols in declaration order so external `jsr.w`/`jsr.l` from -; bank-02 (sram_patches.s, message.s, RedrawMainMenu redirects, -; etc.) resolve to stable in-bank addresses. - -.pool bank02_battle_redraw_helpers { - range 0x0297AB 0x029824 - fill 0 - strategy order +.alloc at 0x03A482 { + jsr.l set_active_char_and_dirty + .db 0xEA ; nop padding so $03:A487 still aligns to `jsr ValidateArrows` + + ; --- Monster-death dirty hook: `UpdateDead` entry at $03:B1A0 --- + ; Replaces the 4-byte prelude (`tdc; tax; stx $a9`) with a JSL to + ; the bank-20 shim that ORs in REGION_DIRTY_MONSTERS, replays the + ; prelude, and RTLs. Engine reaches B1A4 with identical state to + ; vanilla. Fires every time the engine applies dead-status to a + ; battle slot (post-attack, regen tick, etc.) ; the gated monster- + ; name trampoline picks up the dirty bit on the next frame. +} +.alloc at 0x03B1A0 { + jsr.l mark_monsters_dirty_and_init + + ; --- Phase 2: NMI-safe UpdateFlyingHDMA --- + ; Vanilla `UpdateFlyingHDMA` ($02:82E1) spin-waits on the IRQ flag + ; `$f353` at $02:82E8 (5 bytes: `lda $f353; beq -5`) to avoid HDMA + ; mid-fetch tearing in main-loop context. In NMI/vblank the HDMA + ; engine is idle, so the wait is safe to skip and required to + ; avoid hanging when called from NMI (the IRQ won't fire while we're + ; in vblank). NOP out the 5 bytes ; main-loop callers still work + ; (just take the wait-loop hit one less time per frame). +} +.alloc at 0x0282E8 { + nop + nop + nop + nop + nop + + ; --- Phase 5: deduplicate UpdateFlyingHDMA --- + ; Main-loop `UpdateObjPos` ($02:82B9) calls UpdateFlyingHDMA at + ; $02:82BC ; our NMI hook in `messages_vwf.dma_transfer` already + ; fires it every vblank, so the main-loop call is redundant. + ; NOP the 3-byte JSR to reclaim ~5K cycles/NMI. +} +.alloc at 0x0282BC { + nop + nop + nop + + ; --- Bank-02 free-space pool: vanilla TfrEquipWindow body --- + ; TfrEquipWindow was relocated to bank-20 (see items_patches.s) + ; leaving $02:97AB..$02:9824 free. Pool-allocate our helpers here + ; instead of hand-placing each via `*=` ; `strategy order` keeps + ; symbols in declaration order so external `jsr.w`/`jsr.l` from + ; bank-02 (sram_patches.s, message.s, RedrawMainMenu redirects, + ; etc.) resolve to stable in-bank addresses. + + .pool bank02_battle_redraw_helpers { + range 0x0297AB 0x029824 + strategy order + } } - .alloc battle_redraw_helpers in bank02_battle_redraw_helpers { - ; --- Names + monster gated trampolines (slice 2, queue-side bits) --- - ; The init -> DrawText -> deinit pipeline still fires every frame so - ; battle_flags symmetry is preserved (other VWF callers like HP/MP - ; digits keep seeing a consistent dispatcher state). The gating is - ; INSIDE `init_*_gated`: it reads the region's clean bit from - ; `region_dirty_bits` at $703C01. If clean, it sets `render_skipped` - ; ($703C02) to $FF; the trampoline reads that and skips DrawText, and - ; `deinit_gated` skips the DMA-signal. The WRAM tile buffer is left - ; untouched, no DMA fires from this region's deinit, and the VRAM - ; tilemap persists from the previous render. - _msg_monster_window_gated: - """Gated DrawMonsterNames trampoline (slice-2 queue-side bit).""" +; --- Names + monster gated trampolines (slice 2, queue-side bits) --- +; The init -> DrawText -> deinit pipeline still fires every frame so +; battle_flags symmetry is preserved (other VWF callers like HP/MP +; digits keep seeing a consistent dispatcher state). The gating is +; INSIDE `init_*_gated`: it reads the region's clean bit from +; `region_dirty_bits` at $703C01. If clean, it sets `render_skipped` +; ($703C02) to $FF; the trampoline reads that and skips DrawText, and +; `deinit_gated` skips the DMA-signal. The WRAM tile buffer is left +; untouched, no DMA fires from this region's deinit, and the VRAM +; tilemap persists from the previous render. +msg_monster_window_gated: +"""Gated DrawMonsterNames trampoline (slice-2 queue-side bit).""" jsr.l messages_vwf.init_monsters_gated lda.l battle_render.render_skipped bne _mmwg_after_draw jsr 0xA455 - ; DrawText - _mmwg_after_draw: +; DrawText +_mmwg_after_draw: jsr.l messages_vwf.deinit_gated rts - ; --- Bank-02 trampoline for NMI-side UpdateFlyingHDMA --- - ; `dma_transfer` (bank-20) calls this via JSL ; trampoline JSR's - ; into UpdateFlyingHDMA ($02:82E1, phase-2 spin-wait nopped) and - ; RTLs back. Matches the JSL push/pop convention ; the vanilla - ; function ends in rts, our trampoline rtl's instead. - flying_hdma_trampoline: - """Bank-02 RTL wrapper around vanilla `UpdateFlyingHDMA` ($02:82E1).""" +; --- Bank-02 trampoline for NMI-side UpdateFlyingHDMA --- +; `dma_transfer` (bank-20) calls this via JSL ; trampoline JSR's +; into UpdateFlyingHDMA ($02:82E1, phase-2 spin-wait nopped) and +; RTLs back. Matches the JSL push/pop convention ; the vanilla +; function ends in rts, our trampoline rtl's instead. +flying_hdma_trampoline: +""" +Bank-02 RTL wrapper around vanilla `UpdateFlyingHDMA` ($02:82E1). + NMI caller leaves M=X=16 ; vanilla expects M=X=8 (main-loop + convention). Without forcing M=8 the routine reads/writes 16-bit + through `$ED4E`, `$1813`, and the buffer fill at `$7614,x`, so + every BG vscroll entry gets 0x0000 and the flying animation + freezes the moment our NMI dispatch takes over. +""" + + + php + rep #0x30 + lda.w #0x0000 + sep #0x20 jsr 0x82E1 + plp rtl - ; --- Battle-init seed: zero/seed all redraw-gate state at the - ; `Battle_ext` root entry ($03:8000). Runs exactly once per battle - ; (from field.asm:1005 `jsl Battle_ext` and menu.asm:163 - ; `jml Battle_ext` ; no other callers per ff4decomp). Earlier we - ; tried `InitMenuWindows` ($02:9A63) but that ran INSIDE the - ; UpdateCharNames per-char loop entry, and a 2nd-battle kss restore - ; mid-state could skip it entirely. The root entry is the only - ; correct seed point. - ; - ; Original `Battle_ext` was `jmp ExecBattle` (3 bytes). JML is 4 - ; bytes so we clobber 1 byte of `Battle_ext2` at $03:8003. Decomp - ; grep confirms zero callers of Battle_ext2 anywhere. - _battle_ext_seed: - """Battle_ext root tail: seed redraw-gate state then jump ExecBattle.""" +; --- Battle-init seed: zero/seed all redraw-gate state at the +; `Battle_ext` root entry ($03:8000). Runs exactly once per battle +; (from field.asm:1005 `jsl Battle_ext` and menu.asm:163 +; `jml Battle_ext` ; no other callers per ff4decomp). Earlier we +; tried `InitMenuWindows` ($02:9A63) but that ran INSIDE the +; UpdateCharNames per-char loop entry, and a 2nd-battle kss restore +; mid-state could skip it entirely. The root entry is the only +; correct seed point. +; +; Original `Battle_ext` was `jmp ExecBattle` (3 bytes). JML is 4 +; bytes so we clobber 1 byte of `Battle_ext2` at $03:8003. Decomp +; grep confirms zero callers of Battle_ext2 anywhere. +battle_ext_seed: +"""Battle_ext root tail: seed redraw-gate state then jump ExecBattle.""" jsr.l reset_queue_dirty_bits jmp.l 0x038009 - ; ExecBattle - ; --- DrawStatusText gate (hash-based) --- - ; RedrawMainMenu @96C8 = `jsr DrawStatusText` ; 9.33M cycles per 60f - ; (top remaining hitter after the cmd-window gate). Skip when char - ; status state is unchanged. Hash = XOR of `$2003+slot*$40` for the - ; 5 char slots (status 1 byte). Cached at `status_hash` ; first call - ; per battle always renders (cache initialized to 0 by Battle_ext - ; seed but state hash != 0 in normal play). Status flicker pulse - ; freezes when no status changes ; acceptable trade for ~9M cycles. - gate_draw_status_text: - """Bank-02 trampoline ; JSL gate_status_check, jmp $A2A1 on dirty, rts on clean.""" +; ExecBattle +; --- DrawStatusText gate (hash-based) --- +; RedrawMainMenu @96C8 = `jsr DrawStatusText` ; 9.33M cycles per 60f +; (top remaining hitter after the cmd-window gate). Skip when char +; status state is unchanged. Hash = XOR of `$2003+slot*$40` for the +; 5 char slots (status 1 byte). Cached at `status_hash` ; first call +; per battle always renders (cache initialized to 0 by Battle_ext +; seed but state hash != 0 in normal play). Status flicker pulse +; freezes when no status changes ; acceptable trade for ~9M cycles. +gate_draw_status_text: +"""Bank-02 trampoline ; JSL gate_status_check, jmp $A2A1 on dirty, rts on clean.""" jsr.l gate_status_check bcc _gdst_skip jmp 0xA2A1 - _gdst_skip: +_gdst_skip: rts - ; --- DrawObjNames gate (hash of monster slots + $1822) --- - ; Bank-20 body returns carry-set when re-render needed ; bank-02 - ; trampoline tail-jumps to vanilla DrawObjNames on dirty, rts on - ; clean. Earlier we noop'd UpdateMagicList ($02:96D4) to reclaim - ; 36 bytes for this gate's body, but that broke Rydia's summon - ; menu display (vanilla UpdateMagicList still needed for periodic - ; magic list refresh ; ImmediateMenuUpdate alone doesn't cover - ; all dispatch paths). Restored. - gate_draw_obj_names: - """Bank-02 trampoline ; JSL gate_obj_names_check, jmp $99D3 on dirty, rts on clean.""" +; --- DrawObjNames gate (hash of monster slots + $1822) --- +; Bank-20 body returns carry-set when re-render needed ; bank-02 +; trampoline tail-jumps to vanilla DrawObjNames on dirty, rts on +; clean. Earlier we noop'd UpdateMagicList ($02:96D4) to reclaim +; 36 bytes for this gate's body, but that broke Rydia's summon +; menu display (vanilla UpdateMagicList still needed for periodic +; magic list refresh ; ImmediateMenuUpdate alone doesn't cover +; all dispatch paths). Restored. +gate_draw_obj_names: +"""Bank-02 trampoline ; JSL gate_obj_names_check, jmp $99D3 on dirty, rts on clean.""" jsr.l gate_obj_names_check bcc _gdon_skip jmp 0x99D3 - _gdon_skip: +_gdon_skip: rts - ; --- Battle-init highlight stamp --- - ; Reclaim the 3-nop slot at $02:9A69 (previously vanilla `jsr - ; InitMagicListTextBuf` ; we noop'd that in magic/patches.s since - ; our two-column magic display drives its own buffer). Insert - ; `jsr walker_helper` so the active-char palette gets stamped - ; right after UpdateCharNames built $B966 ; subsequent init steps - ; (DrawHPText, DrawMonsterNames, etc.) target different WRAM - ; regions ($B9DE, $BB1E) so the stamp survives. - walker_helper: - """5-byte bank-02 trampoline: JSL bank-20 walker, RTS to caller.""" +; --- Battle-init highlight stamp --- +; Reclaim the 3-nop slot at $02:9A69 (previously vanilla `jsr +; InitMagicListTextBuf` ; we noop'd that in magic/patches.s since +; our two-column magic display drives its own buffer). Insert +; `jsr walker_helper` so the active-char palette gets stamped +; right after UpdateCharNames built $B966 ; subsequent init steps +; (DrawHPText, DrawMonsterNames, etc.) target different WRAM +; regions ($B9DE, $BB1E) so the stamp survives. +walker_helper: +"""5-byte bank-02 trampoline: JSL bank-20 walker, RTS to caller.""" jsr.l walker_rtl rts - _msg_names_window_gated: - """Gated DrawCharNames trampoline (slice-2 queue-side bit).""" +msg_names_window_gated: +"""Gated DrawCharNames trampoline (slice-2 queue-side bit).""" lda.b 0x4A and.b 0x04 bne _mnwg_done - ; inventory open -> skip whole pipeline +; inventory open -> skip whole pipeline jsr.l messages_vwf.init_names_gated lda.l battle_render.render_skipped bne _mnwg_after_draw jsr 0xA455 - _mnwg_after_draw: +_mnwg_after_draw: jsr.l messages_vwf.deinit_gated - _mnwg_done: +_mnwg_done: rts } ; end .alloc battle_redraw_helpers @@ -206,20 +219,21 @@ follow-up patches. ; Redirect `Battle_ext` entry to our seed helper. -*=0x038000 - jmp.l _battle_ext_seed - -; Redirect RedrawMainMenu's `jsr DrawStatusText` to our gate. - -*=0x0296C8 - jsr.w gate_draw_status_text - -; Redirect RedrawMainMenu's `jsr DrawObjNames` to our gate. +.alloc at 0x038000 { + jmp.l battle_ext_seed -*=0x0296CE - jsr.w gate_draw_obj_names + ; Redirect RedrawMainMenu's `jsr DrawStatusText` to our gate. +} +.alloc at 0x0296C8 { + jsr.w gate_draw_status_text -; Battle-init palette stamp (replaces noop'd InitMagicListTextBuf jsr). + ; Redirect RedrawMainMenu's `jsr DrawObjNames` to our gate. +} +.alloc at 0x0296CE { + jsr.w gate_draw_obj_names -*=0x029A69 - jsr.w walker_helper + ; Battle-init palette stamp (replaces noop'd InitMagicListTextBuf jsr). +} +.alloc at 0x029A69 { + jsr.w walker_helper +} diff --git a/src/battle/render_defs.i b/src/battle/render_defs.i new file mode 100644 index 0000000..9885976 --- /dev/null +++ b/src/battle/render_defs.i @@ -0,0 +1,27 @@ +""" +Shared battle_render WRAM-layout + bit constants. + +Compile-time definitions consumed by more than one module (message.s owns the +renderer; redraw_gates.s and the writer-site shims read the same dirty-bit and +tilemap-pending interface). Included inside `.scope battle_render { ... }` so +every consumer sees them as `battle_render.`. Constants don't link, so +sharing them via `.include` (not `.extern`) is the correct, link-free idiom. +""" + +; --- Per-region dirty bits (normal sense: 1 = dirty, 0 = clean) --- +; Sits next to the DMA queue byte; writers SET bits on state change. +region_dirty_bits = 0x703f01 +REGION_DIRTY_MESSAGES = 0x01 +REGION_DIRTY_MONSTERS = 0x02 +REGION_DIRTY_NAMES = 0x04 +REGION_DIRTY_COMMANDS = 0x08 + +; Transient marker: $FF if `init_*_with_gate` short-circuited because the +; region was clean; $00 if it ran the full init. +render_skipped = 0x703f02 + +; Per-region tilemap-DMA pending bitmask. Set by `init_*_gated` on the render +; path; consumed by `dma_transfer` in NMI to fire a per-region tilemap DMA. +tilemap_pending_mask = 0x703f03 +TILEMAP_PENDING_COMMANDS = 0x01 +TILEMAP_PENDING_MAIN = 0x02 diff --git a/src/battle/sram.s b/src/battle/sram.s index 6512167..cce3ccd 100644 --- a/src/battle/sram.s +++ b/src/battle/sram.s @@ -4,6 +4,7 @@ put_char/put_char_with_dakuten, `battle_display_char` jump-table dispatch and th helper. """ .include "src/battle/sram.i" +.include "../bank20.i" .import "assets" .import "dakuten" @@ -11,137 +12,142 @@ helper. BATTLE_DAKUTEN_TABLE = 0x16FA40 -.scope battle_flags { - """Battle-flags toggles for switching the message renderer between WRAM tiles and VWF.""" -set_vwf_render: -"""NOTE: set_sram_copy and clear_sram_copy removed - SRAM mode no longer used""" - battle_flags_set(0x02) - rtl -clear_vwf_render: - battle_flags_clear(0x02) - rtl -} +; root-scope extern for the included message.s (an .alloc body can't host one). +.extern flying_hdma_trampoline + +.alloc battle_sram_block in bank20_reloc { + .scope battle_flags { + """Battle-flags toggles for switching the message renderer between WRAM tiles and VWF.""" + set_vwf_render: + """NOTE: set_sram_copy and clear_sram_copy removed - SRAM mode no longer used""" + battle_flags_set(0x02) + rtl + clear_vwf_render: + battle_flags_clear(0x02) + rtl + } -copy_battle_char: -"""Copy a glyph + its dakuten companion from the SRAM staging area to the destination pair.""" - lda.l sram_base + 0x2E00, x - sta (0x00), y - lda.l sram_base + 0x2E00 + 0x30, x - sta (0x02), y - rtl - -.scope wram { - """WRAM-mode put-char primitives used by the original battle text renderer.""" -put_char: - phx - sta (0x34), y - lda #0xFF - sta (0x32), y - iny - lda 0x36 - sta (0x32), y - sta (0x34), y - iny - plx - rtl -put_char_with_dakuten: - phx - .if 0 { - sec - sbc #0xF - asl - tax - lda.l BATTLE_DAKUTEN_TABLE, x - sta (0x32), y - lda.l BATTLE_DAKUTEN_TABLE + 1, x - sta (0x34), y - } else { - jsr.l lookup_dakuten - sta (0x32), y - xba - sta (0x34), y - lda #0x00 - xba + copy_battle_char: + """Copy a glyph + its dakuten companion from the SRAM staging area to the destination pair.""" + lda.l sram_base + 0x2E00, x + sta (0x00), y + lda.l sram_base + 0x2E00 + 0x30, x + sta (0x02), y + rtl + + .scope wram { + """WRAM-mode put-char primitives used by the original battle text renderer.""" + put_char: + phx + sta (0x34), y + lda #0xFF + sta (0x32), y + iny + lda 0x36 + sta (0x32), y + sta (0x34), y + iny + plx + rtl + put_char_with_dakuten: + phx + .if 0 { + sec + sbc #0xF + asl + tax + lda.l BATTLE_DAKUTEN_TABLE, x + sta (0x32), y + lda.l BATTLE_DAKUTEN_TABLE + 1, x + sta (0x34), y + } else { + jsr.l lookup_dakuten + sta (0x32), y + xba + sta (0x34), y + lda #0x00 + xba + } + iny + lda 0x36 + sta (0x32), y + sta (0x34), y + iny + plx + rtl } - iny - lda 0x36 - sta (0x32), y - sta (0x34), y - iny - plx - rtl -} -; NOTE: sram scope removed - SRAM mode no longer used -; .scope sram { put_char, put_char_with_dakuten } + ; NOTE: sram scope removed - SRAM mode no longer used + ; .scope sram { put_char, put_char_with_dakuten } + + battle_display_char: + """ + Dispatch a fixed-mode char draw to either the WRAM put_char or the messages_vwf renderer based on the active + battle_flags. + """ + { + battle_flag_switch(battle_flags_jump_table) + battle_flags_jump_table: + .dw wram.put_char ; index 0 (flags = 0) + .dw wram.put_char ; index 2 (flags = 1) - fallback to WRAM + .dw messages_vwf.put_fixed_char_dakuten_far ; index 4 (flags = 2) + .dw messages_vwf.put_fixed_char_dakuten_far ; index 6 (flags = 3) - fallback + } -battle_display_char: -""" -Dispatch a fixed-mode char draw to either the WRAM put_char or the messages_vwf renderer based on the active -battle_flags. -""" -{ - battle_flag_switch(battle_flags_jump_table) -battle_flags_jump_table: - .dw wram.put_char ; index 0 (flags = 0) - .dw wram.put_char ; index 2 (flags = 1) - fallback to WRAM - .dw messages_vwf.put_fixed_char_dakuten_far ; index 4 (flags = 2) - .dw messages_vwf.put_fixed_char_dakuten_far ; index 6 (flags = 3) - fallback -} + battle_display_dakuten_char: + """ + Dakuten-aware variant of `battle_display_char`: routes to the dakuten put_char or to messages_vwf depending on + battle_flags. + """ + { + battle_flag_switch(battle_flags_jump_table) + battle_flags_jump_table: + .dw wram.put_char_with_dakuten ; index 0 (flags = 0) + .dw wram.put_char_with_dakuten ; index 2 (flags = 1) - fallback to WRAM + .dw messages_vwf.put_fixed_char_no_dakuten_far ; index 4 (flags = 2) + .dw messages_vwf.put_fixed_char_no_dakuten_far ; index 6 (flags = 3) - fallback + } -battle_display_dakuten_char: -""" -Dakuten-aware variant of `battle_display_char`: routes to the dakuten put_char or to messages_vwf depending on -battle_flags. -""" -{ - battle_flag_switch(battle_flags_jump_table) -battle_flags_jump_table: - .dw wram.put_char_with_dakuten ; index 0 (flags = 0) - .dw wram.put_char_with_dakuten ; index 2 (flags = 1) - fallback to WRAM - .dw messages_vwf.put_fixed_char_no_dakuten_far ; index 4 (flags = 2) - .dw messages_vwf.put_fixed_char_no_dakuten_far ; index 6 (flags = 3) - fallback + _sink: + rtl + + clear_names_window_buffer: + """ + Fill the names-window WRAM tilemap buffer with $FF (transparent / blank tile) starting at the address held in + $EF52. + """ + phx + phy + rep #0x20 + ldy.w #0 + ldx.w 0xef52 + + _clear_name_loop: + lda.w #0x00ff + sta.l 0x7e0000, x + sta.l 0x7e0002, x + sta.l 0x7e0004, x + sta.l 0x7e0006, x + sta.l 0x7e0008, x + sta.l 0x7e000a, x + + txa + clc + adc.w #6 * 2 + tax + iny + tya + cmp.w #5 * 2 + + bne _clear_name_loop + + + sep #0x20 + ply + plx + tdc + sta 0x74FC, y + rtl + + .include "src/battle/message.s" } - -_sink: - rtl - -clear_names_window_buffer: -""" -Fill the names-window WRAM tilemap buffer with $FF (transparent / blank tile) starting at the address held in -$EF52. -""" - phx - phy - rep #0x20 - ldy.w #0 - ldx.w 0xef52 - -_clear_name_loop: - lda.w #0x00ff - sta.l 0x7e0000, x - sta.l 0x7e0002, x - sta.l 0x7e0004, x - sta.l 0x7e0006, x - sta.l 0x7e0008, x - sta.l 0x7e000a, x - - txa - clc - adc.w #6 * 2 - tax - iny - tya - cmp.w #5 * 2 - - bne _clear_name_loop - - - sep #0x20 - ply - plx - tdc - sta 0x74FC, y - rtl - - .include "src/battle/message.s" diff --git a/src/battle/sram_patches.s b/src/battle/sram_patches.s index a98e2b7..c23fcbc 100644 --- a/src/battle/sram_patches.s +++ b/src/battle/sram_patches.s @@ -2,14 +2,15 @@ ROM patches that route every battle-text draw call (commands, monster names, char names, attack names, message window) through our messages-VWF init/deinit trampolines. """ +.include "config.i" .extern draw_command_list_for_character .extern battle_display_char .extern battle_display_dakuten_char .extern clear_names_window_buffer .extern battle_render .extern messages_vwf -.extern _msg_monster_window_gated -.extern _msg_names_window_gated +.extern msg_monster_window_gated +.extern msg_names_window_gated .extern gated_clear_names_window_buffer ; inventory buffer @@ -38,19 +39,19 @@ window) through our messages-VWF init/deinit trampolines. ; nop ; disable dakuten check ? -*=0x02A497 +.alloc at 0x02A497 { cmp #0xA0 bcs 0x02A4AC ; patch normal display_char to include 7FFFFF based switch - -*=0x02A49B +} +.alloc at 0x02A49B { jsr.l battle_display_char rts ; patch normal display_dakuten_char to include 7FFFFF based switch - -*=0x02A4AC +} +.alloc at 0x02A4AC { jsr.l battle_display_dakuten_char rts @@ -65,10 +66,10 @@ window) through our messages-VWF init/deinit trampolines. ; patches show attack window - -*=0x02c99c + 4 +} +.alloc at 0x02c99c + 4 { .dw attack_names - +} ; disable menu forced update every loop that might be too much ;*=0x028230 @@ -78,31 +79,44 @@ window) through our messages-VWF init/deinit trampolines. .if BATTLE_MONSTERS_VWF { ;; monster names vwf try but being clear at every monster ;; needs a way to have immortal renders and temporary ones (used only for a few instants) - *=0x02a40d - jsr.w _msg_monster_window_gated + .alloc at 0x02a40d { + jsr.w msg_monster_window_gated + } + - *=0x029486 + 12 + .alloc at 0x029486 + 12 { .dw 0x949a ; noop for monster names + } } .if BATTLE_NAMES_VWF { ; this gets redrawn quite often ; char names - *=0x02A29D - jsr.w _msg_names_window_gated + .alloc at 0x02A29D { + jsr.w msg_names_window_gated ; wait frame runs a shite load of updates - *=0x029486 + 2 + } + + + .alloc at 0x029486 + 2 { .dw 0x949a ; noop for char names + } - *=0x0296c0 + + .alloc at 0x0296c0 { .dw 0x949a ; noop for periodic names update + } + - *=0x0296b0 + 16 + .alloc at 0x0296b0 + 16 { .dw 0x949a ; noop for periodic names update + } + - *=0x02A299 + .alloc at 0x02A299 { jsr.l gated_clear_names_window_buffer + } } ; that's battle graphics 0xf that's a wait frame ;*=0x028517 @@ -127,14 +141,15 @@ window) through our messages-VWF init/deinit trampolines. .if BATTLE_NAMES_VWF + BATTLE_MONSTERS_VWF + BATTLE_CMD_VWF > 0 { ; patches the newline control code handler to clear bitsleft on the current tile, ; allowing the monster string to be rendered. - *=0x02a637 + .alloc at 0x02a637 { jsr.l messages_vwf.new_line_escape_code_handler rts + } } .if BATTLE_CMD_VWF { - *=0x0296b0 + 2 + .alloc at 0x0296b0 + 2 { .dw 0x949a ; noop for periodic cmd window ;*=0x029CBF @@ -142,24 +157,30 @@ window) through our messages-VWF init/deinit trampolines. ; rts ; nukes the draw all command list (pre renders all the windows) - *=0x029ca1 + } + + + .alloc at 0x029ca1 { rts ; always use the same buffer for all chars command list, ; the buffer shall be updated if before the window is opened ; due to those updates being quite costly now, we may want to avoid to re render too often + } - *=0x0299f1 + + .alloc at 0x0299f1 { pha lda #0 nop + } + - *=0x029989 + .alloc at 0x029989 { jsr.w draw_window_render_hook + } } - - -*=0x02FFC2 +.alloc at 0x02FFC2 { draw_window_render_hook: """Battle-command-window render hook: render command list, then load X with original loop count.""" jsr.w _draw_command_list @@ -176,7 +197,7 @@ msg_monster_window_trampoline: bra _draw_text_battle _msg_names_window_trampoline: - ; Skip names rendering if inventory is active (bit 2 of $4A) +; Skip names rendering if inventory is active (bit 2 of $4A) lda 0x4A and #0x04 bne _skip_names @@ -195,7 +216,7 @@ _draw_text_battle: jsr.l messages_vwf.deinit rts -_draw_text_battle_far: +draw_text_battle_far: jsr.w _draw_text_battle rtl @@ -205,7 +226,9 @@ attack_names: jsr 0xcb32 jsr.l messages_vwf.deinit rts -{ + { _end: .debug '{_end} < 0x02FFFF ?' + } } + diff --git a/src/dakuten.s b/src/dakuten.s index 756dafc..eb15f03 100755 --- a/src/dakuten.s +++ b/src/dakuten.s @@ -1,17 +1,21 @@ """ +.include "bank20.i" + Dakuten composite-glyph table + `lookup_dakuten` helper that maps a (prev_char, current_char) pair to the dakuten/handakuten composite tile pair. """ -.incbin "assets/dakuten.bin" - -lookup_dakuten: +.include "bank20.i" +.import "assets" +.alloc lookup_dakuten in bank20_reloc { """ -input: A 8bit: current char -output: A 16bits: the resolved char + input: A 8bit: current char + output: A 16bits: the resolved char """ -{ + + + { php sep #0x20 cmp.l assets_dakuten_bin @@ -39,10 +43,10 @@ _char_out_of_range: _exit: plp rtl -} + } _store_char_with_dakuten: -{ + { jsr.l lookup_dakuten cmp #0xff beq _skip @@ -56,4 +60,6 @@ _skip: ;0187AE E8 INX ;0187AF C8 INY rtl + } } + diff --git a/src/dialog.s b/src/dialog.s index 6c4544c..85dadc5 100644 --- a/src/dialog.s +++ b/src/dialog.s @@ -3,199 +3,206 @@ Dialog-text plumbing: 24-bit pointer resolver for translated bank 1-1/1-2/2 stri parser to load the next message. """ .include "src/vwf.i" +.include "bank20.i" .import "assets" .extern dialog_bank_ptr_base -get_bank1_1_pointer: -"""Get a 24-bit dialog pointer for bank 1-1.""" - rep #0x20 - lda.l assets_bank1_1_ptr, x - sta.b dialog_ptr - lda.w #0x0000 - sep #0x20 - lda.l assets_bank1_1_ptr + 2, x - sta.b dialog_ptr + 2 - lda.b #0x01 - rtl - -get_bank1_2_pointer: -""" -Get a 24-bit dialog pointer for bank 1-2. - -> Note: bank 1-1 is only 0x100 pointers long, not 0x200 as the text dump suggests. -""" - - rep #0x20 - lda.l assets_bank1_1_ptr + 0x300, x - sta.b dialog_ptr - lda.w #0x0000 - sep #0x20 - lda.l assets_bank1_1_ptr + 0x300 + 2, x - sta.b dialog_ptr + 2 - lda #0x01 - rtl - -get_bank3_pointer: -""" -Compute pointer for NPC dialogs. - -Organized per room ; a linear lookup inside the room finds the start of the string. -""" - - rep #0x20 - lda.l dialog_bank_ptr_base + 0x600, x - sta.b dialog_ptr - lda.w #0x0000 - sep #0x20 - lda.l dialog_bank_ptr_base + 0x600 + 2, x - sta.b dialog_ptr + 2 - lda #0x02 - rtl - -compute_dialog_text_offset: -"""Compute index into dialog pointer table from current text id ($B2).""" - lda.b 0xB2 - sta.b dialog_ptr - stz.b dialog_ptr + 1 - rep #0x20 - lda.b dialog_ptr - clc - asl - adc.b dialog_ptr - tax - sep #0x20 - rtl - -get_bank2_pointer: -""" -Get a 24-bit dialog pointer for bank 2. - -Walks the string character-by-character to handle variable-length encoding. -""" -{ - rep #0x20 - lda.b dialog_ptr - asl - clc - adc.b dialog_ptr - tax - lda.l assets_bank2_ptr, x - sta.b dialog_ptr - lda.w #0x0000 - sep #0x20 - lda.l assets_bank2_ptr + 2, x - sta.b dialog_ptr + 2 - ldx.b dialog_ptr - lda.b 0xB2 - beq _end - tay - -_loop: - jsr.w _load_letter_inc - bne _loop - jsr.w _load_letter_dec - pha - jsr.w _load_letter_inc - pla - cmp #0x03 - beq _loop - pha - pla - cmp #0x04 - beq _loop - cmp #0xfe - beq _loop - dey - bne _loop - inx - -_end: - stx.w 0x0772 - stz.b 0xDD - rtl - -_load_letter_dec: - ldx.b dialog_ptr - dex - bmi _ok - dec.b dialog_ptr + 2 - ldx.w #0xFFFF - bra _ok - -_load_letter_inc: - ldx.b dialog_ptr - inx - bmi _ok - inc.b dialog_ptr + 2 - ldx.w #0x8000 - -_ok: - stx.b dialog_ptr - -_load_letter: - ldx.b dialog_ptr - phb - lda.b dialog_ptr + 2 - pha - plb - lda.w 0x0000, x - plb - pha - pla - rts +.alloc dialog_block in bank20_reloc { + get_bank1_1_pointer: + """Get a 24-bit dialog pointer for bank 1-1.""" + rep #0x20 + lda.l assets_bank1_1_ptr, x + sta.b dialog_ptr + lda.w #0x0000 + sep #0x20 + lda.l assets_bank1_1_ptr + 2, x + sta.b dialog_ptr + 2 + lda.b #0x01 + rtl + + get_bank1_2_pointer: + """ + Get a 24-bit dialog pointer for bank 1-2. + + > Note: bank 1-1 is only 0x100 pointers long, not 0x200 as the text dump suggests. + """ + + + rep #0x20 + lda.l assets_bank1_1_ptr + 0x300, x + sta.b dialog_ptr + lda.w #0x0000 + sep #0x20 + lda.l assets_bank1_1_ptr + 0x300 + 2, x + sta.b dialog_ptr + 2 + lda #0x01 + rtl + + get_bank3_pointer: + """ + Compute pointer for NPC dialogs. + + Organized per room ; a linear lookup inside the room finds the start of the string. + """ + + + rep #0x20 + lda.l dialog_bank_ptr_base + 0x600, x + sta.b dialog_ptr + lda.w #0x0000 + sep #0x20 + lda.l dialog_bank_ptr_base + 0x600 + 2, x + sta.b dialog_ptr + 2 + lda #0x02 + rtl + + compute_dialog_text_offset: + """Compute index into dialog pointer table from current text id ($B2).""" + lda.b 0xB2 + sta.b dialog_ptr + stz.b dialog_ptr + 1 + rep #0x20 + lda.b dialog_ptr + clc + asl + adc.b dialog_ptr + tax + sep #0x20 + rtl + + get_bank2_pointer: + """ + Get a 24-bit dialog pointer for bank 2. + + Walks the string character-by-character to handle variable-length encoding. + """ + + + { + rep #0x20 + lda.b dialog_ptr + asl + clc + adc.b dialog_ptr + tax + lda.l assets_bank2_ptr, x + sta.b dialog_ptr + lda.w #0x0000 + sep #0x20 + lda.l assets_bank2_ptr + 2, x + sta.b dialog_ptr + 2 + ldx.b dialog_ptr + lda.b 0xB2 + beq _end + tay + + _loop: + jsr.w _load_letter_inc + bne _loop + jsr.w _load_letter_dec + pha + jsr.w _load_letter_inc + pla + cmp #0x03 + beq _loop + pha + pla + cmp #0x04 + beq _loop + cmp #0xfe + beq _loop + dey + bne _loop + inx + + _end: + stx.w 0x0772 + stz.b 0xDD + rtl + + _load_letter_dec: + ldx.b dialog_ptr + dex + bmi _ok + dec.b dialog_ptr + 2 + ldx.w #0xFFFF + bra _ok + + _load_letter_inc: + ldx.b dialog_ptr + inx + bmi _ok + inc.b dialog_ptr + 2 + ldx.w #0x8000 + + _ok: + stx.b dialog_ptr + + _load_letter: + ldx.b dialog_ptr + phb + lda.b dialog_ptr + 2 + pha + plb + lda.w 0x0000, x + plb + pha + pla + rts + } + + + _incpointer: + { + phx + ldx.w 0x0772 + inx + bne _no_overflow + inc.b dialog_ptr + 2 + ldx.w #0x8000 + + _no_overflow: + stx.w 0x0772 + plx + rts + } + + + load_letter_inc: + """Advance dialog cursor by one character.""" + { + ldx.w 0x0772 + inx + cpx.w #0x0000 + bne _no_overflow + inc.b dialog_ptr + 2 + ldx.w #0x8000 + + _no_overflow: + stx.w 0x0772 + } + + + load_letter: + """Peek the current character from the dialog stream into CURRENT_C.""" + ldx.w 0x0772 + phb + lda.b dialog_ptr + 2 + pha + plb + lda.b #0x00 + xba + lda.b #0x00 + rep #0x20 + lda.w 0x0000, x + sta.b CURRENT_C + + sep #0x20 + plb + pha + pla + + rts } - - -_incpointer: -{ - phx - ldx.w 0x0772 - inx - bne _no_overflow - inc.b dialog_ptr + 2 - ldx.w #0x8000 - -_no_overflow: - stx.w 0x0772 - plx - rts -} - - -load_letter_inc: -"""Advance dialog cursor by one character.""" -{ - ldx.w 0x0772 - inx - cpx.w #0x0000 - bne _no_overflow - inc.b dialog_ptr + 2 - ldx.w #0x8000 - -_no_overflow: - stx.w 0x0772 -} - - -load_letter: -"""Peek the current character from the dialog stream into CURRENT_C.""" - ldx.w 0x0772 - phb - lda.b dialog_ptr + 2 - pha - plb - lda.b #0x00 - xba - lda.b #0x00 - rep #0x20 - lda.w 0x0000, x - sta.b CURRENT_C - - sep #0x20 - plb - pha - pla - - rts diff --git a/src/ingame/credits.s b/src/ingame/credits.s index e0734ac..e8d3571 100644 --- a/src/ingame/credits.s +++ b/src/ingame/credits.s @@ -3,19 +3,20 @@ In-place patches for the staff credits screen: re-point the credits text loader `assets_credits_text_bin` block. """ -*=0x13d7ef - ldx.w #assets_credits_text_bin & 0xffff +.alloc at 0x13d7ef { + ldx.w #assets_credits_text_bin & 0xffff +} +.alloc at 0x13d7f5 { + lda.b #assets_credits_text_bin >> 16 -*=0x13d7f5 - lda.b #assets_credits_text_bin >> 16 - -; Augments cutscene duration to show the additional text. - -*=0x13d61d - lda.b #0x20 - -*=0x13d623 - lda.b #0x0b - -*=0x13f016 -.incbin "assets/the_end_gfx.bin" + ; Augments cutscene duration to show the additional text. +} +.alloc at 0x13d61d { + lda.b #0x20 +} +.alloc at 0x13d623 { + lda.b #0x0b +} +.alloc at 0x13f016 { + .incbin "assets/the_end_gfx.bin" +} diff --git a/src/ingame/drops_rolling.s b/src/ingame/drops_rolling.s index 3e434c3..a70c058 100644 --- a/src/ingame/drops_rolling.s +++ b/src/ingame/drops_rolling.s @@ -7,7 +7,7 @@ Drops are the 8-item array at $7E:FF28 shown in the upper window of the treasure menu (Tente / Baguette / etc.). Original renders them as a 4-row x 2-col grid via DrawTreasureList ($01:A15C). Item names in French don't fit two columns, so drops moves to a single-column -rolling buffer matching the treasure inventory below it — engine +rolling buffer matching the treasure inventory below it - engine configured with 4 visible / 8 total / 5 buffer slots. The drops buffer lives on BG3 alongside the treasure inventory ; both @@ -39,7 +39,6 @@ State RAM layout (12 bytes from $1BE0, struct: RollingBufferState): $1BEE hdma_copy_pending """ - DROPS_VISIBLE_ITEMS := 5 DROPS_BUFFER_SLOTS := 6 DROPS_TOTAL_ITEMS := 8 @@ -47,25 +46,17 @@ DROPS_SCROLL_LIMIT := 3 DROPS_SCROLL_PIXELS_PER_FRAME := 8 DROPS_SCROLL_TOTAL_PIXELS := 16 -drops_rolling := 0x1BE0 -drops_rolling_top_row := drops_rolling + RollingBufferState.top_row -drops_rolling_buffer_pos := drops_rolling + RollingBufferState.buffer_pos -drops_rolling_edge_row := drops_rolling + RollingBufferState.edge_row -drops_rolling_slot_index := drops_rolling + RollingBufferState.slot_index -drops_rolling_base_scroll := drops_rolling + RollingBufferState.base_scroll -drops_hdma_enable := drops_rolling + RollingBufferState.hdma_enable -drops_scroll_state := drops_rolling + RollingBufferState.scroll_state -drops_scroll_remaining := drops_rolling + RollingBufferState.scroll_remaining -drops_scroll_direction := drops_rolling + RollingBufferState.scroll_direction -drops_transfer_pending := drops_rolling + RollingBufferState.transfer_pending -drops_scroll_anim_offset := drops_rolling + RollingBufferState.scroll_anim_offset -drops_hdma_copy_pending := drops_rolling + RollingBufferState.hdma_copy_pending +; Drops rolling state moved out of $1B00-$1BFF (vanilla menu / sprite +; code writes to bytes past $1BEB) to clean $7E:9C30. Engine path needs +; the full 35-byte struct ; the macro path only ever touched the first +; 12 bytes so the original $1BE0 base worked there. +drops_rolling := (0x7E9C30 as RollingBufferState) ; Drops scroll position lives one byte past the state block so it ; doesn't collide with the engine's RollingBufferState fields. Other ; profiles read scroll_pos from a original menu byte ($1B1A field / ; $1BB7 treasure inventory); drops has no original equivalent. -drops_scroll_pos := 0x1BEF +drops_scroll_pos := 0x9C5F ; HDMA channel 4 (free in original treasure: enabled mask is $AD = ; ch7|ch5|ch3|ch2|ch0). Treasure inventory took ch6. @@ -89,14 +80,20 @@ DROPS_HDMA_BANK := 0x7E DROPS_SCROLL_STATE_IDLE := 0 DROPS_SCROLL_STATE_SCROLLING := 1 - ; --- Profile hooks (stubs, real implementations land alongside the ; drops geometry + tilemap layout work) ----------------------------------- +.include "../bank20.i" + +.alloc drops_rolling_block in bank20_reloc { drops_ensure_hdma_initialized: """Lazy init: pin BG4VOFS shadow to 0 + configure ch4 driving BG4VOFS on first scroll.""" +; Long addressing on every state read/write - engine `_engine_call_hook` +; jumps in with DB unchanged from the vanilla caller (DB=$00), so abs +; reads would hit ROM and the gate would always read "not $FFFF" → +; bail. Same fix shape as treasure_ensure_hdma_initialized. rep #0x20 - lda.w drops_rolling_base_scroll + lda.l drops_rolling.base_scroll cmp.w #0xFFFF bne _drops_hdma_already_init @@ -104,17 +101,17 @@ drops_ensure_hdma_initialized: ; row 0 = BG line 0. The HDMA header band offsets BG4VOFS so the ; window appears on screen at y=24 (matching where the original ; TreasureItemsWindow at $01:E275 lived). Items render at staging -; rows 4,6,8,10 — engine body math uses base_scroll=0 so screen +; rows 4,6,8,10 - engine body math uses base_scroll=0 so screen ; y=32..95 reads exactly those rows. lda.w #0xFFE8 - ; -24: drops band starts at screen y=24. Items render at staging - ; rows 2,4,6,8 (DrawItemName lays glyphs into the row pointed at by - ; $1d = $29 + $40, so slot 0 with Y=$44 lands at row 2, not row 1). - ; Engine body math `scroll = base + slot*16 - row*16` collapses to - ; base for visible row 0..3 with buffer_pos=0; VOFS=-24 maps screen - ; y=32..95 to BG lines 8..71. Item 0 at BG row 2 = lines 16..23 lies - ; in the upper half of the body row 0 band (y=32..47). - sta.w drops_rolling_base_scroll +; -24: drops band starts at screen y=24. Items render at staging +; rows 2,4,6,8 (DrawItemName lays glyphs into the row pointed at by +; $1d = $29 + $40, so slot 0 with Y=$44 lands at row 2, not row 1). +; Engine body math `scroll = base + slot*16 - row*16` collapses to +; base for visible row 0..3 with buffer_pos=0; VOFS=-24 maps screen +; y=32..95 to BG lines 8..71. Item 0 at BG row 2 = lines 16..23 lies +; in the upper half of the body row 0 band (y=32..47). + sta.l drops_rolling.base_scroll ; Configure HDMA channel 4: DIRECT mode 2 bytes, dest BG4VOFS ($2114), ; src $7E:9880 (drops active table). @@ -130,19 +127,19 @@ drops_ensure_hdma_initialized: lda #DROPS_HDMA_BANK sta.l DROPS_HDMA4_SRC_BANK -; Enable ch4 (BG4VOFS) only. TM HDMA mask via ch1 disabled — writes +; Enable ch4 (BG4VOFS) only. TM HDMA mask via ch1 disabled - writes ; to $212C per-scanline blank the screen for reasons not yet ; understood (probably PPU/HDMA timing quirk in BGMODE 0). For now ; rely on BG4 staging being zero-filled past the drops content (tile ; 0 = transparent if CHR slot 0 is blank); residual bleed is the ; trade-off. - lda.l 0x7E1BAE + lda.l field_menu_rolling.hdma_enable ora #0x10 - sta.l 0x7E1BAE - sta.w drops_hdma_enable - ; Build the initial HDMA shadow table + signal the NMI shadow→active - ; copy so ch4 has valid scroll values on the very first frame, not - ; just after the first scroll-trigger update. + sta.l field_menu_rolling.hdma_enable + sta.l drops_rolling.hdma_enable +; Build the initial HDMA shadow table + signal the NMI shadow→active +; copy so ch4 has valid scroll values on the very first frame, not +; just after the first scroll-trigger update. jsr.w update_drops_scroll_hdma rts @@ -175,14 +172,14 @@ drops_render_item_to_slot: lda.b 0xDB pha rep #0x20 - ; Pin $29 = $C600 (BG4 staging). TreasureItemsWindow is already - ; drawn on BG4 by original at $01:D817, so rendering drops items - ; INTO that same BG4 frame keeps the layout self-contained: one - ; window + items on one layer, one DMA path to VRAM. +; Pin $29 = $C600 (BG4 staging). TreasureItemsWindow is already +; drawn on BG4 by original at $01:D817, so rendering drops items +; INTO that same BG4 frame keeps the layout self-contained: one +; window + items on one layer, one DMA path to VRAM. lda.w #0xC600 sta.b 0x29 sep #0x20 - lda.w drops_rolling_edge_row + lda.w drops_rolling.edge_row asl clc adc #0x28 @@ -201,10 +198,33 @@ drops_render_item_to_slot: stz.b 0x34 pla jsr.l check_can_use_item_trampoline - lda.w drops_rolling_slot_index +; --- VWF tile-id offset for drops (disjoint from treasure inventory) --- +; `items_menu_vwf.draw_field_item_name` computes +; `tile_id_base = FIELD_ITEM_VWF_TILE_BASE + $5D * K`. Treasure inventory +; calls the same helper with $5D = treasure_rolling.slot_index ; if drops were +; to pass its raw slot_index too, both panels would land on the same +; tile_ids in BG3 CHR ($5000..$56E0) and the second renderer of each +; frame would overwrite the first one's glyphs (the garbage-tiles bug +; visible in the chest-with-monster-drop screen). Offset by 11 so +; drops slots 0..5 land at tile_ids $16E..$1A9 (CHR $56E0..$5A90), +; past treasure's 6-buffer-slot region at $100..$13B. drops's tilemap +; target stays at $7E:C600 (its own BG3 staging surface) so the offset +; only affects CHR allocation, not where the glyphs land on screen. +; +; VWF_CALLER_CTX=1 also tells items_menu_vwf to write the SECONDARY +; flush descriptor (DROPS_VWF_VRAM_DEST_WORD + DROPS_VWF_BYTE_COUNT + +; DROPS_VWF_CHR_SRC_OFFSET) and redirect VWF_CHR_DIRTY -> DIRTY_B so +; the NMI flush hits drops's VRAM range without trampling treasure's +; primary descriptor. Cleared after the render so subsequent +; treasure-side calls fall back to primary. + lda.w drops_rolling.slot_index + clc + adc #DROPS_VWF_TILE_SLOT_OFFSET sta.b 0x5d + lda #0x01 + sta.l VWF_CALLER_CTX rep #0x20 - lda.w drops_rolling_slot_index + lda.w drops_rolling.slot_index and.w #0x00FF xba lsr @@ -213,6 +233,8 @@ drops_render_item_to_slot: tay sep #0x20 jsr.l draw_item_slot_inner_trampoline + lda #0x00 + sta.l VWF_CALLER_CTX pla sta.b 0xDB pla @@ -240,16 +262,96 @@ clear_drops_slot: update_drops_scroll_hdma: """Build the drops HDMA shadow table via the shared engine.""" - engine_update_scroll_hdma(drops_rolling, DROPS_HDMA_SHADOW, DROPS_BUFFER_SLOTS, DROPS_VISIBLE_ITEMS, _drops_hdma_header, _drops_hdma_footer, _drops_hdma_signal) ; noqa: E501 +; Build drops HDMA scroll table. Inlined from the former +; engine_update_scroll_hdma macro for the same reason as treasure. + { + php + rep #0x30 + pha + phx + phy + lda.b 0x40 + pha + lda.b 0x42 + pha + ldx.w #0x0000 + jsr.w _drops_hdma_header + stz.b 0x42 + +_row_loop: + lda.w drops_rolling + RollingBufferState.buffer_pos + and.w #0x00FF + clc + adc.b 0x42 + +_mod_loop: + cmp.w #DROPS_BUFFER_SLOTS + bcc _mod_done + sec + sbc.w #DROPS_BUFFER_SLOTS + bra _mod_loop + +_mod_done: + asl + asl + asl + asl + sta.b 0x40 + lda.b 0x42 + and.w #0x00FF + asl + asl + asl + asl + eor.w #0xFFFF + inc + clc + adc.b 0x40 + clc + adc.w drops_rolling + RollingBufferState.base_scroll + sta.b 0x40 + sep #0x20 + lda #16 + sta.l DROPS_HDMA_SHADOW, x + inx + rep #0x20 + lda.b 0x40 + sta.l DROPS_HDMA_SHADOW, x + inx + inx + rep #0x20 + inc.b 0x42 + lda.b 0x42 + cmp.w #DROPS_VISIBLE_ITEMS + bcs _row_loop_done + jmp.w _row_loop + +_row_loop_done: + jsr.w _drops_hdma_footer + sep #0x20 + lda #0x00 + sta.l DROPS_HDMA_SHADOW, x + jsr.w _drops_hdma_signal + rep #0x20 + pla + sta.b 0x42 + pla + sta.b 0x40 + ply + plx + pla + plp + rts + } _drops_hdma_header: -"""Header: 32 lines at base (-24) — off-screen 24 lines + 8 lines top border.""" +"""Header: 32 lines at base (-24) - off-screen 24 lines + 8 lines top border.""" sep #0x20 lda #32 sta.l DROPS_HDMA_SHADOW, x inx rep #0x20 - lda.w drops_rolling_base_scroll + lda.w drops_rolling.base_scroll sta.l DROPS_HDMA_SHADOW, x inx inx @@ -257,31 +359,30 @@ _drops_hdma_header: _drops_hdma_footer: """ -Footer: 25 lines bottom border + 87 lines trail keeping BG4 off content. + Footer: 25 lines bottom border + 87 lines trail keeping BG4 off content. -Header (32) + body (5 visible × 16 = 80) + footer (25 + 87 = 112) = 224 visible scanlines. + Header (32) + body (5 visible × 16 = 80) + footer (25 + 87 = 112) = 224 visible scanlines. -The border zone runs 25 lines (vs the natural 16) to step the trail's starting BG line past -the bottom-border row. With the border zone @ -8, scanlines y=112..136 read BG rows 13..15 -(border + 2 blank padding rows). The trail @ -24 then starts at y=137, reading BG row 14 -— past the border row 13 — so the border tile doesn't get repeated for several scanlines as -the trail kicks in. + The border zone runs 25 lines (vs the natural 16) to step the trail's starting BG line past + the bottom-border row. With the border zone @ -8, scanlines y=112..136 read BG rows 13..15 + (border + 2 blank padding rows). The trail @ -24 then starts at y=137, reading BG row 14 + - past the border row 13 - so the border tile doesn't get repeated for several scanlines as + the trail kicks in. -The +1 odd count (25 vs even) shifts the trail boundary to y=137 instead of y=136. ch6 -(treasure BG3VOFS) has a band boundary at y=136 (border-to-body) ; the +1 keeps ch4 (drops) -and ch6 from reloading on the same scanline. + The +1 odd count (25 vs even) shifts the trail boundary to y=137 instead of y=136. ch6 + (treasure BG3VOFS) has a band boundary at y=136 (border-to-body) ; the +1 keeps ch4 (drops) + and ch6 from reloading on the same scanline. """ - sep #0x20 lda #25 sta.l DROPS_HDMA_SHADOW, x inx rep #0x20 lda.w #0xFFF8 - ; -8: bottom border (BG row 13) at screen y=112..137. 25 lines covers BG rows 13..15 - ; (border + 2 blank padding) so the trail @ -24 starts at y=137 reading BG row 14 - ; instead of looping back to row 13 — prevents the border tile repeating visually. +; -8: bottom border (BG row 13) at screen y=112..137. 25 lines covers BG rows 13..15 +; (border + 2 blank padding) so the trail @ -24 starts at y=137 reading BG row 14 +; instead of looping back to row 13 - prevents the border tile repeating visually. sta.l DROPS_HDMA_SHADOW, x inx inx @@ -291,7 +392,7 @@ and ch6 from reloading on the same scanline. inx rep #0x20 lda.w #0xFFE8 - ; -24 again: y=137..223 reads BG rows 14..24 (empty padding) so BG4 stays off content. +; -24 again: y=137..223 reads BG rows 14..24 (empty padding) so BG4 stays off content. sta.l DROPS_HDMA_SHADOW, x inx inx @@ -301,59 +402,162 @@ _drops_hdma_signal: """NMI shadow-copy signal for the drops ch4 table.""" sep #0x20 lda #0x01 - sta.w drops_hdma_copy_pending + sta.l drops_rolling.hdma_copy_pending rts - ; --- Engine instantiations ------------------------------------------------- drops_init_impl: """ -Init drops rolling buffer. Drops render onto BG4 staging ($7E:C600) alongside TreasureItemsWindow -($01:E275 def_window 1, 3, 27, 10) which the draw_window hook redraws first so the engine can write items into -the just-drawn frame without the original $01:D817 DrawWindow call clobbering them. + Init drops rolling buffer via the bank-20 engine. State + hook + far-ptrs live at $7E:9C30 (relocated out of $1B00-$1BFF). Drops render + onto BG4 staging ($7E:C600) alongside TreasureItemsWindow ($01:E275 + def_window 1, 3, 27, 10) which the draw_window hook redraws first so + the engine can write items into the just-drawn frame without the + original $01:D817 DrawWindow call clobbering them. """ - engine_init_rolling_buffer(drops_rolling, DROPS_VISIBLE_ITEMS, _drops_draw_window, drops_ensure_hdma_initialized, drops_render_item_to_slot) ; noqa: E501 + php + rep #0x30 + sep #0x20 + lda.b #DROPS_VISIBLE_ITEMS + sta.l drops_rolling + RollingBufferState.visible_rows + lda.b #0x02 + sta.l drops_rolling + RollingBufferState.slot_height_tiles +; item_list_ptr = $7E:FF28 (treasure drops table) + lda.b #0x28 + sta.l drops_rolling + RollingBufferState.item_list_ptr + lda.b #0xFF + sta.l drops_rolling + RollingBufferState.item_list_ptr + 1 + lda.b #0x7E + sta.l drops_rolling + RollingBufferState.item_list_ptr + 2 + lda.b #DROPS_TOTAL_ITEMS + sta.l drops_rolling + RollingBufferState.item_count + lda.b #0x04 + sta.l drops_rolling + RollingBufferState.hdma_channel + lda.b #0x80 + sta.l drops_rolling + RollingBufferState.vwf_cfg_ptr + lda.b #0x70 + sta.l drops_rolling + RollingBufferState.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l drops_rolling + RollingBufferState.vwf_cfg_ptr + 2 + lda.b #drops_fn_render_slot_trampoline & 0xFF + sta.l drops_rolling + RollingBufferState.fn_render_slot + lda.b #( drops_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l drops_rolling + RollingBufferState.fn_render_slot + 1 + lda.b #( drops_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l drops_rolling + RollingBufferState.fn_render_slot + 2 + lda.b #drops_fn_update_hdma_trampoline & 0xFF + sta.l drops_rolling + RollingBufferState.fn_update_hdma + lda.b #( drops_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l drops_rolling + RollingBufferState.fn_update_hdma + 1 + lda.b #( drops_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l drops_rolling + RollingBufferState.fn_update_hdma + 2 + lda.b #drops_fn_draw_window_trampoline & 0xFF + sta.l drops_rolling + RollingBufferState.fn_draw_window + lda.b #( drops_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l drops_rolling + RollingBufferState.fn_draw_window + 1 + lda.b #( drops_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l drops_rolling + RollingBufferState.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_DROPS + sta.l drops_rolling + RollingBufferState.menu_id + plp + php + rep #0x10 + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_init + plp + rtl + +drops_fn_render_slot_trampoline: +"""Bank-20 RTL wrapper around `drops_render_item_to_slot`.""" + php + jsr.w drops_render_item_to_slot + plp + rtl + +drops_fn_update_hdma_trampoline: +"""Bank-20 RTL wrapper around `drops_ensure_hdma_initialized`.""" + php + jsr.w drops_ensure_hdma_initialized + plp + rtl + +drops_fn_draw_window_trampoline: +"""Bank-20 RTL wrapper around `_drops_draw_window`.""" + php + jsr.w _drops_draw_window + plp + rtl _drops_draw_window: """Pre-render hook: SelectBG4 + DrawWindow(treasure_drops_window) so $29=$C600 is live before items render.""" sep #0x20 - jsr.l _drops_select_bg4_trampoline + jsr.l drops_select_bg4_trampoline rep #0x10 ldy.w #treasure_drops_window jsr.l draw_window_trampoline sep #0x10 rts -drops_scroll_down_prepare: -"""Drops profile scroll-down pre-render.""" - engine_scroll_down_prepare(drops_rolling, drops_scroll_pos, DROPS_SCROLL_LIMIT, DROPS_VISIBLE_ITEMS, DROPS_BUFFER_SLOTS, drops_ensure_hdma_initialized, drops_render_item_to_slot, update_drops_scroll_hdma) ; noqa: E501 - -drops_scroll_up_prepare: -"""Drops profile scroll-up pre-render.""" - engine_scroll_up_prepare(drops_rolling, drops_scroll_pos, DROPS_BUFFER_SLOTS, drops_ensure_hdma_initialized, drops_render_item_to_slot, update_drops_scroll_hdma) ; noqa: E501 - drops_start_scroll_down_impl: -"""Drops profile: kick scroll-down state machine.""" - engine_start_scroll_down(drops_rolling, drops_scroll_pos, DROPS_VISIBLE_ITEMS, DROPS_BUFFER_SLOTS, DROPS_SCROLL_TOTAL_PIXELS, DROPS_SCROLL_PIXELS_PER_FRAME, drops_ensure_hdma_initialized, drops_render_item_to_slot, update_drops_scroll_hdma) ; noqa: E501 +"""Drops profile: kick scroll-down state machine via the engine.""" + php + rep #0x10 + lda.l drops_scroll_pos + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_down + plp + rtl drops_start_scroll_up_impl: -"""Drops profile: kick scroll-up state machine.""" - engine_start_scroll_up(drops_rolling, drops_scroll_pos, DROPS_BUFFER_SLOTS, DROPS_SCROLL_TOTAL_PIXELS, drops_ensure_hdma_initialized, drops_render_item_to_slot, update_drops_scroll_hdma) ; noqa: E501 +"""Drops profile: kick scroll-up state machine via the engine.""" + php + rep #0x10 + lda.l drops_scroll_pos + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_up + plp + rtl drops_update_scroll_frame_impl: """Drops profile: per-frame scroll animation tick.""" - engine_update_scroll_frame(drops_rolling, DROPS_SCROLL_PIXELS_PER_FRAME, update_drops_scroll_hdma) + php + rep #0x10 + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_update_scroll_frame + plp + rtl drops_finish_scroll_impl: -"""Drops profile: end-of-animation pre-render + cleanup.""" - engine_finish_scroll(drops_rolling, drops_scroll_pos, DROPS_VISIBLE_ITEMS, DROPS_BUFFER_SLOTS, DROPS_TOTAL_ITEMS, drops_render_item_to_slot, update_drops_scroll_hdma) ; noqa: E501 +"""Drops profile: end-of-animation cleanup via the bank-20 engine.""" + php + rep #0x10 + lda.l drops_scroll_pos + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_finish_scroll + plp + rtl drops_refresh_slots_impl: -"""Drops profile: re-render all 5 slots (original redraw helper path).""" - engine_refresh_slots(drops_rolling, drops_scroll_pos, DROPS_BUFFER_SLOTS, drops_render_item_to_slot) +"""Drops profile: re-render all 6 slots via the bank-20 engine.""" + php + sep #0x20 + rep #0x10 + lda.l drops_scroll_pos + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_refresh_slots + plp + rtl drops_swap_redraw_impl: -"""Drops profile: post-swap re-render.""" - engine_swap_redraw(drops_rolling, drops_scroll_pos, DROPS_BUFFER_SLOTS, DROPS_TOTAL_ITEMS, drops_ensure_hdma_initialized, drops_render_item_to_slot, clear_drops_slot, update_drops_scroll_hdma) ; noqa: E501 +"""Drops profile: post-swap re-render via the engine.""" + php + rep #0x10 + lda.l drops_scroll_pos + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_swap_redraw + plp + rtl +} + diff --git a/src/ingame/equip.s b/src/ingame/equip.s index d04d207..4d3ae60 100644 --- a/src/ingame/equip.s +++ b/src/ingame/equip.s @@ -3,40 +3,40 @@ In-place patches for the equip menu: load the system-menu text pointer for the e character-name row up so it does not collide with the dextrality string. """ -*=0x01bd0f - load_system_menu_text_pointer(equip.menu) - -; Moves Character name one line up to avoid colision with dextrality text. - -*=0x01bd54 - ldy.w #0x01c6 - 0x40 - 2 - - _item_delta = 8 - -*=0x01bd7b - ldx.w #0x0164 + _item_delta - -*=0x01bd82 - ldx.w #0x01e4 + _item_delta - -*=0x01bd89 - ldx.w #0x0264 + _item_delta - -*=0x01bd92 - ldx.w #0x0064 + _item_delta - -*=0x01bda8 - ldx.w #0x00e4 + _item_delta - - -*=0x1bd5c - jsr.l load_dextrelity_pointer - destrelity_return = 0x1bd73 - ldx.w #0x244 - jmp.w destrelity_return - -*=0x01e2d9 - .dw ( dextrality.string_0 & 0xFFFF ) - 0x8000 - .dw ( dextrality.string_1 & 0xFFFF ) - 0x8000 - .dw ( dextrality.string_2 & 0xFFFF ) - 0x8000 - .dw ( dextrality.string_3 & 0xFFFF ) - 0x8000 +.alloc at 0x01bd0f { + load_system_menu_text_pointer(equip.menu) + + ; Moves Character name one line up to avoid colision with dextrality text. +} +.alloc at 0x01bd54 { + ldy.w #0x01c6 - 0x40 - 2 + + item_delta = 8 +} +.alloc at 0x01bd7b { + ldx.w #0x0164 + item_delta +} +.alloc at 0x01bd82 { + ldx.w #0x01e4 + item_delta +} +.alloc at 0x01bd89 { + ldx.w #0x0264 + item_delta +} +.alloc at 0x01bd92 { + ldx.w #0x0064 + item_delta +} +.alloc at 0x01bda8 { + ldx.w #0x00e4 + item_delta +} +.alloc at 0x1bd5c { + jsr.l load_dextrelity_pointer + destrelity_return = 0x1bd73 + ldx.w #0x244 + jmp.w destrelity_return +} +.alloc at 0x01e2d9 { + .dw ( dextrality.string_0 & 0xFFFF ) - 0x8000 + .dw ( dextrality.string_1 & 0xFFFF ) - 0x8000 + .dw ( dextrality.string_2 & 0xFFFF ) - 0x8000 + .dw ( dextrality.string_3 & 0xFFFF ) - 0x8000 +} diff --git a/src/ingame/free_space.s b/src/ingame/free_space.s index 3b8d511..5803e05 100644 --- a/src/ingame/free_space.s +++ b/src/ingame/free_space.s @@ -6,6 +6,7 @@ relocating other code. ; Bank $01 Free Space - starts at $01FF35 ; ============================================================================ +.include "config.i" .pool bank01_slack { range 0x01ff35 0x01ffff strategy order @@ -14,12 +15,13 @@ relocating other code. draw_window = 0x0180d9 .alloc bank01_slack_pre_inventory in bank01_slack { - check_if_description_was_rendered: """ Skip redrawing an item description if its text pointer matches `render.last_drawn_text_ptr` and the auto-counter ($4218/$4219) is non-zero. """ + + pha lda.l 0x004218 ora.l 0x004219 @@ -41,7 +43,7 @@ _continue: pha ldy.w #0xdcd6 - jmp _back + jmp item_desc_back draw_vwf_message: """Render the VWF message at the current text pointer via the items_description trampoline.""" @@ -54,8 +56,9 @@ Open a menu window at the cursor and render its VWF message ; advances Y past t delegating to `_draw_vwf_message_pos`. """ + jsr.w draw_window - ; NOTE: quirks from the hardcore bank switching can be solved by loading the bank in A before the call. +; NOTE: quirks from the hardcore bank switching can be solved by loading the bank in A before the call. pha rep #0x20 tya @@ -74,24 +77,26 @@ draw_vwf_message_pos_with_bank: Like `_draw_vwf_message_pos` but pre-loads the menu-strings bank into A so the trampoline can pick the right asset bank. """ + + lda.b #messages.use_on_whom >> 16 _draw_vwf_message_pos: jsr.l items_description.draw_trampoline_pos rts -.if 0 { + .if 0 { transform_window_trampoline: """JML trampoline into `transform_window_far`.""" jmp.l transform_window_far -} + } copy_text_with_dakuten: """Near-call wrapper around `copy_text_with_dakuten_far` for callers in the same bank.""" jsr.l copy_text_with_dakuten_far rts -.if DEBUG { + .if DEBUG { display_build_number: """Render the `BUILD_DATE + version` string at column 1, row 27 of the title screen (DEBUG builds only).""" { @@ -103,15 +108,16 @@ display_build_number: jsr.w 0x8798 ; copy text at position. rts } + } } -} ; end .alloc bank01_slack_pre_inventory +; end .alloc bank01_slack_pre_inventory ; ============================================================================ ; Inventory Rolling Buffer Trampolines and Handlers ; ============================================================================ .alloc bank01_slack_inventory in bank01_slack { -.if INVENTORY_ROLLING_BUFFER { + .if INVENTORY_ROLLING_BUFFER { swap_redraw_trampoline: """JML trampoline into `swap_redraw_hook_impl` for the inventory swap redraw path.""" jsr.w swap_redraw_hook_impl @@ -122,10 +128,10 @@ main_loop_scroll_check: """Called from $019FF2 via jmp.w""" - lda.w menu_scroll_state + lda.w menu_rolling.scroll_state beq _main_loop_do_input jsr.w update_scroll_frame - lda.w menu_scroll_remaining + lda.w menu_rolling.scroll_remaining bne _main_loop_skip_input jsr.w finish_scroll jmp.w _main_loop_skip_input ; Skip input on the frame scroll finishes @@ -185,7 +191,7 @@ Must copy shadow -> active HDMA table BEFORE enabling HDMA jsr.w nmi_dma_transfer_check ; Copy shadow table to active (if pending) .db 0xAF ; LDA.L opcode - .dw menu_hdma_enable ; $1BAE + .dw menu_rolling.hdma_enable ; $1BAE .db 0x7E ; Bank $7E sta.w 0x420C rts @@ -217,7 +223,8 @@ Re-renders all visible slots to show updated quantity or empty slot jsr.w swap_redraw_hook_impl rts + } } -} ; end .alloc bank01_slack_inventory +; end .alloc bank01_slack_inventory diff --git a/src/ingame/init_bg_scroll_hdma.s b/src/ingame/init_bg_scroll_hdma.s index 3e1668f..d3f554e 100644 --- a/src/ingame/init_bg_scroll_hdma.s +++ b/src/ingame/init_bg_scroll_hdma.s @@ -1,244 +1,249 @@ """Relocated `InitBGScrollHDMA` (from $01:EBD2): builds the BG2/BG3 V-scroll HDMA tables in WRAM.""" -init_bg_scroll_hdma: -"""Init BG Scroll HDMA — relocated from $01:EBD2.""" - tdc - tax - -_at_ebd4: - lda 0x0DFF29, x ; load bg scroll hdma tables - sta 0x75FD, x - inx - cpx.w #0x0015 - bne _at_ebd4 - tdc - tax - -_at_ebe3: - sta 0x7612, x ; clear hdma data - inx - cpx #0x1620 - bne _at_ebe3 - tdc - tax - lda #0xFE - -_at_ebf0: - sta 0x7D14, x ; bg3 v-scroll - dec - inx - inx - inx - inx - cpx #0x0380 - bne _at_ebf0 - ldx #0x0230 - -_at_ec00: - lda 0x7D14, x - dec - sta 0x7994, x ; bg2 v-scroll - inx - inx - inx - inx - cpx #0x0280 - bne _at_ec00 - rep #0x20 - tdc - tax - lda #0x0173 - ldy.w #0x0008 - -_at_ec1a: - sta 0x8094, x - pha - clc - adc.w #0x0068 - sta 0x8314, x - clc - adc.w #0x00F0 - sta 0x8AF4, x - pla - dey - bne _at_ec37 - clc - adc.w #0x0004 - ldy.w #0x000C - -_at_ec37: - cpx #0x0110 - bne _at_ec40 - clc - adc.w #0x0004 - -_at_ec40: - inx - inx - inx - inx - cpx #0x0130 - bne _at_ec1a - tdc - tax - lda #0x016F - ldy.w #0x0008 - -_at_ec51: - inc 0x81D3, x - sta 0x81D4, x - dey - bne _at_ec61 - clc - adc.w #0x0004 - ldy.w #0x000C - -_at_ec61: - cpx #0x0110 - bne _at_ec6a - clc - adc #0x0134 - -_at_ec6a: - inx - inx - inx - inx - cpx #0x0130 - bne _at_ec51 - tdc - tax - lda.w #0x006B - ldy.w #0x0008 - -_at_ec7b: - sta 0x8454, x - dey - bne _at_ec88 - clc - adc.w #0x0004 - ldy.w #0x000C - -_at_ec88: - cpx.w #0x0088 - bne _at_ec91 - clc - adc.w #0x0004 - -_at_ec91: - inx - inx - inx - inx - cpx.w #0x00A0 - bne _at_ec7b - tdc - tax - -_at_ec9c: - lda 0x8072, x - sta 0x81C2, x - sta 0x8442, x - inx - inx - cpx.w #0x0010 - bne _at_ec9c - tdc - tax - -_at_ecae: - lda #0x0101 - sta 0x84F2, x - inx - inx - inx - inx - cpx #0x0100 - bne _at_ecae - tdc - tax - lda.w #0x0053 - ldy.w #0x0008 - -_at_ecc5: - sta 0x85F4, x - sta 0x8874, x - pha - sec - sbc.w #0x000C - sta 0x8674, x - sta 0x88F4, x - sec - sbc.w #0x000C - sta 0x86F4, x - sta 0x8974, x - sec - sbc.w #0x000C - sta 0x8774, x - sta 0x89F4, x - sec - sbc.w #0x000C - sta 0x87F4, x - sta 0x8A74, x - pla - dey - bne _at_ecfc - clc - adc.w #0x0004 - -_at_ecfc: - pha - lda.w #0x00AC - sta 0x8872, x - sta 0x88F2, x - sta 0x8972, x - sta 0x89F2, x - sta 0x8A72, x - lda #0x01BC - sta 0x85F2, x - sta 0x8672, x - sta 0x86F2, x - sta 0x8772, x - sta 0x87F2, x - pla - inx - inx - inx - inx - cpx.w #0x0070 - bne _at_ecc5 - ldx.w #0x001C - ldy.w #0x0004 - lda #0x0134 - -_at_ed34: - sta 0x7D14, x - dey - bne _at_ed3e - clc - adc.w #0x0004 - -_at_ed3e: - inx - inx - inx - inx - cpx.w #0x0080 - bne _at_ed34 - tdc - tax - -_at_ed49: - lda #0x0100 - sta 0x8C32, x - lda #0x0160 - sta 0x8C34, x - inx - inx - inx - inx - cpx.w #0x0080 - bne _at_ed49 - tdc - sep #0x20 - rtl + +.include "../bank20.i" + +.alloc init_bg_scroll_hdma_block in bank20_reloc { + init_bg_scroll_hdma: + """Init BG Scroll HDMA — relocated from $01:EBD2.""" + tdc + tax + + _at_ebd4: + lda 0x0DFF29, x ; load bg scroll hdma tables + sta 0x75FD, x + inx + cpx.w #0x0015 + bne _at_ebd4 + tdc + tax + + _at_ebe3: + sta 0x7612, x ; clear hdma data + inx + cpx #0x1620 + bne _at_ebe3 + tdc + tax + lda #0xFE + + _at_ebf0: + sta 0x7D14, x ; bg3 v-scroll + dec + inx + inx + inx + inx + cpx #0x0380 + bne _at_ebf0 + ldx #0x0230 + + _at_ec00: + lda 0x7D14, x + dec + sta 0x7994, x ; bg2 v-scroll + inx + inx + inx + inx + cpx #0x0280 + bne _at_ec00 + rep #0x20 + tdc + tax + lda #0x0173 + ldy.w #0x0008 + + _at_ec1a: + sta 0x8094, x + pha + clc + adc.w #0x0068 + sta 0x8314, x + clc + adc.w #0x00F0 + sta 0x8AF4, x + pla + dey + bne _at_ec37 + clc + adc.w #0x0004 + ldy.w #0x000C + + _at_ec37: + cpx #0x0110 + bne _at_ec40 + clc + adc.w #0x0004 + + _at_ec40: + inx + inx + inx + inx + cpx #0x0130 + bne _at_ec1a + tdc + tax + lda #0x016F + ldy.w #0x0008 + + _at_ec51: + inc 0x81D3, x + sta 0x81D4, x + dey + bne _at_ec61 + clc + adc.w #0x0004 + ldy.w #0x000C + + _at_ec61: + cpx #0x0110 + bne _at_ec6a + clc + adc #0x0134 + + _at_ec6a: + inx + inx + inx + inx + cpx #0x0130 + bne _at_ec51 + tdc + tax + lda.w #0x006B + ldy.w #0x0008 + + _at_ec7b: + sta 0x8454, x + dey + bne _at_ec88 + clc + adc.w #0x0004 + ldy.w #0x000C + + _at_ec88: + cpx.w #0x0088 + bne _at_ec91 + clc + adc.w #0x0004 + + _at_ec91: + inx + inx + inx + inx + cpx.w #0x00A0 + bne _at_ec7b + tdc + tax + + _at_ec9c: + lda 0x8072, x + sta 0x81C2, x + sta 0x8442, x + inx + inx + cpx.w #0x0010 + bne _at_ec9c + tdc + tax + + _at_ecae: + lda #0x0101 + sta 0x84F2, x + inx + inx + inx + inx + cpx #0x0100 + bne _at_ecae + tdc + tax + lda.w #0x0053 + ldy.w #0x0008 + + _at_ecc5: + sta 0x85F4, x + sta 0x8874, x + pha + sec + sbc.w #0x000C + sta 0x8674, x + sta 0x88F4, x + sec + sbc.w #0x000C + sta 0x86F4, x + sta 0x8974, x + sec + sbc.w #0x000C + sta 0x8774, x + sta 0x89F4, x + sec + sbc.w #0x000C + sta 0x87F4, x + sta 0x8A74, x + pla + dey + bne _at_ecfc + clc + adc.w #0x0004 + + _at_ecfc: + pha + lda.w #0x00AC + sta 0x8872, x + sta 0x88F2, x + sta 0x8972, x + sta 0x89F2, x + sta 0x8A72, x + lda #0x01BC + sta 0x85F2, x + sta 0x8672, x + sta 0x86F2, x + sta 0x8772, x + sta 0x87F2, x + pla + inx + inx + inx + inx + cpx.w #0x0070 + bne _at_ecc5 + ldx.w #0x001C + ldy.w #0x0004 + lda #0x0134 + + _at_ed34: + sta 0x7D14, x + dey + bne _at_ed3e + clc + adc.w #0x0004 + + _at_ed3e: + inx + inx + inx + inx + cpx.w #0x0080 + bne _at_ed34 + tdc + tax + + _at_ed49: + lda #0x0100 + sta 0x8C32, x + lda #0x0160 + sta 0x8C34, x + inx + inx + inx + inx + cpx.w #0x0080 + bne _at_ed49 + tdc + sep #0x20 + rtl +} diff --git a/src/ingame/init_bg_scroll_hdma_patches.s b/src/ingame/init_bg_scroll_hdma_patches.s index dd9cd8e..fb9d797 100644 --- a/src/ingame/init_bg_scroll_hdma_patches.s +++ b/src/ingame/init_bg_scroll_hdma_patches.s @@ -7,5 +7,6 @@ after the original's ROM space is reclaimed. ;; (In LoROM bank $21 is the only thing that changes; offset $EBD2 within ;; the bank is identical so internal in-bank JSR/JMP targets stay valid.) -*=0x02818A - jsr.l init_bg_scroll_hdma +.alloc at 0x02818A { + jsr.l init_bg_scroll_hdma +} diff --git a/src/ingame/inventory_rolling.s b/src/ingame/inventory_rolling.s index 31546ad..1a07150 100644 --- a/src/ingame/inventory_rolling.s +++ b/src/ingame/inventory_rolling.s @@ -14,6 +14,7 @@ approach for the main menu items list with HDMA-based circular scrolling. ; CONSTANTS ; Layout (single column) +.include "config.i" MENU_VISIBLE_ITEMS := 10 ; Visible items at once MENU_BUFFER_SLOTS := 11 ; 11 slots (10 visible + 1 pre-render) MENU_TOTAL_ITEMS := 48 ; Total inventory items @@ -34,23 +35,17 @@ MENU_ITEM_LIST_HEIGHT := 160 ; 10 items × 16 pixels = 160 scanlines ; RAM VARIABLES ; Using menu RAM area (unused bytes) -; Field rolling-buffer state RAM block (12 bytes from $1BA8). Each -; named symbol below is just a typed offset into the shared struct -; — keeps existing call sites working byte-for-byte while making the -; layout self-documenting (and trivially relocatable later). -menu_rolling := 0x1BA8 -menu_rolling_top_row := menu_rolling + RollingBufferState.top_row -menu_rolling_buffer_pos := menu_rolling + RollingBufferState.buffer_pos -menu_rolling_edge_row := menu_rolling + RollingBufferState.edge_row -menu_rolling_slot_index := menu_rolling + RollingBufferState.slot_index -menu_rolling_base_scroll := menu_rolling + RollingBufferState.base_scroll -menu_hdma_enable := menu_rolling + RollingBufferState.hdma_enable -menu_scroll_state := menu_rolling + RollingBufferState.scroll_state -menu_scroll_remaining := menu_rolling + RollingBufferState.scroll_remaining -menu_scroll_direction := menu_rolling + RollingBufferState.scroll_direction -menu_transfer_pending := menu_rolling + RollingBufferState.transfer_pending -menu_scroll_anim_offset := menu_rolling + RollingBufferState.scroll_anim_offset -menu_hdma_copy_pending := menu_rolling + RollingBufferState.hdma_copy_pending +; Field rolling-buffer state RAM block. Base lives in items.i as +; FIELD_MENU_ROLLING_BASE so the bank-$20 NMI handler can derive its +; HDMA-flag addresses from the same struct typedef. Previously at +; $7E:1BA8 where it collided with vanilla DrawItemCharSelect scratch +; ($1BC1 / $1BC3) - char-target screen during item-use stomped +; dirty_mask and fn_render_slot mid byte, sending the hook into ROM +; padding. +; Module scope can't see items.i's FIELD_MENU_ROLLING_BASE constant +; so bind to the literal addr here. items.i mirrors this base under +; the name `field_menu_rolling` for cross-module struct access. +menu_rolling := (0x7E9C90 as RollingBufferState) ; Scroll State Constants SCROLL_STATE_IDLE := 0 @@ -90,895 +85,1109 @@ HDMA5_SRC_BANK := 0x4354 ; Source bank HDMA5_IND_BANK := 0x4357 ; Indirect bank HDMAEN := 0x420C ; HDMA enable register -init_menu_inventory_hdma: -""" -Sets up HDMA channel 5 for per-scanline BG1 vertical scroll control -Called when entering the item menu - -HDMA mode: $02 = write 2 bytes to same register, DIRECT mode (like FF6) -Register: $210E (BG1VOFS) -Table format: count_byte, lo_byte, hi_byte per entry, $00 to end -""" - - - php - sep #0x20 ; 8-bit A - -; Build the HDMA data table in WRAM -; Use UpdateMenuScrollHDMA directly - InitMenuHDMATable is redundant -; since UpdateMenuScrollHDMA is called immediately after anyway - jsr.w update_menu_scroll_hdma - -; Configure HDMA channel 5 for DIRECT mode -; Must use long addressing - DB may be $7E but registers are at $00:43xx - lda #0x02 ; Mode: DIRECT, write 2 bytes to same PPU reg - sta.l HDMA5_CTRL ; $004350 - - lda #0x0E ; BG1VOFS register ($210E) - sta.l HDMA5_DEST ; $004351 - -; Source = HDMA table in WRAM at $7E9800 - rep #0x20 ; 16-bit A - lda.w #MENU_HDMA_TABLE_ADDR ; $9800 - sta.l HDMA5_SRC_LO ; $004352-$004353 - sep #0x20 ; 8-bit A - lda #MENU_HDMA_BANK ; $7E - sta.l HDMA5_SRC_BANK ; $004354 - -; HDMA channel 5 is now enabled via shadow variable (menu_hdma_enable) -; The NMI hook at $8083 reads the shadow and writes to HDMAEN - - plp - rts - -disable_menu_inventory_hdma: -""" -Disables HDMA channel 5 when leaving item menu -The shadow variable is cleared by menu_exit_hook -""" - php - sep #0x20 ; 8-bit A - ; Shadow variable cleared by caller, NMI will write 0 to HDMAEN - plp - rts - -init_menu_hdma_table: -""" -Builds direct mode HDMA table in WRAM at $7E9800 -Format: count, lo, hi per entry, $00 to end -Initial state: all rows at BASE scroll (no circular buffer offset) -""" - rep #0x30 ; 16-bit A, X, Y - php - pha ; Save A - phx ; Save X - phy ; Save Y - -; Save DP bytes we'll use as scratch - lda.b 0x40 - pha ; Save $40-$41 - -; X = table write offset - ldx.w #0x0000 - -; Entry 0: Border area - 48 scanlines at BASE scroll - sep #0x20 ; 8-bit A for count byte - lda #48 ; 48 scanlines - sta.l MENU_HDMA_TABLE, x - inx - rep #0x20 ; 16-bit A for value - lda.w menu_rolling_base_scroll - sta.l MENU_HDMA_TABLE, x - inx - inx - -; Entries 1-10: Item rows - 16 scanlines each at BASE scroll (initial) -; For init, all rows use BASE (no circular buffer offset yet) - lda.w #0x0000 - sta.b 0x40 ; Row counter - -_init_item_rows: - sep #0x20 ; 8-bit A for count - lda #16 ; 16 scanlines per item row - sta.l MENU_HDMA_TABLE, x - inx - rep #0x20 ; 16-bit A for value - lda.w menu_rolling_base_scroll - sta.l MENU_HDMA_TABLE, x - inx - inx - - inc.b 0x40 - lda.b 0x40 - - cmp.w #1 ; 10 rows - bcc _init_item_rows - -; Entry 11: Below items - 16 scanlines at BASE + 16 -; Lock to show the bottom window border area - sep #0x20 - lda #16 ; 16 scanlines - sta.l MENU_HDMA_TABLE, x - inx - rep #0x20 - lda.w menu_rolling_base_scroll - clc - adc.w #16 ; Lock at base + 16 - sta.l MENU_HDMA_TABLE, x - inx - inx - -; End marker - sep #0x20 - lda #0x00 - sta.l MENU_HDMA_TABLE, x - -; Restore DP bytes - rep #0x20 - pla - sta.b 0x40 ; Restore $40-$41 - -; Restore registers - ply - plx - pla - plp - rts - -update_menu_scroll_hdma: -"""Build the field-menu HDMA scroll table via the shared engine.""" - engine_update_scroll_hdma(menu_rolling, MENU_HDMA_SHADOW, MENU_BUFFER_SLOTS, MENU_VISIBLE_ITEMS, _menu_hdma_header, _menu_hdma_footer, _menu_hdma_signal) ; noqa: E501 - -_menu_hdma_header: -"""Field profile header: 48-scanline border at BASE scroll (one entry).""" - sep #0x20 - lda #48 - sta.l MENU_HDMA_SHADOW, x - inx - rep #0x20 - lda.w menu_rolling_base_scroll - sta.l MENU_HDMA_SHADOW, x - inx - inx - rts - -_menu_hdma_footer: -"""Field profile footer: 16 scanlines at BASE+16 (locks bottom-border row 24).""" - sep #0x20 - lda #16 - sta.l MENU_HDMA_SHADOW, x - inx - rep #0x20 - lda.w menu_rolling_base_scroll - clc - adc.w #16 - sta.l MENU_HDMA_SHADOW, x - inx - inx - rts - -_menu_hdma_signal: -"""Field profile NMI signal: set copy-pending shadow flag.""" - sep #0x20 - lda #0x01 - sta.w menu_hdma_copy_pending - rts - -init_menu_rolling_buffer_impl: -"""Init field rolling buffer (10 visible, lazy 11th slot).""" - engine_init_rolling_buffer(menu_rolling, MENU_VISIBLE_ITEMS, _menu_draw_inventory_window, ensure_hdma_initialized, _menu_render_item_to_slot) ; noqa: E501 - -_menu_draw_inventory_window: -"""Field menu draws the InventoryWindow ($DCCE) frame on entry.""" - rep #0x10 - ldy.w #0xDCCE - jsr.l draw_window_trampoline - sep #0x10 - rts - -menu_scroll_down_prepare: -"""Field profile scroll-down pre-render.""" - engine_scroll_down_prepare(menu_rolling, 0x1B1A, MENU_SCROLL_LIMIT, MENU_VISIBLE_ITEMS, MENU_BUFFER_SLOTS, ensure_hdma_initialized, _menu_render_item_to_slot, update_menu_scroll_hdma) ; noqa: E501 - -menu_scroll_up_prepare: -"""Field profile scroll-up pre-render.""" - engine_scroll_up_prepare(menu_rolling, 0x1B1A, MENU_BUFFER_SLOTS, ensure_hdma_initialized, _menu_render_item_to_slot, update_menu_scroll_hdma) ; noqa: E501 - -; _menu_render_item_to_slot -; Renders an item to a specific circular buffer slot in the tilemap. -; -; Input: menu_rolling_edge_row = item index (0-47) for data lookup -; menu_rolling_slot_index = slot index (0-11) for destination -; -; Strategy: Set up $5d = slot_index (for Y position calculation), -; $5a = pointer to item data, then call game's DrawItemSlot. - -_menu_render_item_to_slot: - php - phb - -; Set data bank to $7E for WRAM access - lda #0x7E - pha - plb - -; Set 16-bit A and X/Y for consistent register handling - rep #0x30 ; 16-bit A and X/Y - -; Save registers and key direct page variables - pha - phx ; Save X (16-bit) - phy ; Save Y (16-bit) - lda.b 0x5a - pha - lda.b 0x29 ; Save tilemap buffer pointer - pha - lda.b 0x45 ; Save $45-$46 (used by game routines) - pha - lda.b 0x33 ; Save $33-$34 (used for tile attributes) - pha - sep #0x20 ; 8-bit A - lda.b 0x5d - pha - lda.b 0xDB ; Save tile attribute byte - pha - -; Set $29 = $B600 for BG1 tilemap buffer - rep #0x20 ; 16-bit A (X/Y still 16-bit) - lda.w #0xB600 - sta.b 0x29 - sep #0x20 ; 8-bit A - -; Calculate item data pointer: $1440 + (edge_row * Item.__size) - lda.w menu_rolling_edge_row - asl ; * Item.__size (2 bytes per Item) - clc - adc #0x40 ; Low byte of $1440 - sta.b 0x5a - lda #0x14 ; High byte of $1440 - adc #0x00 ; Add carry - sta.b 0x5b - -; Load Item.id and Item.qty from ($5A) via long-addressing into WRAM. - rep #0x20 - lda.b 0x5a ; Pointer value = $1440 + edge_row * Item.__size - tax - sep #0x20 - lda.l 0x7E0000 + Item.id, x - pha ; Save Item.id for CheckCanUseItem - lda.l 0x7E0000 + Item.qty, x - sta.b 0x5C ; Store qty in $5C - -; Call CheckCanUseItem to set palette in $DB -; Input: A = item ID -; Output: $DB = $00 (usable) or $04 (not usable) - stz.b 0x34 ; Clear $34 (no priority/flip bits) - pla ; Restore item ID - jsr.l check_can_use_item_trampoline ; bank-$01 trampoline for original @ $A25D (sets $DB) - -; Set $5d = slot_index (for AND #$01 check, but we patched to AND #$00) - lda.w menu_rolling_slot_index - sta.b 0x5d - -; Calculate Y = slot_index * 128 + 70 -; Y is the tilemap offset for this slot -; +64 for window border (1 tile row = 32 tiles × 2 bytes) -; +6 for left margin (3 tiles) - rep #0x20 ; 16-bit A (X/Y already 16-bit) - lda.w menu_rolling_slot_index - and.w #0x00FF ; Clear high byte - xba ; Swap bytes: A = slot * 256 - lsr ; A = slot * 128 - clc - adc.w #0x0044 ; + 68 (64 border + 4 margin) - tay ; Y = tilemap offset (16-bit transfer) - sep #0x20 ; 8-bit A - -; Check for trash can item ($FF) - needs special 2x2 tile graphic - lda (0x5a) ; Load item ID - cmp #0xFF - bne _not_trash_item - jsr.w draw_trash_single_column ; Draw trash icon - bra _skip_draw_item_slot - -_not_trash_item: - ; Clear the 2x2 trash can area first (in case we scrolled from trash position) - jsr.w _clear_trash_area - -; Call DrawItemSlot inner at $A1ED -; Expects: Y = tilemap offset, ($5A) = item pointer, ($29) = tilemap base - jsr.l draw_item_slot_inner_trampoline ; bank-$01 trampoline for original @ $A1ED - -_skip_draw_item_slot: - -; Restore direct page variables and registers (reverse order) - pla - sta.b 0xDB ; Restore tile attribute byte - pla - sta.b 0x5d - rep #0x20 ; 16-bit A - pla - sta.b 0x33 ; Restore $33-$34 (tile attributes) - pla - sta.b 0x45 ; Restore $45-$46 (used by game routines) - pla - sta.b 0x29 ; Restore tilemap buffer pointer - pla - sta.b 0x5a - rep #0x10 ; 16-bit X/Y for pop (match push) - ply ; Restore Y (16-bit) - plx ; Restore X (16-bit) - pla ; Restore A (16-bit - still in 16-bit A from above) - - plb - plp - rts - -ensure_hdma_initialized: -""" -Lazy initialization: captures $93 and sets up HDMA on first scroll. -Called from scroll prepare functions. -Checks if base_scroll == 0xFFFF (sentinel) and if so, initializes. -""" - -; Check if already initialized (base_scroll != 0xFFFF) - rep #0x20 ; 16-bit A - lda.w menu_rolling_base_scroll - cmp.w #0xFFFF - bne _hdma_already_init - -; Capture base scroll from $0193 (BG1VOFS shadow, menu uses DP=$0100) -; Use long addressing to ensure we read from WRAM - .db 0xAF ; LDA.L opcode - .db 0x93, 0x01, 0x7E ; $7E0193 - sta.w menu_rolling_base_scroll - -; Initialize HDMA channel configuration - sep #0x20 ; Back to 8-bit for InitMenuInventoryHDMA - jsr.w init_menu_inventory_hdma - -; NOW enable HDMA via shadow variable (channel is configured) -; Force long addressing: STA.L $7E1BAE - lda #0x20 ; Channel 5 - .db 0x8F ; STA.L opcode - .dw menu_hdma_enable ; $1BAE - .db 0x7E ; Bank $7E - rts - -_hdma_already_init: - sep #0x20 ; Restore 8-bit mode - rts - -; STATE MACHINE ROUTINES (FF6-style non-blocking scroll) - -scroll_state_check: -""" -Called at main loop entry ($019FF2) to handle scroll animation frames. -If scrolling is active, processes one frame and skips input handling. - -Returns: Carry clear = process input normally - Carry set = skip input (still scrolling) -""" - - - php - sep #0x20 ; 8-bit A - -; Check if we're scrolling - lda.w menu_scroll_state - beq _scroll_state_idle - -; We're scrolling - process one animation frame - jsr.l update_scroll_frame_impl - -; Check if scroll finished - lda.w menu_scroll_remaining - bne _scroll_still_active - -; Scroll finished - clean up and return to idle - jsr.l finish_scroll_impl - -_scroll_state_idle: - plp - clc ; Carry clear = process input - rts - -_scroll_still_active: - plp - sec ; Carry set = skip input - rts - -start_scroll_down_impl: -"""Field profile: kick scroll-down state machine.""" - engine_start_scroll_down(menu_rolling, 0x1B1A, MENU_VISIBLE_ITEMS, MENU_BUFFER_SLOTS, SCROLL_TOTAL_PIXELS, SCROLL_PIXELS_PER_FRAME, ensure_hdma_initialized, _menu_render_item_to_slot, update_menu_scroll_hdma) ; noqa: E501 - -start_scroll_up_impl: -"""Field profile: kick scroll-up state machine.""" - engine_start_scroll_up(menu_rolling, 0x1B1A, MENU_BUFFER_SLOTS, SCROLL_TOTAL_PIXELS, ensure_hdma_initialized, _menu_render_item_to_slot, update_menu_scroll_hdma) ; noqa: E501 - -update_scroll_frame_impl: -"""Field profile: per-frame scroll animation tick.""" - engine_update_scroll_frame(menu_rolling, SCROLL_PIXELS_PER_FRAME, update_menu_scroll_hdma) - -finish_scroll_impl: -"""Field profile: end-of-animation pre-render + cleanup.""" - engine_finish_scroll(menu_rolling, 0x1B1A, MENU_VISIBLE_ITEMS, MENU_BUFFER_SLOTS, MENU_TOTAL_ITEMS, _menu_render_item_to_slot, update_menu_scroll_hdma) ; noqa: E501 - -; Inventory Rolling Buffer Patches - Relocated to Bank $20 -; These routines are called via JSL from bank $01 hooks. - -.if INVENTORY_ROLLING_BUFFER { -menu_entry_hook_impl: -"""Field-menu entry hook: lazy-init HDMA + force shadow flush before first frame.""" - stz.w 0x1B1F - lda #0x00 - sta.l 0x7E1BAE -; menu_hdma_enable - stz.w menu_scroll_state - stz.w menu_scroll_remaining - stz.w menu_scroll_direction - stz.w menu_transfer_pending - stz.w menu_hdma_copy_pending -; Clear HDMA copy flag - stz.w menu_scroll_anim_offset -; Clear low byte - stz.w menu_scroll_anim_offset + 1 -; Clear high byte -; Initialize cursor column to 0 for single-column mode -; This ensures $1b22 is always 0 even if it had a value from previous menu - stz.w 0x1B22 -; cursor_x = 0 -; InitMenuRollingBuffer_Impl is called later via patched JSR at $9F7B - rtl - -menu_exit_hook_impl: -"""Field menu teardown: zero HDMA shadow, full 12-byte rolling state, ch5 registers.""" - php - sep #0x20 - lda #0x00 - sta.l 0x7E1BAE -; menu_hdma_enable shadow off so NMI writes 0 to HDMAEN this frame - sta.l 0x004350 - sta.l 0x004351 - sta.l 0x004352 - sta.l 0x004353 - sta.l 0x004354 -; HDMA5 ctrl/dest/src cleared so a stale config can't restart on next mode switch - rep #0x20 - lda.w #0x0000 - sta.w menu_rolling - sta.w menu_rolling + 2 - sta.w menu_rolling + 4 - sta.w menu_rolling + 6 - sta.w menu_rolling + 8 - sta.w menu_rolling + 10 - plp - jsr.l reset_sprites_trampoline - rtl -; swap_redraw_hook_impl_body -; Called after item swap to redraw visible items correctly. -; Must render to the correct circular buffer slots based on current buffer_pos. -; Does NOT reset buffer_pos - we stay at the current scroll position. - -swap_redraw_hook_impl_body: -"""Field profile: post-swap re-render of all 11 slots.""" - engine_swap_redraw(menu_rolling, 0x1B1A, MENU_BUFFER_SLOTS, MENU_TOTAL_ITEMS, ensure_hdma_initialized, _menu_render_item_to_slot, _clear_inventory_slot, update_menu_scroll_hdma) ; noqa: E501 - - -; _clear_inventory_slot -; Clears a single inventory slot in the tilemap buffer. -; Input: menu_rolling_slot_index = slot to clear (0-10) -; Used when item index is out of bounds (>= 48) - -_clear_inventory_slot: - php - phb -; Set data bank to $7E for WRAM access - lda #0x7E - pha - plb -; Save registers - use 16-bit mode for consistent push/pop - rep #0x30 -; 16-bit A and X/Y - pha - phx - phy - lda.b 0x29 - pha -; Set $29 = $B600 for BG1 tilemap buffer - lda.w #0xB600 - sta.b 0x29 -; Calculate Y = slot_index * 128 + 70 - lda.w menu_rolling_slot_index - and.w #0x00FF - xba -; A = slot * 256 - lsr -; A = slot * 128 - clc - adc.w #0x0044 -; + 68 (64 border + 4 margin) - tay -; Y = tilemap offset (16-bit) - sep #0x20 -; 8-bit A for tile writes (X/Y stay 16-bit) -; Clear the item name area (12 tiles = 24 bytes) -; Use $FF as blank tile -; Note: X is 16-bit but we only use low byte; loop works correctly - ldx.w #12 -; 12 tiles for item name + quantity (force 16-bit immediate) - -_clear_slot_loop: - lda #0xFF -; Blank tile - sta (0x29), y - iny - lda #0x04 -; Palette 4 (matches normal items) - sta (0x29), y - iny - dex - bne _clear_slot_loop -; Restore registers - must match push mode - rep #0x20 -; 16-bit A for pop (X/Y already 16-bit) - pla - sta.b 0x29 - ply - plx - pla - plb - plp - rts -; _clear_trash_area -; Clears the 2x2 trash can area with blank tiles ($FF). -; Called before drawing normal items to remove any leftover trash icon. -; Input: Y = tilemap offset -; ($29) = tilemap base -; - -_clear_trash_area: - phy -; Save Y -; First row: 2 tiles - lda #0xFF - sta (0x29), y - iny - lda #0x00 - sta (0x29), y - iny - lda #0xFF - sta (0x29), y - iny - lda #0x00 - sta (0x29), y -; Second row: Y + 64 from start - ply -; Restore original Y - phy -; Save again - rep #0x20 - tya - clc - adc.w #64 - tay - sep #0x20 - lda #0xFF - sta (0x29), y - iny - lda #0x00 - sta (0x29), y - iny - lda #0xFF - sta (0x29), y - iny - lda #0x00 - sta (0x29), y - ply -; Restore Y - rts - -draw_trash_single_column: - - -""" -Draws the trash can 2x2 tile graphic for single-column inventory. -Input: Y = tilemap offset (from slot calculation) -($29) = tilemap base ($B600) -menu_rolling_slot_index = current slot -Tiles: $04 (top-left), $05 (top-right), $06 (bottom-left), $07 (bottom-right) - -Tilemap format: [tile_number, attributes] pairs -Each row is 64 bytes (32 tiles × 2 bytes) -""" - - -; Y points to start of item slot area -; Draw 2x2 trash can icon, then clear remaining 10 tiles per row -; Save starting Y for second row calculation - rep #0x20 -; 16-bit for push - phy -; Save starting Y - sep #0x20 -; 8-bit A -; First row: tiles $04, $05 - lda #0x04 -; Tile $04 (top-left of trash can) - sta (0x29), y - iny - lda #0x00 -; Attribute: palette 0, no flip - sta (0x29), y - iny - lda #0x05 -; Tile $05 (top-right) - sta (0x29), y - iny - lda #0x00 -; Attribute - sta (0x29), y - iny -; Clear remaining 13 tiles on first row (10 name + colon + 2 digits) - ldx.w #13 - -_clear_row1: - lda #0xFF -; Blank tile - sta (0x29), y - iny - lda #0x00 -; Attribute - sta (0x29), y - iny - dex - bne _clear_row1 -; Restore starting Y and add 64 for second row - rep #0x20 -; 16-bit A - pla -; Get starting Y - clc - adc.w #64 -; +64 bytes = next tilemap row - tay - sep #0x20 -; 8-bit A -; Second row: tiles $06, $07 - lda #0x06 -; Tile $06 (bottom-left) - sta (0x29), y - iny - lda #0x00 -; Attribute - sta (0x29), y - iny - lda #0x07 -; Tile $07 (bottom-right) - sta (0x29), y - iny - lda #0x00 -; Attribute - sta (0x29), y - iny -; Clear remaining 13 tiles on second row (10 name + colon + 2 digits) - ldx.w #13 - -_clear_row2: - lda #0xFF -; Blank tile - sta (0x29), y - iny - lda #0x00 -; Attribute - sta (0x29), y - iny - dex - bne _clear_row2 - rts -; NmiDmaTransferCheck_Impl moved to bank $20 (battle/inventory_rolling.s) -; as field_menu_nmi_dma_transfer_check_impl to save space in bank $01 -; check_and_clear_count_impl -; Called from DrawItemSlot to check item ID and handle count display. -; - If item ID is 0: writes $FF tiles to clear count, skips to RTS -; - If item ID is $FE: skips to RTS (no clearing needed) -; - Otherwise: returns normally to draw count -; -; Input: $5a = pointer to item data, Y = tilemap offset, $29 = tilemap ptr -; Modifies: A, return address on stack if skipping - -check_and_clear_count_impl: -"""Drop the count column for empty/used slots.""" - lda (0x5a) -; Load item ID - beq _clear_count -; If 0, clear and skip - cmp #0xFE -; Check for special item $FE - bne _normal_return -; If not $FE, return normally to draw count -; Item is $FE - skip count but don't clear - bra _skip_to_rts - -_clear_count: -; Write $FF (blank tiles) to count area: colon + 2 digits = 3 tiles - lda #0xFF -; Blank tile - sta (0x29), y -; Colon position - iny - lda.b 0xdb -; Attribute byte - sta (0x29), y - iny - lda #0xFF - sta (0x29), y -; First digit position - iny - lda.b 0xdb - sta (0x29), y - iny - lda #0xFF - sta (0x29), y -; Second digit position - iny - lda.b 0xdb - sta (0x29), y - -_skip_to_rts: -; Replace the entire DrawItemSlot return chain so we land on the RTS at $01:A222 -; instead of falling back to $A205 (NOPs + colon write). -; -; Stack on entry to Impl (pushed by trampoline + caller, top first): -; [JSL Impl return: PCL, PCH, PB ($01)] -; [JSR check_and_clear_count return: PCL, PCH ($A204)] -; [DrawItemSlot caller's return ...] -; -; Pop both the JSL Impl return (3 bytes) AND the JSR trampoline return (2 bytes), -; then push a synthetic return to $01:A222 (RTS) for our RTL. - pla ; PCL of JSL Impl return - pla ; PCH of JSL Impl return - pla ; PB of JSL Impl return - pla ; PCL of JSR check_and_clear_count return - pla ; PCH of JSR check_and_clear_count return -; Stack top now = DrawItemSlot's own caller return. -; Push return for RTL: $01:A221 (so RTL lands PC=$01:A222 = RTS). - lda #0x01 - pha - lda #0xA2 - pha - lda #0x21 - pha - -_normal_return: - rtl - -circular_slot_calc: - - -""" -Calculate tilemap Y offset using circular buffer position. -Called from patched code at $A1BA via CircularSlotCalc_ext. - -Input: $5D = game's slot counter (0, 2, 4, 6... incremented by 2 per row) -$5A = src pointer ($1440 = inventory, $FF28 = treasure drops) -Output: Y = tilemap offset for circular buffer slot -Preserves: 16-bit A mode on exit -""" - - - sep #0x20 -; 8-bit A -; Drops list at $FF28 reuses _a181 but doesn't have any rolling state. -; The original 2-col Y calc collides slots after we forced col=0 globally -; via the `and #$00` patch at DrawItemSlot, so two drops would render at -; the same scanline. Detect drops via $5B high byte and emit a unique -; per-slot Y (slot * 64 + 4) instead of any circular math. - lda.b 0x5b - cmp #0xFF - bne _circ_slot_not_drops - lda.b 0x5d - rep #0x20 - and.w #0x00FF - asl - asl - asl - asl - asl - asl ; * 64. $5D increments by 2 → step = 128 bytes = 2 tilemap rows, -; matching the 16-px-per-item layout original uses for col-0 -; drops with BG1VOFS=-32 ($93) pushing them down to scanline 32. - clc - adc.w #0x0044 ; col 2 + 1 tilemap row down (+$40) so the first slot -; clears the window header before BG1VOFS shifts it. - tay - rts -_circ_slot_not_drops: -; Inventory in treasure context uses treasure_rolling_buffer_pos. - lda.l 0x7E0000 + 0x1BD6 ; treasure_hdma_enable - beq _circ_check_field - lda.b 0x5d - lsr - clc - adc.l 0x7E0000 + 0x1BD1 ; treasure_rolling_buffer_pos -_t_circ_mod: - cmp #6 ; TREASURE_BUFFER_SLOTS - bcc _t_circ_done - sec - sbc #6 - bra _t_circ_mod -_t_circ_done: - rep #0x20 - and.w #0x00FF - xba - lsr - clc - adc.w #0x0004 - tay - rts -_circ_check_field: -; Check if circular buffer mode is active (HDMA enabled) - lda.l 0x7E0000 + menu_hdma_enable - beq _circ_slot_original -; Not active, use original calculation -; Circular buffer Y calculation -; NOTE: Game increments $5D by 2 for each row (0, 2, 4, 6, 8, 10, 12, 14, 16, 18) -; We must divide by 2 first to get the visual slot (0-9) - lda.b 0x5d -; Load slot counter (0, 2, 4...) - lsr -; Divide by 2 to get visual slot (0-9) - clc - adc.l 0x7E0000 + menu_rolling_buffer_pos -; Add buffer_pos - -_circ_slot_mod: - cmp #MENU_BUFFER_SLOTS -; >= 11? - bcc _circ_slot_done - sec - sbc #MENU_BUFFER_SLOTS -; Subtract 11 to wrap - bra _circ_slot_mod - -_circ_slot_done: -; A = circular slot (0-10) - rep #0x20 -; 16-bit A - and.w #0x00FF -; Clear high byte (force 16-bit immediate) - xba -; A = slot * 256 - lsr -; A = slot * 128 - clc - adc.w #0x0004 -; + 4 (margin only, $29 already includes border) - tay -; Y = tilemap offset - rts - -_circ_slot_original: -; Original game calculation: Y = ($5D / 2) * 128 + 4 - lda.b 0x5d - lsr -; /2 - rep #0x20 -; 16-bit A - and.w #0x00FF -; Clear high byte (force 16-bit immediate) - xba -; *256 - lsr -; *128 - clc - adc.w #0x0004 -; +4 - tay - rts - -circular_slot_calc_ext: -"""Trampoline to call CircularSlotCalc from bank $01 patch at $A1BA""" - jsr.w circular_slot_calc - rtl +.include "../bank20.i" + +.alloc inventory_rolling_block in bank20_reloc { + init_menu_inventory_hdma: + """ + Sets up HDMA channel 5 for per-scanline BG1 vertical scroll control + Called when entering the item menu + + HDMA mode: $02 = write 2 bytes to same register, DIRECT mode (like FF6) + Register: $210E (BG1VOFS) + Table format: count_byte, lo_byte, hi_byte per entry, $00 to end + """ + + php + sep #0x20 ; 8-bit A + + ; Build the HDMA data table in WRAM + ; Use UpdateMenuScrollHDMA directly - InitMenuHDMATable is redundant + ; since UpdateMenuScrollHDMA is called immediately after anyway + jsr.w update_menu_scroll_hdma + + ; Configure HDMA channel 5 for DIRECT mode + ; Must use long addressing - DB may be $7E but registers are at $00:43xx + lda #0x02 ; Mode: DIRECT, write 2 bytes to same PPU reg + sta.l HDMA5_CTRL ; $004350 + + lda #0x0E ; BG1VOFS register ($210E) + sta.l HDMA5_DEST ; $004351 + + ; Source = HDMA table in WRAM at $7E9800 + rep #0x20 ; 16-bit A + lda.w #MENU_HDMA_TABLE_ADDR ; $9800 + sta.l HDMA5_SRC_LO ; $004352-$004353 + sep #0x20 ; 8-bit A + lda #MENU_HDMA_BANK ; $7E + sta.l HDMA5_SRC_BANK ; $004354 + + ; HDMA channel 5 is now enabled via shadow variable (menu_rolling.hdma_enable) + ; The NMI hook at $8083 reads the shadow and writes to HDMAEN + + plp + rts + + disable_menu_inventory_hdma: + """ + Disables HDMA channel 5 when leaving item menu + The shadow variable is cleared by menu_exit_hook + """ + php + sep #0x20 ; 8-bit A + ; Shadow variable cleared by caller, NMI will write 0 to HDMAEN + plp + rts + + init_menu_hdma_table: + """ + Builds direct mode HDMA table in WRAM at $7E9800 + Format: count, lo, hi per entry, $00 to end + Initial state: all rows at BASE scroll (no circular buffer offset) + """ + rep #0x30 ; 16-bit A, X, Y + php + pha ; Save A + phx ; Save X + phy ; Save Y + + ; Save DP bytes we'll use as scratch + lda.b 0x40 + pha ; Save $40-$41 + + ; X = table write offset + ldx.w #0x0000 + + ; Entry 0: Border area - 48 scanlines at BASE scroll + sep #0x20 ; 8-bit A for count byte + lda #48 ; 48 scanlines + sta.l MENU_HDMA_TABLE, x + inx + rep #0x20 ; 16-bit A for value + lda.w menu_rolling.base_scroll + sta.l MENU_HDMA_TABLE, x + inx + inx + + ; Entries 1-10: Item rows - 16 scanlines each at BASE scroll (initial) + ; For init, all rows use BASE (no circular buffer offset yet) + lda.w #0x0000 + sta.b 0x40 ; Row counter + + _init_item_rows: + sep #0x20 ; 8-bit A for count + lda #16 ; 16 scanlines per item row + sta.l MENU_HDMA_TABLE, x + inx + rep #0x20 ; 16-bit A for value + lda.w menu_rolling.base_scroll + sta.l MENU_HDMA_TABLE, x + inx + inx + + inc.b 0x40 + lda.b 0x40 + + cmp.w #1 ; 10 rows + bcc _init_item_rows + + ; Entry 11: Below items - 16 scanlines at BASE + 16 + ; Lock to show the bottom window border area + sep #0x20 + lda #16 ; 16 scanlines + sta.l MENU_HDMA_TABLE, x + inx + rep #0x20 + lda.w menu_rolling.base_scroll + clc + adc.w #16 ; Lock at base + 16 + sta.l MENU_HDMA_TABLE, x + inx + inx + + ; End marker + sep #0x20 + lda #0x00 + sta.l MENU_HDMA_TABLE, x + + ; Restore DP bytes + rep #0x20 + pla + sta.b 0x40 ; Restore $40-$41 + + ; Restore registers + ply + plx + pla + plp + rts + + update_menu_scroll_hdma: + """Build the field-menu HDMA scroll table via the shared engine.""" + ; Build field-menu HDMA scroll table. Inlined from the former + ; engine_update_scroll_hdma macro for the same reason as treasure. + { + php + rep #0x30 + pha + phx + phy + lda.b 0x40 + pha + lda.b 0x42 + pha + ldx.w #0x0000 + jsr.w _menu_hdma_header + stz.b 0x42 + + _row_loop: + lda.w menu_rolling + RollingBufferState.buffer_pos + and.w #0x00FF + clc + adc.b 0x42 + + _mod_loop: + cmp.w #MENU_BUFFER_SLOTS + bcc _mod_done + sec + sbc.w #MENU_BUFFER_SLOTS + bra _mod_loop + + _mod_done: + asl + asl + asl + asl + sta.b 0x40 + lda.b 0x42 + and.w #0x00FF + asl + asl + asl + asl + eor.w #0xFFFF + inc + clc + adc.b 0x40 + clc + adc.w menu_rolling + RollingBufferState.base_scroll + sta.b 0x40 + sep #0x20 + lda #16 + sta.l MENU_HDMA_SHADOW, x + inx + rep #0x20 + lda.b 0x40 + sta.l MENU_HDMA_SHADOW, x + inx + inx + rep #0x20 + inc.b 0x42 + lda.b 0x42 + cmp.w #MENU_VISIBLE_ITEMS + bcs _row_loop_done + jmp.w _row_loop + + _row_loop_done: + jsr.w _menu_hdma_footer + sep #0x20 + lda #0x00 + sta.l MENU_HDMA_SHADOW, x + jsr.w _menu_hdma_signal + rep #0x20 + pla + sta.b 0x42 + pla + sta.b 0x40 + ply + plx + pla + plp + rts + } + + _menu_hdma_header: + """Field profile header: 48-scanline border at BASE scroll (one entry).""" + sep #0x20 + lda #48 + sta.l MENU_HDMA_SHADOW, x + inx + rep #0x20 + lda.w menu_rolling.base_scroll + sta.l MENU_HDMA_SHADOW, x + inx + inx + rts + + _menu_hdma_footer: + """Field profile footer: 16 scanlines at BASE+16 (locks bottom-border row 24).""" + sep #0x20 + lda #16 + sta.l MENU_HDMA_SHADOW, x + inx + rep #0x20 + lda.w menu_rolling.base_scroll + clc + adc.w #16 + sta.l MENU_HDMA_SHADOW, x + inx + inx + rts + + _menu_hdma_signal: + """Field profile NMI signal: set copy-pending shadow flag.""" + sep #0x20 + lda #0x01 + sta.w menu_rolling.hdma_copy_pending + rts + + init_menu_rolling_buffer_impl: + """ + Init field rolling buffer (10 visible, lazy 11th slot). + + Populates the phase-1 engine config + hook fields so the bank-20 + `rolling_engine_*` entries can drive the same instance the macro + expansion below already manages. Engine consumers stay inert until + phase 2 swaps the macro for the JSL path ; populating now lets the + integration tests assert real config values and gives us a stable + ABI surface to port against. + """ + + php + rep #0x30 ; M=16, X=16 + sep #0x20 ; M=8 (X stays 16) + ; visible_rows + slot_height_tiles + lda.b #MENU_VISIBLE_ITEMS + sta.l menu_rolling.visible_rows + lda.b #0x02 ; 2 BG rows per slot + sta.l menu_rolling.slot_height_tiles + ; item_list_ptr = $7E:1440 (vanilla inventory array) + lda.b #0x40 + sta.l menu_rolling.item_list_ptr + lda.b #0x14 + sta.l menu_rolling.item_list_ptr + 1 + lda.b #0x7E + sta.l menu_rolling.item_list_ptr + 2 + ; item_count = 48 (vanilla field inventory) + lda.b #0x30 + sta.l menu_rolling.item_count + ; hdma_channel = 5 (BG1VOFS HDMA used by field-items rolling buffer) + lda.b #0x05 + sta.l menu_rolling.hdma_channel + ; vwf_cfg_ptr = $70:7080 (VWF_CONFIG_BASE) + lda.b #0x80 + sta.l menu_rolling.vwf_cfg_ptr + lda.b #0x70 + sta.l menu_rolling.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l menu_rolling.vwf_cfg_ptr + 2 + ; fn_render_slot = menu_fn_render_slot_trampoline (bank-20 RTL wrapper) + lda.b #menu_fn_render_slot_trampoline & 0xFF + sta.l menu_rolling.fn_render_slot + lda.b #( menu_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l menu_rolling.fn_render_slot + 1 + lda.b #( menu_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l menu_rolling.fn_render_slot + 2 + ; fn_update_hdma = menu_fn_update_hdma_trampoline + lda.b #menu_fn_update_hdma_trampoline & 0xFF + sta.l menu_rolling.fn_update_hdma + lda.b #( menu_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l menu_rolling.fn_update_hdma + 1 + lda.b #( menu_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l menu_rolling.fn_update_hdma + 2 + ; fn_draw_window = menu_fn_draw_window_trampoline + lda.b #menu_fn_draw_window_trampoline & 0xFF + sta.l menu_rolling.fn_draw_window + lda.b #( menu_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l menu_rolling.fn_draw_window + 1 + lda.b #( menu_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l menu_rolling.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_FIELD + sta.l menu_rolling.menu_id + plp + php + rep #0x10 + ldx.w #menu_rolling + jsr.l rolling_engine.rolling_engine_init + plp + rtl + + menu_fn_render_slot_trampoline: + """ + Bank-20 RTL wrapper around `_menu_render_item_to_slot`. + + php/plp keeps M/X flag mutations inside the inner routine from + leaking back to the engine. The engine relies on X-flag = 16 + throughout init ; hooks that toggle X-flag truncate X to its low + byte, which sends subsequent `sta.l abs,X` writes to garbage + addresses. + """ + + php + jsr.w _menu_render_item_to_slot + plp + rtl + + menu_fn_update_hdma_trampoline: + """ + Bank-20 RTL wrapper around `ensure_hdma_initialized`. + + Named fn_update_hdma in the struct but semantically the + "arm-HDMA-channel + first-frame setup" hook for init. Phase 3 may + split into ensure-once vs per-scroll-update if scroll-tick needs a + distinct entry, but the existing field-menu macro chain treats + ensure as idempotent so reusing it for both is fine. + + php/plp guards the engine's X-flag = 16 from inner sep/rep. + """ + + php + jsr.w ensure_hdma_initialized + plp + rtl + + menu_fn_draw_window_trampoline: + """ + Bank-20 RTL wrapper around `_menu_draw_inventory_window`. + + php/plp protects the engine's X-flag = 16 ; the inner draw does + `rep #$10 ; jsr ; sep #$10` for its own 8-bit-X dance and would + otherwise leave X-flag = 8 when we return, corrupting `,X` indexed + state writes downstream of the hook in the engine_init body. + """ + + php + jsr.w _menu_draw_inventory_window + plp + rtl + + _menu_draw_inventory_window: + """Field menu draws the InventoryWindow ($DCCE) frame on entry.""" + rep #0x10 + ldy.w #0xDCCE + jsr.l draw_window_trampoline + sep #0x10 + rts + + ; _menu_render_item_to_slot + ; Renders an item to a specific circular buffer slot in the tilemap. + ; + ; Input: menu_rolling.edge_row = item index (0-47) for data lookup + ; menu_rolling.slot_index = slot index (0-11) for destination + ; + ; Strategy: Set up $5d = slot_index (for Y position calculation), + ; $5a = pointer to item data, then call game's DrawItemSlot. + + _menu_render_item_to_slot: + php + phb + + ; Set data bank to $7E for WRAM access + lda #0x7E + pha + plb + + ; Set 16-bit A and X/Y for consistent register handling + rep #0x30 ; 16-bit A and X/Y + + ; Save registers and key direct page variables + pha + phx ; Save X (16-bit) + phy ; Save Y (16-bit) + lda.b 0x5a + pha + lda.b 0x29 ; Save tilemap buffer pointer + pha + lda.b 0x45 ; Save $45-$46 (used by game routines) + pha + lda.b 0x33 ; Save $33-$34 (used for tile attributes) + pha + sep #0x20 ; 8-bit A + lda.b 0x5d + pha + lda.b 0xDB ; Save tile attribute byte + pha + + ; Set $29 = $B600 for BG1 tilemap buffer + rep #0x20 ; 16-bit A (X/Y still 16-bit) + lda.w #0xB600 + sta.b 0x29 + sep #0x20 ; 8-bit A + + ; Calculate item data pointer: $1440 + (edge_row * Item.__size) + lda.w menu_rolling.edge_row + asl ; * Item.__size (2 bytes per Item) + clc + adc #0x40 ; Low byte of $1440 + sta.b 0x5a + lda #0x14 ; High byte of $1440 + adc #0x00 ; Add carry + sta.b 0x5b + + ; Load Item.id and Item.qty from ($5A) via long-addressing into WRAM. + rep #0x20 + lda.b 0x5a ; Pointer value = $1440 + edge_row * Item.__size + tax + sep #0x20 + lda.l 0x7E0000 + Item.id, x + pha ; Save Item.id for CheckCanUseItem + lda.l 0x7E0000 + Item.qty, x + sta.b 0x5C ; Store qty in $5C + + ; Call CheckCanUseItem to set palette in $DB + ; Input: A = item ID + ; Output: $DB = $00 (usable) or $04 (not usable) + stz.b 0x34 ; Clear $34 (no priority/flip bits) + pla ; Restore item ID + jsr.l check_can_use_item_trampoline ; bank-$01 trampoline for original @ $A25D (sets $DB) + + ; Set $5d = slot_index (for AND #$01 check, but we patched to AND #$00) + lda.w menu_rolling.slot_index + sta.b 0x5d + + ; Calculate Y = slot_index * 128 + 70 + ; Y is the tilemap offset for this slot + ; +64 for window border (1 tile row = 32 tiles × 2 bytes) + ; +6 for left margin (3 tiles) + rep #0x20 ; 16-bit A (X/Y already 16-bit) + lda.w menu_rolling.slot_index + and.w #0x00FF ; Clear high byte + xba ; Swap bytes: A = slot * 256 + lsr ; A = slot * 128 + clc + adc.w #0x0044 ; + 68 (64 border + 4 margin) + tay ; Y = tilemap offset (16-bit transfer) + sep #0x20 ; 8-bit A + + ; Check for trash can item ($FF) - needs special 2x2 tile graphic + lda (0x5a) ; Load item ID + cmp #0xFF + bne _not_trash_item + jsr.w draw_trash_single_column ; Draw trash icon + bra _skip_draw_item_slot + + _not_trash_item: + ; Clear the 2x2 trash can area first (in case we scrolled from trash position) + jsr.w _clear_trash_area + + ; Call DrawItemSlot inner at $A1ED + ; Expects: Y = tilemap offset, ($5A) = item pointer, ($29) = tilemap base + jsr.l draw_item_slot_inner_trampoline ; bank-$01 trampoline for original @ $A1ED + + _skip_draw_item_slot: + + ; Restore direct page variables and registers (reverse order) + pla + sta.b 0xDB ; Restore tile attribute byte + pla + sta.b 0x5d + rep #0x20 ; 16-bit A + pla + sta.b 0x33 ; Restore $33-$34 (tile attributes) + pla + sta.b 0x45 ; Restore $45-$46 (used by game routines) + pla + sta.b 0x29 ; Restore tilemap buffer pointer + pla + sta.b 0x5a + rep #0x10 ; 16-bit X/Y for pop (match push) + ply ; Restore Y (16-bit) + plx ; Restore X (16-bit) + pla ; Restore A (16-bit - still in 16-bit A from above) + + plb + plp + rts + + ensure_hdma_initialized: + """ + Lazy initialization: captures $93 and sets up HDMA on first scroll. + Called from scroll prepare functions. + Checks if base_scroll == 0xFFFF (sentinel) and if so, initializes. + """ + + ; Check if already initialized (base_scroll != 0xFFFF) + rep #0x20 ; 16-bit A + lda.w menu_rolling.base_scroll + cmp.w #0xFFFF + bne _hdma_already_init + + ; Capture base scroll from $0193 (BG1VOFS shadow, menu uses DP=$0100) + ; Use long addressing to ensure we read from WRAM + .db 0xAF ; LDA.L opcode + .db 0x93, 0x01, 0x7E ; $7E0193 + sta.w menu_rolling.base_scroll + + ; Initialize HDMA channel configuration + sep #0x20 ; Back to 8-bit for InitMenuInventoryHDMA + jsr.w init_menu_inventory_hdma + + ; NOW enable HDMA via shadow variable (channel is configured) + ; Force long addressing: STA.L $7E1BAE + lda #0x20 ; Channel 5 + .db 0x8F ; STA.L opcode + .dw menu_rolling.hdma_enable ; $1BAE + .db 0x7E ; Bank $7E + rts + + _hdma_already_init: + sep #0x20 ; Restore 8-bit mode + rts + + ; STATE MACHINE ROUTINES (FF6-style non-blocking scroll) + + scroll_state_check: + """ + Called at main loop entry ($019FF2) to handle scroll animation frames. + If scrolling is active, processes one frame and skips input handling. + + Returns: Carry clear = process input normally + Carry set = skip input (still scrolling) + """ + + php + sep #0x20 ; 8-bit A + + ; Check if we're scrolling + lda.w menu_rolling.scroll_state + beq _scroll_state_idle + + ; We're scrolling - process one animation frame + jsr.l update_scroll_frame_impl + + ; Check if scroll finished + lda.w menu_rolling.scroll_remaining + bne _scroll_still_active + + ; Scroll finished - clean up and return to idle + jsr.l finish_scroll_impl + + _scroll_state_idle: + plp + clc ; Carry clear = process input + rts + + _scroll_still_active: + plp + sec ; Carry set = skip input + rts + + start_scroll_down_impl: + """Field profile: kick scroll-down state machine via the bank-20 engine.""" + php + rep #0x10 + lda.l 0x7E1B1A ; field scroll_pos + ldx.w #menu_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_down + plp + rtl + + start_scroll_up_impl: + """Field profile: kick scroll-up state machine via the bank-20 engine.""" + php + rep #0x10 + lda.l 0x7E1B1A + ldx.w #menu_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_up + plp + rtl + + update_scroll_frame_impl: + """Field profile: per-frame scroll animation tick via the bank-20 engine.""" + php + rep #0x10 + ldx.w #menu_rolling + jsr.l rolling_engine.rolling_engine_update_scroll_frame + plp + rtl + + finish_scroll_impl: + """Field profile: end-of-animation cleanup via the bank-20 engine.""" + php + rep #0x10 + lda.l 0x7E1B1A + ldx.w #menu_rolling + jsr.l rolling_engine.rolling_engine_finish_scroll + plp + rtl + + ; Inventory Rolling Buffer Patches - Relocated to Bank $20 + ; These routines are called via JSL from bank $01 hooks. + + .if INVENTORY_ROLLING_BUFFER { + menu_entry_hook_impl: + """Field-menu entry hook: lazy-init HDMA + force shadow flush before first frame.""" + stz.w 0x1B1F + lda #0x00 + sta.l menu_rolling.hdma_enable + ; menu_rolling.hdma_enable + stz.w menu_rolling.scroll_state + stz.w menu_rolling.scroll_remaining + stz.w menu_rolling.scroll_direction + stz.w menu_rolling.transfer_pending + stz.w menu_rolling.hdma_copy_pending + ; Clear HDMA copy flag + stz.w menu_rolling.scroll_anim_offset + ; Clear low byte + stz.w menu_rolling.scroll_anim_offset + 1 + ; Clear high byte + ; Initialize cursor column to 0 for single-column mode + ; This ensures $1b22 is always 0 even if it had a value from previous menu + stz.w 0x1B22 + ; cursor_x = 0 + ; InitMenuRollingBuffer_Impl is called later via patched JSR at $9F7B + rtl + + menu_exit_hook_impl: + """Field menu teardown: zero HDMA shadow, full 12-byte rolling state, ch5 registers.""" + php + sep #0x20 + lda #0x00 + sta.l menu_rolling.hdma_enable + ; menu_rolling.hdma_enable shadow off so NMI writes 0 to HDMAEN this frame + sta.l 0x004350 + sta.l 0x004351 + sta.l 0x004352 + sta.l 0x004353 + sta.l 0x004354 + ; HDMA5 ctrl/dest/src cleared so a stale config can't restart on next mode switch + rep #0x20 + lda.w #0x0000 + sta.w menu_rolling + sta.w menu_rolling + 2 + sta.w menu_rolling + 4 + sta.w menu_rolling + 6 + sta.w menu_rolling + 8 + sta.w menu_rolling + 10 + plp + jsr.l reset_sprites_trampoline + rtl + ; swap_redraw_hook_impl_body + ; Called after item swap to redraw visible items correctly. + ; Must render to the correct circular buffer slots based on current buffer_pos. + ; Does NOT reset buffer_pos - we stay at the current scroll position. + + swap_redraw_hook_impl_body: + """Field profile: post-swap re-render of all 11 slots via the engine.""" + php + rep #0x10 + lda.l 0x7E1B1A + ldx.w #menu_rolling + jsr.l rolling_engine.rolling_engine_swap_redraw + plp + rtl + + ; clear_inventory_slot + ; Clears a single inventory slot in the tilemap buffer. + ; Input: menu_rolling.slot_index = slot to clear (0-10) + ; Used when item index is out of bounds (>= 48) + + clear_inventory_slot: + php + phb + ; Set data bank to $7E for WRAM access + lda #0x7E + pha + plb + ; Save registers - use 16-bit mode for consistent push/pop + rep #0x30 + ; 16-bit A and X/Y + pha + phx + phy + lda.b 0x29 + pha + ; Set $29 = $B600 for BG1 tilemap buffer + lda.w #0xB600 + sta.b 0x29 + ; Calculate Y = slot_index * 128 + 70 + lda.w menu_rolling.slot_index + and.w #0x00FF + xba + ; A = slot * 256 + lsr + ; A = slot * 128 + clc + adc.w #0x0044 + ; + 68 (64 border + 4 margin) + tay + ; Y = tilemap offset (16-bit) + sep #0x20 + ; 8-bit A for tile writes (X/Y stay 16-bit) + ; Clear the item name area (12 tiles = 24 bytes) + ; Use $FF as blank tile + ; Note: X is 16-bit but we only use low byte; loop works correctly + ldx.w #12 + ; 12 tiles for item name + quantity (force 16-bit immediate) + + _clear_slot_loop: + lda #0xFF + ; Blank tile + sta (0x29), y + iny + lda #0x04 + ; Palette 4 (matches normal items) + sta (0x29), y + iny + dex + bne _clear_slot_loop + ; Restore registers - must match push mode + rep #0x20 + ; 16-bit A for pop (X/Y already 16-bit) + pla + sta.b 0x29 + ply + plx + pla + plb + plp + rts + ; _clear_trash_area + ; Clears the 2x2 trash can area with blank tiles ($FF). + ; Called before drawing normal items to remove any leftover trash icon. + ; Input: Y = tilemap offset + ; ($29) = tilemap base + ; + + _clear_trash_area: + phy + ; Save Y + ; First row: 2 tiles + lda #0xFF + sta (0x29), y + iny + lda #0x00 + sta (0x29), y + iny + lda #0xFF + sta (0x29), y + iny + lda #0x00 + sta (0x29), y + ; Second row: Y + 64 from start + ply + ; Restore original Y + phy + ; Save again + rep #0x20 + tya + clc + adc.w #64 + tay + sep #0x20 + lda #0xFF + sta (0x29), y + iny + lda #0x00 + sta (0x29), y + iny + lda #0xFF + sta (0x29), y + iny + lda #0x00 + sta (0x29), y + ply + ; Restore Y + rts + + draw_trash_single_column: + + """ + Draws the trash can 2x2 tile graphic for single-column inventory. + Input: Y = tilemap offset (from slot calculation) + ($29) = tilemap base ($B600) + menu_rolling.slot_index = current slot + Tiles: $04 (top-left), $05 (top-right), $06 (bottom-left), $07 (bottom-right) + + Tilemap format: [tile_number, attributes] pairs + Each row is 64 bytes (32 tiles × 2 bytes) + """ + + ; Y points to start of item slot area + ; Draw 2x2 trash can icon, then clear remaining 10 tiles per row + ; Save starting Y for second row calculation + rep #0x20 + ; 16-bit for push + phy + ; Save starting Y + sep #0x20 + ; 8-bit A + ; First row: tiles $04, $05 + lda #0x04 + ; Tile $04 (top-left of trash can) + sta (0x29), y + iny + lda #0x00 + ; Attribute: palette 0, no flip + sta (0x29), y + iny + lda #0x05 + ; Tile $05 (top-right) + sta (0x29), y + iny + lda #0x00 + ; Attribute + sta (0x29), y + iny + ; Clear remaining 13 tiles on first row (10 name + colon + 2 digits) + ldx.w #13 + + _clear_row1: + lda #0xFF + ; Blank tile + sta (0x29), y + iny + lda #0x00 + ; Attribute + sta (0x29), y + iny + dex + bne _clear_row1 + ; Restore starting Y and add 64 for second row + rep #0x20 + ; 16-bit A + pla + ; Get starting Y + clc + adc.w #64 + ; +64 bytes = next tilemap row + tay + sep #0x20 + ; 8-bit A + ; Second row: tiles $06, $07 + lda #0x06 + ; Tile $06 (bottom-left) + sta (0x29), y + iny + lda #0x00 + ; Attribute + sta (0x29), y + iny + lda #0x07 + ; Tile $07 (bottom-right) + sta (0x29), y + iny + lda #0x00 + ; Attribute + sta (0x29), y + iny + ; Clear remaining 13 tiles on second row (10 name + colon + 2 digits) + ldx.w #13 + + _clear_row2: + lda #0xFF + ; Blank tile + sta (0x29), y + iny + lda #0x00 + ; Attribute + sta (0x29), y + iny + dex + bne _clear_row2 + rts + ; NmiDmaTransferCheck_Impl moved to bank $20 (battle/inventory_rolling.s) + ; as field_menu_nmi_dma_transfer_check_impl to save space in bank $01 + ; check_and_clear_count_impl + ; Called from DrawItemSlot to check item ID and handle count display. + ; - If item ID is 0: writes $FF tiles to clear count, skips to RTS + ; - If item ID is $FE: skips to RTS (no clearing needed) + ; - Otherwise: returns normally to draw count + ; + ; Input: $5a = pointer to item data, Y = tilemap offset, $29 = tilemap ptr + ; Modifies: A, return address on stack if skipping + + check_and_clear_count_impl: + """Drop the count column for empty/used slots.""" + lda (0x5a) + ; Load item ID + beq _clear_count + ; If 0, clear and skip + cmp #0xFE + ; Check for special item $FE + bne _normal_return + ; If not $FE, return normally to draw count + ; Item is $FE - skip count but don't clear + bra _skip_to_rts + + _clear_count: + ; Write $FF (blank tiles) to count area: colon + 2 digits = 3 tiles + lda #0xFF + ; Blank tile + sta (0x29), y + ; Colon position + iny + lda.b 0xdb + ; Attribute byte + sta (0x29), y + iny + lda #0xFF + sta (0x29), y + ; First digit position + iny + lda.b 0xdb + sta (0x29), y + iny + lda #0xFF + sta (0x29), y + ; Second digit position + iny + lda.b 0xdb + sta (0x29), y + + _skip_to_rts: + ; Replace the entire DrawItemSlot return chain so we land on the RTS at $01:A222 + ; instead of falling back to $A205 (NOPs + colon write). + ; + ; Stack on entry to Impl (pushed by trampoline + caller, top first): + ; [JSL Impl return: PCL, PCH, PB ($01)] + ; [JSR check_and_clear_count return: PCL, PCH ($A204)] + ; [DrawItemSlot caller's return ...] + ; + ; Pop both the JSL Impl return (3 bytes) AND the JSR trampoline return (2 bytes), + ; then push a synthetic return to $01:A222 (RTS) for our RTL. + pla ; PCL of JSL Impl return + pla ; PCH of JSL Impl return + pla ; PB of JSL Impl return + pla ; PCL of JSR check_and_clear_count return + pla ; PCH of JSR check_and_clear_count return + ; Stack top now = DrawItemSlot's own caller return. + ; Push return for RTL: $01:A221 (so RTL lands PC=$01:A222 = RTS). + lda #0x01 + pha + lda #0xA2 + pha + lda #0x21 + pha + + _normal_return: + rtl + + circular_slot_calc: + + """ + Calculate tilemap Y offset using circular buffer position. + Called from patched code at $A1BA via CircularSlotCalc_ext. + + Input: $5D = game's slot counter (0, 2, 4, 6... incremented by 2 per row) + $5A = src pointer ($1440 = inventory, $FF28 = treasure drops) + Output: Y = tilemap offset for circular buffer slot + Preserves: 16-bit A mode on exit + """ + + sep #0x20 + ; 8-bit A + ; Drops list at $FF28 reuses _a181 but doesn't have any rolling state. + ; The original 2-col Y calc collides slots after we forced col=0 globally + ; via the `and #$00` patch at DrawItemSlot, so two drops would render at + ; the same scanline. Detect drops via $5B high byte and emit a unique + ; per-slot Y (slot * 64 + 4) instead of any circular math. + lda.b 0x5b + cmp #0xFF + bne _circ_slot_not_drops + lda.b 0x5d + rep #0x20 + and.w #0x00FF + asl + asl + asl + asl + asl + asl ; * 64. $5D increments by 2 → step = 128 bytes = 2 tilemap rows, + ; matching the 16-px-per-item layout original uses for col-0 + ; drops with BG1VOFS=-32 ($93) pushing them down to scanline 32. + clc + adc.w #0x0044 ; col 2 + 1 tilemap row down (+$40) so the first slot + ; clears the window header before BG1VOFS shifts it. + tay + rts + _circ_slot_not_drops: + ; Inventory in treasure context uses treasure_rolling.buffer_pos. + lda.l 0x7E0000 + 0x9C06 ; treasure_rolling.hdma_enable (treasure_rolling + 0x06) + beq _circ_check_field + lda.b 0x5d + lsr + clc + adc.l 0x7E0000 + 0x9C01 ; treasure_rolling.buffer_pos (treasure_rolling + 0x01) + _t_circ_mod: + cmp #6 ; TREASURE_BUFFER_SLOTS + bcc _t_circ_done + sec + sbc #6 + bra _t_circ_mod + _t_circ_done: + rep #0x20 + and.w #0x00FF + xba + lsr + clc + adc.w #0x0004 + tay + rts + _circ_check_field: + ; Check if circular buffer mode is active (HDMA enabled) + lda.l menu_rolling.hdma_enable + beq _circ_slot_original + ; Not active, use original calculation + ; Circular buffer Y calculation + ; NOTE: Game increments $5D by 2 for each row (0, 2, 4, 6, 8, 10, 12, 14, 16, 18) + ; We must divide by 2 first to get the visual slot (0-9) + lda.b 0x5d + ; Load slot counter (0, 2, 4...) + lsr + ; Divide by 2 to get visual slot (0-9) + clc + adc.l menu_rolling.buffer_pos + ; Add buffer_pos + + _circ_slot_mod: + cmp #MENU_BUFFER_SLOTS + ; >= 11? + bcc _circ_slot_done + sec + sbc #MENU_BUFFER_SLOTS + ; Subtract 11 to wrap + bra _circ_slot_mod + + _circ_slot_done: + ; A = circular slot (0-10) + rep #0x20 + ; 16-bit A + and.w #0x00FF + ; Clear high byte (force 16-bit immediate) + xba + ; A = slot * 256 + lsr + ; A = slot * 128 + clc + adc.w #0x0004 + ; + 4 (margin only, $29 already includes border) + tay + ; Y = tilemap offset + rts + + _circ_slot_original: + ; Original game calculation: Y = ($5D / 2) * 128 + 4 + lda.b 0x5d + lsr + ; /2 + rep #0x20 + ; 16-bit A + and.w #0x00FF + ; Clear high byte (force 16-bit immediate) + xba + ; *256 + lsr + ; *128 + clc + adc.w #0x0004 + ; +4 + tay + rts + + circular_slot_calc_ext: + """Trampoline to call CircularSlotCalc from bank $01 patch at $A1BA""" + jsr.w circular_slot_calc + rtl + } } diff --git a/src/ingame/inventory_rolling_patches.s b/src/ingame/inventory_rolling_patches.s index 63a05f0..b372dec 100644 --- a/src/ingame/inventory_rolling_patches.s +++ b/src/ingame/inventory_rolling_patches.s @@ -13,111 +13,125 @@ machine, scroll up/down hooks, redraw and exit-cleanup hooks). ; ; ============================================================================ +.include "config.i" .if INVENTORY_ROLLING_BUFFER { ; ============================================================================ ; MAIN LOOP HOOK - Process scroll animation frames ; ============================================================================ ; Hook at $019FF2 - the start of the input processing loop in SelectItem. - *=0x019FF2 - jmp.w main_loop_scroll_check -; 3 bytes - replaces LDA $01 - *=0x019FF5 - nop - nop - nop -; ============================================================================ -; SCROLL DOWN - Replace blocking loop with state machine -; ============================================================================ - *=0x01A076 - jsr.w scroll_down_trigger - jmp.w 0xA0BC -; Skip to after scroll block (A button check) -; ============================================================================ -; SCROLL UP - Replace blocking loop with state machine -; ============================================================================ - *=0x01A01F - jsr.w scroll_up_trigger - jmp.w 0xA066 -; Skip to after scroll block (down button check) -; ============================================================================ -; Menu Entry/Exit Hooks -; ============================================================================ - *=0x019F27 - jsr.w menu_entry_hook - *=0x019F87 - jsr.w menu_exit_hook -; ============================================================================ -; NMI HDMA Hook -; ============================================================================ - *=0x018081 - jsr.w hdma_enable_hook - nop -; ============================================================================ -; UpdateScrollRegs BG1VOFS Hook - Skip when menu HDMA is active -; ============================================================================ - *=0x14FF2D - jsr.l conditional_bg1_vofs - nop - nop - nop - nop - nop - nop -; ============================================================================ -; Refresh inventory after SelectItem2 (swap OR use) -; ============================================================================ -; After SelectItem2 completes (swap or use item), we need to refresh the -; display. For swaps, swap_redraw_hook_impl already handled it. For item use, -; we need to refresh the current slot to show updated quantity. -; Call our refresh hook instead of the original DrawInventoryList. - *=0x01A0D2 - jsr.w item_use_refresh_hook -; ============================================================================ -; DrawItemSlot Column Check Patch - Force single column mode -; ============================================================================ - *=0x01A1F0 - .db 0x00 -; Change operand from $01 to $00 -; ============================================================================ -; Replace DrawInventoryList with our rolling buffer init -; ============================================================================ - *=0x019F7B - jsr.w init_menu_rolling_buffer -; Replace JSR DrawInventoryList -; ============================================================================ -; DrawItemSlot - Clear count display for item ID 0 (empty slot) -; ============================================================================ -; Original code at $A202: lda ($5a); cmp #$fe; beq @a222 -; We replace with JSR that handles clearing for empty slots -; Original bytes: B2 5A C9 FE F0 1A (6 bytes at $A202-$A207) - *=0x01A202 - jsr.w check_and_clear_count -; 3 bytes - checks item, clears if empty - nop -; 1 byte - padding - nop -; 1 byte - padding - nop -; 1 byte - padding -; ============================================================================ -; Patch $A1BA: Replace sequential Y calculation with circular buffer version -; ============================================================================ +.alloc at 0x019FF2 { + jmp.w main_loop_scroll_check + ; 3 bytes - replaces LDA $01 +} +.alloc at 0x019FF5 { + nop + nop + nop + ; ============================================================================ + ; SCROLL DOWN - Replace blocking loop with state machine + ; ============================================================================ +} +.alloc at 0x01A076 { + jsr.w scroll_down_trigger + jmp.w 0xA0BC + ; Skip to after scroll block (A button check) + ; ============================================================================ + ; SCROLL UP - Replace blocking loop with state machine + ; ============================================================================ +} +.alloc at 0x01A01F { + jsr.w scroll_up_trigger + jmp.w 0xA066 + ; Skip to after scroll block (down button check) + ; ============================================================================ + ; Menu Entry/Exit Hooks + ; ============================================================================ +} +.alloc at 0x019F27 { + jsr.w menu_entry_hook +} +.alloc at 0x019F87 { + jsr.w menu_exit_hook + ; ============================================================================ + ; NMI HDMA Hook + ; ============================================================================ +} +.alloc at 0x018081 { + jsr.w hdma_enable_hook + nop + ; ============================================================================ + ; UpdateScrollRegs BG1VOFS Hook - Skip when menu HDMA is active + ; ============================================================================ +} +.alloc at 0x14FF2D { + jsr.l conditional_bg1_vofs + nop + nop + nop + nop + nop + nop + ; ============================================================================ + ; Refresh inventory after SelectItem2 (swap OR use) + ; ============================================================================ + ; After SelectItem2 completes (swap or use item), we need to refresh the + ; display. For swaps, swap_redraw_hook_impl already handled it. For item use, + ; we need to refresh the current slot to show updated quantity. + ; Call our refresh hook instead of the original DrawInventoryList. +} +.alloc at 0x01A0D2 { + jsr.w item_use_refresh_hook + ; ============================================================================ + ; DrawItemSlot Column Check Patch - Force single column mode + ; ============================================================================ +} +.alloc at 0x01A1F0 { + .db 0x00 + ; Change operand from $01 to $00 + ; ============================================================================ + ; Replace DrawInventoryList with our rolling buffer init + ; ============================================================================ +} +.alloc at 0x019F7B { + jsr.w init_menu_rolling_buffer + ; Replace JSR DrawInventoryList + ; ============================================================================ + ; DrawItemSlot - Clear count display for item ID 0 (empty slot) + ; ============================================================================ + ; Original code at $A202: lda ($5a); cmp #$fe; beq @a222 + ; We replace with JSR that handles clearing for empty slots + ; Original bytes: B2 5A C9 FE F0 1A (6 bytes at $A202-$A207) +} +.alloc at 0x01A202 { + jsr.w check_and_clear_count + ; 3 bytes - checks item, clears if empty + nop + ; 1 byte - padding + nop + ; 1 byte - padding + nop + ; 1 byte - padding + ; ============================================================================ + ; Patch $A1BA: Replace sequential Y calculation with circular buffer version + ; ============================================================================ -; Original code at $A1BA-$A1C7 (14 bytes): -; LDA $5D / LSR / ASL×7 / ADC #$0004 / TAY -; Replace with JSL to CircularSlotCalc_ext trampoline + NOPs -; - *=0x01A1BA - jsr.l circular_slot_calc_ext -; 4 bytes - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop + ; Original code at $A1BA-$A1C7 (14 bytes): + ; LDA $5D / LSR / ASL×7 / ADC #$0004 / TAY + ; Replace with JSL to CircularSlotCalc_ext trampoline + NOPs + ; +} +.alloc at 0x01A1BA { + jsr.l circular_slot_calc_ext + ; 4 bytes + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop +} } diff --git a/src/ingame/inventory_rolling_trampolines.s b/src/ingame/inventory_rolling_trampolines.s index 8463c9b..df90189 100644 --- a/src/ingame/inventory_rolling_trampolines.s +++ b/src/ingame/inventory_rolling_trampolines.s @@ -2,8 +2,11 @@ Bank-$01 trampolines (jsr.l + rts) into the inventory rolling routines that live in bank $21, plus small wrappers around original bank-$01 helpers used by the rolling code. """ +.include "config.i" .include "src/ingame/macros.i" +.extern items_menu_vwf.draw_field_item_name + ;; Bank-$01 trampolines for inventory rolling routines living in bank $21. ;; Reclaimed space: $01:EBD2 onwards (from init_bg_scroll_hdma relocation). ;; Each trampoline = jsr.l + rts = 5 bytes. @@ -17,7 +20,7 @@ wrappers around original bank-$01 helpers used by the rolling code. } .if INVENTORY_ROLLING_BUFFER { -.alloc bank01_inventory_trampolines in bank01_trampolines { + .alloc bank01_inventory_trampolines in bank01_trampolines { check_and_clear_count: """Bank-$01 trampoline: bridge to `check_and_clear_count_impl` in bank $21.""" jsr.l check_and_clear_count_impl @@ -87,27 +90,27 @@ tfr_bg2_tiles_vblank_trampoline: ; original @ $01:9420 rtl -_tfr_bg3_tiles_vblank_trampoline: +tfr_bg3_tiles_vblank_trampoline: jsr 0x9447 -; original @ $01:9447 — pushes BG3 buffer at $7E:D600 to VRAM $7000 +; original @ $01:9447 - pushes BG3 buffer at $7E:D600 to VRAM $7000 ; over a vblank-bounded chunked DMA. Used by the treasure rolling ; buffer: render writes go to the BG3 staging area but original's ; treasure main loop only refreshes BG2/sprites mid-menu, so without ; this call the rolling-buffer slot updates never make it on screen. rtl -_tfr_bg4_tiles_vblank_trampoline: +tfr_bg4_tiles_vblank_trampoline: jsr 0x943A -; original @ $01:943A — pushes BG4 buffer at $7E:C600 to VRAM $7800. +; original @ $01:943A - pushes BG4 buffer at $7E:C600 to VRAM $7800. ; Used by the drops rolling buffer: drops items render into the BG4 ; frame that already holds TreasureItemsWindow, but the treasure main ; loop never re-DMAs BG4 mid-menu so swap/scroll updates would stay ; in WRAM without this call. rtl -_drops_select_bg4_trampoline: +drops_select_bg4_trampoline: jsr 0x8485 -; original SelectClearBG4 at $01:8485 — wipes BG4 staging to blank +; original SelectClearBG4 at $01:8485 - wipes BG4 staging to blank ; tiles before falling through to SelectBG4 ($8488). Without the ; clear, $C600..$CDFF holds whatever the previous menu/screen left ; there, which gets DMA'd to BG4 VRAM and bleeds across the screen @@ -136,8 +139,30 @@ reset_sprites_trampoline: jsr 0x8D6A ; original @ $01:8D6A rtl -} ; end .alloc bank01_inventory_trampolines -} ; end .if INVENTORY_ROLLING_BUFFER + +draw_field_item_name_trampoline: +""" +Bank-$01 JSR-callable wrapper around the bank-20 +`items_menu_vwf.draw_field_item_name` RTL helper. The vanilla +`DrawItemName` patch at $01:9060 / `DrawEquipItemName` at $01:9013 +must stay 4 bytes (jsr.w + rts) so the very next byte at $01:9064 +keeps holding the vanilla sprite-render sub-routine's `phx` opcode : +overrunning into $01:9064 with a JSL + RTS (5 bytes) replaced that +phx with the wrapper's RTS, which short-circuited the save-selection +sprite path's `jsr $9064` and parked every sprite off-screen on the +title-screen-press-A entry. Use this trampoline so the call site +stays at 3+1 bytes and the vanilla sprite routine is preserved. +""" + + + jsr.l items_menu_vwf.draw_field_item_name + rts + } + + +; end .alloc bank01_inventory_trampolines +} +; end .if INVENTORY_ROLLING_BUFFER ;; Bank-$01 thunks + wrappers for the treasure exchange rolling buffer. ;; Mirror the field-menu set above but call the treasure_-prefixed bodies @@ -145,7 +170,7 @@ reset_sprites_trampoline: ;; screen so the HDMA channel + tilemap buffer + WRAM shadow tables are ;; reused; only the per-menu state RAM differs. .if TREASURE_INVENTORY_ROLLING { -.alloc bank01_treasure_trampolines in bank01_trampolines { + .alloc bank01_treasure_trampolines in bank01_trampolines { _treasure_check_and_clear_count: jsr.l treasure_check_and_clear_count_impl rts @@ -186,17 +211,22 @@ _treasure_finish_scroll: ; machine so the existing field NMI hook copies the shadow→active table ; even when the rolling-buffer init at $01:D933 never ran (some treasure ; flows skip the redraw helper). -; Original writes $1BB7 each frame DOWN/UP is held — no built-in debounce +; Original writes $1BB7 each frame DOWN/UP is held - no built-in debounce ; once the blocking scroll loop is gone. Gate the trigger on -; `treasure_scroll_state == 0` and undo original's $1BB7 update when an +; `treasure_rolling.scroll_state == 0` and undo original's $1BB7 update when an ; animation is still in flight, so the rolling buffer steps once per ; press instead of advancing dozens of times per held button. treasure_scroll_down_trigger: """Treasure profile: input-driven scroll-down trigger.""" - lda.w treasure_scroll_state + lda.w treasure_rolling.scroll_state + bne _t_down_abort + lda.w treasure_scroll_cooldown bne _t_down_abort jsr.w _treasure_force_hdma_setup jsr.w _treasure_start_scroll_down + sep #0x20 + lda.b #TREASURE_SCROLL_COOLDOWN_FRAMES + sta.w treasure_scroll_cooldown rts _t_down_abort: dec.w 0x1BB7 @@ -204,10 +234,15 @@ _t_down_abort: treasure_scroll_up_trigger: """Treasure profile: input-driven scroll-up trigger.""" - lda.w treasure_scroll_state + lda.w treasure_rolling.scroll_state + bne _t_up_abort + lda.w treasure_scroll_cooldown bne _t_up_abort jsr.w _treasure_force_hdma_setup jsr.w _treasure_start_scroll_up + sep #0x20 + lda.b #TREASURE_SCROLL_COOLDOWN_FRAMES + sta.w treasure_scroll_cooldown rts _t_up_abort: inc.w 0x1BB7 @@ -240,23 +275,23 @@ _t_setup_in_treasure: lda #0x7E sta.l 0x004364 ; HDMA6 src bank rep #0x20 -; Capture original BG3VOFS shadow ($9F) — original treasure draws inventory +; Capture original BG3VOFS shadow ($9F) - original treasure draws inventory ; rows starting at screen scanline ~120 with $9F = -120, which keeps the ; existing window/dialog tilemap content visible on the header band. lda.l 0x7E019F - sta.w treasure_rolling_base_scroll + sta.w treasure_rolling.base_scroll sep #0x20 ; Original treasure ROM enables HDMAEN=$AD = ch7|ch5|ch3|ch2|ch0. ch2 ; is an HDMA INDIRECT mode-3 channel that writes BG3HOFS+BG3VOFS for ; the drops-band parallax. Even with our scroll moved to ch6 (which ; iterates after ch2 and should "win" the BG3VOFS at scanlines past ; the drops band), the rolling buffer scroll never takes effect while -; ch2 is enabled — likely because ch2 keeps reloading entries via its +; ch2 is enabled - likely because ch2 keeps reloading entries via its ; indirect table past scanline 128. Mask ch2 entirely; the drops-band ; original parallax is purely cosmetic and the drops list still lands ; at the right scanline without it. lda #0xF9 ; $AD & ~0x04 | $40 | $10 = ch7|ch6|ch5|ch4|ch3|ch0 (drops on ch4) - sta.l 0x7E1BAE + sta.l field_menu_rolling.hdma_enable rts treasure_main_loop_scroll_check: @@ -271,42 +306,53 @@ by calling the original $82C0 so original per-frame work still runs. """ - lda.w treasure_scroll_state +; Cooldown tick : runs every popup frame regardless of button state +; so the held-DOWN debounce drains uniformly. Without this the +; trigger path's per-call dec only fired while DOWN was held, which +; let auto-repeat consume the cooldown in 2-3 frames and scrolled +; the treasure inv two items per visible tap. + sep #0x20 + lda.w treasure_scroll_cooldown + beq _t_main_cd_done + dec.w treasure_scroll_cooldown + +_t_main_cd_done: + lda.w treasure_rolling.scroll_state beq _treasure_main_check_drops_tick jsr.w _treasure_update_scroll_frame - lda.w treasure_scroll_remaining + lda.w treasure_rolling.scroll_remaining bne _treasure_main_block_input jsr.w _treasure_finish_scroll _treasure_main_check_drops_tick: ; Drops scroll state machine shares the treasure menu's per-frame ; tick. While drops is animating, zero $01 (input mask) so cursor -; input is frozen until the scroll lands — same shape as the +; input is frozen until the scroll lands - same shape as the ; treasure-inventory branch above. - lda.w drops_scroll_state + lda.w drops_rolling.scroll_state beq _treasure_main_check_xfer jsr.l drops_update_scroll_frame_impl - lda.w drops_scroll_remaining + lda.w drops_rolling.scroll_remaining bne _treasure_main_block_input jsr.l drops_finish_scroll_impl _treasure_main_check_xfer: -; Drain treasure_transfer_pending — the rolling buffer renderer writes +; Drain treasure_rolling.transfer_pending - the rolling buffer renderer writes ; to the BG3 staging buffer at $7E:D600, but original's treasure main ; loop only DMAs BG2 + sprites each frame, so we have to push the BG3 ; tilemap to VRAM ourselves whenever a slot was just re-rendered. - lda.w treasure_transfer_pending + lda.w treasure_rolling.transfer_pending beq _treasure_main_after_bg3 - jsr.l _tfr_bg3_tiles_vblank_trampoline - stz.w treasure_transfer_pending + jsr.l tfr_bg3_tiles_vblank_trampoline + stz.w treasure_rolling.transfer_pending _treasure_main_after_bg3: -; Drain drops_transfer_pending — drops render into BG4 staging at +; Drain drops_rolling.transfer_pending - drops render into BG4 staging at ; $7E:C600 (alongside TreasureItemsWindow), so push BG4 to VRAM ; whenever drops re-rendered. - lda.w drops_transfer_pending + lda.w drops_rolling.transfer_pending beq _treasure_main_after_xfer - jsr.l _tfr_bg4_tiles_vblank_trampoline - stz.w drops_transfer_pending + jsr.l tfr_bg4_tiles_vblank_trampoline + stz.w drops_rolling.transfer_pending _treasure_main_after_xfer: - lda.w treasure_scroll_state + lda.w treasure_rolling.scroll_state beq _treasure_main_call_orig _treasure_main_block_input: stz.b 0x01 @@ -348,7 +394,7 @@ reveal. Returns with the row stored or an animation kicked. cmp #DROPS_VISIBLE_ITEMS bcc _drops_down_store pha - lda.l drops_scroll_state + lda.l drops_rolling.scroll_state bne _drops_down_busy ; Clamp scroll_pos at TOTAL - VISIBLE (3 for 8-total / 5-visible). lda.l drops_scroll_pos @@ -383,7 +429,7 @@ a fresh top row down. rts _drops_up_scroll: pha - lda.l drops_scroll_state + lda.l drops_rolling.scroll_state bne _drops_up_busy lda.l drops_scroll_pos beq _drops_up_busy ; already at top @@ -411,7 +457,7 @@ treasure_inventory_window: ; -24 so the window appears on screen at y=24 (matching where the ; original TreasureItemsWindow at $01:E275 lived). Anchoring at the ; tilemap origin keeps the per-row HDMA offsets consistent with the -; treasure-inventory layout — same shape, easier math. +; treasure-inventory layout - same shape, easier math. treasure_drops_window: """ Bank-$01 window data for the treasure drops list (5 visible rows, BG4). @@ -423,5 +469,63 @@ the bottom border at screen y=112..120 (just below the 5th item). menu_window(0, 0, 30, 12) -} ; end .alloc bank01_treasure_trampolines -} ; end .if TREASURE_INVENTORY_ROLLING + } + + +; end .alloc bank01_treasure_trampolines +} +; end .if TREASURE_INVENTORY_ROLLING + +.alloc bank01_shop_trampolines in bank01_trampolines { +shop_quantity_text_hook: +""" +Render the owner welcome ('Que désirez vous ?') through the small-VWF +description region, then fall through to the vanilla menu-text engine +for the rest of the quantity block (Quantité + initial '1' digit). + +Called in place of the original `jsr $8301` at the buy ($01:C442) and +sell ($01:C7E7) entry points ; the matching `LDY #shops.quantity` at +$01:C43F / $01:C7E4 is left in place so an unrelated future caller +could still chain into $8301 with the slimmed block. +""" + + + ldy.w #shops.que_desirez_vous + jsr.l items_description.draw_trampoline_pos + ldy.w #shops.quantity - 0x8000 + jsr.w 0x8301 ; draw text at position (= display_text_in_menus thunk) + rts + +shop_welcome_text_hook: +""" +Render the shop owner's greeting ('Puis-je vous aider ?') through the +small-VWF description region, then tail-jump to the vanilla menu-text +engine for the remaining `Achat Vente Sortir` action labels. + +Called in place of the original `jmp $8301` at $01:C353. The matching +`LDY #shops.welcome_and_actions` at $01:C350 is left in place ; the +slimmed `welcome_and_actions` block now holds only the action line. +""" + + + ldy.w #shops.puis_je_vous_aider + jsr.l items_description.draw_trampoline_pos + ldy.w #shops.welcome_and_actions - 0x8000 + jmp.w 0x8301 ; tail-call to draw positioned text + +shop_thanks_text_hook: +""" +Draw the thank-you window via the vanilla `$82FB` (which expects an +empty trailing text block ; see `thank_you_window` data), then render +the actual 'Merci !' copy through the small-VWF description region. + +Called in place of the original `jsr $82FB` at $01:C751. The matching +`LDY #shops.thank_you_window` at $01:C74E is left in place. +""" + + + jsr.w 0x82FB ; draw window + (empty) text + ldy.w #shops.merci + jsr.l items_description.draw_trampoline_pos + rts +} diff --git a/src/ingame/inventory_single_column.s b/src/ingame/inventory_single_column.s index 77db14e..987e7a8 100644 --- a/src/ingame/inventory_single_column.s +++ b/src/ingame/inventory_single_column.s @@ -46,251 +46,252 @@ SCROLL_LIMIT := 38 ; 48 - 10 = 38 (max scroll position) ; Original: lda #$30 at A17D-A17E (a9 30) ; Just patch the operand byte at A17E, not the full instruction -*=0x01A17E - .db VISIBLE_ITEMS + 1 ; Draw 11 items (10 visible + 1 pre-render slot) - -; ============================================================================ -; DrawItemSlot - Single Column Version -; ============================================================================ -; Original checks odd/even item index for left/right column -; Single column always uses left column position -; - -; Original at $01A1ED: -; @a1ed: lda $5d -; and #$01 -; bne @a223 ; Branch if right column -; -; Patch: Always use left column code path (never branch) - -*=0x01A1EF - ; Change BNE to BRA skip (effectively disable right-column branch) - ; Original: AND #$01 / BNE @a223 - ; New: AND #$00 / BNE @a223 (always zero, never branches) - and #0x00 - -; ============================================================================ -; Tilemap Y Position - Single Column Fix -; ============================================================================ - -; Original code at $01A1B9 calculates tilemap Y position: -; LDA $5D ; item index (0-47) -; LSR ; divide by 2 (for 2-column: items 0,1→row 0, items 2,3→row 1) -; ASL x7 ; multiply by 128 (tilemap row offset) -; ADC #$0002 ; base offset -; -; For single column, each item needs its own row, so remove the LSR. -; This makes Y = item_index * 128 + 2 instead of (item_index/2) * 128 + 2 - -*=0x01A1BC - nop ; Replace LSR with NOP - -; ============================================================================ -; Scroll Limit Patch -; ============================================================================ - -; Original scroll down limit check at $01A076: -; @a076: cmp #$0e ; 14 = 24 items - 10 visible -; beq @a0bc ; Don't scroll if at limit -; -; The CMP opcode is at $A076, operand at $A077 -; New limit for single column: 48 - 10 = 38 - -*=0x01A077 - .db SCROLL_LIMIT ; 38 instead of 14 - -; Also patch the cursor Y limit check - -; Original at $01A071: -; @a06c: lda $1b23 ; cursor Y -; cmp #$09 ; max Y = 9 (for 10 visible rows) -; -; This stays the same for 10 visible items, so no change needed - -; ============================================================================ -; Cursor Drawing - Fixed X Position -; ============================================================================ -; Original at $01A105 checks $1b22 (X position) for left/right column -; Single column: always use left position -; - -; Original: -; @a105: ... -; @a116: lda $1b22 ; cursor 1 x position -; beq @a11b -; lda #$6c ; right column X -; @a11b: clc -; adc #$04 -; -; Patch: Skip the X position check, always use left column - -*=0x01A114 - lda #0x00 ; Always 0 (left column) - replaces LDA $1B22 (3 bytes) - nop ; Was high byte of $1B22 address - nop ; Skip BEQ opcode - nop ; Skip BEQ offset - nop ; Skip LDA #$6c opcode - nop ; Skip LDA #$6c operand - -; ============================================================================ -; Input Handling - Disable Left/Right Toggle -; ============================================================================ -; Original at $01A003 and $01A014 handle left/right button presses -; to switch columns. We disable this for single column. -; - -; Left button handler at @9ff2: -; @9ff2: lda $01 -; and #JOY_LEFT -; beq @a003 -; lda $1b22 ; toggle x position -; inc -; and #$01 -; sta $1b22 -; bne @a01a ; move up -; -; Right button handler similar at @a003 -; -; Patch: Make left/right do nothing (skip to down button check) - -*=0x019FF4 - ; Change AND #JOY_LEFT to AND #0x00 (never matches) - and #0x00 - -*=0x01A005 - ; Change AND #JOY_RIGHT to AND #0x00 (never matches) - and #0x00 - -; ============================================================================ -; Scroll Position Adjustment for Drawing -; ============================================================================ -; When drawing, need to start from scroll position, not always item 0 -; This requires adding $1B1A (scroll position) to the item pointer -; - -; Original _a181 loop at $01A181: -; _a181: stz $5d ; item counter = 0 -; stz $5e -; -; We need to initialize $5d to scroll_pos * 2 (since items are 2 bytes each) -; But actually, the loop uses $5a as a pointer, and $5d as a counter -; The pointer $5a = $1440 + (scroll_pos * 2) -; -; Add patch to adjust $5a before the loop - -*=0x01A181 - jsr.w adjust_inventory_pointer - nop - -; ============================================================================ -; Cursor Y Limit for Scroll Down -; ============================================================================ -; Original at $01A071: cmp #$09 for 10 visible rows -; This is correct for single column with 10 visible items -; No change needed - -; ============================================================================ -; Second Cursor (for item swap) -; ============================================================================ -; The second cursor also uses $1b24 (X position) and $1b25 (Y position) -; For single column, $1b24 should always be 0 -; - -; When storing first item selection at $01A2C3: -; @a2c3: lda $1b1a -; clc -; adc $1b23 -; sta $1b25 -; lda $1b22 -; sta $1b24 ; Store X position -; -; Patch: Always store 0 for X position +.alloc at 0x01A17E { + .db VISIBLE_ITEMS + 1 ; Draw 11 items (10 visible + 1 pre-render slot) + + ; ============================================================================ + ; DrawItemSlot - Single Column Version + ; ============================================================================ + ; Original checks odd/even item index for left/right column + ; Single column always uses left column position + ; -; $1B25 stores absolute position (scroll + cursor) - keep original storage -; Just patch $A2CD to store 0 for X position + ; Original at $01A1ED: + ; @a1ed: lda $5d + ; and #$01 + ; bne @a223 ; Branch if right column + ; + ; Patch: Always use left column code path (never branch) +} +.alloc at 0x01A1EF { + ; Change BNE to BRA skip (effectively disable right-column branch) + ; Original: AND #$01 / BNE @a223 + ; New: AND #$00 / BNE @a223 (always zero, never branches) + and #0x00 + + ; ============================================================================ + ; Tilemap Y Position - Single Column Fix + ; ============================================================================ + + ; Original code at $01A1B9 calculates tilemap Y position: + ; LDA $5D ; item index (0-47) + ; LSR ; divide by 2 (for 2-column: items 0,1→row 0, items 2,3→row 1) + ; ASL x7 ; multiply by 128 (tilemap row offset) + ; ADC #$0002 ; base offset + ; + ; For single column, each item needs its own row, so remove the LSR. + ; This makes Y = item_index * 128 + 2 instead of (item_index/2) * 128 + 2 +} +.alloc at 0x01A1BC { + nop ; Replace LSR with NOP + + ; ============================================================================ + ; Scroll Limit Patch + ; ============================================================================ + + ; Original scroll down limit check at $01A076: + ; @a076: cmp #$0e ; 14 = 24 items - 10 visible + ; beq @a0bc ; Don't scroll if at limit + ; + ; The CMP opcode is at $A076, operand at $A077 + ; New limit for single column: 48 - 10 = 38 +} +.alloc at 0x01A077 { + .db SCROLL_LIMIT ; 38 instead of 14 -*=0x01A2CD - lda #0x00 ; Always 0 for single column X (was LDA $1B22) - nop ; Pad to 3 bytes + ; Also patch the cursor Y limit check -; ============================================================================ -; Item Index Calculation for Selection -; ============================================================================ -; Original two-column: ((Y + scroll) * 2 + X) * 2 = item_index * 2 -; Single column: (Y + scroll) * 2 -; Need to remove one ASL at each calculation - -; First item (current cursor) at $A398 -; Original: ASL / ADC $1B22 / ASL -> ((val*2)+col)*2 -; New: CLC / ADC $1B22 / ASL -> (val+col)*2 -; MUST use CLC because the previous CMP leaves carry set! - -*=0x01A398 - clc ; Clear carry (was ASL which also clears carry) - -; Second item (swap target) at $A3A6 -; $1B25 already has absolute position -; Original: ASL / ADC $1B24 / ASL -> ((val*2)+col)*2 -; New: CLC / ADC $1B24 / ASL -> (val+col)*2 - -*=0x01A3A6 - clc ; Clear carry (was ASL which also clears carry) - -; Another second item calculation at $A320 -; Used when selecting same item twice to use it -; CRITICAL: CMP $1B24 at $A318 sets carry if $1B22 >= $1B24 (always true when both are 0) -; Original ASL would clear carry, but NOP leaves carry SET, causing ADC to add +1! - -*=0x01A320 - clc ; Clear carry (was ASL which also clears carry) - ; Original calculates: (scroll_pos + cursor_y) * 2 + cursor_x * 2 - ; For single column: (scroll_pos + cursor_y) * 2 + ; Original at $01A071: + ; @a06c: lda $1b23 ; cursor Y + ; cmp #$09 ; max Y = 9 (for 10 visible rows) ; + ; This stays the same for 10 visible items, so no change needed -; At $01A309 (SelectItem2): -; @a309: lda $1b23 -; clc -; adc $1b1a -; cmp $1b25 -; bne _a38e ; items don't match -; lda $1b22 -; cmp $1b24 -; bne _a38e -; -; The X position compare should always match for single column -; Already handled by setting both to 0 - -; At $01A320 to calculate item offset: -; @a320: lda $1b25 -; asl -; adc $1b24 ; +0 or +1 for column -; asl ; x2 for 2 bytes per slot -; -; For single column, $1b24=0, so this simplifies to: $1b25 * 2 -; No patch needed if $1b24 is always 0 + ; ============================================================================ + ; Cursor Drawing - Fixed X Position + ; ============================================================================ + ; Original at $01A105 checks $1b22 (X position) for left/right column + ; Single column: always use left position + ; -; ============================================================================ -; DrawItemDesc - Single Column Fix -; ============================================================================ + ; Original: + ; @a105: ... + ; @a116: lda $1b22 ; cursor 1 x position + ; beq @a11b + ; lda #$6c ; right column X + ; @a11b: clc + ; adc #$04 + ; + ; Patch: Skip the X position check, always use left column +} +.alloc at 0x01A114 { + lda #0x00 ; Always 0 (left column) - replaces LDA $1B22 (3 bytes) + nop ; Was high byte of $1B22 address + nop ; Skip BEQ opcode + nop ; Skip BEQ offset + nop ; Skip LDA #$6c opcode + nop ; Skip LDA #$6c operand + + ; ============================================================================ + ; Input Handling - Disable Left/Right Toggle + ; ============================================================================ + ; Original at $01A003 and $01A014 handle left/right button presses + ; to switch columns. We disable this for single column. + ; -; Original at $01A7C8 uses two-column calculation (ABSOLUTE addressing): -; $A7C8: LDA.W $1B23 (AD 23 1B) - cursor_y -; $A7CB: CLC (18) -; $A7CC: ADC.W $1B1A (6D 1A 1B) - + scroll_pos -; $A7CF: ASL (0A) - * 2 (for two columns per row) <-- PATCH HERE -; $A7D0: ADC.W $1B22 (6D 22 1B) - + column -; $A7D3: ASL (0A) - * 2 (for 2 bytes per item) -; -; For single column: remove first ASL at $A7CF -; This matches the patches at $A398, $A3A6, $A320 + ; Left button handler at @9ff2: + ; @9ff2: lda $01 + ; and #JOY_LEFT + ; beq @a003 + ; lda $1b22 ; toggle x position + ; inc + ; and #$01 + ; sta $1b22 + ; bne @a01a ; move up + ; + ; Right button handler similar at @a003 + ; + ; Patch: Make left/right do nothing (skip to down button check) +} +.alloc at 0x019FF4 { + ; Change AND #JOY_LEFT to AND #0x00 (never matches) + and #0x00 +} +.alloc at 0x01A005 { + ; Change AND #JOY_RIGHT to AND #0x00 (never matches) + and #0x00 + + ; ============================================================================ + ; Scroll Position Adjustment for Drawing + ; ============================================================================ + ; When drawing, need to start from scroll position, not always item 0 + ; This requires adding $1B1A (scroll position) to the item pointer + ; -*=0x01A7CF - nop ; Remove first ASL for single column + ; Original _a181 loop at $01A181: + ; _a181: stz $5d ; item counter = 0 + ; stz $5e + ; + ; We need to initialize $5d to scroll_pos * 2 (since items are 2 bytes each) + ; But actually, the loop uses $5a as a pointer, and $5d as a counter + ; The pointer $5a = $1440 + (scroll_pos * 2) + ; + ; Add patch to adjust $5a before the loop +} +.alloc at 0x01A181 { + jsr.w adjust_inventory_pointer + nop + + ; ============================================================================ + ; Cursor Y Limit for Scroll Down + ; ============================================================================ + ; Original at $01A071: cmp #$09 for 10 visible rows + ; This is correct for single column with 10 visible items + ; No change needed + + ; ============================================================================ + ; Second Cursor (for item swap) + ; ============================================================================ + ; The second cursor also uses $1b24 (X position) and $1b25 (Y position) + ; For single column, $1b24 should always be 0 + ; -; ============================================================================ -; Initialize $1B22 (cursor column) to 0 on menu entry -; ============================================================================ -; Ensure $1b22 starts at 0 even if it had a value from a previous menu. -; Patch at $01A181 which runs after SelectClearBG1 and before DrawInventoryList. -; We already have adjust_inventory_pointer at $A181, so add $1b22 init there. -; Actually, we'll add it to the menu_entry_hook_impl in inventory_rolling.s + ; When storing first item selection at $01A2C3: + ; @a2c3: lda $1b1a + ; clc + ; adc $1b23 + ; sta $1b25 + ; lda $1b22 + ; sta $1b24 ; Store X position + ; + ; Patch: Always store 0 for X position + + ; $1B25 stores absolute position (scroll + cursor) - keep original storage + ; Just patch $A2CD to store 0 for X position +} +.alloc at 0x01A2CD { + lda #0x00 ; Always 0 for single column X (was LDA $1B22) + nop ; Pad to 3 bytes + + ; ============================================================================ + ; Item Index Calculation for Selection + ; ============================================================================ + ; Original two-column: ((Y + scroll) * 2 + X) * 2 = item_index * 2 + ; Single column: (Y + scroll) * 2 + ; Need to remove one ASL at each calculation + + ; First item (current cursor) at $A398 + ; Original: ASL / ADC $1B22 / ASL -> ((val*2)+col)*2 + ; New: CLC / ADC $1B22 / ASL -> (val+col)*2 + ; MUST use CLC because the previous CMP leaves carry set! +} +.alloc at 0x01A398 { + clc ; Clear carry (was ASL which also clears carry) + + ; Second item (swap target) at $A3A6 + ; $1B25 already has absolute position + ; Original: ASL / ADC $1B24 / ASL -> ((val*2)+col)*2 + ; New: CLC / ADC $1B24 / ASL -> (val+col)*2 +} +.alloc at 0x01A3A6 { + clc ; Clear carry (was ASL which also clears carry) + + ; Another second item calculation at $A320 + ; Used when selecting same item twice to use it + ; CRITICAL: CMP $1B24 at $A318 sets carry if $1B22 >= $1B24 (always true when both are 0) + ; Original ASL would clear carry, but NOP leaves carry SET, causing ADC to add +1! +} +.alloc at 0x01A320 { + clc ; Clear carry (was ASL which also clears carry) + ; Original calculates: (scroll_pos + cursor_y) * 2 + cursor_x * 2 + ; For single column: (scroll_pos + cursor_y) * 2 + ; + + ; At $01A309 (SelectItem2): + ; @a309: lda $1b23 + ; clc + ; adc $1b1a + ; cmp $1b25 + ; bne _a38e ; items don't match + ; lda $1b22 + ; cmp $1b24 + ; bne _a38e + ; + ; The X position compare should always match for single column + ; Already handled by setting both to 0 + + ; At $01A320 to calculate item offset: + ; @a320: lda $1b25 + ; asl + ; adc $1b24 ; +0 or +1 for column + ; asl ; x2 for 2 bytes per slot + ; + ; For single column, $1b24=0, so this simplifies to: $1b25 * 2 + ; No patch needed if $1b24 is always 0 + + ; ============================================================================ + ; DrawItemDesc - Single Column Fix + ; ============================================================================ + + ; Original at $01A7C8 uses two-column calculation (ABSOLUTE addressing): + ; $A7C8: LDA.W $1B23 (AD 23 1B) - cursor_y + ; $A7CB: CLC (18) + ; $A7CC: ADC.W $1B1A (6D 1A 1B) - + scroll_pos + ; $A7CF: ASL (0A) - * 2 (for two columns per row) <-- PATCH HERE + ; $A7D0: ADC.W $1B22 (6D 22 1B) - + column + ; $A7D3: ASL (0A) - * 2 (for 2 bytes per item) + ; + ; For single column: remove first ASL at $A7CF + ; This matches the patches at $A398, $A3A6, $A320 +} +.alloc at 0x01A7CF { + nop ; Remove first ASL for single column + + ; ============================================================================ + ; Initialize $1B22 (cursor column) to 0 on menu entry + ; ============================================================================ + ; Ensure $1b22 starts at 0 even if it had a value from a previous menu. + ; Patch at $01A181 which runs after SelectClearBG1 and before DrawInventoryList. + ; We already have adjust_inventory_pointer at $A181, so add $1b22 init there. + ; Actually, we'll add it to the menu_entry_hook_impl in inventory_rolling.s +} diff --git a/src/ingame/items.s b/src/ingame/items.s index 2c7baef..d66d98e 100644 --- a/src/ingame/items.s +++ b/src/ingame/items.s @@ -2,97 +2,96 @@ Field-menu item screen patches: scroll-region tweaks, layout fixes and entry-point hooks (separate from rolling-buffer code which lives in `inventory_rolling.s`). """ +.include "config.i" .include "src/ingame/macros.i" ; during scroll -*=0x01A814 +.alloc at 0x01A814 { load_system_menu_text_pointer(items_menu.item) ; when scroll done - -*=0x019F56 +} +.alloc at 0x019F56 { load_system_menu_text_pointer(items_menu.item) - -*=0x1a750 +} +.alloc at 0x1a750 { load_system_menu_text_pointer(items_menu.item) ; patching find description setup bank and offset - -*=0x01a7f6 +} +.alloc at 0x01a7f6 { ldx.w #assets_item_descriptions_dat & 0xffff - -*=0x01a7fc +} +.alloc at 0x01a7fc { addr = ( assets_item_descriptions_dat & 0xff0000 ) lda.l addr, x - - -*=0x01a7da - ; In the original code the item description is rendered multiple times (once per tick ?) - ; rendering the variable width font that often cause noticeable slowdowns, we are trying to render the text only - ; when the description should change. Because the draw window clears the tilemap the check must happen before it. +} +.alloc at 0x01a7da { +; In the original code the item description is rendered multiple times (once per tick ?) +; rendering the variable width font that often cause noticeable slowdowns, we are trying to render the text only +; when the description should change. Because the draw window clears the tilemap the check must happen before it. jmp.w check_if_description_was_rendered nop -_back: +item_desc_back: ; Hook in the display_item_description function, draw the window and render the string - -*=0x01a808 +} +.alloc at 0x01a808 { lda.b #assets_item_descriptions_dat >> 16 ldx.w #0x0054 jsr.w draw_vwf_message - - -*=0x1a439 +} +.alloc at 0x1a439 { ldy.w #messages.use_on_whom jsr.w draw_vwf_message_pos_with_bank - -*=0x1a36f +} +.alloc at 0x1a36f { ldy.w #messages.cantuse jsr.w draw_vwf_message_pos_with_bank ; inventory window (22 rows tall for 10 visible items + borders) - -*=0x01dcce +} +.alloc at 0x01dcce { menu_window(0, 0, 30, 23) ; - -*=0x01dcd6 +} +.alloc at 0x01dcd6 { menu_window(9, 0, 21, 3) ; item select character on the left side (selected item in the right column) - -*=0x01dd38 +} +.alloc at 0x01dd38 { menu_window(0, 5, 16, 21) ; item select character on the right side (selected item in the left column) - -*=0x01dd3c +} +.alloc at 0x01dd3c { menu_window(14, 5, 16, 21) ; moves the right item column one tile to the right - -*=0x01a227 +} +.alloc at 0x01a227 { adc.w #0x001c + 2 - -*=0x01a1c4 +} +.alloc at 0x01a1c4 { adc.w #0x0006 ; +2 tiles to the right - -*=0x1efd7d +} +.alloc at 0x1efd7d { _delta_l = 0 _delta_r = 2 .dw 0x039e - _delta_l, 0x019e - _delta_l, 0x059e - _delta_l, 0x029e - _delta_l, 0x049e - _delta_l .dw 0x0384 - _delta_r, 0x0184 - _delta_r, 0x0584 - _delta_r, 0x0284 - _delta_r, 0x0484 - _delta_r - -*=0x01a4f4 +} +.alloc at 0x01a4f4 { adc.w #0x0082 - -*=0x01a51a +} +.alloc at 0x01a51a { draw_hp_mp = 0x018a2a lda.w #0x0046 + 0x40 ldy.w #0x0007 @@ -106,35 +105,34 @@ _back: lda.w #0x0090 + 0x40 ldy.w #0x000d jsr.w draw_hp_mp - -*=0x01aed6 +} +.alloc at 0x01aed6 { ldy.w #messages.cant_use_magic - 0x8000 jsr.w draw_window_and_vwf_message ; choice window - -*=0x01db40 +} +.alloc at 0x01db40 { load_system_menu_text_pointer(treasure.choice_window) ; label window - -*=0x01db46 +} +.alloc at 0x01db46 { load_system_menu_text_pointer(treasure.header_window) - -*=0x01d83d +} +.alloc at 0x01d83d { load_system_menu_text_pointer(treasure.take_all) - - -*=0x01db2e +} +.alloc at 0x01db2e { load_system_menu_text_pointer(treasure.key_items_left_warning) - -*=0x01d95b +} +.alloc at 0x01d95b { load_system_menu_text_pointer(treasure.exchange) - -*=0x1d88b +} +.alloc at 0x1d88b { lda.b #0x48 - 8 - -*=0x1d88f +} +.alloc at 0x1d88f { lda.b #0xb8 - 8 ;01D887 A5 60 LDA $60 @@ -146,12 +144,13 @@ _back: ;01D893 A9 0E LDA #$0E ;01D895 85 46 STA $46 ;01D897 4C 81 82 JMP $8281 - -*=0x01D814 +} +.alloc at 0x01D814 { load_system_menu_text_pointer(treasure.items_window) - -*=0x1d792 +} +.alloc at 0x1d792 { _treasure_menu_entry: +} ; Test Overrides: ; would be nice to automate that @@ -165,20 +164,28 @@ _treasure_menu_entry: ; ============================================================================ ; Scroll limit: 48 items - 10 visible = 38 max scroll position ; CMP opcode at $A076, operand at $A077 - *=0x01A077 + .alloc at 0x01A077 { .db 38 ; 38 = new scroll limit ; Visible items count - handled in inventory_single_column.s ; Disable left button (AND #$00 instead of AND #$02) - *=0x019FF4 + } + + .alloc at 0x019FF4 { and #0x00 ; Disable right button (AND #$00 instead of AND #$01) - *=0x01A005 + } + + .alloc at 0x01A005 { and #0x00 ; Hook swap redraw to reset rolling buffer - *=0x01A401 + } + + .alloc at 0x01A401 { jmp.w swap_redraw_trampoline ; Replace JSR $A172 + } } + diff --git a/src/ingame/items_menu.s b/src/ingame/items_menu.s index f9e60f4..80e083d 100644 --- a/src/ingame/items_menu.s +++ b/src/ingame/items_menu.s @@ -1,56 +1,89 @@ """ -Item-name expansion for the field-menu (mirror of `battle/items_patches.s`): mul-by-9 → mul-by-12 stride -changes, $0F8000 → `assets_items_dat` pointer remaps. +Item-name expansion for the field-menu (mirror of `battle/items_patches.s`): mul-by-9 -> mul-by-17 stride +changes, $0F8000 -> `assets_items_unleashed_dat` pointer remaps. +Field / drops / treasure rolling inventory all defer to vanilla +DrawItemSlot at $01:9000, so patching here switches them in one +go. """ + +.extern items_menu_vwf.draw_field_item_name ; Item name expansion for menu system -; Patches the multiply-by-9 to multiply-by-12 -; Also redirects $0F8000 references to assets_items_dat +; Patches the multiply-by-9 to multiply-by-17 +; Also redirects $0F8000 references to assets_items_unleashed_dat ; --- Patch loop counter --- ; Original: 01/903F: A9 08 LDA #$08 +; Name field is ITEM_UNLEASHED_TEXT_SIZE chars in items_unleashed +; (records are ITEM_UNLEASHED_RECORD_SIZE bytes = symbol + name). -*=0x01903F - lda #0x0b - -; --- Patch multiply logic --- - -; Original at $019023-902A (7 bytes): -; LDA $43, ASL, ASL, ASL, ADC $43, TAX -; New: JSL to relocated routine (4 bytes) + 3 NOPs +.alloc at 0x01903F { + lda #ITEM_UNLEASHED_TEXT_SIZE -*=0x019023 - jsr.l multiply_item_index_12 - nop - nop - nop - nop + ; --- Patch multiply logic --- -; ===== ITEM TABLE ADDRESS REDIRECTS ===== + ; Original at $019023-902A (7 bytes): + ; LDA $43, ASL, ASL, ASL, ADC $43, TAX + ; New: JSL to relocated routine (4 bytes) + 3 NOPs +} +.alloc at 0x019023 { + jsr.l multiply_item_index_17 + nop + nop + nop + nop -; --- menu: item symbol --- -; Original: 01/902E: BF 00 80 0F LDA $0F8000,X + ; ===== ITEM TABLE ADDRESS REDIRECTS ===== -*=0x01902E - lda.l assets_items_dat, x + ; --- menu: item symbol --- + ; Original: 01/902E: BF 00 80 0F LDA $0F8000,X +} +.alloc at 0x01902E { + lda.l assets_items_unleashed_dat, x -; --- menu: item name (in loop) --- -; Original: 01/9043: BF 00 80 0F LDA $0F8000,X + ; --- menu: item name (in loop) --- + ; Original: 01/9043: BF 00 80 0F LDA $0F8000,X +} +.alloc at 0x019043 { + lda.l assets_items_unleashed_dat, x -*=0x019043 - lda.l assets_items_dat, x + ; ===== DRAWITEMNAME JSL HOOKS ===== + ; Both vanilla entry points relocate to `items_menu_vwf.draw_field_item_name` + ; in bank $20. Initial stub mirrors the vanilla fixed-font body so the + ; visible output stays identical; subsequent phases will swap in the + ; small_vwf glyph blit + per-slot CHR allocator. -; ===== COLON/QUANTITY POSITION PATCHES ===== -; Item names expanded from 9 to 12 bytes (+3 chars = +6 VRAM bytes) -; Change offset from $0052 to $0058 + ; DrawEquipItemName ($01:9013): vanilla `phy ; phx ; lda ($60),y ; bra _9017`. + ; Route through the bank-01 jsr.w trampoline so the patch stays inside + ; the vanilla 4-byte slot and the byte at $01:9017 stays untouched. +} +.alloc at 0x019013 { + lda (0x60), y + jsr.w draw_field_item_name_trampoline + rts -; --- DrawItemSlot: left column colon/qty position --- -; Original: 01/A1FC: 69 52 00 ADC #$0052 + ; DrawItemName ($01:9060): vanilla `phy ; phy ; bra _9017` -> caller already + ; passed A = item_id. Use the bank-01 jsr.w trampoline (3 bytes) + rts + ; (1 byte) so the FIVE-byte JSL.L + RTS no longer spills into the + ; sprite-render sub-routine at $01:9064 (the save-screen sprite path + ; jsr's $9064 directly). +} +.alloc at 0x019060 { + jsr.w draw_field_item_name_trampoline + rts -*=0x01A1FC - adc #0x0058 + ; ===== COLON/QUANTITY POSITION PATCHES ===== + ; Item names expanded from 9 to 16 bytes (+7 chars = +14 VRAM bytes) + ; Change offset from $0052 to $0060 -; --- DrawItemSlot: right column colon/qty position --- -; Original: 01/A236: 69 52 00 ADC #$0052 + ; --- DrawItemSlot: left column colon/qty position --- + ; Original: 01/A1FC: 69 52 00 ADC #$0052 +} +.alloc at 0x01A1FC { + adc #0x0060 -*=0x01A236 - adc #0x0058 + ; --- DrawItemSlot: right column colon/qty position --- + ; Original: 01/A236: 69 52 00 ADC #$0052 +} +.alloc at 0x01A236 { + adc #0x0060 +} diff --git a/src/ingame/items_menu_vwf.s b/src/ingame/items_menu_vwf.s new file mode 100644 index 0000000..542af0c --- /dev/null +++ b/src/ingame/items_menu_vwf.s @@ -0,0 +1,345 @@ +""" +Field-menu item-name VWF renderer. + +Replaces vanilla `DrawItemName` / `DrawEquipItemName` at $01:9013 / +$01:9060. The vanilla routine wrote 16-pixel-tall fixed-font tile +ids across two tilemap rows ($29) (dakuten / top half) and +($1D = $29 + $40) (kana / bottom half). We keep the 16-pixel-tall +slot layout but render the name as 8-pixel-tall variable-width +glyphs in the BOTTOM tile row only ; the TOP row is filled with +blank ($FF) tiles + the menu palette byte ($DB) so the slot still +takes two tilemap rows of vertical space. + +Inputs (caller convention preserved): + A = item id (8-bit), set by either entry point. + Y = tilemap byte offset within the row (set by the rolling + engine before each row render). + $29 = top-row tilemap pointer (16-bit, in WRAM bank $7E). + $5D = slot index 0..N-1 (set by `_menu_render_item_to_slot` in + `src/ingame/inventory_rolling.s` before each per-row call). + $DB = palette / attribute byte for fixed cells. + +Outputs: + Top row at ($29),y..($29),y+31 filled with $FF + ($DB|$34). + Bottom row at ($29+$40),y.. filled with VWF glyphs + + palette via small_vwf. + X = `items_unleashed` offset for the last byte consumed. + +Tile-id allocation: + Each visible slot owns ITEM_VWF_TILE_BUDGET (=10) tile ids + starting at ITEM_VWF_TILE_BASE + slot * budget, same scheme the + battle inventory uses. The render_allocator clamp at + slot_limit_low keeps overflow contained to the slot. + +Status: + Wiring scaffold. The bank-20 entry point is in place and the + $01:9013 / $01:9060 hooks JSL into it, but the body still falls + back to the fixed-font path so the visible output matches what + vanilla DrawItemName produced before the switch. Subsequent + phases will swap the loop body to `small_vwf.render.display_char` + + per-slot CHR clear + top-row blanking. +""" + + +.include "src/items.i" +.include "../bank20.i" +.include "src/battle/inventory_budget.i" + +.include "src/libmz.i" +.include "src/vwf_state.i" + +.extern assets_items_unleashed_dat +.extern render +.extern render.render_with_config +.extern wait_for_vblank_long +.extern dma_transfer_to_vram + + +.alloc items_menu_vwf_block in bank20_reloc { + .scope items_menu_vwf { + """ + VWF render path for field-menu / treasure / drops item-name slots. + Hijacks vanilla `DrawItemName` ($01:9060) to populate VwfConfig at + VWF_CONFIG_BASE and dispatch through `render.render_with_config`, so + the patched menu chrome keeps the slot layout while the glyphs go + through the variable-width font engine. + """ + draw_field_item_name: + """ + Bank-20 field-menu item-name render driven by VwfConfig. + + Stages the item name in `VWF_TEXT_BUFFER` with a $00 terminator, + fills `VWF_CONFIG_BASE` with per-slot tile budget + tilemap dest, + fills the top tilemap row with $FF blanks (so the 16-pixel-tall + slot keeps its height), writes the items_unleashed symbol byte + + palette to the bottom row's first tile, then calls the unified + `render.render_with_config` to blit the rest. Field uses K=5 + tiles/slot (FIELD_ITEM_VWF_TILE_BUDGET) so 11 slots fit inside the + $C0..$F6 tile-id window. + """ + + + sta.b 0x43 ; item id + php + sep #0x20 + rep #0x10 + ; Preserve caller's Y. Vanilla `DrawEquipItemName` (`$01:9013`) and + ; `DrawItemName` (`$01:9060`) each phy at entry and ply before rts ; + ; the caller `DrawItemSlot` continues with `tya ; adc #$60 ; tay` + ; to position the colon glyph, so Y must come back unchanged. + phy + ; --- X = item id * ITEM_UNLEASHED_RECORD_SIZE (inline mul-by-17) --- + rep #0x20 + lda.b 0x43 + and.w #0x00FF + pha + asl + asl + asl + asl ; * 16 + clc + adc 0x01, s ; + id = * 17 + tax + pla + sep #0x20 + ; --- Copy items_unleashed name bytes into VWF_TEXT_BUFFER, terminate $00 --- + ; Byte 0 of the record is the symbol (rendered separately as fixed + ; tile below) ; bytes 1..ITEM_UNLEASHED_TEXT_SIZE go into the buffer. + ; X is the destination index (only abs,x works with sta.l) ; the + ; source offset rides in long SRAM scratch `VWF_SRC_OFFSET` so we + ; do not steal a direct-page byte from vanilla's menu loop (the + ; original placement on DP $45 clashed with the items code's row + ; counter and broke the per-slot copy). + rep #0x20 + txa + inc ; skip symbol byte + sta.l VWF_SRC_OFFSET + sep #0x20 + ldx.w #0x0000 + + _copy_loop: + phx + rep #0x20 + lda.l VWF_SRC_OFFSET + tax + sep #0x20 + lda.l assets_items_unleashed_dat, x + pha ; save the byte so the 16-bit src-pointer update does not clobber it + inx + rep #0x20 + txa + sta.l VWF_SRC_OFFSET + sep #0x20 + pla + plx + sta.l VWF_TEXT_BUFFER, x + inx + cpx.w #ITEM_UNLEASHED_TEXT_SIZE + bne _copy_loop + lda.b #0x00 + sta.l VWF_TEXT_BUFFER, x ; null terminator + ; --- Populate VwfConfig.tile_id_base = FIELD base + $5D * K --- + ; K = FIELD_ITEM_VWF_TILE_BUDGET (=10). slot * 10 = slot*8 + slot*2. + ; Store the full 16-bit value: slots 6..10 produce tile_id_base + ; $C0 + 10*K = $C0 + 100 = $124 which the 8-bit allocator wrapped + ; back into the menu font CHR range. The wide init keeps tile_id + ; bit 8 (which `tilemap_write_no_inc` ORs in via $01 on the high + ; tilemap byte) intact. + rep #0x20 + lda.b 0x5D + and.w #0x00FF + pha + asl + asl + asl ; * 8 + clc + adc 0x01, s ; + slot = * 9 + clc + adc 0x01, s ; + slot = * 10 + clc + adc.w #FIELD_ITEM_VWF_TILE_BASE + sta.l VWF_CONFIG_BASE + VwfConfig.tile_id_base ; word + pla ; balance + sep #0x20 + lda.b #FIELD_ITEM_VWF_TILE_BUDGET + sta.l VWF_CONFIG_BASE + VwfConfig.slot_budget + ; CHR -> VRAM flush descriptor. Menu PPU runs in Mode 0 + ; (BGMODE = $00 at `ff4decomp/menu/menu.asm:3878`) with BG34NBA = $22 + ; -> BG3 CHR at VRAM word $2000 (byte $4000). Two descriptors live in + ; SRAM so the NMI flush can DMA treasure (primary) and drops (secondary) + ; in disjoint slices without one panel's range trampling the other's + ; through a single combined DMA. VWF_CALLER_CTX (set by drops_rolling + ; around its vanilla JSR chain) picks which descriptor this call's + ; flush targets ; tile_id_base / slot_budget / tilemap_base / flags + ; stay primary regardless since they are per-call render inputs, not + ; per-region flush params. + ; Tight `== 1` check so stale SRAM from old savestates (or any future + ; CTX value not yet defined) falls back to the primary path instead + ; of misrouting to drops. Without this, savestates captured before + ; the secondary descriptor existed carried random bytes at + ; VWF_CALLER_CTX and any non-zero value sent the very first render + ; into the drops branch on the wrong inventory. + sep #0x20 + lda.l VWF_CALLER_CTX + cmp.b #0x01 + beq _write_secondary_desc + rep #0x20 + ; --- Primary descriptor (treasure / field-items / default) --- + lda.w #FIELD_VWF_VRAM_DEST_WORD + sta.l VWF_CONFIG_BASE + VwfConfig.chr_vram_word + lda.w #FIELD_VWF_PRIMARY_BYTE_COUNT + sta.l VWF_CONFIG_BASE + VwfConfig.chr_byte_count + bra _desc_done + + _write_secondary_desc: + ; --- Secondary descriptor (drops in treasure popup) --- + rep #0x20 + lda.w #DROPS_VWF_VRAM_DEST_WORD + sta.l VWF_CHR_VRAM_WORD_B + lda.w #DROPS_VWF_BYTE_COUNT + sta.l VWF_CHR_BYTE_COUNT_B + lda.w #DROPS_VWF_CHR_SRC_OFFSET + sta.l VWF_CHR_SRC_OFFSET_B + + _desc_done: + sep #0x20 + ; Tilemap attr OR mask. Field VWF tile_ids live in the 9-bit + ; window ($100..$169) so bit 0 of the attr byte (= tile_id bit 8) + ; must be set ; the allocator stores $0100+, the tilemap entry + ; writes the low byte to the tile_id slot and ORs `$01` into the + ; attr byte to give the PPU the full 9-bit tile_id. + lda.b #0x01 + sta.l VWF_CONFIG_BASE + VwfConfig.flags + ; --- VwfConfig.tilemap_base = $29 + $40 + Y + 2 (skip symbol slot) --- + ; Caller's Y is the 16-bit byte offset of the top row tile we are + ; about to write the symbol into ; VWF chars start two bytes later. + ; X-flag is 16-bit (we did rep #$10 at entry) so `tya` returns the + ; full Y. An earlier version of this helper masked Y to its low + ; byte with `and #$00FF`, which made every slot past slot 1 + ; collapse onto slot 0's tilemap base (slot 2's Y = $0144 -> $44). + rep #0x20 + tya + clc + adc.b 0x29 + clc + adc.w #0x0042 ; + $40 (next row) + $02 (past symbol) + sta.l VWF_CONFIG_BASE + VwfConfig.tilemap_base + sep #0x20 + ; --- Top row: $FF tile + palette across the full slot width + ; (1 symbol + ITEM_UNLEASHED_TEXT_SIZE name + trailing blanks fit + ; into the same Y window the vanilla loop walked) --- + ldx.w #0x0000 + + _top_loop: + lda.b #0xFF + sta (0x29), y + iny + lda.b 0xDB + ora.b 0x34 + sta (0x29), y + iny + inx + cpx.w #( 1 + ITEM_UNLEASHED_TEXT_SIZE ) + bne _top_loop + ; --- Bottom row: $FF blank pre-fill across the slot width so any + ; tilemap entries past the new glyph count stop showing PREVIOUS + ; frame's tile_ids. display_char rewrites the front-of-row entries + ; as it allocates new tile_ids per glyph ; the leftover positions + ; stay $FF blank instead of pointing at stale CHR (e.g. before this + ; pre-fill, drops slot 1's bottom row kept references to slot 0's + ; tile range whenever the previous frame had a longer name, which + ; rendered the start of "Aiguille d'or" inside the next slot's row + ; as soon as drops + treasure stopped sharing a single tile region). + ; + ; Y enters this block at Y_orig + (1+ITEM_UNLEASHED_TEXT_SIZE)*2, + ; pointing past the top-row run ; advance to the bottom-row start + ; (next BG row = +$40 from top-row base = +$40 - run_size from + ; current Y), blank 17 cells, then restore Y back to Y_orig for the + ; symbol-write block below. + rep #0x20 + tya + clc + adc.w #( 0x0040 - ( 1 + ITEM_UNLEASHED_TEXT_SIZE ) * 2 ) + tay + sep #0x20 + ldx.w #0x0000 + + _bottom_blank_loop: + lda.b #0xFF + sta (0x29), y + iny + lda.b 0xDB + ora.b 0x34 + sta (0x29), y + iny + inx + cpx.w #( 1 + ITEM_UNLEASHED_TEXT_SIZE ) + bne _bottom_blank_loop + ; --- Restore caller's Y to point at the symbol slot, write symbol + + ; palette to the bottom row first tile. Y is currently + ; Y_orig + $40 + (1+ITEM_UNLEASHED_TEXT_SIZE)*2 after the bottom + ; blank ; subtract both terms to land back at Y_orig. + rep #0x20 + tya + sec + sbc.w #( 0x0040 + ( 1 + ITEM_UNLEASHED_TEXT_SIZE ) * 2 ) + tay + lda.b 0x29 + clc + adc.w #0x0040 + sta.b 0x1D + sep #0x20 + ; X currently 0 from the top-row loop ; re-fetch items_unleashed offset. + rep #0x20 + lda.b 0x43 + and.w #0x00FF + pha + asl + asl + asl + asl + clc + adc 0x01, s + tax + pla + sep #0x20 + lda.l assets_items_unleashed_dat, x + sta (0x1D), y ; bottom-row symbol tile + iny + lda.b 0xDB + ora.b 0x34 + sta (0x1D), y ; bottom-row symbol palette + iny + ; --- Run the unified renderer over VWF_TEXT_BUFFER --- + jsr.l render_with_config_trampoline + ; render_with_config sets VWF_CHR_DIRTY=1 unconditionally. For drops + ; (CTX=1) ADDITIONALLY raise DIRTY_B so the NMI's secondary flush + ; covers drops's region this frame. Treasure's primary DIRTY must + ; stay set untouched : treasure rendered its own slots earlier in the + ; same frame and clearing here would skip its flush, leaking last- + ; frame's CHR back into the treasure inventory band. Leaving DIRTY=1 + ; is harmless on drops-only frames: the primary DMA covers treasure + ; VRAM range only ($5000..$5400) which drops never writes into. + sep #0x20 + lda.l VWF_CALLER_CTX + cmp.b #0x01 + bne _dirty_done + lda.b #0x01 + sta.l VWF_CHR_DIRTY_B + + _dirty_done: + ply + plp + rtl + + render_with_config_trampoline: + """ + Trampoline that pops the bank then jsr's the bank-local entry + in small_vwf, paired RTL so callers stay long-jmp clean. + """ + + + jsr.w render.render_with_config + rtl + } +} diff --git a/src/ingame/key_item_picker.s b/src/ingame/key_item_picker.s index 9a89fc6..ecaed3e 100644 --- a/src/ingame/key_item_picker.s +++ b/src/ingame/key_item_picker.s @@ -44,7 +44,6 @@ State RAM layout (12 bytes from $1BF0, struct: RollingBufferState): $1BFE hdma_copy_pending """ - KEY_ITEM_VISIBLE_ITEMS := 6 KEY_ITEM_BUFFER_SLOTS := 7 KEY_ITEM_TOTAL_ITEMS := 48 @@ -52,21 +51,13 @@ KEY_ITEM_SCROLL_LIMIT := 42 KEY_ITEM_SCROLL_PIXELS_PER_FRAME := 8 KEY_ITEM_SCROLL_TOTAL_PIXELS := 16 -key_item_rolling := 0x1BF0 -key_item_rolling_top_row := key_item_rolling + RollingBufferState.top_row -key_item_rolling_buffer_pos := key_item_rolling + RollingBufferState.buffer_pos -key_item_rolling_edge_row := key_item_rolling + RollingBufferState.edge_row -key_item_rolling_slot_index := key_item_rolling + RollingBufferState.slot_index -key_item_rolling_base_scroll := key_item_rolling + RollingBufferState.base_scroll -key_item_hdma_enable := key_item_rolling + RollingBufferState.hdma_enable -key_item_scroll_state := key_item_rolling + RollingBufferState.scroll_state -key_item_scroll_remaining := key_item_rolling + RollingBufferState.scroll_remaining -key_item_scroll_direction := key_item_rolling + RollingBufferState.scroll_direction -key_item_transfer_pending := key_item_rolling + RollingBufferState.transfer_pending -key_item_scroll_anim_offset := key_item_rolling + RollingBufferState.scroll_anim_offset -key_item_hdma_copy_pending := key_item_rolling + RollingBufferState.hdma_copy_pending - -key_item_scroll_pos := 0x1BFF +; Key-item picker state moved out of $1B00-$1BFF to clean $7E:9C60 for +; the same reason as treasure ($9C00) + drops ($9C30) : engine path +; needs 35 bytes per instance, $1B00-$1BFF is too small and vanilla +; sprite code stomps past $1BEB. +key_item_rolling := (0x7E9C60 as RollingBufferState) + +key_item_scroll_pos := 0x9C8F KEY_ITEM_HDMA_TABLE_ADDR := 0x9900 KEY_ITEM_HDMA_TABLE := 0x7E9900 @@ -76,39 +67,40 @@ KEY_ITEM_HDMA_BANK := 0x7E KEY_ITEM_FILTER_BUFFER := 0x0712 - KEY_ITEM_HDMA_CHANNEL_BIT := 0x10 KEY_ITEM_HDMA4_CTRL := 0x4340 KEY_ITEM_HDMA4_DEST := 0x4341 KEY_ITEM_HDMA4_SRC_LO := 0x4342 KEY_ITEM_HDMA4_SRC_BANK := 0x4344 +.include "../bank20.i" +.alloc key_item_picker_block in bank20_reloc { key_item_ensure_hdma_initialized: """ -Lazy-capture $9F (BG3VOFS shadow) on first call, stash in base_scroll. HDMA channel enable deferred until the -picker has its own window draw + visible loop wired — leaving ch4 enabled here corrupts the field BG3 layer -(Cecil walks on a split screen) since the engine's RTL goes back to the field via EventCmd_f7 without any -teardown. + Lazy-capture $9F (BG3VOFS shadow) on first call, stash in base_scroll. HDMA channel enable deferred until the + picker has its own window draw + visible loop wired - leaving ch4 enabled here corrupts the field BG3 layer + (Cecil walks on a split screen) since the engine's RTL goes back to the field via EventCmd_f7 without any + teardown. """ + rep #0x20 - lda.w key_item_rolling_base_scroll + lda.w key_item_rolling.base_scroll cmp.w #0xFFFF bne _key_item_hdma_already_init lda.l 0x7E019F - sta.w key_item_rolling_base_scroll + sta.w key_item_rolling.base_scroll sep #0x20 jsr.w _key_item_init_hdma_channel lda #KEY_ITEM_HDMA_CHANNEL_BIT - sta.l 0x7E1BAE - sta.w key_item_hdma_enable + sta.l field_menu_rolling.hdma_enable + sta.w key_item_rolling.hdma_enable rts _key_item_hdma_already_init: sep #0x20 rts - _key_item_init_hdma_channel: """Configure HDMA ch4: DIRECT mode, dest BG3VOFS ($2112), source = picker shadow table at $7E:9900.""" php @@ -127,7 +119,6 @@ _key_item_init_hdma_channel: plp rts - key_item_render_item_to_slot: """Render filtered item from $7E:0712 + edge_row*Item.__size into BG3 buffer at $7E:D600 + slot_index*128 + 0x44.""" php @@ -156,7 +147,7 @@ key_item_render_item_to_slot: lda.w #0xD600 sta.b 0x29 sep #0x20 - lda.w key_item_rolling_edge_row + lda.w key_item_rolling.edge_row asl clc adc #0x12 @@ -175,10 +166,10 @@ key_item_render_item_to_slot: stz.b 0x34 pla jsr.l check_can_use_item_trampoline - lda.w key_item_rolling_slot_index + lda.w key_item_rolling.slot_index sta.b 0x5d rep #0x20 - lda.w key_item_rolling_slot_index + lda.w key_item_rolling.slot_index and.w #0x00FF xba lsr @@ -208,29 +199,28 @@ key_item_render_item_to_slot: plp rts - key_item_render_all: """ -Replacement for original UpdateItemText. Original just clears $0774 (text-buffer scratch) and walks $0712 to lay -out 4x4 grid into the BG3 buffer at $7E:D600. We replace with engine_init_rolling_buffer which renders 6 -single-col slots from $0712 into the same buffer. Original NMI's BG3 transfer then pushes them to VRAM as part -of the existing item-window flow ($EB=$01 latched by original preamble at $00:AF53). + Replacement for original UpdateItemText. Original just clears $0774 (text-buffer scratch) and walks $0712 to lay + out 4x4 grid into the BG3 buffer at $7E:D600. We replace with engine_init_rolling_buffer which renders 6 + single-col slots from $0712 into the same buffer. Original NMI's BG3 transfer then pushes them to VRAM as part + of the existing item-window flow ($EB=$01 latched by original preamble at $00:AF53). """ + php rep #0x10 sep #0x20 jsr.l key_item_init_impl - ; engine's ensure_hdma turned $1BAE bit 4 on; clear so original NMI - ; doesn't try to drive HDMA we haven't fully wired (per-scanline - ; bands not yet matched to the picker rows). +; engine's ensure_hdma turned $1BAE bit 4 on; clear so original NMI +; doesn't try to drive HDMA we haven't fully wired (per-scanline +; bands not yet matched to the picker rows). lda #0x00 - sta.l 0x7E1BAE + sta.l field_menu_rolling.hdma_enable lda #0x00 sta.l 0x00420C plp rtl - clear_key_item_slot: """Blank one tilemap row at slot_index in the BG3 buffer.""" php @@ -246,7 +236,7 @@ clear_key_item_slot: pha lda.w #0xD600 sta.b 0x29 - lda.w key_item_rolling_slot_index + lda.w key_item_rolling.slot_index and.w #0x00FF xba lsr @@ -276,10 +266,11 @@ _clear_key_loop: key_item_init_filter: """ -Filter $1440 -> $0712. Faithful inline port of original InitItemList ($00:B2D5 in actual ROM, off-by-2 from -ff4decomp notes). Clears the 96-byte filter buffer, walks 48 inventory items, copies (id, qty) pairs whose IDs -are key items: [$CE..$E6] u [$EB..$FD]. + Filter $1440 -> $0712. Faithful inline port of original InitItemList ($00:B2D5 in actual ROM, off-by-2 from + ff4decomp notes). Clears the 96-byte filter buffer, walks 48 inventory items, copies (id, qty) pairs whose IDs + are key items: [$CE..$E6] u [$EB..$FD]. """ + php phb sep #0x20 @@ -326,29 +317,109 @@ _filter_next: update_key_item_scroll_hdma: """Build the key-item HDMA shadow table via the shared engine.""" - engine_update_scroll_hdma(key_item_rolling, KEY_ITEM_HDMA_SHADOW, KEY_ITEM_BUFFER_SLOTS, KEY_ITEM_VISIBLE_ITEMS, _key_item_hdma_header, _key_item_hdma_footer, _key_item_hdma_signal) ; noqa: E501 +; Build key-item picker HDMA scroll table. Inlined from the former +; engine_update_scroll_hdma macro for the same reason as treasure. + { + php + rep #0x30 + pha + phx + phy + lda.b 0x40 + pha + lda.b 0x42 + pha + ldx.w #0x0000 + jsr.w _key_item_hdma_header + stz.b 0x42 + +_row_loop: + lda.w key_item_rolling + RollingBufferState.buffer_pos + and.w #0x00FF + clc + adc.b 0x42 + +_mod_loop: + cmp.w #KEY_ITEM_BUFFER_SLOTS + bcc _mod_done + sec + sbc.w #KEY_ITEM_BUFFER_SLOTS + bra _mod_loop + +_mod_done: + asl + asl + asl + asl + sta.b 0x40 + lda.b 0x42 + and.w #0x00FF + asl + asl + asl + asl + eor.w #0xFFFF + inc + clc + adc.b 0x40 + clc + adc.w key_item_rolling + RollingBufferState.base_scroll + sta.b 0x40 + sep #0x20 + lda #16 + sta.l KEY_ITEM_HDMA_SHADOW, x + inx + rep #0x20 + lda.b 0x40 + sta.l KEY_ITEM_HDMA_SHADOW, x + inx + inx + rep #0x20 + inc.b 0x42 + lda.b 0x42 + cmp.w #KEY_ITEM_VISIBLE_ITEMS + bcs _row_loop_done + jmp.w _row_loop + +_row_loop_done: + jsr.w _key_item_hdma_footer + sep #0x20 + lda #0x00 + sta.l KEY_ITEM_HDMA_SHADOW, x + jsr.w _key_item_hdma_signal + rep #0x20 + pla + sta.b 0x42 + pla + sta.b 0x40 + ply + plx + pla + plp + rts + } _key_item_hdma_header: -"""Picker HDMA header — 112 lines at BASE (top half = field map preserved).""" +"""Picker HDMA header - 112 lines at BASE (top half = field map preserved).""" sep #0x20 lda #112 sta.l KEY_ITEM_HDMA_SHADOW, x inx rep #0x20 - lda.w key_item_rolling_base_scroll + lda.w key_item_rolling.base_scroll sta.l KEY_ITEM_HDMA_SHADOW, x inx inx rts _key_item_hdma_footer: -"""Picker HDMA footer — 16 lines at BASE+16 to hide prefetch slot.""" +"""Picker HDMA footer - 16 lines at BASE+16 to hide prefetch slot.""" sep #0x20 lda #16 sta.l KEY_ITEM_HDMA_SHADOW, x inx rep #0x20 - lda.w key_item_rolling_base_scroll + lda.w key_item_rolling.base_scroll clc adc.w #16 sta.l KEY_ITEM_HDMA_SHADOW, x @@ -357,50 +428,148 @@ _key_item_hdma_footer: rts _key_item_hdma_signal: -"""NMI shadow-copy signal — set both picker copy_pending + shared $1BB6 mirror.""" +"""NMI shadow-copy signal - set both picker copy_pending + shared $1BB6 mirror.""" sep #0x20 lda #0x01 - sta.w key_item_hdma_copy_pending + sta.w key_item_rolling.hdma_copy_pending rts - key_item_init_impl: -"""Init key-item picker (filter $1440 -> $0712, then engine init).""" +""" + Init key-item picker (filter $1440 -> $0712 then engine init). State + + hook far-ptrs live at $7E:9C60 (relocated out of $1B00-$1BFF). +""" + jsr.w key_item_init_filter - engine_init_rolling_buffer(key_item_rolling, KEY_ITEM_BUFFER_SLOTS, _key_item_draw_window, key_item_ensure_hdma_initialized, key_item_render_item_to_slot) ; noqa: E501 + php + rep #0x30 + sep #0x20 + lda.b #KEY_ITEM_BUFFER_SLOTS + sta.l key_item_rolling + RollingBufferState.visible_rows + lda.b #0x02 + sta.l key_item_rolling + RollingBufferState.slot_height_tiles +; item_list_ptr = $7E:0712 (filtered key-item array) + lda.b #0x12 + sta.l key_item_rolling + RollingBufferState.item_list_ptr + lda.b #0x07 + sta.l key_item_rolling + RollingBufferState.item_list_ptr + 1 + lda.b #0x7E + sta.l key_item_rolling + RollingBufferState.item_list_ptr + 2 + lda.b #KEY_ITEM_TOTAL_ITEMS + sta.l key_item_rolling + RollingBufferState.item_count + lda.b #0x04 + sta.l key_item_rolling + RollingBufferState.hdma_channel + lda.b #0x80 + sta.l key_item_rolling + RollingBufferState.vwf_cfg_ptr + lda.b #0x70 + sta.l key_item_rolling + RollingBufferState.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l key_item_rolling + RollingBufferState.vwf_cfg_ptr + 2 + lda.b #key_item_fn_render_slot_trampoline & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_render_slot + lda.b #( key_item_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_render_slot + 1 + lda.b #( key_item_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_render_slot + 2 + lda.b #key_item_fn_update_hdma_trampoline & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_update_hdma + lda.b #( key_item_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_update_hdma + 1 + lda.b #( key_item_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_update_hdma + 2 + lda.b #key_item_fn_draw_window_trampoline & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_draw_window + lda.b #( key_item_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_draw_window + 1 + lda.b #( key_item_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l key_item_rolling + RollingBufferState.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_KEY_ITEM + sta.l key_item_rolling + RollingBufferState.menu_id + plp + php + rep #0x10 + ldx.w #key_item_rolling + jsr.l rolling_engine.rolling_engine_init + plp + rtl +key_item_fn_render_slot_trampoline: +"""Bank-20 RTL wrapper around `key_item_render_item_to_slot`.""" + php + jsr.w key_item_render_item_to_slot + plp + rtl + +key_item_fn_update_hdma_trampoline: +"""Bank-20 RTL wrapper around `key_item_ensure_hdma_initialized`.""" + php + jsr.w key_item_ensure_hdma_initialized + plp + rtl + +key_item_fn_draw_window_trampoline: +"""Bank-20 RTL wrapper around `_key_item_draw_window`.""" + php + jsr.w _key_item_draw_window + plp + rtl _key_item_draw_window: """ -Picker is invoked from inside original ShowItemWindow which already drew the -picker frame via its IRQ slide. No-op. + Picker is invoked from inside original ShowItemWindow which already drew the + picker frame via its IRQ slide. No-op. """ - rts - -key_item_scroll_down_prepare: -"""Picker scroll-down pre-render.""" - engine_scroll_down_prepare(key_item_rolling, key_item_scroll_pos, KEY_ITEM_SCROLL_LIMIT, KEY_ITEM_VISIBLE_ITEMS, KEY_ITEM_BUFFER_SLOTS, key_item_ensure_hdma_initialized, key_item_render_item_to_slot, update_key_item_scroll_hdma) ; noqa: E501 -key_item_scroll_up_prepare: -"""Picker scroll-up pre-render.""" - engine_scroll_up_prepare(key_item_rolling, key_item_scroll_pos, KEY_ITEM_BUFFER_SLOTS, key_item_ensure_hdma_initialized, key_item_render_item_to_slot, update_key_item_scroll_hdma) ; noqa: E501 + rts key_item_start_scroll_down_impl: -"""Picker: kick scroll-down state machine.""" - engine_start_scroll_down(key_item_rolling, key_item_scroll_pos, KEY_ITEM_VISIBLE_ITEMS, KEY_ITEM_BUFFER_SLOTS, KEY_ITEM_SCROLL_TOTAL_PIXELS, KEY_ITEM_SCROLL_PIXELS_PER_FRAME, key_item_ensure_hdma_initialized, key_item_render_item_to_slot, update_key_item_scroll_hdma) ; noqa: E501 +"""Picker: kick scroll-down state machine via the engine.""" + php + rep #0x10 + lda.l key_item_scroll_pos + ldx.w #key_item_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_down + plp + rtl key_item_start_scroll_up_impl: -"""Picker: kick scroll-up state machine.""" - engine_start_scroll_up(key_item_rolling, key_item_scroll_pos, KEY_ITEM_BUFFER_SLOTS, KEY_ITEM_SCROLL_TOTAL_PIXELS, key_item_ensure_hdma_initialized, key_item_render_item_to_slot, update_key_item_scroll_hdma) ; noqa: E501 +"""Picker: kick scroll-up state machine via the engine.""" + php + rep #0x10 + lda.l key_item_scroll_pos + ldx.w #key_item_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_up + plp + rtl key_item_update_scroll_frame_impl: """Picker: per-frame scroll animation tick.""" - engine_update_scroll_frame(key_item_rolling, KEY_ITEM_SCROLL_PIXELS_PER_FRAME, update_key_item_scroll_hdma) + php + rep #0x10 + ldx.w #key_item_rolling + jsr.l rolling_engine.rolling_engine_update_scroll_frame + plp + rtl key_item_finish_scroll_impl: -"""Picker: end-of-animation pre-render + cleanup.""" - engine_finish_scroll(key_item_rolling, key_item_scroll_pos, KEY_ITEM_VISIBLE_ITEMS, KEY_ITEM_BUFFER_SLOTS, KEY_ITEM_TOTAL_ITEMS, key_item_render_item_to_slot, update_key_item_scroll_hdma) ; noqa: E501 +"""Picker: end-of-animation cleanup via the bank-20 engine.""" + php + rep #0x10 + lda.l key_item_scroll_pos + ldx.w #key_item_rolling + jsr.l rolling_engine.rolling_engine_finish_scroll + plp + rtl key_item_refresh_slots_impl: -"""Picker: re-render all slots.""" - engine_refresh_slots(key_item_rolling, key_item_scroll_pos, KEY_ITEM_BUFFER_SLOTS, key_item_render_item_to_slot) +"""Picker: re-render all slots via the bank-20 engine.""" + php + sep #0x20 + rep #0x10 + lda.l key_item_scroll_pos + ldx.w #key_item_rolling + jsr.l rolling_engine.rolling_engine_refresh_slots + plp + rtl +} + diff --git a/src/ingame/key_item_picker_patches.s b/src/ingame/key_item_picker_patches.s index dd9c243..398f71c 100644 --- a/src/ingame/key_item_picker_patches.s +++ b/src/ingame/key_item_picker_patches.s @@ -30,7 +30,16 @@ Patched: """ +.include "config.i" .if TREASURE_INVENTORY_ROLLING { +; TODO : wire `jsr.l key_item_render_all` at $00:AF4D once the picker +; has a VRAM strategy that doesn't garble the room/map underneath. The +; picker triggers from event scripts mid-map (EventCmd_f7), so unlike +; the field menu we cannot blow away the BG3 CHR slice the room is +; using. Two viable paths : (a) save/restore around picker entry/exit, +; (b) reserve picker-only VRAM that the map provably never touches. +; Engine code at `key_item_render_all` + `key_item_init_impl` is in +; place but currently unreachable. ; UpdateItemText at $00:B22B reads scroll pos $BA, multiplies by 4 ; (asl asl) for 2-col x 2-byte stride into $0712. Single-col layout ; needs stride = 2, so replace the 2nd `asl` at $00:B232 with NOP. @@ -50,74 +59,91 @@ Patched: ; (text buffer is 13 chars/half-row × 2 = 26 wide) which is more ; invasive. Keep 2-col layout for now, single-stride scroll lets ; us walk the filtered $0712 in step with the engine's worldview. - *=0x00B23C - nop - -; Items per page: original `lda #$08` → 4 (single-col 4 rows). - *=0x00B245 - lda #0x04 - -; Replace the inline id*9 multiplier at $00:B253-B26C with a -; jsl multiply_by_12 chain. A is item-id on entry (just loaded via -; lda $0712,x at $B24B), returns A=id*12. Move into X for the -; existing inner-loop indexed `lda.l assets_items_dat, x` read. -; The original block was 26 bytes ($B253..$B26C); replacement uses -; 9 bytes, padded with NOP to keep downstream instruction -; addresses ($B26F lda#, $B273 lda.l ...) anchored. - *=0x00B253 - rep #0x10 - jsr.l multiply_by_12 - tax - inx - pad_nop(18) - -; Inner-name-write loop count at $00:B26F: original `lda #$08` -; (8 letters per name). Bump to ITEM_NAME_TEXT_SIZE. - *=0x00B26F - lda #ITEM_NAME_TEXT_SIZE - -; Item-name table source at $00:B273: original `lda.l $0F8000,x` -; (JP layout). Redirect to French assets_items_dat. - *=0x00B273 - lda.l assets_items_dat, x - -; Make both column-toggle branches advance Y by 24 (full text-buffer -; row, 12 chars). Both → `adc #$18` so every item lands on its own -; row regardless of which column the toggle picks. - *=0x00B2B5 - adc #0x18 - *=0x00B2BF - adc #0x18 - -; Item-text layout post-name: original writes ":" at +8 ($077C), tens -; at +9 ($077D), ones at +10 ($077E). Names were 8 chars (slots 0..7). -; French names are 11 chars (slots 0..10) + 1 tile spacer = 12, so -; push the qty trio out by +12 (one full tile beyond the name): - *=0x00B28E - sta 0x0780, y - *=0x00B2A4 - sta 0x0781, y - *=0x00B2A9 - sta 0x0782, y - -; Single-col picker has no col-1 to move cursor to. NOP the JOY_RIGHT -; check at $00:AFB0 by replacing the AND mask with $00 — beq always -; taken, right-button branch skipped. Original: -; AFB0: A5 03 lda $03 -; AFB2: 29 01 and #$01 ← JOY_RIGHT mask -; AFB4: F0 1A beq +$1A - *=0x00AFB2 - and #0x00 - -; A-select index calc at $00:AF9C: original -; lda $ba / clc / adc $8c / asl / clc / adc $8b / asl / tax -; index = (($ba + $8c) * 2 + $8b) * 2 = ($ba+$8c)*4 -; Single col: drop the col mix-in AND the second asl, so -; index = ($ba + $8c) * 2 (one item = 2 bytes id+qty). -; $00:AFA2 clc / adc $8b (3 bytes) → 3 NOPs -; $00:AFA5 asl (1 byte) → NOP - *=0x00AFA2 - pad_nop(3) - *=0x00AFA5 - nop +.alloc at 0x00B23C { + nop + + ; Items per page: original `lda #$08` → 4 (single-col 4 rows). +} +.alloc at 0x00B245 { + lda #0x04 + + ; Replace the inline id*9 multiplier at $00:B253-B26C with a + ; jsl multiply_by_17 chain. A is item-id on entry (just loaded via + ; lda $0712,x at $B24B), returns A = id * ITEM_UNLEASHED_RECORD_SIZE. + ; Move into X for the existing inner-loop indexed + ; `lda.l assets_items_unleashed_dat, x` read. The original block was + ; 26 bytes ($B253..$B26C); replacement uses 9 bytes, padded with NOP + ; to keep downstream instruction addresses ($B26F lda#, $B273 lda.l + ; ...) anchored. +} +.alloc at 0x00B253 { + rep #0x10 + jsr.l multiply_by_17 + tax + inx + pad_nop(18) + + ; Inner-name-write loop count at $00:B26F: original `lda #$08` + ; (8 letters per name). Bump to ITEM_UNLEASHED_TEXT_SIZE. +} +.alloc at 0x00B26F { + lda #ITEM_UNLEASHED_TEXT_SIZE + + ; Item-name table source at $00:B273: original `lda.l $0F8000,x` + ; (JP layout). Redirect to assets_items_unleashed_dat. +} +.alloc at 0x00B273 { + lda.l assets_items_unleashed_dat, x + + ; Make both column-toggle branches advance Y by 24 (full text-buffer + ; row, 12 chars). Both → `adc #$18` so every item lands on its own + ; row regardless of which column the toggle picks. Keep at 24 + ; for now ; widening to fit a 16-char name + colon + qty requires + ; revisiting the picker's tilemap row stride too. +} +.alloc at 0x00B2B5 { + adc #0x18 +} +.alloc at 0x00B2BF { + adc #0x18 + + ; Item-text layout post-name: original writes ":" at +8 ($077C), tens + ; at +9 ($077D), ones at +10 ($077E). 12-char names pushed the trio + ; to $0780/81/82. 16-char names need another +4 to land past the name + ; (assets_items_unleashed_dat = symbol + 16 chars), so trio sits at + ; $0784/85/86 with the same 1-tile spacer between name and colon. +} +.alloc at 0x00B28E { + sta 0x0784, y +} +.alloc at 0x00B2A4 { + sta 0x0785, y +} +.alloc at 0x00B2A9 { + sta 0x0786, y + + ; Single-col picker has no col-1 to move cursor to. NOP the JOY_RIGHT + ; check at $00:AFB0 by replacing the AND mask with $00 — beq always + ; taken, right-button branch skipped. Original: + ; AFB0: A5 03 lda $03 + ; AFB2: 29 01 and #$01 ← JOY_RIGHT mask + ; AFB4: F0 1A beq +$1A +} +.alloc at 0x00AFB2 { + and #0x00 + + ; A-select index calc at $00:AF9C: original + ; lda $ba / clc / adc $8c / asl / clc / adc $8b / asl / tax + ; index = (($ba + $8c) * 2 + $8b) * 2 = ($ba+$8c)*4 + ; Single col: drop the col mix-in AND the second asl, so + ; index = ($ba + $8c) * 2 (one item = 2 bytes id+qty). + ; $00:AFA2 clc / adc $8b (3 bytes) → 3 NOPs + ; $00:AFA5 asl (1 byte) → NOP +} +.alloc at 0x00AFA2 { + pad_nop(3) +} +.alloc at 0x00AFA5 { + nop +} } diff --git a/src/ingame/magic.s b/src/ingame/magic.s index 4d0b996..ea89ca2 100644 --- a/src/ingame/magic.s +++ b/src/ingame/magic.s @@ -6,94 +6,109 @@ the magic-render path. ; Columns offsets -*=0x1efebd -first_column := 0x0248 - 4 -.dw first_column + 10 * 2 * 2, first_column + 10 * 2, first_column +.alloc at 0x1efebd { + first_column := 0x0248 - 4 + .dw first_column + 10 * 2 * 2, first_column + 10 * 2, first_column ; columns cursor postion - -*=0x01b211 +} +.alloc at 0x01b211 { .db 0x0, 10 * 8, 10 * 8 * 2 ; White spells - -*=0x01AFA2 +} +.alloc at 0x01AFA2 { load_system_menu_text_pointer(spells.white) ; Black spells - -*=0x01AFB6 +} +.alloc at 0x01AFB6 { load_system_menu_text_pointer(spells.black) ; summons - -*=0x01AFCA +} +.alloc at 0x01AFCA { load_system_menu_text_pointer(spells.summon) - -*=0x01AFDE +} +.alloc at 0x01AFDE { load_system_menu_text_pointer(spells.ninja) - -*=0x1d95e +} +.alloc at 0x1d95e { load_system_menu_text_pointer(spells.kokan) - -*=0x01b0ec +} +.alloc at 0x01b0ec { ldx.w #0x020A ldy.w #spells.mp_needed & 0xffff jsr.w copy_text_with_dakuten ; Grisement des types sorts : 'Blancs' 'Noirs' etc ... - -*=0x01B419 +} +.alloc at 0x01B419 { ldy.w #0x0007 sta.w 0xC5FF + 2, x ; Spells type cursor offset - -*=0x01B0CE +} +.alloc at 0x01B0CE { lda.b #0x00 ; Spells cost before char - -*=0x01B0F7 +} +.alloc at 0x01B0F7 { sta.w 0xC816 + 0x40 + 2 ; Spells cost - -*=0x01B0E6 +} +.alloc at 0x01B0E6 { ldy.w #0x021A + 0x40 + 2 ; [use spell] Choose Spell character target window. - -*=0x01dd6d +} +.alloc at 0x01dd6d { menu_window(8, 0, 22, 26) ; [use spell] Use spell on whom window. - -*=0x01dd71 +} +.alloc at 0x01dd71 { menu_window(0, 8, 8, 5) ; [use spell] Magic name position. - -*=0x01b4a7 +} +.alloc at 0x01b4a7 { ldx.w #0x0042 - +} ; move character blocks one tile back .if 0 { - *=0x01b4c5 + .alloc at 0x01b4c5 { ldx.w #0x02e0 - 2 - *=0x01b4ce + } + + + .alloc at 0x01b4ce { ldx.w #0x0060 - 2 - *=0x01b4d7 + } + + + .alloc at 0x01b4d7 { ldx.w #0x0560 - 2 - *=0x01b4e0 + } + + + .alloc at 0x01b4e0 { ldx.w #0x01a0 - 2 - *=0x01b4e9 + } + + + .alloc at 0x01b4e9 { ldx.w #0x0420 - 2 ; moves character portrait 8 pixels back - *=0x1efea9 + } + + + .alloc at 0x1efea9 { delta = 8 .db 0x58 - delta, 0x68 .db 0x58 - delta, 0x18 @@ -105,30 +120,33 @@ first_column := 0x0248 - 4 .db 0x60 - delta, 0xb8 .db 0x58 - delta, 0x40 .db 0x58 - delta, 0x90 ; 2 front/3 back + } } -; do not display on whom window -*=0x01b498 +; do not display on whom window +.alloc at 0x01b498 { nop nop nop ; do not display to "on whom ?" text - -*=0x01b4ad +} +.alloc at 0x01b4ad { nop nop nop - ; load_system_menu_text_pointer(use_spell.use_on_whom) - ;ldy.w #0xdd79 +; load_system_menu_text_pointer(use_spell.use_on_whom) +;ldy.w #0xdd79 nop nop nop - -*=0x01b4b3 +} +.alloc at 0x01b4b3 { load_system_menu_text_pointer(use_spell.mp_cost) ; moves the magic target cursor X 8 pixels - -*=0x01B5FF +} +.alloc at 0x01B5FF { lda #0x40 + 8 +} + diff --git a/src/ingame/main.s b/src/ingame/main.s index f401ccc..8aba45c 100644 --- a/src/ingame/main.s +++ b/src/ingame/main.s @@ -6,300 +6,325 @@ in-game menu wiring. { - *=0x01DB61 - .scope _main_menu { -characters_window: - menu_window(0, 0, 22, 26) -gil_window: - menu_window(22, 23, 8, 3) -time_window: - menu_window(23, 19, 7, 2) -menu: -; ptr 0xdb6d - menu_window(23, 0, 7, 17) - } - - *=0x01dd51 - menu_window(1, 8, 29, 17) - - *=0x01892E - load_system_menu_text_pointer(in_game_menu.menu) -; jsr.w draw_window_and_vwf_message - -; Gils - *=0x0187CE - load_system_menu_text_pointer(in_game_menu.gils) - -; moves gils two chars on the right - *=0x0187DA - ldy.w #0x062A + 4 - -; TIME - *=0x0187C5 - ldx.w #0x52E + 2 - load_system_menu_text_pointer(in_game_menu.time) - -; disable Save text - *=0x018939 - jmp.l disable_save - -; Moves the classes on the next line - *=0x018C1A - adc.w #0x004e - -; disable class name - *=0x018b30 - rts - -;*=0x0188d0 -; ldx.w #0x02CE - - - *=0x018FD3 - jsr.l load_classes_pointer - nop - sta 0x45 - xba - sta 0x46 - ldx 0x45 - lda #0x0F - - *=0x018fe3 - { -load_next_char: - lda.l assets_classes_dat, x - beq end -; dakuten - jsr.w 0x8E32 - sta.w 0x0000, y - xba - sta.w 0x0040, y - iny - lda.b 0x34 - sta.w 0x0000, y - sta.w 0x0040, y - inx - iny +.alloc at 0x01DB61 { + .scope _main_menu { + characters_window: + menu_window(0, 0, 22, 26) + gil_window: + menu_window(22, 23, 8, 3) + time_window: + menu_window(23, 19, 7, 2) + menu: + ; ptr 0xdb6d + menu_window(23, 0, 7, 17) + } +} +.alloc at 0x01dd51 { + menu_window(1, 8, 29, 17) +} +.alloc at 0x01892E { + load_system_menu_text_pointer(in_game_menu.menu) + ; jsr.w draw_window_and_vwf_message - bra load_next_char -end: - rts - } + ; Gils +} +.alloc at 0x0187CE { + load_system_menu_text_pointer(in_game_menu.gils) - *=0x0189b9 -; Level offset - adc.w #0x0044 - 2 + ; moves gils two chars on the right +} +.alloc at 0x0187DA { + ldy.w #0x062A + 4 - *=0x018a03 - draw_hp_mp = 0x018a2a - lda.w #0x0046 + 0x40 - ldy.w #0x0007 ; current hp - jsr.w draw_hp_mp - lda.w #0x0050 + 0x40 - ldy.w #0x0009 ; max hp - jsr.w draw_hp_mp - lda.w #0x0086 + 0x40 - ldy.w #0x000b ; current mp - jsr.w draw_hp_mp - lda.w #0x0090 + 0x40 - ldy.w #0x000d ; max mp - jsr.w draw_hp_mp + ; TIME +} +.alloc at 0x0187C5 { + ldx.w #0x52E + 2 + load_system_menu_text_pointer(in_game_menu.time) -; LEVEL - *=0x0189C3 - { - level_offset = 7 * 2 - lda #0xFF - sta.w 0 + level_offset, x - lda #0x4F ; N - sta.w 2 + level_offset, x - lda #0x57 ; V - sta.w 4 + level_offset, x - lda #0xFF - sta.w 6 + level_offset, x - nop + ; disable Save text +} +.alloc at 0x018939 { + jmp.l disable_save - lda #0x57 ; H 49 V 57 - sta.w 0x40 + 2 + 0x40, x - lda #0x51 ; P - sta.w 0x42 - 2 + 0x40, x - sta.w 0x82 - 2 + 0x40, x - lda #0x4E ; M - sta.w 0x80 + 2 + 0x40, x - lda #0xC7 ; / - sta.w 0x4E + 0x40, x - sta.w 0x8E + 0x40, x - } + ; Moves the classes on the next line +} +.alloc at 0x018C1A { + adc.w #0x004e -; Moves the level down in the digest - *=0x0189FA - sta.w 0x0016, x - xba - sta.w 0x0018, x + ; disable class name +} +.alloc at 0x018b30 { + rts -;; Move character name. - *=0x0183D5 - sta.w 0x0000, y - xba - sta.w 0x0040, y + ;*=0x0188d0 + ; ldx.w #0x02CE +} +.alloc at 0x018FD3 { + jsr.l load_classes_pointer + nop + sta 0x45 + xba + sta 0x46 + ldx 0x45 + lda #0x0F +} +.alloc at 0x018fe3 { + { + load_next_char: + lda.l assets_classes_dat, x + beq end + ; dakuten + jsr.w 0x8E32 + sta.w 0x0000, y + xba + sta.w 0x0040, y + iny + lda.b 0x34 + sta.w 0x0000, y + sta.w 0x0040, y + inx + iny + + bra load_next_char + end: + rts + } +} +.alloc at 0x0189b9 { + ; Level offset + adc.w #0x0044 - 2 +} +.alloc at 0x018a03 { + draw_hp_mp = 0x018a2a + lda.w #0x0046 + 0x40 + ldy.w #0x0007 ; current hp + jsr.w draw_hp_mp + lda.w #0x0050 + 0x40 + ldy.w #0x0009 ; max hp + jsr.w draw_hp_mp + lda.w #0x0086 + 0x40 + ldy.w #0x000b ; current mp + jsr.w draw_hp_mp + lda.w #0x0090 + 0x40 + ldy.w #0x000d ; max mp + jsr.w draw_hp_mp + + ; LEVEL +} +.alloc at 0x0189C3 { + { + level_offset = 7 * 2 + lda #0xFF + sta.w 0 + level_offset, x + lda #0x4F ; N + sta.w 2 + level_offset, x + lda #0x57 ; V + sta.w 4 + level_offset, x + lda #0xFF + sta.w 6 + level_offset, x + nop + + lda #0x57 ; H 49 V 57 + sta.w 0x40 + 2 + 0x40, x + lda #0x51 ; P + sta.w 0x42 - 2 + 0x40, x + sta.w 0x82 - 2 + 0x40, x + lda #0x4E ; M + sta.w 0x80 + 2 + 0x40, x + lda #0xC7 ; / + sta.w 0x4E + 0x40, x + sta.w 0x8E + 0x40, x + } + + ; Moves the level down in the digest +} +.alloc at 0x0189FA { + sta.w 0x0016, x + xba + sta.w 0x0018, x -; translate can't fight text + ;; Move character name. +} +.alloc at 0x0183D5 { + sta.w 0x0000, y + xba + sta.w 0x0040, y - *=0x018B2A - load_system_menu_text_pointer(in_game_menu.cant_fight) + ; translate can't fight text +} +.alloc at 0x018B2A { + load_system_menu_text_pointer(in_game_menu.cant_fight) -; grey out more tiles for the first line in char block - *=0x018C30 - lda #15 + ; grey out more tiles for the first line in char block +} +.alloc at 0x018C30 { + lda #15 -;*=0x018b6b -; lda #0x42 + ;*=0x018b6b + ; lda #0x42 -; Time offset - *=0x018BC1 - lda.b #0x80 - sta.w 0x0578, y - xba - sta.w 0x057A, y - rep #0x20 - lda.b 0x73 - sep #0x20 - jsr.w 0x81D6 - lda.b 0x5B - sta.w 0x0570, y - lda.b 0x5D - sta.w 0x0572, y - lda.b 0x5E - sta.w 0x0574, y - lda.b #0xC8 - sta.w 0x0576, y + ; Time offset +} +.alloc at 0x018BC1 { + lda.b #0x80 + sta.w 0x0578, y + xba + sta.w 0x057A, y + rep #0x20 + lda.b 0x73 + sep #0x20 + jsr.w 0x81D6 + lda.b 0x5B + sta.w 0x0570, y + lda.b 0x5D + sta.w 0x0572, y + lda.b 0x5E + sta.w 0x0574, y + lda.b #0xC8 + sta.w 0x0576, y +} } ; main menu spells ; length of spells names -*=0x01B345 - lda.b #0x08 - -; compute spell pointer -;01b319 rep #0x20 -;01b31b asl a -;01b31c sta 0x45 -;01b31e asl a -;01b31f adc 0x45 -;01b321 adc #0x8900 -;01b324 tay -;01b325 sep #0x20 -;01b327 lda #0x0f - -*=0x01b319 - rep #0x20 - pha - asl - asl - asl - adc 1, s - nop - nop - ; nop - ; adc.w #assets_magic_dat - tay - pla - sep #0x20 - lda.b #assets_magic_dat >> 16 - -; instead of adding asset_magic_dat to Y move it to the lda to save 3 bytes - -*=0x1b32b - lda.w assets_magic_dat, y - -*=0x1b349 - lda.w assets_magic_dat, y - -; Copy of save and restore vram routines from menu, save 0x1300 instead of 0x1000 and store it to the sram +.alloc at 0x01B345 { + lda.b #0x08 + + ; compute spell pointer + ;01b319 rep #0x20 + ;01b31b asl a + ;01b31c sta 0x45 + ;01b31e asl a + ;01b31f adc 0x45 + ;01b321 adc #0x8900 + ;01b324 tay + ;01b325 sep #0x20 + ;01b327 lda #0x0f +} +.alloc at 0x01b319 { + rep #0x20 + pha + asl + asl + asl + adc 1, s + nop + nop + ; nop + ; adc.w #assets_magic_dat + tay + pla + sep #0x20 + lda.b #assets_magic_dat >> 16 + + ; instead of adding asset_magic_dat to Y move it to the lda to save 3 bytes +} +.alloc at 0x1b32b { + lda.w assets_magic_dat, y +} +.alloc at 0x1b349 { + lda.w assets_magic_dat, y + + ; Save / restore covers VRAM byte $4000-$5FFF ($2000 bytes) instead of + ; vanilla's $1000. The extra $1000 bytes reach past the static-font / + + ; vanilla BG3 CHR area to cover the full VWF region union : + ; Region 1 $5000-$56A0 (field / treasure item-name CHR) + ; Region 1B $56E0-$5AA0 (drops item-name CHR, treasure popup) + ; Region D $5800-$5FF0 (item description CHR) + ; Without the extension, menu glyph bytes at $5300+ stayed in VRAM + ; forever (overworld never writes those addresses), so any BG tilemap + ; entry referencing VWF tile_ids $100+ rendered the leftover glyphs + ; over the overworld map. SRAM buffer at $70:5000-$70:6FFF stays + ; clear of the battle-magic region at $70:7000. +} { - *=0x14ff62 - sram_buffer = 0x705000 - save_size = 0x1300 - phb - tdc - pha - plb - lda #0x80 - sta 0x2100 ; screen off - sta 0x88 - lda #0x80 - sta 0x2115 - ldx #0x2000 ; ppu 0x2000 - stx 0x2116 - ldx 0x2139 ; read "dummy" value - lda #0x81 ; single address, auto-increment - sta 0x4300 - lda #0x39 ; source: 0x2139 (vram data read) - sta 0x4301 - ldx.w #sram_buffer & 0xffff ; destination: 0x7ee600 - stx 0x4302 - lda.b #sram_buffer >> 16 - sta 0x4304 - ldx.w #save_size ; size: 0x1000 - stx 0x4305 - lda #0x01 - sta 0x420b - plb - rtl - - *=0x14ffd6 -;RestoreDlgGfx_ext: - lda #0x00 - pha - plb - ldx #0x2000 - stx 0x011d - ldx.w #sram_buffer & 0xffff - stx 0x011f - lda.b #sram_buffer >> 16 - sta 0x0121 - ldx.w #save_size - stx 0x0122 - rtl +.alloc at 0x14ff62 { + sram_buffer = 0x705000 + save_size = 0x2000 + phb + tdc + pha + plb + lda #0x80 + sta 0x2100 ; screen off + sta 0x88 + lda #0x80 + sta 0x2115 + ldx #0x2000 ; ppu 0x2000 + stx 0x2116 + ldx 0x2139 ; read "dummy" value + lda #0x81 ; single address, auto-increment + sta 0x4300 + lda #0x39 ; source: 0x2139 (vram data read) + sta 0x4301 + ldx.w #sram_buffer & 0xffff ; destination: 0x7ee600 + stx 0x4302 + lda.b #sram_buffer >> 16 + sta 0x4304 + ldx.w #save_size ; size: 0x1000 + stx 0x4305 + lda #0x01 + sta 0x420b + plb + rtl +} +.alloc at 0x14ffd6 { + ;RestoreDlgGfx_ext: + lda #0x00 + pha + plb + ldx #0x2000 + stx 0x011d + ldx.w #sram_buffer & 0xffff + stx 0x011f + lda.b #sram_buffer >> 16 + sta 0x0121 + ldx.w #save_size + stx 0x0122 + rtl +} } -*=0x018E32 - jsr.l lookup_dakuten - rts - -*=0x00b670 - jsr.l lookup_dakuten - xba - rts - -;*=0x00b66e -;jsr.l lookup_dakuten -;rts - -; --------sub start-------- -;018E32 DA PHX -;018E33 C9 42 CMP #$42 -;018E35 B0 15 BCS $018E4C -;018E37 38 SEC -;018E38 E9 0F SBC #$0F -;018E3A 0A ASL -;018E3B EB XBA -;018E3C A9 00 LDA #$00 -;018E3E EB XBA -;018E3F AA TAX -;018E40 BF 1F FE 1E LDA $1EFE1F,X -;018E44 EB XBA -;018E45 BF 1E FE 1E LDA $1EFE1E,X -;018E49 EB XBA -;018E4A FA PLX -;018E4B 60 RTS -; ---------------- -;018E4C EB XBA -;018E4D A9 FF LDA #$FF -;018E4F FA PLX -;018E50 60 RTS -; ---------------- +.alloc at 0x018E32 { + jsr.l lookup_dakuten + rts +} +.alloc at 0x00b670 { + jsr.l lookup_dakuten + xba + rts + + ;*=0x00b66e + ;jsr.l lookup_dakuten + ;rts + + ; --------sub start-------- + ;018E32 DA PHX + ;018E33 C9 42 CMP #$42 + ;018E35 B0 15 BCS $018E4C + ;018E37 38 SEC + ;018E38 E9 0F SBC #$0F + ;018E3A 0A ASL + ;018E3B EB XBA + ;018E3C A9 00 LDA #$00 + ;018E3E EB XBA + ;018E3F AA TAX + ;018E40 BF 1F FE 1E LDA $1EFE1F,X + ;018E44 EB XBA + ;018E45 BF 1E FE 1E LDA $1EFE1E,X + ;018E49 EB XBA + ;018E4A FA PLX + ;018E4B 60 RTS + ; ---------------- + ;018E4C EB XBA + ;018E4D A9 FF LDA #$FF + ;018E4F FA PLX + ;018E50 60 RTS + ; ---------------- +} diff --git a/src/ingame/new_game.s b/src/ingame/new_game.s index f01ae34..317cfb2 100644 --- a/src/ingame/new_game.s +++ b/src/ingame/new_game.s @@ -1,3 +1,4 @@ +.include "config.i" """New-game / save-slot screen patches: text-table rewiring and pointer setup for the relocated start-screen strings.""" .include "src/ingame/macros.i" @@ -5,84 +6,90 @@ .table "text/ff4_menus.tbl" { ; new game window - *=0x01dfc7 - .db 0x12 ; width - .db 0x02 ; height +.alloc at 0x01dfc7 { + .db 0x12 ; width + .db 0x02 ; height -; Cecil sprite position on the new game item - *=0x019904 - .db 0x85 ; x - .db 0x04 ; y - - -; load game message window - *=0x01dfdc - menu_window(23, 0, 7, 8) - -; LoadTimeWindow: - *=0x01dfe0 - menu_window(23, 16, 7, 5) - -; LoadGilWindow: - *=0x01dfe4 - menu_window(23, 23, 7, 3) - -; save number display - *=0x019A62 - sta.w 0xC8 - 0x40 + 8, y - - *=0x019A55 - ldx.w #0x82 - 0x40 - - - *=0x019A52 - load_system_menu_text_pointer(newgame.save) - - *=0x01962E - load_system_menu_text_pointer(newgame.new_game) - - .if DEBUG { - jsr.w display_build_number - } - - *=0x019826 - load_system_menu_text_pointer(newgame.load_this_save) - -; for save menu we use the same string. - *=0x01981E - load_system_menu_text_pointer(newgame.load_this_save) - - - *=0x01982C - load_system_menu_text_pointer(newgame.yes_no) - - *=0x01983E - load_system_menu_text_pointer(newgame.time_load_save) + ; Cecil sprite position on the new game item +} +.alloc at 0x019904 { + .db 0x85 ; x + .db 0x04 ; y - *=0x01984D - load_system_menu_text_pointer(newgame.gils_load_game) -; gils text position - ldx.w #0x676 - jsr.w 0x82cd ; menu_draw_text -; gils count position - ldy #0x062c + 2 -; Time position - *=0x019838 - ldy.w #0xcb2e + 2 - *=0x019AC5 - load_system_menu_text_pointer(newgame.empty_save) + ; load game message window +} +.alloc at 0x01dfdc { + menu_window(23, 0, 7, 8) - *=0x01cbad - load_system_menu_text_pointer(newgame.saves) + ; LoadTimeWindow: +} +.alloc at 0x01dfe0 { + menu_window(23, 16, 7, 5) - *=0x01e050 - menu_window(7, 10, 19, 2) + ; LoadGilWindow: +} +.alloc at 0x01dfe4 { + menu_window(23, 23, 7, 3) + ; save number display +} +.alloc at 0x019A62 { + sta.w 0xC8 - 0x40 + 8, y +} +.alloc at 0x019A55 { + ldx.w #0x82 - 0x40 +} +.alloc at 0x019A52 { + load_system_menu_text_pointer(newgame.save) +} +.alloc at 0x01962E { + load_system_menu_text_pointer(newgame.new_game) - *=0x1cc0d - load_system_menu_text_pointer(newgame.save_completed) + .if DEBUG { + jsr.w display_build_number + } +} +.alloc at 0x019826 { + load_system_menu_text_pointer(newgame.load_this_save) - *=0x1cc12 - load_system_menu_text_pointer(newgame.did_not_save) + ; for save menu we use the same string. +} +.alloc at 0x01981E { + load_system_menu_text_pointer(newgame.load_this_save) +} +.alloc at 0x01982C { + load_system_menu_text_pointer(newgame.yes_no) +} +.alloc at 0x01983E { + load_system_menu_text_pointer(newgame.time_load_save) +} +.alloc at 0x01984D { + load_system_menu_text_pointer(newgame.gils_load_game) + ; gils text position + ldx.w #0x676 + jsr.w 0x82cd ; menu_draw_text + ; gils count position + ldy #0x062c + 2 + + ; Time position +} +.alloc at 0x019838 { + ldy.w #0xcb2e + 2 +} +.alloc at 0x019AC5 { + load_system_menu_text_pointer(newgame.empty_save) +} +.alloc at 0x01cbad { + load_system_menu_text_pointer(newgame.saves) +} +.alloc at 0x01e050 { + menu_window(7, 10, 19, 2) +} +.alloc at 0x1cc0d { + load_system_menu_text_pointer(newgame.save_completed) +} +.alloc at 0x1cc12 { + load_system_menu_text_pointer(newgame.did_not_save) +} } diff --git a/src/ingame/options.s b/src/ingame/options.s index 7462f84..384f16e 100644 --- a/src/ingame/options.s +++ b/src/ingame/options.s @@ -2,40 +2,40 @@ ;RGB -> RVB :o) -*=0x01D1BB - lda.b #0x57 - -; déplacement du curseur principal des options - -*=0x01D247 - lda.b #0x00 - -; Cursor offset in controls menu (x) - -*=0x01D4E6 - lda.b #0x03 - -; cursor y - -*=0x01D4E2 - adc.b #0x4C - -*=0x01D1B0 - load_system_menu_text_pointer(options.title) - -*=0x01D1A4 - load_system_menu_text_pointer(options.config) - -; move controls title window - -*=0x01E204 - menu_window(4, 0, 22, 2) - - -*=0x01D487 - ldy.w #0xE204 - -; controles - -*=0x01D48D - load_system_menu_text_pointer(options.controls) +.alloc at 0x01D1BB { + lda.b #0x57 + + ; déplacement du curseur principal des options +} +.alloc at 0x01D247 { + lda.b #0x00 + + ; Cursor offset in controls menu (x) +} +.alloc at 0x01D4E6 { + lda.b #0x03 + + ; cursor y +} +.alloc at 0x01D4E2 { + adc.b #0x4C +} +.alloc at 0x01D1B0 { + load_system_menu_text_pointer(options.title) +} +.alloc at 0x01D1A4 { + load_system_menu_text_pointer(options.config) + + ; move controls title window +} +.alloc at 0x01E204 { + menu_window(4, 0, 22, 2) +} +.alloc at 0x01D487 { + ldy.w #0xE204 + + ; controles +} +.alloc at 0x01D48D { + load_system_menu_text_pointer(options.controls) +} diff --git a/src/ingame/places_names.s b/src/ingame/places_names.s index 79964cf..d57f18d 100644 --- a/src/ingame/places_names.s +++ b/src/ingame/places_names.s @@ -4,77 +4,84 @@ pointer table. """ { place_name_length = 0x1A - *=0x00B90E - lda.b #place_name_length - - *=0x00B922 - cpx.b #place_name_length - - *=0x00B8F1 - lda.l assets_places_names_dat, x - - *=0x00B901 - lda.l assets_places_names_dat, x - - *=0x00B92C - lda.l assets_places_names_dat, x - - *=0x00B938 - sta.w 0x774 + place_name_length, y - - *=0x00B91E - sta.w 0x774 + place_name_length, x - - -; window transfer related stuff - *=0x00B963 - lda.l places_top_window, x - - *=0x00B96B - lda.l places_top_window, x - - *=0x00B973 - cpx.w #0x003C ; place_name_length * 2 + 8 - -;.00:B98D LDA $780,X - *=0x00B98D - lda.w 0x774 + place_name_length, x - - *=0x00B999 - cpx.w #place_name_length - - *=0x00B9D1 - cpx.w #place_name_length - - - *=0x00B9F6 - lda.l places_bottom_window, x +.alloc at 0x00B90E { + lda.b #place_name_length +} +.alloc at 0x00B922 { + cpx.b #place_name_length +} +.alloc at 0x00B8F1 { + lda.l assets_places_names_dat, x +} +.alloc at 0x00B901 { + lda.l assets_places_names_dat, x +} +.alloc at 0x00B92C { + lda.l assets_places_names_dat, x +} +.alloc at 0x00B938 { + sta.w 0x774 + place_name_length, y +} +.alloc at 0x00B91E { + sta.w 0x774 + place_name_length, x - *=0x00B9FE - lda.l places_bottom_window, x - *=0x00BA06 - cpx.w #0x003C ; place_name_length + 8 + ; window transfer related stuff +} +.alloc at 0x00B963 { + lda.l places_top_window, x +} +.alloc at 0x00B96B { + lda.l places_top_window, x +} +.alloc at 0x00B973 { + cpx.w #0x003C ; place_name_length * 2 + 8 + ;.00:B98D LDA $780,X +} +.alloc at 0x00B98D { + lda.w 0x774 + place_name_length, x +} +.alloc at 0x00B999 { + cpx.w #place_name_length +} +.alloc at 0x00B9D1 { + cpx.w #place_name_length +} +.alloc at 0x00B9F6 { + lda.l places_bottom_window, x +} +.alloc at 0x00B9FE { + lda.l places_bottom_window, x +} +.alloc at 0x00BA06 { + cpx.w #0x003C ; place_name_length + 8 - vram_ptr = 0x2840 + 1 - *=0x00B95A -;.00:B95A LDX #$2848 ; top window - ldx.w #vram_ptr - *=0x00B978 -;.00:B978 LDX #$2868 ; left window piece - ldx.w #vram_ptr + 0x20 - *=0x00B99E -;.00:B99E LDX #$2876 ; right window piece - ldx.w #vram_ptr + 0x20 + place_name_length + 2 - *=0x00B9B0 -;.00:B9B0 LDX #$2888 ; left window piece - ldx.w #vram_ptr + 0x40 - *=0x00B9D6 -;.00:B9D6 LDX #$2896 ; right window piece - ldx.w #vram_ptr + 0x40 + place_name_length + 2 - *=0x00B9ED -;.00:B9ED LDX #$28A8 ; bottom window - ldx.w #vram_ptr + 0x60 + vram_ptr = 0x2840 + 1 +} +.alloc at 0x00B95A { + ;.00:B95A LDX #$2848 ; top window + ldx.w #vram_ptr +} +.alloc at 0x00B978 { + ;.00:B978 LDX #$2868 ; left window piece + ldx.w #vram_ptr + 0x20 +} +.alloc at 0x00B99E { + ;.00:B99E LDX #$2876 ; right window piece + ldx.w #vram_ptr + 0x20 + place_name_length + 2 +} +.alloc at 0x00B9B0 { + ;.00:B9B0 LDX #$2888 ; left window piece + ldx.w #vram_ptr + 0x40 +} +.alloc at 0x00B9D6 { + ;.00:B9D6 LDX #$2896 ; right window piece + ldx.w #vram_ptr + 0x40 + place_name_length + 2 +} +.alloc at 0x00B9ED { + ;.00:B9ED LDX #$28A8 ; bottom window + ldx.w #vram_ptr + 0x60 +} } diff --git a/src/ingame/places_names_window.s b/src/ingame/places_names_window.s index 74f57d3..cafe514 100644 --- a/src/ingame/places_names_window.s +++ b/src/ingame/places_names_window.s @@ -1,9 +1,14 @@ """Tilemap rows for the top + bottom edges of the location-name window.""" -places_top_window: -"""Tilemap row for the top edge of the location-name window (60 bytes, 30 tile/attr pairs).""" - .db 0x16, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x18, 0x20 ; noqa: E501 -places_bottom_window: -"""Tilemap row for the bottom edge of the location-name window (60 bytes, 30 tile/attr pairs).""" - .db 0x1B, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1D, 0x20 ; noqa: E501 +.include "../bank20.i" + +.alloc places_names_window_block in bank20_reloc { + places_top_window: + """Tilemap row for the top edge of the location-name window (60 bytes, 30 tile/attr pairs).""" + .db 0x16, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x17, 0x20, 0x18, 0x20 ; noqa: E501 + + places_bottom_window: + """Tilemap row for the bottom edge of the location-name window (60 bytes, 30 tile/attr pairs).""" + .db 0x1B, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1C, 0x20, 0x1D, 0x20 ; noqa: E501 +} diff --git a/src/ingame/shop.s b/src/ingame/shop.s index aae0818..fa8ee3f 100644 --- a/src/ingame/shop.s +++ b/src/ingame/shop.s @@ -6,89 +6,109 @@ small-VWF item descriptions. ; move gils window -*=0x1dea6 - menu_window(23, 4, 7, 3) +.alloc at 0x1dea6 { + menu_window(23, 4, 7, 3) -; move character 1 tile below - -*=0x1deba - menu_window(23, 9, 7, 11) - -; message window - -*=0x01deb2 - menu_window(8, 0, 20, 2) - -; shop title - -*=0x01deae - menu_window(0, 0, 8, 2) - -; actions window - -*=0x01deaa - menu_window(1, 4, 20, 3) - -; list window - -*=0x01deb6 - menu_window(1, 8, 21, 17) - - -; Moves gils 7 digits 2 tiles to the right. - -*=0x1c3f5 - ldy.w #0x01a6 + 8 - -; ギル : gils - -*=0x01C3EC - load_system_menu_text_pointer(shops.gils) - -*=0x01C350 - load_system_menu_text_pointer(shops.welcome_and_actions) - -*=0x01C43F - load_system_menu_text_pointer(shops.quantity) - -*=0x01c7e4 - load_system_menu_text_pointer(shops.quantity) - -*=0x01C568 - load_system_menu_text_pointer(shops.gils + 2) - -*=0x01c74e - load_system_menu_text_pointer(shops.thank_you_window) - -*=0x01c962 - load_system_menu_text_pointer(shops.sell_window) + ; move character 1 tile below +} +.alloc at 0x1deba { + menu_window(23, 9, 7, 11) -*=0x01c41e - load_system_menu_text_pointer(shops.inventory_full) + ; message window +} +.alloc at 0x01deb2 { + menu_window(8, 0, 20, 2) -*=0x01c700 - load_system_menu_text_pointer(shops.not_enough_gils) + ; shop title +} +.alloc at 0x01deae { + menu_window(0, 0, 8, 2) + ; actions window +} +.alloc at 0x01deaa { + menu_window(1, 4, 20, 3) -; Buy menu -; quantity hand pointer position "10" + ; list window +} +.alloc at 0x01deb6 { + menu_window(1, 8, 21, 17) -*=0x01cb12 - ldx #0x3058 + 48 -; quantity hand pointer position "1" + ; Moves gils 7 digits 2 tiles to the right. +} +.alloc at 0x1c3f5 { + ldy.w #0x01a6 + 8 -*=0x01cb17 - ldx.w #0x3040 + 54 + ; ギル : gils +} +.alloc at 0x01C3EC { + load_system_menu_text_pointer(shops.gils) +} +.alloc at 0x01C350 { + load_system_menu_text_pointer(shops.welcome_and_actions) + ; Route the owner greeting ("Puis-je vous aider ?") through the small-VWF + ; description region ; the slimmed welcome_and_actions block holds only the + ; "Achat Vente Sortir" action labels rendered by the vanilla engine. +} +.alloc at 0x01C353 { + jmp.w shop_welcome_text_hook +} +.alloc at 0x01C43F { + load_system_menu_text_pointer(shops.quantity) + ; Route the buy-flow welcome ("Que désirez vous ?") through the small-VWF + ; description region ; vanilla quantity block (Quantité + "1") continues to + ; render via $8301 inside the hook. +} +.alloc at 0x01C442 { + jsr.w shop_quantity_text_hook +} +.alloc at 0x01c7e4 { + load_system_menu_text_pointer(shops.quantity) +} +.alloc at 0x01C7E7 { + jsr.w shop_quantity_text_hook +} +.alloc at 0x01C568 { + load_system_menu_text_pointer(shops.gils + 2) +} +.alloc at 0x01c74e { + load_system_menu_text_pointer(shops.thank_you_window) + ; Route the owner thank-you ("Merci !") through the small-VWF description + ; region ; the window itself still draws via vanilla $82FB inside the hook. +} +.alloc at 0x01C751 { + jsr.w shop_thanks_text_hook +} +.alloc at 0x01c962 { + load_system_menu_text_pointer(shops.sell_window) +} +.alloc at 0x01c41e { + load_system_menu_text_pointer(shops.inventory_full) +} +.alloc at 0x01c700 { + load_system_menu_text_pointer(shops.not_enough_gils) -; Sell menu -*=0x01c80b - ldx.w #0x3058 + 48 + ; Buy menu + ; quantity hand pointer position "10" +} +.alloc at 0x01cb12 { + ldx #0x3058 + 48 -*=0x01c810 - ldx.w #0x3040 + 54 + ; quantity hand pointer position "1" +} +.alloc at 0x01cb17 { + ldx.w #0x3040 + 54 + ; Sell menu +} +.alloc at 0x01c80b { + ldx.w #0x3058 + 48 +} +.alloc at 0x01c810 { + ldx.w #0x3040 + 54 +} { ; the 10 is drawn using a draw number function while the 1 is in the text. ; the 10 is dynamic if you place the cursor on 10 and press X it'll increase the number (10 increments) @@ -96,63 +116,66 @@ small-VWF item descriptions. _quantity_10_position = 0x019a + 12 ; Sell menu - *=0x01c81e - ldy.w #_quantity_10_position +.alloc at 0x01c81e { + ldy.w #_quantity_10_position -; Buy menu - *=0x01c464 - ldy.w #_quantity_10_position + ; Buy menu +} +.alloc at 0x01c464 { + ldy.w #_quantity_10_position +} } ; Changes the offset of the hand pointer -*=0x01C37C - lda 0x1B79 - asl - asl - asl - asl - sta 0x45 - asl - adc 0x45 - nop - nop - sta 0x45 - -*=0x01920a - ; shop party sprite positions - ; Attack formation 3 front, 2 back - .db 0x00 + 8, 0x1c + 8 - .db 0x00 + 8, 0x00 + 8 - .db 0x00 + 8, 0x38 + 8 - .db 0x18 + 8, 0x0c + 8 - .db 0x18 + 8, 0x2c + 8 - -; Defense formation 3 back, 2 front - .db 0x18 + 8, 0x1c + 8 - .db 0x18 + 8, 0x00 + 8 - .db 0x18 + 8, 0x38 + 8 - .db 0x00 + 8, 0x0c + 8 - .db 0x00 + 8, 0x2c + 8 - -*=0x01debe -_shop_title_ptr: - .dw shops.weapons_title - 0x8000 - .dw shops.armor_title - 0x8000 - .dw shops.items_title - 0x8000 - -*=0x01c336 - lda.b #0 - xba - lda 0x1a01 - asl - tax - rep #0x20 - lda.l _shop_title_ptr, x - tay - sep #0x20 - nop - nop - nop - ldx.w #0x0042 +.alloc at 0x01C37C { + lda 0x1B79 + asl + asl + asl + asl + sta 0x45 + asl + adc 0x45 + nop + nop + sta 0x45 +} +.alloc at 0x01920a { + ; shop party sprite positions + ; Attack formation 3 front, 2 back + .db 0x00 + 8, 0x1c + 8 + .db 0x00 + 8, 0x00 + 8 + .db 0x00 + 8, 0x38 + 8 + .db 0x18 + 8, 0x0c + 8 + .db 0x18 + 8, 0x2c + 8 + + ; Defense formation 3 back, 2 front + .db 0x18 + 8, 0x1c + 8 + .db 0x18 + 8, 0x00 + 8 + .db 0x18 + 8, 0x38 + 8 + .db 0x00 + 8, 0x0c + 8 + .db 0x00 + 8, 0x2c + 8 +} +.alloc at 0x01debe { + shop_title_ptr: + .dw shops.weapons_title - 0x8000 + .dw shops.armor_title - 0x8000 + .dw shops.items_title - 0x8000 +} +.alloc at 0x01c336 { + lda.b #0 + xba + lda 0x1a01 + asl + tax + rep #0x20 + lda.l shop_title_ptr, x + tay + sep #0x20 + nop + nop + nop + ldx.w #0x0042 +} diff --git a/src/ingame/status.s b/src/ingame/status.s index ac58834..5700677 100644 --- a/src/ingame/status.s +++ b/src/ingame/status.s @@ -6,18 +6,19 @@ attributes). ;*=0x01A9B7 ; ldy.w #0x0044 -*=0x01A99E - load_system_menu_text_pointer(status.status) - -*=0x01AAE9 - load_system_menu_text_pointer(status.exp_for_next_level) - -*=0x01ABBC - load_system_menu_text_pointer(status.char_stats) - -*=0x01A9CA - ldy.w #0x15C - 0x80 - -*=0x1aab3 - jsr.l load_dextrelity_pointer - jmp.w 0x1aac9 +.alloc at 0x01A99E { + load_system_menu_text_pointer(status.status) +} +.alloc at 0x01AAE9 { + load_system_menu_text_pointer(status.exp_for_next_level) +} +.alloc at 0x01ABBC { + load_system_menu_text_pointer(status.char_stats) +} +.alloc at 0x01A9CA { + ldy.w #0x15C - 0x80 +} +.alloc at 0x1aab3 { + jsr.l load_dextrelity_pointer + jmp.w 0x1aac9 +} diff --git a/src/ingame/treasure_rolling.s b/src/ingame/treasure_rolling.s index 5e7f4d2..aa07094 100644 --- a/src/ingame/treasure_rolling.s +++ b/src/ingame/treasure_rolling.s @@ -15,6 +15,7 @@ from `inventory_rolling.s` and tuned for the chest UI. ; menus are mutually exclusive on screen. ; Layout (single column) +.include "config.i" TREASURE_VISIBLE_ITEMS := 5 ; Visible items at once TREASURE_BUFFER_SLOTS := 6 ; 6 slots (5 visible + 1 pre-render) TREASURE_TOTAL_ITEMS := 48 ; Total inventory items @@ -37,22 +38,28 @@ TREASURE_ITEM_LIST_HEIGHT := 80 ; 5 items × 16 pixels ; channel + tilemap buffer + shadow table are shared (re-init on entry). ; Treasure rolling-buffer state RAM block (12 bytes from $1BD0). Same -; struct layout as the field-menu state block — fields resolved via +; struct layout as the field-menu state block - fields resolved via ; `RollingBufferState` from src/items.i so any layout change applies ; uniformly across profiles. -treasure_rolling := 0x1BD0 -treasure_rolling_top_row := treasure_rolling + RollingBufferState.top_row -treasure_rolling_buffer_pos := treasure_rolling + RollingBufferState.buffer_pos -treasure_rolling_edge_row := treasure_rolling + RollingBufferState.edge_row -treasure_rolling_slot_index := treasure_rolling + RollingBufferState.slot_index -treasure_rolling_base_scroll := treasure_rolling + RollingBufferState.base_scroll -treasure_hdma_enable := treasure_rolling + RollingBufferState.hdma_enable -treasure_scroll_state := treasure_rolling + RollingBufferState.scroll_state -treasure_scroll_remaining := treasure_rolling + RollingBufferState.scroll_remaining -treasure_scroll_direction := treasure_rolling + RollingBufferState.scroll_direction -treasure_transfer_pending := treasure_rolling + RollingBufferState.transfer_pending -treasure_scroll_anim_offset := treasure_rolling + RollingBufferState.scroll_anim_offset -treasure_hdma_copy_pending := treasure_rolling + RollingBufferState.hdma_copy_pending +; Treasure rolling state moved out of $1B00-$1BFF (which vanilla menu / +; sprite code writes to via indexed STA past $1BEB) to a clean $7E:9C00 +; region. Engine path needs the full 35-byte struct (state + config + +; hook far-ptrs) ; the macro path only ever touched the first 12 bytes +; so the original $1BD0 base worked despite vanilla's later collisions. +treasure_rolling := (0x7E9C00 as RollingBufferState) + +; Cooldown counter sitting one byte past the shared struct so the +; engine layout stays untouched. Decremented each frame in +; treasure_scroll_state_check ; non-zero means treasure_scroll_*_trigger +; aborts and undoes vanilla's $1BB7 increment, debouncing the +; "hold-DOWN auto-repeat fires every 2 frames" issue that scrolled the +; inventory two items per visible tap. +; RollingBufferState ends at offset 35 inclusive (menu_id byte added +; in the engine port). Bump cooldown past that ; was +35 = collided +; with menu_id and treasure_ensure_hdma_initialized's STZ wiped it, +; sending engine dispatch to the wrong menu's HDMA path. +treasure_scroll_cooldown := treasure_rolling + 36 +TREASURE_SCROLL_COOLDOWN_FRAMES := 0x18 ; ~24 frames between scrolls (long enough to outlast a typical button hold) ; Scroll State Constants TREASURE_SCROLL_STATE_IDLE := 0 @@ -83,15 +90,16 @@ TREASURE_SCROLL_TOTAL_PIXELS := INVENTORY_SCROLL_TOTAL_PIXELS ; Share field-menu HDMA tables (mutually exclusive on screen). ; Active (read by HDMA channel 5): $7E:9800 ; Shadow (written by game): $7E:9840 -; NMI hook copies shadow → active during VBlank when `menu_hdma_copy_pending` -; ($1BB6) is set, gated on `menu_hdma_enable` ($1BAE) being non-zero. +; NMI hook copies shadow → active during VBlank when `menu_rolling.hdma_copy_pending` +; ($1BB6) is set, gated on `menu_rolling.hdma_enable` ($1BAE) being non-zero. TREASURE_HDMA_TABLE_ADDR := 0x9800 TREASURE_HDMA_TABLE := 0x7E9800 TREASURE_HDMA_SHADOW_ADDR := 0x9840 TREASURE_HDMA_SHADOW := 0x7E9840 TREASURE_HDMA_TABLE_SIZE := 40 TREASURE_HDMA_BANK := 0x7E -treasure_hdma_copy_pending_shared := 0x1BB6 ; field-menu copy-pending flag +; Shared menu HDMA signals defined in src/items.i - referenced here as +; field_menu_rolling.hdma_enable / field_menu_rolling.hdma_copy_pending. ; HDMA channel 6 for the treasure rolling buffer. Original treasure ; enables HDMAEN=$AD (ch7|ch5|ch3|ch2|ch0); ch2 in particular is an @@ -108,17 +116,19 @@ TREASURE_HDMA6_SRC_BANK := 0x4364 TREASURE_HDMA6_IND_BANK := 0x4367 HDMAEN := 0x420C ; HDMA enable register +.include "../bank20.i" + +.alloc treasure_rolling_block in bank20_reloc { init_treasure_inventory_hdma: """ -Sets up HDMA channel 5 for per-scanline BG1 vertical scroll control -Called when entering the item menu + Sets up HDMA channel 5 for per-scanline BG1 vertical scroll control + Called when entering the item menu -HDMA mode: $02 = write 2 bytes to same register, DIRECT mode (like FF6) -Register: $210E (BG1VOFS) -Table format: count_byte, lo_byte, hi_byte per entry, $00 to end + HDMA mode: $02 = write 2 bytes to same register, DIRECT mode (like FF6) + Register: $210E (BG1VOFS) + Table format: count_byte, lo_byte, hi_byte per entry, $00 to end """ - php sep #0x20 ; 8-bit A @@ -132,7 +142,7 @@ Table format: count_byte, lo_byte, hi_byte per entry, $00 to end lda #0x02 ; Mode: DIRECT, write 2 bytes to same PPU reg sta.l TREASURE_HDMA6_CTRL ; $004360 - lda #0x12 ; BG3VOFS register ($2112) — treasure inventory is on BG3 + lda #0x12 ; BG3VOFS register ($2112) - treasure inventory is on BG3 sta.l TREASURE_HDMA6_DEST ; $004361 ; Source = HDMA table in WRAM at $7E9800 @@ -143,7 +153,7 @@ Table format: count_byte, lo_byte, hi_byte per entry, $00 to end lda #TREASURE_HDMA_BANK ; $7E sta.l TREASURE_HDMA6_SRC_BANK ; $004364 -; HDMA channel 5 is now enabled via shadow variable (treasure_hdma_enable) +; HDMA channel 5 is now enabled via shadow variable (treasure_rolling.hdma_enable) ; The NMI hook at $8083 reads the shadow and writes to HDMAEN plp @@ -151,21 +161,23 @@ Table format: count_byte, lo_byte, hi_byte per entry, $00 to end disable_treasure_inventory_hdma: """ -Disables HDMA channel 5 when leaving item menu -The shadow variable is cleared by menu_exit_hook + Disables HDMA channel 5 when leaving item menu + The shadow variable is cleared by menu_exit_hook """ + php sep #0x20 ; 8-bit A - ; Shadow variable cleared by caller, NMI will write 0 to HDMAEN +; Shadow variable cleared by caller, NMI will write 0 to HDMAEN plp rts init_treasure_hdma_table: """ -Builds direct mode HDMA table in WRAM at $7E9800 -Format: count, lo, hi per entry, $00 to end -Initial state: all rows at BASE scroll (no circular buffer offset) + Builds direct mode HDMA table in WRAM at $7E9800 + Format: count, lo, hi per entry, $00 to end + Initial state: all rows at BASE scroll (no circular buffer offset) """ + rep #0x30 ; 16-bit A, X, Y php pha ; Save A @@ -185,7 +197,7 @@ Initial state: all rows at BASE scroll (no circular buffer offset) sta.l TREASURE_HDMA_TABLE, x inx rep #0x20 ; 16-bit A for value - lda.w treasure_rolling_base_scroll + lda.w treasure_rolling.base_scroll sta.l TREASURE_HDMA_TABLE, x inx inx @@ -201,7 +213,7 @@ _t_init_item_rows: sta.l TREASURE_HDMA_TABLE, x inx rep #0x20 ; 16-bit A for value - lda.w treasure_rolling_base_scroll + lda.w treasure_rolling.base_scroll sta.l TREASURE_HDMA_TABLE, x inx inx @@ -219,7 +231,7 @@ _t_init_item_rows: sta.l TREASURE_HDMA_TABLE, x inx rep #0x20 - lda.w treasure_rolling_base_scroll + lda.w treasure_rolling.base_scroll clc adc.w #16 ; Lock at base + 16 sta.l TREASURE_HDMA_TABLE, x @@ -245,18 +257,101 @@ _t_init_item_rows: update_treasure_scroll_hdma: """Build the treasure-menu HDMA scroll table via the shared engine.""" - engine_update_scroll_hdma(treasure_rolling, TREASURE_HDMA_SHADOW, TREASURE_BUFFER_SLOTS, TREASURE_VISIBLE_ITEMS, _treasure_hdma_header, _treasure_hdma_footer, _treasure_hdma_signal) ; noqa: E501 +; Build treasure HDMA scroll table. Inlined from the former +; `engine_update_scroll_hdma` macro since its per-row writes use +; compile-time state_base + hdma_shadow_addr ; routing through the +; bank-20 engine would need menu_id dispatch at every state read, +; which costs more than the modest 80-line duplication across the +; four menus. + { + php + rep #0x30 + pha + phx + phy + lda.b 0x40 + pha + lda.b 0x42 + pha + ldx.w #0x0000 + jsr.w _treasure_hdma_header + stz.b 0x42 + +_row_loop: + lda.w treasure_rolling + RollingBufferState.buffer_pos + and.w #0x00FF + clc + adc.b 0x42 + +_mod_loop: + cmp.w #TREASURE_BUFFER_SLOTS + bcc _mod_done + sec + sbc.w #TREASURE_BUFFER_SLOTS + bra _mod_loop + +_mod_done: + asl + asl + asl + asl + sta.b 0x40 + lda.b 0x42 + and.w #0x00FF + asl + asl + asl + asl + eor.w #0xFFFF + inc + clc + adc.b 0x40 + clc + adc.w treasure_rolling + RollingBufferState.base_scroll + sta.b 0x40 + sep #0x20 + lda #16 + sta.l TREASURE_HDMA_SHADOW, x + inx + rep #0x20 + lda.b 0x40 + sta.l TREASURE_HDMA_SHADOW, x + inx + inx + rep #0x20 + inc.b 0x42 + lda.b 0x42 + cmp.w #TREASURE_VISIBLE_ITEMS + bcs _row_loop_done + jmp.w _row_loop + +_row_loop_done: + jsr.w _treasure_hdma_footer + sep #0x20 + lda #0x00 + sta.l TREASURE_HDMA_SHADOW, x + jsr.w _treasure_hdma_signal + rep #0x20 + pla + sta.b 0x42 + pla + sta.b 0x40 + ply + plx + pla + plp + rts + } _treasure_hdma_header: """Treasure profile header: drops band (120 lines at BASE-16) + inventory border (8 lines at BASE).""" - sep #0x20 lda #120 sta.l TREASURE_HDMA_SHADOW, x inx rep #0x20 - lda.w treasure_rolling_base_scroll + lda.w treasure_rolling.base_scroll sec sbc.w #16 sta.l TREASURE_HDMA_SHADOW, x @@ -267,7 +362,7 @@ _treasure_hdma_header: sta.l TREASURE_HDMA_SHADOW, x inx rep #0x20 - lda.w treasure_rolling_base_scroll + lda.w treasure_rolling.base_scroll sta.l TREASURE_HDMA_SHADOW, x inx inx @@ -276,13 +371,12 @@ _treasure_hdma_header: _treasure_hdma_footer: """Treasure profile footer: 16 scanlines at BASE+16 (hides prefetch slot).""" - sep #0x20 lda #16 sta.l TREASURE_HDMA_SHADOW, x inx rep #0x20 - lda.w treasure_rolling_base_scroll + lda.w treasure_rolling.base_scroll clc adc.w #16 sta.l TREASURE_HDMA_SHADOW, x @@ -294,13 +388,13 @@ _treasure_hdma_signal: """Treasure profile signal: set both treasure flag and shared $1BB6 mirror.""" sep #0x20 lda #0x01 - sta.w treasure_hdma_copy_pending - sta.w treasure_hdma_copy_pending_shared + sta.l treasure_rolling.hdma_copy_pending + sta.l field_menu_rolling.hdma_copy_pending rts ; Re-render all BUFFER_SLOTS (6) entries from $1440 using current ; $1BB7 (scroll_pos) and the existing buffer_pos rotation. Called from -; the redraw helper at $01:D933 after a swap completes — the swap +; the redraw helper at $01:D933 after a swap completes - the swap ; mutates $1440 in place and original's DrawInventoryList would redraw ; the whole 48-item list, but we only need to refresh the 5 visible ; slots + 1 prefetch. Crucially does NOT touch buffer_pos or any of @@ -308,12 +402,97 @@ _treasure_hdma_signal: ; was on before the swap survives. treasure_refresh_slots_impl: -"""Treasure profile: re-render all 6 slots without resetting scroll state (original redraw helper at $01:D933).""" - engine_refresh_slots(treasure_rolling, 0x1BB7, TREASURE_BUFFER_SLOTS, _treasure_render_item_to_slot) +"""Treasure profile: re-render all 6 slots via the bank-20 engine (post-swap redraw at $01:D933).""" + php + sep #0x20 + rep #0x10 + lda.l 0x7E0000 + 0x1BB7 ; treasure scroll_pos (shared with vanilla cursor row) + ldx.w #treasure_rolling + jsr.l rolling_engine.rolling_engine_refresh_slots + plp + rtl init_treasure_rolling_buffer_impl: -"""Init treasure rolling buffer (5 visible + prefetch slot 6).""" - engine_init_rolling_buffer(treasure_rolling, TREASURE_BUFFER_SLOTS, _treasure_draw_inventory_window, treasure_ensure_hdma_initialized, _treasure_render_item_to_slot) ; noqa: E501 +""" + Init treasure rolling buffer (5 visible + prefetch slot 6) via the + bank-20 rolling-inventory engine. State + hook far-ptrs live at + $7E:9C00 ; the engine writes the config block then JSLs into + `rolling_engine.rolling_engine_init` to zero the scratch, stamp + `base_scroll = $FFFF`, fire draw_window + ensure_hdma hooks, and + render `visible_rows` slots. +""" + + php + rep #0x30 + sep #0x20 + lda.b #TREASURE_BUFFER_SLOTS + sta.l treasure_rolling + RollingBufferState.visible_rows + lda.b #0x02 + sta.l treasure_rolling + RollingBufferState.slot_height_tiles + lda.b #0x40 + sta.l treasure_rolling + RollingBufferState.item_list_ptr + lda.b #0x14 + sta.l treasure_rolling + RollingBufferState.item_list_ptr + 1 + lda.b #0x7E + sta.l treasure_rolling + RollingBufferState.item_list_ptr + 2 + lda.b #TREASURE_TOTAL_ITEMS + sta.l treasure_rolling + RollingBufferState.item_count + lda.b #0x06 + sta.l treasure_rolling + RollingBufferState.hdma_channel + lda.b #0x80 + sta.l treasure_rolling + RollingBufferState.vwf_cfg_ptr + lda.b #0x70 + sta.l treasure_rolling + RollingBufferState.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l treasure_rolling + RollingBufferState.vwf_cfg_ptr + 2 + lda.b #treasure_fn_render_slot_trampoline & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_render_slot + lda.b #( treasure_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_render_slot + 1 + lda.b #( treasure_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_render_slot + 2 + lda.b #treasure_fn_update_hdma_trampoline & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_update_hdma + lda.b #( treasure_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_update_hdma + 1 + lda.b #( treasure_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_update_hdma + 2 + lda.b #treasure_fn_draw_window_trampoline & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_draw_window + lda.b #( treasure_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_draw_window + 1 + lda.b #( treasure_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l treasure_rolling + RollingBufferState.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_TREASURE + sta.l treasure_rolling + RollingBufferState.menu_id + plp + php + rep #0x10 + ldx.w #treasure_rolling + jsr.l rolling_engine.rolling_engine_init + plp + rtl + +treasure_fn_render_slot_trampoline: +"""Bank-20 RTL wrapper around `_treasure_render_item_to_slot`.""" + php + jsr.w _treasure_render_item_to_slot + plp + rtl + +treasure_fn_update_hdma_trampoline: +"""Bank-20 RTL wrapper around `treasure_ensure_hdma_initialized`.""" + php + jsr.w treasure_ensure_hdma_initialized + plp + rtl + +treasure_fn_draw_window_trampoline: +"""Bank-20 RTL wrapper around `_treasure_draw_inventory_window`.""" + php + jsr.w _treasure_draw_inventory_window + plp + rtl _treasure_draw_inventory_window: """Treasure menu draws a 5-row inventory window so the bottom border lands on the rolling-buffer footer scanlines.""" @@ -323,19 +502,11 @@ _treasure_draw_inventory_window: sep #0x10 rts -treasure_scroll_down_prepare: -"""Treasure profile scroll-down pre-render.""" - engine_scroll_down_prepare(treasure_rolling, 0x1BB7, TREASURE_SCROLL_LIMIT, TREASURE_VISIBLE_ITEMS, TREASURE_BUFFER_SLOTS, treasure_ensure_hdma_initialized, _treasure_render_item_to_slot, update_treasure_scroll_hdma) ; noqa: E501 - -treasure_scroll_up_prepare: -"""Treasure profile scroll-up pre-render.""" - engine_scroll_up_prepare(treasure_rolling, 0x1BB7, TREASURE_BUFFER_SLOTS, treasure_ensure_hdma_initialized, _treasure_render_item_to_slot, update_treasure_scroll_hdma) ; noqa: E501 - ; _treasure_render_item_to_slot ; Renders an item to a specific circular buffer slot in the tilemap. ; -; Input: treasure_rolling_edge_row = item index (0-47) for data lookup -; treasure_rolling_slot_index = slot index (0-11) for destination +; Input: treasure_rolling.edge_row = item index (0-47) for data lookup +; treasure_rolling.slot_index = slot index (0-11) for destination ; ; Strategy: Set up $5d = slot_index (for Y position calculation), ; $5a = pointer to item data, then call game's DrawItemSlot. @@ -377,7 +548,7 @@ _treasure_render_item_to_slot: sep #0x20 ; 8-bit A ; Calculate item data pointer: $1440 + (edge_row * Item.__size) - lda.w treasure_rolling_edge_row + lda.w treasure_rolling.edge_row asl ; * Item.__size (2 bytes per Item) clc adc #0x40 ; Low byte of $1440 @@ -404,7 +575,7 @@ _treasure_render_item_to_slot: jsr.l check_can_use_item_trampoline ; bank-$01 trampoline for original @ $A25D (sets $DB) ; Set $5d = slot_index (for AND #$01 check, but we patched to AND #$00) - lda.w treasure_rolling_slot_index + lda.w treasure_rolling.slot_index sta.b 0x5d ; Calculate Y = slot_index * 128 + $44 @@ -412,7 +583,7 @@ _treasure_render_item_to_slot: ; +64 = 1 BG row (top window border at staging row 0) ; +4 for left margin (2 tiles) rep #0x20 ; 16-bit A (X/Y already 16-bit) - lda.w treasure_rolling_slot_index + lda.w treasure_rolling.slot_index and.w #0x00FF ; Clear high byte xba ; Swap bytes: A = slot * 256 lsr ; A = slot * 128 @@ -429,7 +600,7 @@ _treasure_render_item_to_slot: bra _t_skip_draw_item_slot _t_not_trash_item: - ; Clear the 2x2 trash can area first (in case we scrolled from trash position) +; Clear the 2x2 trash can area first (in case we scrolled from trash position) jsr.w _clear_treasure_trash_area ; Call DrawItemSlot inner at $A1ED @@ -463,21 +634,33 @@ _t_skip_draw_item_slot: treasure_ensure_hdma_initialized: """ -Lazy initialization: captures $93 and sets up HDMA on first scroll. -Called from scroll prepare functions. -Checks if base_scroll == 0xFFFF (sentinel) and if so, initializes. + Lazy initialization: captures $93 and sets up HDMA on first scroll. + Called from scroll prepare functions. + Checks if base_scroll == 0xFFFF (sentinel) and if so, initializes. """ -; Check if already initialized (base_scroll != 0xFFFF) +; Check if already initialized (base_scroll != 0xFFFF). Use long +; addressing - engine `_engine_call_hook` jumps in with DB unchanged +; from the vanilla caller (DB=$00), so abs reads would hit ROM and the +; gate would always read "not $FFFF" → bail. Hits every DB-relative +; access in this routine. rep #0x20 ; 16-bit A - lda.w treasure_rolling_base_scroll + lda.l treasure_rolling.base_scroll cmp.w #0xFFFF bne _t_hdma_already_init ; Capture original BG3VOFS shadow ($9F). Original treasure menu uses ; $9F = -120 to position items at screen scanline 120; we mirror that. lda.l 0x7E019F - sta.w treasure_rolling_base_scroll + sta.l treasure_rolling.base_scroll +; Clear the held-DOWN debounce counter ; engine_init_rolling_buffer +; zeros the 12-byte engine struct but cooldown lives one byte past, +; so explicitly nuke it here so the very first scroll trigger fires +; immediately after popup-open. + sep #0x20 + lda #0x00 + sta.l treasure_scroll_cooldown + rep #0x20 ; Initialize HDMA channel configuration sep #0x20 ; Back to 8-bit for InitMenuInventoryHDMA @@ -486,16 +669,26 @@ Checks if base_scroll == 0xFFFF (sentinel) and if so, initializes. ; Enable HDMA via the SHARED field-menu shadow at $1BAE. The existing ; field NMI hook (`field_menu_nmi_dma_transfer_check_impl`) reads this ; byte and copies the HDMA shadow→active table each frame. -; Treasure-only `treasure_hdma_enable` ($1BD6) is kept as a tracking +; Treasure-only `treasure_rolling.hdma_enable` ($1BD6) is kept as a tracking ; flag but isn't read by the NMI path. -; OR-in ch6 enable bit ($40) so drops's ch4 bit ($10) — set by -; drops_ensure_hdma_initialized at menu open — survives. Plain +; OR-in ch6 enable bit ($40) so drops's ch4 bit ($10) - set by +; drops_ensure_hdma_initialized at menu open - survives. Plain ; `sta` would clobber the drops enable; same lazy-init order issue ; as treasure_force_hdma_setup which uses $F9 (= ch7|ch6|ch5|ch4|ch3|ch0). - lda.l 0x7E1BAE + lda.l field_menu_rolling.hdma_enable ora #0x40 ; Channel 6 enable (treasure rolling buffer) - sta.l 0x7E1BAE - sta.w treasure_hdma_enable + sta.l field_menu_rolling.hdma_enable + sta.l treasure_rolling.hdma_enable +; Push the enable straight to HDMAEN ($420C) so the very first frame +; after popup-open has ch6 active. Without this immediate write, the +; field NMI hook only copies $1BAE -> $420C on the NEXT vblank, which +; left the first rendered frame with HDMA off and BG3VOFS at whatever +; the previous menu left in $93 / $9F. Treasure inventory items then +; rendered at the WRONG vertical scanline and looked like garbled +; stride for a frame before settling. + lda.l 0x00420C + ora #0x40 + sta.l 0x00420C rts _t_hdma_already_init: @@ -506,26 +699,31 @@ _t_hdma_already_init: treasure_scroll_state_check: """ -Called at main loop entry ($019FF2) to handle scroll animation frames. -If scrolling is active, processes one frame and skips input handling. + Called at main loop entry ($019FF2) to handle scroll animation frames. + If scrolling is active, processes one frame and skips input handling. -Returns: Carry clear = process input normally - Carry set = skip input (still scrolling) + Returns: Carry clear = process input normally + Carry set = skip input (still scrolling) """ - php sep #0x20 ; 8-bit A +; Tick cooldown counter so the next held-DOWN scroll is debounced. + lda.w treasure_scroll_cooldown + beq _t_scroll_cd_done + dec.w treasure_scroll_cooldown + +_t_scroll_cd_done: ; Check if we're scrolling - lda.w treasure_scroll_state + lda.w treasure_rolling.scroll_state beq _t_scroll_state_idle ; We're scrolling - process one animation frame jsr.l treasure_update_scroll_frame_impl ; Check if scroll finished - lda.w treasure_scroll_remaining + lda.w treasure_rolling.scroll_remaining bne _t_scroll_still_active ; Scroll finished - clean up and return to idle @@ -542,40 +740,63 @@ _t_scroll_still_active: rts treasure_start_scroll_down_impl: -"""Treasure profile: kick scroll-down state machine.""" - engine_start_scroll_down(treasure_rolling, 0x1BB7, TREASURE_VISIBLE_ITEMS, TREASURE_BUFFER_SLOTS, TREASURE_SCROLL_TOTAL_PIXELS, TREASURE_SCROLL_PIXELS_PER_FRAME, treasure_ensure_hdma_initialized, _treasure_render_item_to_slot, update_treasure_scroll_hdma) ; noqa: E501 +"""Treasure profile: kick scroll-down state machine via the engine.""" + php + rep #0x10 + lda.l 0x7E1BB7 ; treasure scroll_pos (shared with vanilla cursor) + ldx.w #treasure_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_down + plp + rtl treasure_start_scroll_up_impl: -"""Treasure profile: kick scroll-up state machine.""" - engine_start_scroll_up(treasure_rolling, 0x1BB7, TREASURE_BUFFER_SLOTS, TREASURE_SCROLL_TOTAL_PIXELS, treasure_ensure_hdma_initialized, _treasure_render_item_to_slot, update_treasure_scroll_hdma) ; noqa: E501 +"""Treasure profile: kick scroll-up state machine via the engine.""" + php + rep #0x10 + lda.l 0x7E1BB7 + ldx.w #treasure_rolling + jsr.l rolling_engine.rolling_engine_start_scroll_up + plp + rtl treasure_update_scroll_frame_impl: """Treasure profile: per-frame scroll animation tick.""" - engine_update_scroll_frame(treasure_rolling, TREASURE_SCROLL_PIXELS_PER_FRAME, update_treasure_scroll_hdma) + php + rep #0x10 + ldx.w #treasure_rolling + jsr.l rolling_engine.rolling_engine_update_scroll_frame + plp + rtl treasure_finish_scroll_impl: -"""Treasure profile: end-of-animation pre-render + cleanup.""" - engine_finish_scroll(treasure_rolling, 0x1BB7, TREASURE_VISIBLE_ITEMS, TREASURE_BUFFER_SLOTS, TREASURE_TOTAL_ITEMS, _treasure_render_item_to_slot, update_treasure_scroll_hdma) ; noqa: E501 +"""Treasure profile: end-of-animation cleanup via the bank-20 engine.""" + php + rep #0x10 + lda.l 0x7E1BB7 + ldx.w #treasure_rolling + jsr.l rolling_engine.rolling_engine_finish_scroll + plp + rtl ; Inventory Rolling Buffer Patches - Relocated to Bank $20 ; These routines are called via JSL from bank $01 hooks. -.if INVENTORY_ROLLING_BUFFER { + .if INVENTORY_ROLLING_BUFFER { treasure_menu_entry_hook_impl: """Treasure profile: menu-entry implementation (HDMA capture + flush).""" stz.w 0x1B1F lda #0x00 - sta.l 0x7E1BAE -; treasure_hdma_enable - stz.w treasure_scroll_state - stz.w treasure_scroll_remaining - stz.w treasure_scroll_direction - stz.w treasure_transfer_pending - stz.w treasure_hdma_copy_pending + sta.l field_menu_rolling.hdma_enable +; treasure_rolling.hdma_enable + stz.w treasure_rolling.scroll_state + stz.w treasure_rolling.scroll_remaining + stz.w treasure_rolling.scroll_direction + stz.w treasure_rolling.transfer_pending + stz.w treasure_rolling.hdma_copy_pending ; Clear HDMA copy flag - stz.w treasure_scroll_anim_offset + stz.w treasure_rolling.scroll_anim_offset ; Clear low byte - stz.w treasure_scroll_anim_offset + 1 + stz.w treasure_rolling.scroll_anim_offset + 1 ; Clear high byte ; Initialize cursor column to 0 for single-column mode ; This ensures $1b22 is always 0 even if it had a value from previous menu @@ -589,8 +810,8 @@ treasure_menu_exit_hook_impl: php sep #0x20 lda #0x00 - sta.l 0x7E1BAE -; treasure_hdma_enable shadow off + sta.l field_menu_rolling.hdma_enable +; treasure_rolling.hdma_enable shadow off sta.l 0x7E1BC6 ; restore original "in treasure menu" flag (was original `stz $1BC6` at $01:D7E6 before the hook patch) sta.l 0x004360 @@ -616,13 +837,18 @@ treasure_menu_exit_hook_impl: ; Does NOT reset buffer_pos - we stay at the current scroll position. treasure_swap_redraw_hook_impl_body: -"""Treasure profile: post-swap re-render of all 6 slots.""" - engine_swap_redraw(treasure_rolling, 0x1BB7, TREASURE_BUFFER_SLOTS, TREASURE_TOTAL_ITEMS, treasure_ensure_hdma_initialized, _treasure_render_item_to_slot, _clear_treasure_slot, update_treasure_scroll_hdma) ; noqa: E501 - +"""Treasure profile: post-swap re-render of all 6 slots via the engine.""" + php + rep #0x10 + lda.l 0x7E1BB7 + ldx.w #treasure_rolling + jsr.l rolling_engine.rolling_engine_swap_redraw + plp + rtl ; _clear_treasure_slot ; Clears a single inventory slot in the tilemap buffer. -; Input: treasure_rolling_slot_index = slot to clear (0-10) +; Input: treasure_rolling.slot_index = slot to clear (0-10) ; Used when item index is out of bounds (>= 48) _clear_treasure_slot: @@ -644,7 +870,7 @@ _clear_treasure_slot: lda.w #0xD600 ; BG3 screen buffer (treasure inventory lives on BG3) sta.b 0x29 ; Calculate Y = slot_index * 128 + 70 - lda.w treasure_rolling_slot_index + lda.w treasure_rolling.slot_index and.w #0x00FF xba ; A = slot * 256 @@ -735,19 +961,17 @@ _clear_treasure_trash_area: draw_trash_treasure: - """ -Draws the trash can 2x2 tile graphic for single-column inventory. -Input: Y = tilemap offset (from slot calculation) -($29) = tilemap base ($B600) -treasure_rolling_slot_index = current slot -Tiles: $04 (top-left), $05 (top-right), $06 (bottom-left), $07 (bottom-right) - -Tilemap format: [tile_number, attributes] pairs -Each row is 64 bytes (32 tiles × 2 bytes) + Draws the trash can 2x2 tile graphic for single-column inventory. + Input: Y = tilemap offset (from slot calculation) + ($29) = tilemap base ($B600) + treasure_rolling.slot_index = current slot + Tiles: $04 (top-left), $05 (top-right), $06 (bottom-left), $07 (bottom-right) + + Tilemap format: [tile_number, attributes] pairs + Each row is 64 bytes (32 tiles × 2 bytes) """ - ; Y points to start of item slot area ; Draw 2x2 trash can icon, then clear remaining 10 tiles per row ; Save starting Y for second row calculation @@ -910,21 +1134,19 @@ _t_normal_return: treasure_circular_slot_calc: - """ -Calculate tilemap Y offset using circular buffer position. -Called from patched code at $A1BA via CircularSlotCalc_ext. + Calculate tilemap Y offset using circular buffer position. + Called from patched code at $A1BA via CircularSlotCalc_ext. -Input: $5D = game's slot counter (0, 2, 4, 6... incremented by 2 per row) -Output: Y = tilemap offset for circular buffer slot -Preserves: 16-bit A mode on exit + Input: $5D = game's slot counter (0, 2, 4, 6... incremented by 2 per row) + Output: Y = tilemap offset for circular buffer slot + Preserves: 16-bit A mode on exit """ - sep #0x20 ; 8-bit A ; Check if circular buffer mode is active (HDMA enabled) - lda.l 0x7E0000 + treasure_hdma_enable + lda.l treasure_rolling.hdma_enable beq _t_circ_slot_original ; Not active, use original calculation ; Circular buffer Y calculation @@ -935,7 +1157,7 @@ Preserves: 16-bit A mode on exit lsr ; Divide by 2 to get visual slot (0-9) clc - adc.l 0x7E0000 + treasure_rolling_buffer_pos + adc.l treasure_rolling.buffer_pos ; Add buffer_pos _t_circ_slot_mod: @@ -987,4 +1209,6 @@ treasure_circular_slot_calc_ext: """Trampoline to call CircularSlotCalc from bank $01 patch at $A1BA""" jsr.w treasure_circular_slot_calc rtl + } } + diff --git a/src/ingame/treasure_rolling_patches.s b/src/ingame/treasure_rolling_patches.s index 537ed35..a1f44a0 100644 --- a/src/ingame/treasure_rolling_patches.s +++ b/src/ingame/treasure_rolling_patches.s @@ -18,194 +18,214 @@ ROM patches that wire the treasure inventory rolling buffer in: hooks the treasu ; ; Flag-gated off until the entry/exit/trigger thunks are wired up. +.include "config.i" .if TREASURE_INVENTORY_ROLLING { ; Kill 2-col navigation in the inventory picker (JOY_RIGHT / JOY_LEFT). - *=0x01DA23 - .db 0x00 - - *=0x01DA34 - .db 0x00 - -; Force inventory cursor X to always 0 (left column). -; Original $01D9F5: `lda $1BB6` (AD B6 1B) → `lda #$00 / nop`. - *=0x01D9F5 - lda #0x00 - nop - -; Single-column swap-offset: original _01daac computes -; index = ((($1bb5 + $1bb7) << 1 + $1bb6) << 1 -; for the 2-column inventory ($1bb5 = visible row, $1bb7 = scroll row, -; $1bb6 = column 0/1). The double-shift assumes 2 cols × 2 bytes/item; -; with our forced single-column ($1bb6=0) it multiplies by 4 instead of -; the 2 bytes/item we actually need, so the swap reads/writes -; even-numbered items only and half the inventory becomes unreachable. -; NOP the second `asl` at $01DAC2 so the offset becomes -; ($1bb5 + $1bb7) * 2 = byte index into $1440. - *=0x01DAC2 - nop - -; Extend $1BB7 max from $14 (20) to $2B (43 = 48 - 5). - *=0x01DA86 - .db 0x2B - -; Drops swap byte-index recompute. Original at $01:DAAC computes -; X = (($1BB3 * 2) + $1BB4) * 2 -; for the 4-row x 2-col drops grid: cursor row is $1BB3, column $1BB4. -; With single-column drops + rolling buffer the actual drop slot is -; (cursor_row + drops_scroll_pos) * 2 bytes into $7E:FF28. -; Replace the 8-byte sequence in place — same length, no relocation. -; AD B3 1B lda $1BB3 -; 18 clc -; 6D EF 1B adc drops_scroll_pos -; 0A asl -; 20 B4 87 jsr $87B4 (sta $43 / ldx $43 — A -> X via scratch) - *=0x01DAAC - lda.w 0x1BB3 - clc - adc.w drops_scroll_pos - asl - jsr 0x87B4 - -; Replace `jsr $01A172` (original DrawInventoryList) at TWO call sites: -; - $01:D81D — treasure menu entry (`_01d7f2` flow), fires once on enter -; → full init (zero state, render slots 0..5). -; - $01:D933 — `_01d929` redraw helper, fires after every successful -; swap. Original rebuilds the whole 48-item list here; we only need -; to re-render the 6 buffer slots from the mutated $1440. Crucially -; this MUST NOT reset buffer_pos / $1BB7 / HDMA shadow — otherwise -; swapping with an item past visible row 4 snaps the scroll back to -; the top because init re-zeroes the rolling state. - *=0x01D81D - jsr.w init_treasure_rolling_buffer - *=0x01D933 - jsr.w treasure_refresh_slots - - *=0x01D80E - jsr.w drops_init - *=0x01D92D - jsr.w drops_refresh_slots - -; drops_init draws TreasureItemsWindow on BG4 itself (via its -; draw_window_hook) so the engine can render items into the freshly -; drawn frame. Original still has SelectBG4 + ldy + DrawWindow at -; $01:D811-$01:D819 (9 bytes) which would draw the same window on -; top of our items, overwriting them with body fill. NOP the whole -; block — drops_init has already done both steps. SelectBG3 at -; $01:D81A still runs and primes $29=$D600 for treasure_init at -; $01:D81D. - *=0x01D811 - nop - nop - nop - nop - nop - nop - nop - nop - nop - -; Drops cursor sprite Y base: original `adc #$30` at $01D96F places -; the hand pointer 16 px below the drops band's first slot (legacy -; 4x2 grid expected items at row 6+ on screen). With drops bumped -; to start at tilemap row 4 (+0x80 byte stride from baseline), the -; sprite needs base $28 so cursor row 0 lines up with item 0. - *=0x01D96F - .db 0x28 - -; Drops cursor row clamp + UP/DOWN scroll triggers. Original at -; $01D9D1: `bmi $D9D6 / sta $1BB3` skips store when dec underflows -; (clamp at row 0). Original at $01D9E0: `cmp #$04 / beq $D9E7 / -; sta $1BB3` caps cursor at 4 rows. Hijack both clamp arms so the -; rolling buffer gets a chance to scroll instead of just clamping. - -; Drops cursor loop frame-wait at $01:D98B is the per-frame tick site -; for the cursor-on-drops mode (counterpart to the picker loop's -; $01:DA08). Replace with our combined main-loop check so the drops -; scroll state machine advances while DOWN/UP are held. - *=0x01D98B - jsr.w treasure_main_loop_scroll_check - -; UP clamp -> drops_up_handler (handler reads N flag from preceding -; `dec`, scrolls when row underflowed). - *=0x01D9D1 - jsr.w drops_up_handler - nop - nop - -; DOWN clamp -> drops_down_handler (handler reads A = row+1, scrolls -; when at or past the visible cap). - *=0x01D9E0 - .db 0xEA ; nop (was cmp #$05 byte 1) - .db 0xEA ; nop (was cmp #$05 byte 2) - jsr.w drops_down_handler - nop - nop - -; Replace the up-scroll blocking 8-frame loop ($01:DA57-$01:DA66 = 16 -; bytes) with our state-machine trigger. - *=0x01DA57 - jsr.w treasure_scroll_up_trigger - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - -; Same for the down-scroll loop at $01:DA8C-$01:DA9B. - *=0x01DA8C - jsr.w treasure_scroll_down_trigger - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - -; Main-loop hook replaces `jsr $82C0` at $01:DA08. The hook drives the -; per-frame scroll animation, freezes input via `stz $01` while -; animating, and forwards to the original $82C0 so original per-frame -; work still runs. - *=0x01DA08 - jsr.w treasure_main_loop_scroll_check - -; Same hook at $01:DA9C. The down/up scroll branch ends with -; $DA9C jsr $82C0 / ldx $02 / stx $00 / $DAA3 jmp $DA0B -; which loops back to the input check WITHOUT passing through $DA08. -; While DOWN/UP is held, control bounces between $DA0B and $DAA3 -; forever, so the $DA08 hook never fires and the scroll animation -; never ticks. Replacing the second $82C0 with our hook drives the -; state machine on the held-button inner loop too, matching the -; field-menu scroll cadence (every frame). - *=0x01DA9C - jsr.w treasure_main_loop_scroll_check - -; Up-scroll branch: same pattern as the down branch, ends with -; $DA67 jsr $82C0 / ldx $02 / stx $00 / $DA6E bra $DA0B -; Tick the state machine here too so held-UP scroll animates at the -; same cadence as held-DOWN. - *=0x01DA67 - jsr.w treasure_main_loop_scroll_check - -; Treasure menu exit. Original: `stz $1BC6` (3 bytes) clears the in-menu flag. -; Replace with our exit hook (also 3 bytes), which restores original state and -; additionally tears down the rolling-buffer state + HDMA ch6 registers so -; the next menu mode (field, key-item picker, battle) starts from a clean -; slate. The hook itself re-runs the `stz $1BC6` to preserve original contract. - *=0x01D7E6 - jsr.w treasure_menu_exit_hook +.alloc at 0x01DA23 { + .db 0x00 +} +.alloc at 0x01DA34 { + .db 0x00 + + ; Force inventory cursor X to always 0 (left column). + ; Original $01D9F5: `lda $1BB6` (AD B6 1B) → `lda #$00 / nop`. +} +.alloc at 0x01D9F5 { + lda #0x00 + nop + + ; Single-column swap-offset: original _01daac computes + ; index = ((($1bb5 + $1bb7) << 1 + $1bb6) << 1 + ; for the 2-column inventory ($1bb5 = visible row, $1bb7 = scroll row, + ; $1bb6 = column 0/1). The double-shift assumes 2 cols × 2 bytes/item; + ; with our forced single-column ($1bb6=0) it multiplies by 4 instead of + ; the 2 bytes/item we actually need, so the swap reads/writes + ; even-numbered items only and half the inventory becomes unreachable. + ; NOP the second `asl` at $01DAC2 so the offset becomes + ; ($1bb5 + $1bb7) * 2 = byte index into $1440. +} +.alloc at 0x01DAC2 { + nop + + ; Extend $1BB7 max from $14 (20) to $2B (43 = 48 - 5). +} +.alloc at 0x01DA86 { + .db 0x2B + + ; Drops swap byte-index recompute. Original at $01:DAAC computes + ; X = (($1BB3 * 2) + $1BB4) * 2 + ; for the 4-row x 2-col drops grid: cursor row is $1BB3, column $1BB4. + ; With single-column drops + rolling buffer the actual drop slot is + ; (cursor_row + drops_scroll_pos) * 2 bytes into $7E:FF28. + ; Replace the 8-byte sequence in place — same length, no relocation. + ; AD B3 1B lda $1BB3 + ; 18 clc + ; 6D EF 1B adc drops_scroll_pos + ; 0A asl + ; 20 B4 87 jsr $87B4 (sta $43 / ldx $43 — A -> X via scratch) +} +.alloc at 0x01DAAC { + lda.w 0x1BB3 + clc + adc.w drops_scroll_pos + asl + jsr 0x87B4 + + ; Replace `jsr $01A172` (original DrawInventoryList) at TWO call sites: + ; - $01:D81D — treasure menu entry (`_01d7f2` flow), fires once on enter + ; → full init (zero state, render slots 0..5). + ; - $01:D933 — `_01d929` redraw helper, fires after every successful + ; swap. Original rebuilds the whole 48-item list here; we only need + ; to re-render the 6 buffer slots from the mutated $1440. Crucially + ; this MUST NOT reset buffer_pos / $1BB7 / HDMA shadow — otherwise + ; swapping with an item past visible row 4 snaps the scroll back to + ; the top because init re-zeroes the rolling state. +} +.alloc at 0x01D81D { + jsr.w init_treasure_rolling_buffer +} +.alloc at 0x01D933 { + jsr.w treasure_refresh_slots +} +.alloc at 0x01D80E { + jsr.w drops_init +} +.alloc at 0x01D92D { + jsr.w drops_refresh_slots + + ; drops_init draws TreasureItemsWindow on BG4 itself (via its + ; draw_window_hook) so the engine can render items into the freshly + ; drawn frame. Original still has SelectBG4 + ldy + DrawWindow at + ; $01:D811-$01:D819 (9 bytes) which would draw the same window on + ; top of our items, overwriting them with body fill. NOP the whole + ; block — drops_init has already done both steps. SelectBG3 at + ; $01:D81A still runs and primes $29=$D600 for treasure_init at + ; $01:D81D. +} +.alloc at 0x01D811 { + nop + nop + nop + nop + nop + nop + nop + nop + nop + + ; Drops cursor sprite Y base: original `adc #$30` at $01D96F places + ; the hand pointer 16 px below the drops band's first slot (legacy + ; 4x2 grid expected items at row 6+ on screen). With drops bumped + ; to start at tilemap row 4 (+0x80 byte stride from baseline), the + ; sprite needs base $28 so cursor row 0 lines up with item 0. +} +.alloc at 0x01D96F { + .db 0x28 + + ; Drops cursor row clamp + UP/DOWN scroll triggers. Original at + ; $01D9D1: `bmi $D9D6 / sta $1BB3` skips store when dec underflows + ; (clamp at row 0). Original at $01D9E0: `cmp #$04 / beq $D9E7 / + ; sta $1BB3` caps cursor at 4 rows. Hijack both clamp arms so the + ; rolling buffer gets a chance to scroll instead of just clamping. + + ; Drops cursor loop frame-wait at $01:D98B is the per-frame tick site + ; for the cursor-on-drops mode (counterpart to the picker loop's + ; $01:DA08). Replace with our combined main-loop check so the drops + ; scroll state machine advances while DOWN/UP are held. +} +.alloc at 0x01D98B { + jsr.w treasure_main_loop_scroll_check + + ; UP clamp -> drops_up_handler (handler reads N flag from preceding + ; `dec`, scrolls when row underflowed). +} +.alloc at 0x01D9D1 { + jsr.w drops_up_handler + nop + nop + + ; DOWN clamp -> drops_down_handler (handler reads A = row+1, scrolls + ; when at or past the visible cap). +} +.alloc at 0x01D9E0 { + .db 0xEA ; nop (was cmp #$05 byte 1) + .db 0xEA ; nop (was cmp #$05 byte 2) + jsr.w drops_down_handler + nop + nop + + ; Replace the up-scroll blocking 8-frame loop ($01:DA57-$01:DA66 = 16 + ; bytes) with our state-machine trigger. +} +.alloc at 0x01DA57 { + jsr.w treasure_scroll_up_trigger + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + + ; Same for the down-scroll loop at $01:DA8C-$01:DA9B. +} +.alloc at 0x01DA8C { + jsr.w treasure_scroll_down_trigger + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + + ; Main-loop hook replaces `jsr $82C0` at $01:DA08. The hook drives the + ; per-frame scroll animation, freezes input via `stz $01` while + ; animating, and forwards to the original $82C0 so original per-frame + ; work still runs. +} +.alloc at 0x01DA08 { + jsr.w treasure_main_loop_scroll_check + + ; Same hook at $01:DA9C. The down/up scroll branch ends with + ; $DA9C jsr $82C0 / ldx $02 / stx $00 / $DAA3 jmp $DA0B + ; which loops back to the input check WITHOUT passing through $DA08. + ; While DOWN/UP is held, control bounces between $DA0B and $DAA3 + ; forever, so the $DA08 hook never fires and the scroll animation + ; never ticks. Replacing the second $82C0 with our hook drives the + ; state machine on the held-button inner loop too, matching the + ; field-menu scroll cadence (every frame). +} +.alloc at 0x01DA9C { + jsr.w treasure_main_loop_scroll_check + + ; Up-scroll branch: same pattern as the down branch, ends with + ; $DA67 jsr $82C0 / ldx $02 / stx $00 / $DA6E bra $DA0B + ; Tick the state machine here too so held-UP scroll animates at the + ; same cadence as held-DOWN. +} +.alloc at 0x01DA67 { + jsr.w treasure_main_loop_scroll_check + + ; Treasure menu exit. Original: `stz $1BC6` (3 bytes) clears the in-menu flag. + ; Replace with our exit hook (also 3 bytes), which restores original state and + ; additionally tears down the rolling-buffer state + HDMA ch6 registers so + ; the next menu mode (field, key-item picker, battle) starts from a clean + ; slate. The hook itself re-runs the `stz $1BC6` to preserve original contract. +} +.alloc at 0x01D7E6 { + jsr.w treasure_menu_exit_hook +} } diff --git a/src/ingame/window_animation_patches.s b/src/ingame/window_animation_patches.s index 40738b7..d11f681 100644 --- a/src/ingame/window_animation_patches.s +++ b/src/ingame/window_animation_patches.s @@ -8,85 +8,86 @@ menu transitions are immediate. /* --- _open_window Override (@83E3) --- Replaces the 25-frame vertical wipe loop with a single DMA. */ -*=0x0183E3 -_open_window: - sep #0x20 ; 8-bit A (shorta) - lda #0x7E ; Source RAM bank for tilemap buffers - sta 0x21 - rep #0x10 ; 16-bit X/Y (longi) - ldx.w 0x35 ; VRAM destination address - stx.w 0x1D - ldx.w 0x29 ; Source tilemap buffer address (RAM) - stx.w 0x1F - rep #0x20 ; 16-bit A (longa) - lda #0x0C80 ; Size for full window buffer (25 rows * 128 bytes) - sta.w 0x22 - sep #0x20 ; 8-bit A - /* jsr WaitVblank */ ; Removed loop delay - jsr.w TfrVRAM ; Perform instant DMA - rts +.alloc at 0x0183E3 { + _open_window: + sep #0x20 ; 8-bit A (shorta) + lda #0x7E ; Source RAM bank for tilemap buffers + sta 0x21 + rep #0x10 ; 16-bit X/Y (longi) + ldx.w 0x35 ; VRAM destination address + stx.w 0x1D + ldx.w 0x29 ; Source tilemap buffer address (RAM) + stx.w 0x1F + rep #0x20 ; 16-bit A (longa) + lda #0x0C80 ; Size for full window buffer (25 rows * 128 bytes) + sta.w 0x22 + sep #0x20 ; 8-bit A + /* jsr WaitVblank */ ; Removed loop delay + jsr.w TfrVRAM ; Perform instant DMA + rts -/* --- close_window Override (@8417) --- - Clears the VRAM area instantly by pointing to the empty part of the buffer. */ + /* --- close_window Override (@8417) --- + Clears the VRAM area instantly by pointing to the empty part of the buffer. */ +} +.alloc at 0x018417 { + close_window: + """Override of original close_window: skip per-row wipe, single DMA.""" + sep #0x20 + lda #0x7E + sta 0x21 + rep #0x10 + ldx.w 0x35 ; VRAM destination address + stx.w 0x1D + rep #0x20 + lda.w 0x29 ; Source tilemap buffer address + clc + adc #0x0C00 ; Offset to the 'empty/closed' window state tiles + tax + stx.w 0x1F ; Set source pointer + lda #0x0C80 ; Full size + sta.w 0x22 + sep #0x20 + jsr.w TfrVRAM ; Perform instant DMA + rts -*=0x018417 -close_window: -"""Override of original close_window: skip per-row wipe, single DMA.""" - sep #0x20 - lda #0x7E - sta 0x21 - rep #0x10 - ldx.w 0x35 ; VRAM destination address - stx.w 0x1D - rep #0x20 - lda.w 0x29 ; Source tilemap buffer address - clc - adc #0x0C00 ; Offset to the 'empty/closed' window state tiles - tax - stx.w 0x1F ; Set source pointer - lda #0x0C80 ; Full size - sta.w 0x22 - sep #0x20 - jsr.w TfrVRAM ; Perform instant DMA - rts + /* --- transform_window Override (@84D0) --- + Skips the coordinate growth loop and jumps directly to final dimensions. */ +} +.alloc at 0x0184D0 { + transform_window: + """Kick the window-transform animation: pass source/target window addresses.""" + phb + phk + plb ; Setup Bank Registry for local access + sep #0x20 + /* Snap current coordinates ($63-$66) to target coordinates ($67-$6A) instantly */ + lda.w 0x67 + sta.w 0x63 + lda.w 0x68 + sta.w 0x64 + lda.w 0x69 + sta.w 0x65 + lda.w 0x6A + sta.w 0x66 -/* --- transform_window Override (@84D0) --- - Skips the coordinate growth loop and jumps directly to final dimensions. */ + /* Draw the final static window components */ + jsr.w DrawWindowRowTop + jsr.w DrawWindowRowBtm + jsr.w DrawWindowColLeft + jsr.w DrawWindowColRight -*=0x0184D0 -transform_window: -"""Kick the window-transform animation: pass source/target window addresses.""" - phb - phk - plb ; Setup Bank Registry for local access - sep #0x20 - /* Snap current coordinates ($63-$66) to target coordinates ($67-$6A) instantly */ - lda.w 0x67 - sta.w 0x63 - lda.w 0x68 - sta.w 0x64 - lda.w 0x69 - sta.w 0x65 - lda.w 0x6A - sta.w 0x66 + /* Sync to PPU once */ + jsr.w WaitVblank + lda.w 0xC3 ; Current menu window BG index + ldx.w #TfrBGTilesTbl ; Address of jump table at @85B8 + jsr.w ExecJumpTbl + jsr.w UpdateScrollRegs_far -/* Draw the final static window components */ - jsr.w DrawWindowRowTop - jsr.w DrawWindowRowBtm - jsr.w DrawWindowColLeft - jsr.w DrawWindowColRight - -/* Sync to PPU once */ - jsr.w WaitVblank - lda.w 0xC3 ; Current menu window BG index - ldx.w #TfrBGTilesTbl ; Address of jump table at @85B8 - jsr.w ExecJumpTbl - jsr.w UpdateScrollRegs_far - -/* Point the engine's transformation pointers to an RTS to finish the state. - Original FF4 uses @858C as the terminal RTS for this routine. */ - ldx.w #0x858C - stx.w 0x01CD - stx.w 0x01D0 - plb - rts + /* Point the engine's transformation pointers to an RTS to finish the state. + Original FF4 uses @858C as the terminal RTS for this routine. */ + ldx.w #0x858C + stx.w 0x01CD + stx.w 0x01D0 + plb + rts +} diff --git a/src/ingame/windows.s b/src/ingame/windows.s index f5749f3..4fe40a7 100644 --- a/src/ingame/windows.s +++ b/src/ingame/windows.s @@ -4,79 +4,80 @@ at fixed bank-$01 addresses. """ ; equip main window -*=0x01dda9 - menu_window(0, 0, 30, 11) - -; char 1 - -*=0x01dd95 - menu_window(0, 0, 30, 11) - ; char 2 - -*=0x01dd99 - menu_window(0, 5, 30, 11) - ; char 3 - -*=0x01dd9d - menu_window(0, 10, 30, 11) - ; char 4 - -*=0x01dda1 - menu_window(0, 15, 30, 11) - ; char 5 - -*=0x01dda5 - menu_window(0, 20, 30, 11) - -; magic -;*=0x01B010 -; ldx.w #0xFF18 + 8 - -*=0x01dd55 - menu_window(1, 0, 19, 7) - -*=0x01dd59 - menu_window(1, 5, 19, 7) - -*=0x01dd5d - menu_window(1, 10, 19, 7) - -*=0x01dd61 - menu_window(1, 15, 19, 7) - -*=0x01dd65 - menu_window(1, 20, 19, 7) - -; magic kind window - -*=0x01dd69 - menu_window(23, 2, 7, 7) - -; controls the amount of scroll to apply to bg4 to move the magic window kind window - -*=0x01af90 - ; 01/AF90: A9 34 LDA #$34 - ; 01/AF92: 85 AD STA $AD - lda #0x68 - -; scroll out of the same window. - -*=0x01b072 - lda #0x68 - -; status - -*=0x01de01 - menu_window(0, 1, 29, 24) - menu_window(18, 6, 12, 4) - -; clear next line of text in the right menu - -*=0x01a974 - ldy.w #0x270 - lda #7 - -; item title - -*=0x01dcd2 - menu_window(22, 0, 7, 3) +.alloc at 0x01dda9 { + menu_window(0, 0, 30, 11) + + ; char 1 +} +.alloc at 0x01dd95 { + menu_window(0, 0, 30, 11) + ; char 2 +} +.alloc at 0x01dd99 { + menu_window(0, 5, 30, 11) + ; char 3 +} +.alloc at 0x01dd9d { + menu_window(0, 10, 30, 11) + ; char 4 +} +.alloc at 0x01dda1 { + menu_window(0, 15, 30, 11) + ; char 5 +} +.alloc at 0x01dda5 { + menu_window(0, 20, 30, 11) + + ; magic + ;*=0x01B010 + ; ldx.w #0xFF18 + 8 +} +.alloc at 0x01dd55 { + menu_window(1, 0, 19, 7) +} +.alloc at 0x01dd59 { + menu_window(1, 5, 19, 7) +} +.alloc at 0x01dd5d { + menu_window(1, 10, 19, 7) +} +.alloc at 0x01dd61 { + menu_window(1, 15, 19, 7) +} +.alloc at 0x01dd65 { + menu_window(1, 20, 19, 7) + + ; magic kind window +} +.alloc at 0x01dd69 { + menu_window(23, 2, 7, 7) + + ; controls the amount of scroll to apply to bg4 to move the magic window kind window +} +.alloc at 0x01af90 { + ; 01/AF90: A9 34 LDA #$34 + ; 01/AF92: 85 AD STA $AD + lda #0x68 + + ; scroll out of the same window. +} +.alloc at 0x01b072 { + lda #0x68 + + ; status +} +.alloc at 0x01de01 { + menu_window(0, 1, 29, 24) + menu_window(18, 6, 12, 4) + + ; clear next line of text in the right menu +} +.alloc at 0x01a974 { + ldy.w #0x270 + lda #7 + + ; item title +} +.alloc at 0x01dcd2 { + menu_window(22, 0, 7, 3) +} diff --git a/src/intro.s b/src/intro.s index ab05895..3b69dd5 100644 --- a/src/intro.s +++ b/src/intro.s @@ -8,123 +8,127 @@ and chains into the title screen. ; ---------------------------------------------------------------- .include "libmz.i" ; macros (dma_transfer_to_vram_call, etc.) +.include "bank20.i" .import "libmz" .import "assets" .extern clear_ram -start_splash_screen: -"""Boot-time splash-screen entry point.""" - ; initialise SNES - jsr.w initialize_snes - -; set register modes - rep #0x10 ; make X & Y 16-bits - sep #0x20 ; make A 8-bits - -; initialise graphics hardware - lda #0x03 ; graphics mode 3 - sta 0x2105 - lda #0x01 ; enable plane 0 - sta 0x212c - lda #0x00 ; set plane 0 memory to 0x0000, 32x32 chars - sta 0x2107 - lda #0x01 ; set plane 0 character set to 0x1000 - sta 0x210b - -; copy intro map data - dma_transfer_to_vram_call(assets_intro_map, 0x0000, assets_intro_map__size, 0x1801) - -; copy color palettes - dma_transfer_to_palette_call(assets_intro_col, assets_intro_col__size) - -; copy intro tile set - dma_transfer_to_vram_call(assets_intro_set, 0x1000, assets_intro_set__size, 0x1801) - - jsr.w _splash_screen_fade_in - - lda #0x80 - jsr.w _gamepad_interruptable_loop - - jsr.w _splash_screen_fade_out - - jsr.l clear_ram - ; runs the original jsl routines - jsr.l 0x15C8DF - jsr.l 0x15C9AA - - rtl - -; TODO: rewrite as HDMA table would make it look less hacky. - -_splash_screen_fade_out: -{ - stz 0x00 -loop: - inc 0x00 - lda #0x0F - sbc 0x00 - sta 0x2100 - lda 0x00 - cmp #0x0F - beq exit - lda 0x00 - asl - asl - asl - asl - inc - sta 0x2106 - - jsr.w wait_for_vblank - jsr.w wait_for_vblank - jsr.w wait_for_vblank - bra loop -exit: - rts -} - - -_splash_screen_fade_in: -{ - stz 0x00 -loop: - inc 0x00 - lda 0x00 - sta 0x2100 - asl - asl - asl - asl - sta 0x01 - lda #0xF0 - sec - sbc 0x01 - inc - sta 0x2106 - - jsr.w wait_for_vblank - jsr.w wait_for_vblank - - lda 0x00 - cmp #0x0F - beq exit - bra loop -exit: - rts -} - - -_gamepad_interruptable_loop: - ; 8bit A: Number of iterations -{ - jsr.w enable_gamepad - jsr.w wait_for_vblank - ldx 0x4218 ; lecture depuis joystick - bne exit ; si on appuye sur quelque chose on sort du delay - dec - bne _gamepad_interruptable_loop -exit: - jsr.w disable_gamepad - rts +.alloc intro_block in bank20_reloc { + start_splash_screen: + """Boot-time splash-screen entry point.""" + ; initialise SNES + jsr.w initialize_snes + + ; set register modes + rep #0x10 ; make X & Y 16-bits + sep #0x20 ; make A 8-bits + + ; initialise graphics hardware + lda #0x03 ; graphics mode 3 + sta 0x2105 + lda #0x01 ; enable plane 0 + sta 0x212c + lda #0x00 ; set plane 0 memory to 0x0000, 32x32 chars + sta 0x2107 + lda #0x01 ; set plane 0 character set to 0x1000 + sta 0x210b + + ; copy intro map data + dma_transfer_to_vram_call(assets_intro_map, 0x0000, assets_intro_map__size, 0x1801) + + ; copy color palettes + dma_transfer_to_palette_call(assets_intro_col, assets_intro_col__size) + + ; copy intro tile set + dma_transfer_to_vram_call(assets_intro_set, 0x1000, assets_intro_set__size, 0x1801) + + jsr.w _splash_screen_fade_in + + lda #0x80 + jsr.w _gamepad_interruptable_loop + + jsr.w _splash_screen_fade_out + + jsr.l clear_ram + ; runs the original jsl routines + jsr.l 0x15C8DF + jsr.l 0x15C9AA + + rtl + + ; TODO: rewrite as HDMA table would make it look less hacky. + + _splash_screen_fade_out: + { + stz 0x00 + loop: + inc 0x00 + lda #0x0F + sbc 0x00 + sta 0x2100 + lda 0x00 + cmp #0x0F + beq exit + lda 0x00 + asl + asl + asl + asl + inc + sta 0x2106 + + jsr.w wait_for_vblank + jsr.w wait_for_vblank + jsr.w wait_for_vblank + bra loop + exit: + rts + } + + + _splash_screen_fade_in: + { + stz 0x00 + loop: + inc 0x00 + lda 0x00 + sta 0x2100 + asl + asl + asl + asl + sta 0x01 + lda #0xF0 + sec + sbc 0x01 + inc + sta 0x2106 + + jsr.w wait_for_vblank + jsr.w wait_for_vblank + + lda 0x00 + cmp #0x0F + beq exit + bra loop + exit: + rts + } + + + _gamepad_interruptable_loop: + ; 8bit A: Number of iterations + { + jsr.w enable_gamepad + jsr.w wait_for_vblank + + ldx 0x4218 ; lecture depuis joystick + bne exit ; si on appuye sur quelque chose on sort du delay + dec + bne _gamepad_interruptable_loop + exit: + jsr.w disable_gamepad + rts + } } diff --git a/src/items.i b/src/items.i index 4138348..6693527 100644 --- a/src/items.i +++ b/src/items.i @@ -1,19 +1,23 @@ - - """ -Standard FF4 inventory item layout: 2-byte (id, qty) pairs. +Inventory item layout + VWF region map shared across field, treasure, +drops, and key-item menus. Defines the `Item` struct (2-byte id + qty +pair) plus the BG3 CHR tile-id windows each menu surface owns. +""" -Used by every menu surface that shows player items in single-column +.include "src/vwf_state.i" -rolling-buffer form: - - Field menu Items submenu at $7E:1440 (48 slots) - - Treasure menu inventory list at $7E:1440 (same 48-slot array) - - Treasure menu drops list at $7E:FF28 (8 drop slots) - - Key-item picker filter buffer at $7E:0712 (filtered subset of $1440) +; Standard FF4 inventory item layout: 2-byte (id, qty) pairs. +; +; Used by every menu surface that shows player items in single-column -Battle inventory at $321A is a separate 4-byte layout (flags + id + -qty + spell) and gets its own struct in a follow-up plan. -""" +; rolling-buffer form: +; - Field menu Items submenu at $7E:1440 (48 slots) +; - Treasure menu inventory list at $7E:1440 (same 48-slot array) +; - Treasure menu drops list at $7E:FF28 (8 drop slots) +; - Key-item picker filter buffer at $7E:0712 (filtered subset of $1440) +; +; Battle inventory at $321A is a separate 4-byte layout (flags + id + +; qty + spell) and gets its own struct in a follow-up plan. ; Per-item record size in the French-translated assets_items_dat table: @@ -23,6 +27,88 @@ qty + spell) and gets its own struct in a follow-up plan. ITEM_NAME_RECORD_SIZE := 0x0C ITEM_NAME_TEXT_SIZE := 0x0B +; Per-item record size in the unleashed (16-char) name table at +; `assets_items_unleashed_dat`: 1-byte symbol prefix + 16-byte name = +; 17 bytes total. Battle / field / drops / treasure inventory all +; render from this table once the BATTLE_ITEMS_VWF + field-menu +; switches are on. Keep stride math + loop counters consistent via +; these two symbols. +ITEM_UNLEASHED_RECORD_SIZE := 0x11 +ITEM_UNLEASHED_TEXT_SIZE := 0x10 + +; --- Field-menu BG3 CHR base --- +; Menu PPU runs Mode 0 with BG34NBA = $22 (`ff4decomp/menu/menu.asm:3878+`), +; mapping BG3 CHR to VRAM word $2000 / byte $4000. 256 static-font +; tile_ids live at $00..$FF ; the VWF region starts past that at +; tile_id $100 (9-bit tile_id territory) so glyph blits never +; trample the menu chrome / font CHR. +FIELD_BG3_CHR_VRAM_BYTE := 0x4000 +FIELD_BG3_CHR_VRAM_WORD := FIELD_BG3_CHR_VRAM_BYTE >> 1 + +; VWF window starts at tile_id $100 in CHR = $4000 + $100 * 16 = $5000. +FIELD_VWF_VRAM_DEST_BYTE := FIELD_BG3_CHR_VRAM_BYTE + 0x1000 +FIELD_VWF_VRAM_DEST_WORD := FIELD_VWF_VRAM_DEST_BYTE >> 1 + +; --- Field-menu VWF regions --- +; Mirrors the battle-side `region_size * N` partition in +; `src/battle/message.s`. Each region owns a contiguous tile-id +; window inside the BG3 CHR slot at VRAM $4000 (menu BG34NBA = $22). +; Adding a new VWF surface (item description, treasure list, +; whatever lives on field) means declaring a new region with its +; tile-id base + budget + VRAM dest and the engine routes via +; VwfConfig without touching the existing regions. +; +; Region 0 $000..$0FF vanilla menu font CHR (untouched) +; Region 1 $100..$169 field / treasure inventory item-name buffer +; (FIELD_ITEM_VWF_TILE_BASE + K * slot, +; 11 slots * K=10 tile_ids) +; Region 1B $16E..$1A9 drops item-name buffer (treasure popup only), +; 6 buffer slots offset by DROPS_VWF_TILE_SLOT_OFFSET +; (=11) from the treasure base so the two +; panels coexist without clobbering each other. +; +; Field menu vs treasure-popup vs drops are UI-exclusive at the panel +; level, but inside the treasure popup the treasure inventory AND the +; monster-drops band render every frame. They both route through the +; patched `DrawItemName` JSL hook in `items_menu_vwf.draw_field_item_name` +; which computes tile_id_base from DP $5D. Drops's render hook offsets +; $5D by +11 before calling the vanilla trampoline chain so its CHR +; lands at $16E.. instead of $100.. ; items_menu_vwf bumps chr_byte_count +; from $700 to $B00 so the NMI DMA flush covers both regions across +; all 6 buffer slots per panel. +; +; Pushing the VWF region into 9-bit tile_id territory ($100+) keeps +; it disjoint from the static font window ; the combined treasure + +; drops footprint fits at $100..$1A9 (margin to $1FF before the next +; BG3 CHR boundary). +FIELD_ITEM_VWF_TILE_BASE := 0x100 +FIELD_ITEM_VWF_TILE_BUDGET := 0x0A +DROPS_VWF_TILE_SLOT_OFFSET := 0x0B + +; --- Drops VWF flush descriptor (secondary NMI flush slot) --- +; Hardcoded since drops only ever lives at the +11-slot offset in +; the field BG3 CHR window. NMI runs both primary (treasure) and +; secondary (drops) flushes per frame so each panel's CHR reaches +; VRAM intact regardless of which descriptor was written last. +; src offset = $16E * 16 = $16E0 bytes into VWF_CHR_BUFFER +; vram dest = ($5000 + $6E0) / 2 = $2B70 (= drops's CHR base) +; size = 6 buffer slots * K=10 * 16 = $3C0 bytes +DROPS_VWF_CHR_SRC_OFFSET := 0x16E0 +DROPS_VWF_VRAM_DEST_WORD := 0x2B70 +DROPS_VWF_BYTE_COUNT := 0x03C0 + +; Primary flush size: covers the WORST-CASE primary consumer which +; is the field-items menu (11 buffer slots * K=10 * 16 bytes = $6E0, +; round up to $700). Treasure uses only 6 slots ($3C0) so the +; primary DMA overshoots its region by a few tiles, but the extra +; bytes flush into the start of drops's CHR window ($16E..$170) +; whose secondary DMA overwrites them with the correct drops CHR +; immediately afterwards. Was briefly $400 (treasure-only sized) +; after the two-descriptor split, which cut field-items' last 5 +; buffer slots out of the flush window and tripped a BRK during +; field scroll once tilemap entries pointed at unflushed tile_ids. +FIELD_VWF_PRIMARY_BYTE_COUNT := 0x0700 + .struct Item { byte id @@ -35,9 +121,26 @@ ITEM_NAME_TEXT_SIZE := 0x0B ; 12-byte block at a known WRAM base; routines reference fields via ; ` + RollingBufferState.` instead of hardcoded offsets. ; -; Field menu base: $7E:1BA8 -; Treasure inventory: $7E:1BD0 -; (Drops + key-item bases will be assigned during Phase 4-5.) + +; Bases (all in clean $7E:9C00 arena, off vanilla scratch $1B**): +; Treasure inv: $7E:9C00 +; Drops: $7E:9C30 +; Key-item picker: $7E:9C60 +; Field menu: $7E:9C90 (FIELD_MENU_ROLLING_BASE below) +; +; The whole arena lives inside the spell-list text buffers that the +; magic-direct-render rewrite freed (see battle/inventory_rolling.s +; comment: "freed by magic direct rendering: $97A6, $9E66, …"). Battle +; reuses the low end ($97A6 ring + $9DA7 command buffer), leaving the +; $990E..$9DA7 gap free - that's where our four rolling states sit. +; Menus are mutually exclusive with battle, and init re-seeds the +; struct on every menu open, so battle-side overwrites don't matter. +; +; Only the field base lives here for now - the other three are still +; defined inside their respective modules. Migrate them into shared +; constants when the singleton arena refactor lands. +FIELD_MENU_ROLLING_BASE := 0x7E9C90 + .struct RollingBufferState { byte top_row byte buffer_pos @@ -52,4 +155,32 @@ ITEM_NAME_TEXT_SIZE := 0x0B byte transfer_pending word scroll_anim_offset byte hdma_copy_pending + byte visible_rows + byte slot_height_tiles + long item_list_ptr + byte item_count + byte hdma_channel + long vwf_cfg_ptr + byte dirty_mask + long fn_render_slot + long fn_update_hdma + long fn_draw_window + byte menu_id } + +ROLLING_MENU_ID_FIELD := 0 +ROLLING_MENU_ID_TREASURE := 1 +ROLLING_MENU_ID_DROPS := 2 +ROLLING_MENU_ID_KEY_ITEM := 3 + +; Typed view onto the field state - gives field_menu_rolling.hdma_enable, +; field_menu_rolling.fn_render_slot, etc. as flat symbols (a816 cast, +; eager-expanded in symbols.py::_try_expand_typed_cast). +; +; The field hdma_enable / hdma_copy_pending bytes also act as the +; SHARED menu HDMA signals - all four rolling menus poke them to ask +; the field-menu NMI hook (field_menu_nmi_dma_transfer_check_impl) to +; copy shadow→active during the next vblank. Reference directly as +; `field_menu_rolling.hdma_enable` / `.hdma_copy_pending` everywhere. +field_menu_rolling := (FIELD_MENU_ROLLING_BASE as RollingBufferState) + diff --git a/src/kerning.s b/src/kerning.s index 64f2646..8e0afee 100644 --- a/src/kerning.s +++ b/src/kerning.s @@ -3,109 +3,113 @@ Dialog-VWF kerning helpers: long-form (RTL) wrapper around the binary-search bac kerning offsets. """ .include "src/vwf.i" +.include "bank20.i" -dialog_get_kerning_adjustment_binary_search_ext: -""" -Long-form (RTL) wrapper around `dialog_get_kerning_adjustment_binary_search` for cross-bank callers and Python -tests. -""" - jsr.w dialog_get_kerning_adjustment_binary_search - rtl +.alloc kerning_block in bank20_reloc { + dialog_get_kerning_adjustment_binary_search_ext: + """ + Long-form (RTL) wrapper around `dialog_get_kerning_adjustment_binary_search` for cross-bank callers and Python + tests. + """ + jsr.w dialog_get_kerning_adjustment_binary_search + rtl -dialog_get_kerning_adjustment_binary_search: -""" -Binary-search the dialog font's kerning table for the pair in DP $25 (CURRENT_C) ; returns adjustment in A or -A=0+sec on miss. Trashes A/Y/flags, preserves X. -""" - phx - jsr.w _dialog_get_kerning_adjustment_binary_search - plx - rts -_dialog_get_kerning_adjustment_binary_search: -{ -; Space ($FF) never appears in any font's kerning pair table; bail -; before reading the count when either side of the pair is a space. -; Caller is in 16-bit M. - lda.b CURRENT_C - and.w #0x00ff - cmp.w #0x00ff - beq _return_zero - lda.b CURRENT_C - and.w #0xff00 - cmp.w #0xff00 - beq _return_zero + dialog_get_kerning_adjustment_binary_search: + """ + Binary-search the dialog font's kerning table for the pair in DP $25 (CURRENT_C) ; returns adjustment in A or + A=0+sec on miss. Trashes A/Y/flags, preserves X. + """ + phx + jsr.w _dialog_get_kerning_adjustment_binary_search + plx + rts + + _dialog_get_kerning_adjustment_binary_search: + { + ; Space ($FF) never appears in any font's kerning pair table; bail + ; before reading the count when either side of the pair is a space. + ; Caller is in 16-bit M. + lda.b CURRENT_C + and.w #0x00ff + cmp.w #0x00ff + beq _return_zero + lda.b CURRENT_C + and.w #0xff00 + cmp.w #0xff00 + beq _return_zero - ldy.w #0x1100 - lda.b [font_addr], y ; NumKerningPairs (16-bit) - beq _return_zero + ldy.w #0x1100 + lda.b [font_addr], y ; NumKerningPairs (16-bit) + beq _return_zero - sec - sbc.w #0x0001 ; high = count - 1 - pha ; push high (bottom) - lda.w #0x0000 - pha ; push low (middle) - pea.w 0x0000 ; reserve mid slot (top) + sec + sbc.w #0x0001 ; high = count - 1 + pha ; push high (bottom) + lda.w #0x0000 + pha ; push low (middle) + pea.w 0x0000 ; reserve mid slot (top) -_loop: - lda 0x0005, s ; high - cmp 0x0003, s ; compare against low - bcc _not_found ; high < low -> no match + _loop: + lda 0x0005, s ; high + cmp 0x0003, s ; compare against low + bcc _not_found ; high < low -> no match - lda 0x0005, s - sec - sbc 0x0003, s - lsr - clc - adc 0x0003, s - sta 0x0001, s ; mid + lda 0x0005, s + sec + sbc 0x0003, s + lsr + clc + adc 0x0003, s + sta 0x0001, s ; mid - lda 0x0001, s - asl - clc - adc 0x0001, s ; mid * 3 - clc - adc.w #0x1102 - tay + lda 0x0001, s + asl + clc + adc 0x0001, s ; mid * 3 + clc + adc.w #0x1102 + tay - lda.b [font_addr], y ; load kerning key - cmp.b CURRENT_C - beq _found - bcc _search_upper ; table key < target -> raise low + lda.b [font_addr], y ; load kerning key + cmp.b CURRENT_C + beq _found + bcc _search_upper ; table key < target -> raise low -_search_lower: - lda 0x0001, s ; mid - 1 becomes new high - sec - sbc.w #0x0001 - bcc _not_found ; mid was 0, underflow → not found - sta 0x0005, s - bra _loop + _search_lower: + lda 0x0001, s ; mid - 1 becomes new high + sec + sbc.w #0x0001 + bcc _not_found ; mid was 0, underflow → not found + sta 0x0005, s + bra _loop -_search_upper: - lda 0x0001, s ; mid + 1 becomes new low - clc - adc.w #0x0001 - sta 0x0003, s - bra _loop + _search_upper: + lda 0x0001, s ; mid + 1 becomes new low + clc + adc.w #0x0001 + sta 0x0003, s + bra _loop -_not_found: - pla ; discard mid - pla ; discard low - pla ; discard high -_return_zero: - lda.w #0x0000 - sec - rts + _not_found: + pla ; discard mid + pla ; discard low + pla ; discard high + _return_zero: + lda.w #0x0000 + sec + rts -_found: - iny - iny - lda.b [font_addr], y - and.w #0x00FF - ply ; drop mid (PLY/X preserve A) - ply ; drop low - ply ; drop high - clc - rts + _found: + iny + iny + lda.b [font_addr], y + and.w #0x00FF + ply ; drop mid (PLY/X preserve A) + ply ; drop low + ply ; drop high + clc + rts + } } diff --git a/src/lib/rolling_buffer.s b/src/lib/rolling_buffer.s index e113c56..3ae3d84 100644 --- a/src/lib/rolling_buffer.s +++ b/src/lib/rolling_buffer.s @@ -28,542 +28,3 @@ Conventions: ; settle is counted. INVENTORY_SCROLL_PIXELS_PER_FRAME := 8 INVENTORY_SCROLL_TOTAL_PIXELS := 16 - - -.macro engine_update_scroll_hdma( - state_base, - hdma_shadow_addr, - buffer_slots, - visible, - header_hook, - footer_hook, - signal_hook, -) { - """ - Build the rolling-buffer HDMA scroll table in `hdma_shadow_addr`, based on - `state_base.buffer_pos` / `base_scroll`. Profile supplies header_hook - (above item rows), footer_hook (below item rows), signal_hook (NMI shadow - -> active copy pending). - """ - php - rep #0x30 - pha - phx - phy -; Save DP scratch ($40-$43) - lda.b 0x40 - pha - lda.b 0x42 - pha - ldx.w #0x0000 -; Profile-specific top-of-table band(s). - jsr.w header_hook -; Per-row entries: vram_slot = (buffer_pos + row) % buffer_slots - stz.b 0x42 -_row_loop: - lda.w state_base + RollingBufferState.buffer_pos - and.w #0x00FF - clc - adc.b 0x42 -_mod_loop: - cmp.w #buffer_slots - bcc _mod_done - sec - sbc.w #buffer_slots - bra _mod_loop -_mod_done: -; A = vram_slot (0..buffer_slots-1) - asl - asl - asl - asl -; A = vram_slot * 16 - sta.b 0x40 -; row * 16 = scanline_offset - lda.b 0x42 - and.w #0x00FF - asl - asl - asl - asl -; -scanline_offset - eor.w #0xFFFF - inc - clc - adc.b 0x40 - clc - adc.w state_base + RollingBufferState.base_scroll - sta.b 0x40 - sep #0x20 - lda #16 - sta.l hdma_shadow_addr, x - inx - rep #0x20 - lda.b 0x40 - sta.l hdma_shadow_addr, x - inx - inx - rep #0x20 - inc.b 0x42 - lda.b 0x42 - cmp.w #visible - bcs _row_loop_done - jmp.w _row_loop -_row_loop_done: -; Profile-specific bottom-of-table band. - jsr.w footer_hook -; End marker - sep #0x20 - lda #0x00 - sta.l hdma_shadow_addr, x -; Profile-specific NMI signal (sets copy_pending shadow). - jsr.w signal_hook - rep #0x20 - pla - sta.b 0x42 - pla - sta.b 0x40 - ply - plx - pla - plp - rts -} - - -.macro engine_init_rolling_buffer(state_base, render_count, draw_window_hook, ensure_hdma_hook, render_slot_hook) { - """ - Initialise the rolling buffer on menu entry: zero the 12-byte state block, - mark base_scroll = $FFFF (sentinel), draw the inventory window border, - run the profile's `ensure_hdma_hook`, then render `render_count` slots via - `render_slot_hook` (each call sees `slot_index` already populated). - Field menu uses VISIBLE_ITEMS ; treasure uses BUFFER_SLOTS. - """ - php - pha - jsr.w draw_window_hook - lda.b 0x46 - pha -; Zero 12-byte rolling state, then mark base_scroll = $FFFF sentinel - rep #0x20 - lda.w #0x0000 - sta.w state_base - sta.w state_base + 2 - sta.w state_base + 4 - sta.w state_base + 6 - sta.w state_base + 8 - sta.w state_base + 10 - lda.w #0xFFFF - sta.w state_base + RollingBufferState.base_scroll - sep #0x20 - jsr.w ensure_hdma_hook - lda #0x00 - sta.b 0x46 -_init_loop: - lda.b 0x46 - sta.w state_base + RollingBufferState.edge_row - sta.w state_base + RollingBufferState.slot_index - jsr.w render_slot_hook - inc.b 0x46 - lda.b 0x46 - cmp #render_count - bne _init_loop - pla - sta.b 0x46 - pla - plp - rtl -} - - -.macro engine_scroll_down_prepare( - state_base, - scroll_pos_addr, - scroll_limit, - visible, - buffer_slots, - ensure_hdma_hook, - render_slot_hook, - update_hdma_hook, -) { - """ - Pre-render the new bottom item before a scroll-down animation begins. The slot vacated at top (buffer_pos) - is reused for the new bottom ; buffer_pos then advances modulo buffer_slots. - """ - php - sep #0x20 - jsr.w ensure_hdma_hook - lda.w scroll_pos_addr - cmp #scroll_limit - bcs _down_done - clc - adc #visible - sta.w state_base + RollingBufferState.edge_row - lda.w state_base + RollingBufferState.buffer_pos - sta.w state_base + RollingBufferState.slot_index - jsr.w render_slot_hook - inc.w state_base + RollingBufferState.buffer_pos - lda.w state_base + RollingBufferState.buffer_pos - cmp #buffer_slots - bcc _down_buf_ok - stz.w state_base + RollingBufferState.buffer_pos -_down_buf_ok: - jsr.w update_hdma_hook -_down_done: - plp - rts -} - - -.macro engine_scroll_up_prepare( - state_base, - scroll_pos_addr, - buffer_slots, - ensure_hdma_hook, - render_slot_hook, - update_hdma_hook, -) { - """ - Pre-render the new top item after the scroll-up state machine has decremented scroll_pos. buffer_pos walks - backwards (with wrap) so the slot leaving the bottom edge is reused for the new top. - """ - php - sep #0x20 - jsr.w ensure_hdma_hook - lda.w state_base + RollingBufferState.buffer_pos - beq _up_wrap - dec - bra _up_wrap_done -_up_wrap: - lda #buffer_slots - 1 -_up_wrap_done: - sta.w state_base + RollingBufferState.buffer_pos - sta.w state_base + RollingBufferState.slot_index - lda.w scroll_pos_addr - sta.w state_base + RollingBufferState.edge_row - jsr.w render_slot_hook - jsr.w update_hdma_hook - plp - rts -} - - -.macro engine_start_scroll_down( - state_base, - scroll_pos_addr, - visible, - buffer_slots, - total_pixels, - pixels_per_frame, - ensure_hdma_hook, - render_slot_hook, - update_hdma_hook, -) { - """ - Kick off a non-blocking scroll-down animation: advance buffer_pos with wrap, pre-render the new bottom - item, configure the scroll state machine (-16 anim offset, +pixels_per_frame direction). - """ - php - sep #0x20 - jsr.w ensure_hdma_hook - inc.w state_base + RollingBufferState.buffer_pos - lda.w state_base + RollingBufferState.buffer_pos - cmp #buffer_slots - bcc _start_dn_buf_ok - stz.w state_base + RollingBufferState.buffer_pos - lda #0x00 -_start_dn_buf_ok: - clc - adc #visible - 1 -_start_dn_mod: - cmp #buffer_slots - bcc _start_dn_mod_done - sec - sbc #buffer_slots - bra _start_dn_mod -_start_dn_mod_done: - sta.w state_base + RollingBufferState.slot_index - lda.w scroll_pos_addr - clc - adc #visible - 1 - sta.w state_base + RollingBufferState.edge_row - jsr.w render_slot_hook - lda #0x01 - sta.w state_base + RollingBufferState.scroll_state - lda #total_pixels - sta.w state_base + RollingBufferState.scroll_remaining - lda #pixels_per_frame - sta.w state_base + RollingBufferState.scroll_direction - rep #0x20 - lda.w #0xFFF0 - sta.w state_base + RollingBufferState.scroll_anim_offset - sep #0x20 - lda #0x01 - sta.w state_base + RollingBufferState.transfer_pending - jsr.w update_hdma_hook - plp - rtl -} - - -.macro engine_start_scroll_up( - state_base, - scroll_pos_addr, - buffer_slots, - total_pixels, - ensure_hdma_hook, - render_slot_hook, - update_hdma_hook, -) { - """ - Kick off a non-blocking scroll-up animation: walk buffer_pos backwards (wrap to buffer_slots-1), - pre-render the new top item, configure the state machine (+16 anim offset, -2 direction). - """ - php - sep #0x20 - jsr.w ensure_hdma_hook - lda.w state_base + RollingBufferState.buffer_pos - beq _start_up_wrap - dec - bra _start_up_wrap_done -_start_up_wrap: - lda #buffer_slots - 1 -_start_up_wrap_done: - sta.w state_base + RollingBufferState.buffer_pos - sta.w state_base + RollingBufferState.slot_index - lda.w scroll_pos_addr - sta.w state_base + RollingBufferState.edge_row - jsr.w render_slot_hook - lda #0x01 - sta.w state_base + RollingBufferState.scroll_state - lda #total_pixels - sta.w state_base + RollingBufferState.scroll_remaining - lda #0xFE - sta.w state_base + RollingBufferState.scroll_direction - rep #0x20 - lda.w #0x0010 - sta.w state_base + RollingBufferState.scroll_anim_offset - sep #0x20 - lda #0x01 - sta.w state_base + RollingBufferState.transfer_pending - jsr.w update_hdma_hook - plp - rtl -} - - -.macro engine_update_scroll_frame(state_base, pixels_per_frame, update_hdma_hook) { - """ - One animation frame: advance scroll_anim_offset by pixels_per_frame towards 0 (sign-aware), nudge cursor - sprite at $0311 if 'second item' mode is active, decrement scroll_remaining, refresh HDMA, then push - sprites + BG2 to VRAM. - """ - php - rep #0x20 - lda.w state_base + RollingBufferState.scroll_anim_offset - sep #0x20 - lda.w state_base + RollingBufferState.scroll_direction - bpl _frame_positive - rep #0x20 - lda.w state_base + RollingBufferState.scroll_anim_offset - sec - sbc.w #pixels_per_frame - sta.w state_base + RollingBufferState.scroll_anim_offset - bra _frame_update_cursor -_frame_positive: - rep #0x20 - lda.w state_base + RollingBufferState.scroll_anim_offset - clc - adc.w #pixels_per_frame - sta.w state_base + RollingBufferState.scroll_anim_offset -_frame_update_cursor: - sep #0x20 - lda.w 0x1B19 - beq _frame_no_cursor - lda.w state_base + RollingBufferState.scroll_direction - bpl _frame_cursor_down - inc.w 0x0311 - inc.w 0x0311 - bra _frame_no_cursor -_frame_cursor_down: - dec.w 0x0311 - dec.w 0x0311 -_frame_no_cursor: - lda.w state_base + RollingBufferState.scroll_remaining - sec - sbc #pixels_per_frame - sta.w state_base + RollingBufferState.scroll_remaining - jsr.w update_hdma_hook - jsr.l tfr_sprites_vblank_trampoline - jsr.l tfr_bg2_tiles_vblank_trampoline - plp - rtl -} - - -.macro engine_finish_scroll( - state_base, - scroll_pos_addr, - visible, - buffer_slots, - total_items, - render_slot_hook, - update_hdma_hook, -) { - """ - End-of-animation: pre-render the next-scroll-direction edge slot (skip past list ends), reset scroll_state - and anim_offset, refresh HDMA, run original cursor + post-scroll cleanup. - """ - php - sep #0x20 - lda.w state_base + RollingBufferState.scroll_direction - bmi _finish_was_up - lda.w state_base + RollingBufferState.buffer_pos - beq _finish_dn_wrap - dec - bra _finish_dn_slot_ok -_finish_dn_wrap: - lda #buffer_slots - 1 -_finish_dn_slot_ok: - sta.w state_base + RollingBufferState.slot_index - lda.w scroll_pos_addr - clc - adc #visible - cmp #total_items - bcs _finish_skip - sta.w state_base + RollingBufferState.edge_row - jsr.w render_slot_hook - lda #0x01 - sta.w state_base + RollingBufferState.transfer_pending - bra _finish_skip -_finish_was_up: - lda.w state_base + RollingBufferState.buffer_pos - clc - adc #visible -_finish_up_mod: - cmp #buffer_slots - bcc _finish_up_slot_ok - sec - sbc #buffer_slots - bra _finish_up_mod -_finish_up_slot_ok: - sta.w state_base + RollingBufferState.slot_index - lda.w scroll_pos_addr - beq _finish_skip - dec - sta.w state_base + RollingBufferState.edge_row - jsr.w render_slot_hook - lda #0x01 - sta.w state_base + RollingBufferState.transfer_pending -_finish_skip: - stz.w state_base + RollingBufferState.scroll_state - rep #0x20 - stz.w state_base + RollingBufferState.scroll_anim_offset - sep #0x20 - jsr.w update_hdma_hook - jsr.l draw_item_cursors_trampoline - jsr.l update_ctrl_after_scroll_trampoline - plp - rtl -} - - -.macro engine_swap_redraw( - state_base, - scroll_pos_addr, - buffer_slots, - total_items, - ensure_hdma_hook, - render_slot_hook, - clear_slot_hook, - update_hdma_hook, -) { - """ - Re-render all `buffer_slots` slots from the current buffer_pos after an item swap. Out-of-range items - (scroll_pos+row >= total_items) are blanked via `clear_slot_hook` so the prefetch row doesn't show stale - tiles. Resets scroll state so a mid-animation swap doesn't trigger spurious finish_scroll re-renders. - """ - php - sep #0x20 - jsr.w ensure_hdma_hook - stz.w state_base + RollingBufferState.scroll_state - stz.w state_base + RollingBufferState.scroll_remaining - stz.w state_base + RollingBufferState.scroll_anim_offset - stz.w state_base + RollingBufferState.scroll_anim_offset + 1 - lda.b 0x46 - pha - lda #0x00 - sta.b 0x46 -_swap_loop: - lda.w state_base + RollingBufferState.buffer_pos - clc - adc.b 0x46 -_swap_mod: - cmp #buffer_slots - bcc _swap_mod_done - sec - sbc #buffer_slots - bra _swap_mod -_swap_mod_done: - sta.w state_base + RollingBufferState.slot_index - lda.w scroll_pos_addr - clc - adc.b 0x46 - cmp #total_items - bcs _swap_clear - sta.w state_base + RollingBufferState.edge_row - jsr.w render_slot_hook - bra _swap_next -_swap_clear: - jsr.w clear_slot_hook -_swap_next: - inc.b 0x46 - lda.b 0x46 - cmp #buffer_slots - bne _swap_loop - pla - sta.b 0x46 - lda #0x01 - sta.w state_base + RollingBufferState.transfer_pending - jsr.w update_hdma_hook - plp - rtl -} - - -.macro engine_refresh_slots(state_base, scroll_pos_addr, buffer_slots, render_slot_hook) { - """ - Re-render all `buffer_slots` from current buffer_pos without resetting scroll state. Used by treasure's - original redraw helper (post-swap rebuild path) where the engine itself didn't trigger the redraw. - """ - php - rep #0x10 - sep #0x20 - lda #0x00 - sta.b 0x46 -_refresh_loop: - lda.w scroll_pos_addr - clc - adc.b 0x46 - sta.w state_base + RollingBufferState.edge_row - lda.w state_base + RollingBufferState.buffer_pos - clc - adc.b 0x46 -_refresh_mod: - cmp #buffer_slots - bcc _refresh_mod_done - sec - sbc #buffer_slots - bra _refresh_mod -_refresh_mod_done: - sta.w state_base + RollingBufferState.slot_index - jsr.w render_slot_hook - inc.b 0x46 - lda.b 0x46 - cmp #buffer_slots - bne _refresh_loop - lda #0x01 - sta.w state_base + RollingBufferState.transfer_pending - plp - rtl -} diff --git a/src/lib/rolling_inventory_engine.s b/src/lib/rolling_inventory_engine.s new file mode 100644 index 0000000..f40f2af --- /dev/null +++ b/src/lib/rolling_inventory_engine.s @@ -0,0 +1,844 @@ +""" +Unified rolling-inventory engine - bank-20 entry points. + +Phase 1 skeleton : declares the JSL-callable entry points specified in +plans/rolling_inventory_engine.md. Each entry currently delegates to +the existing macro-driven `engine_*` machinery in +`src/lib/rolling_buffer.s`, so callers can opt-in incrementally. + +ABI : `X` (16-bit, bank $7E implied) holds the pointer to a +`RollingBufferState` instance. Hooks live in bank-01 as far-callable +trampolines and are resolved through the struct's per-instance far +pointers (`vwf_cfg_ptr`, hook ptrs, ...). + +Phases 2..5 will port field-items / shop / key-items / drops + treasure +through these entries one by one ; phase 6 deletes the duplicated macro +sites that remain in the per-menu source files. +""" + + +.include "../bank20.i" + +.alloc rolling_inventory_engine_block in bank20_reloc { + .scope rolling_engine { + """Bank-20 rolling-inventory engine entry points (phase 1 stubs).""" + rolling_engine_init: + """ + Cold init for a rolling-inventory instance. + + Zeroes the 12-byte engine scratch portion of `RollingBufferState` + (everything from top_row through transfer_pending plus the + scroll_anim_offset word + hdma_copy_pending byte) and stamps + `base_scroll = $FFFF` as the "not-yet-HDMA-initialised" sentinel + the per-menu ensure_hdma_hook checks. dirty_mask is cleared too so + the first vblank flush has nothing to fire. + + Caller-managed config fields (visible_rows, slot_height_tiles, + item_list_ptr, item_count, hdma_channel, vwf_cfg_ptr) are NOT + touched ; callers fill them in before invoking this entry and the + phase-2 render path will read them back. + + In/out : + X = state ptr (16-bit, bank $7E implied) + Y = initial top_row (unused in phase 1 ; phase 2 will route this + into the scroll-position pre-render) + All registers clobbered. + """ + + + { + php + sep #0x20 + rep #0x10 + lda.b #0x00 + ; Zero engine scratch (top_row..transfer_pending + anim offset bytes + + ; hdma_copy_pending + dirty_mask). + sta.l 0x7E0000 + RollingBufferState.top_row, x + sta.l 0x7E0000 + RollingBufferState.buffer_pos, x + sta.l 0x7E0000 + RollingBufferState.edge_row, x + sta.l 0x7E0000 + RollingBufferState.slot_index, x + sta.l 0x7E0000 + RollingBufferState.hdma_enable, x + ; _pad byte (offset 7, struct-defined as `byte _pad`) ; a816 hides + ; leading-underscore field names from outer scopes, so write through + ; the literal offset rather than the symbolic name. + sta.l 0x7E0007, x + sta.l 0x7E0000 + RollingBufferState.scroll_state, x + sta.l 0x7E0000 + RollingBufferState.scroll_remaining, x + sta.l 0x7E0000 + RollingBufferState.scroll_direction, x + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset + 1, x + sta.l 0x7E0000 + RollingBufferState.hdma_copy_pending, x + sta.l 0x7E0000 + RollingBufferState.dirty_mask, x + ; base_scroll = $FFFF sentinel ("HDMA not yet armed"). + lda.b #0xFF + sta.l 0x7E0000 + RollingBufferState.base_scroll, x + sta.l 0x7E0000 + RollingBufferState.base_scroll + 1, x + ; Fire fn_draw_window hook when armed. + rep #0x10 + ldy.w #RollingBufferState.fn_draw_window + jsr.w _engine_call_hook + ; Fire fn_update_hdma hook (= per-menu ensure_hdma_initialized in the + ; field-items wiring). Phase 3+ may split this into ensure-once vs + ; per-scroll-update hooks ; for now both share the slot. + ldy.w #RollingBufferState.fn_update_hdma + jsr.w _engine_call_hook + ; Loop `visible_rows` times, calling fn_render_slot for each row. + ; Each iteration plants edge_row + slot_index = loop counter before + ; firing the hook so the per-menu render reads the right values. + sep #0x20 + lda.b #0x00 + + _engine_init_render_loop: + pha + sta.l 0x7E0000 + RollingBufferState.edge_row, x + sta.l 0x7E0000 + RollingBufferState.slot_index, x + ldy.w #RollingBufferState.fn_render_slot + jsr.w _engine_call_hook + pla + inc + cmp.l 0x7E0000 + RollingBufferState.visible_rows, x + bcc _engine_init_render_loop + + plp + rtl + } + + rolling_engine_refresh_slots: + """ + Re-render every slot in the rolling buffer from the current `buffer_pos` + without resetting scroll state. Mirrors the legacy `engine_refresh_slots` + macro : iterates `(visible_rows + 1)` times (visible window + prefetch + slot), each iteration setting edge_row = scroll_pos + counter and + slot_index = (buffer_pos + counter) % buffer_slots, then firing + fn_render_slot. Stamps `transfer_pending = 1` on exit so the per-menu + NMI hook DMAs the BG staging buffer next vblank. + + In : X = state ptr (16-bit, bank $7E implied) + A = current scroll_pos value (8-bit) + Out: all `visible_rows + 1` slots re-rendered, transfer_pending set. + """ + + + { + php + sep #0x20 + rep #0x10 + sta.l 0x001F88 ; stash scroll_pos + lda.l 0x7E0000 + RollingBufferState.visible_rows, x + inc ; buffer_slots = visible_rows + 1 + sta.l 0x001F89 ; stash buffer_slots + lda.b #0x00 + + _engine_refresh_loop: + pha + clc + adc.l 0x001F88 + sta.l 0x7E0000 + RollingBufferState.edge_row, x + pla + pha + clc + adc.l 0x7E0000 + RollingBufferState.buffer_pos, x + + _engine_refresh_mod: + cmp.l 0x001F89 + bcc _engine_refresh_mod_done + sec + sbc.l 0x001F89 + bra _engine_refresh_mod + + _engine_refresh_mod_done: + sta.l 0x7E0000 + RollingBufferState.slot_index, x + ldy.w #RollingBufferState.fn_render_slot + jsr.w _engine_call_hook + pla + inc + cmp.l 0x001F89 + bcc _engine_refresh_loop + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + plp + rtl + } + + rolling_engine_update_scroll_frame: + """ + Advance the scroll animation one frame. Mirrors the legacy + `engine_update_scroll_frame` macro : + - scroll_anim_offset += (scroll_direction < 0 ? -8 : +8) + - if vanilla cursor-row marker $1B19 != 0, bump cursor sprite Y at + $0311 by 2 pixels in the matching direction (down for negative + direction, up for positive) + - scroll_remaining -= 8 (animation timer countdown) + - per-menu update_scroll_hdma trampoline rebuilds the HDMA shadow + table (dispatched via state.menu_id) + - tfr_sprites_vblank_trampoline + tfr_bg2_tiles_vblank_trampoline + push sprite OAM + BG2 tiles into VRAM so the animation frame + becomes visible. + + Pixels-per-frame hardcoded at 8 ; every existing caller + (INVENTORY_SCROLL_PIXELS_PER_FRAME / TREASURE / DROPS / KEY_ITEM) uses + that value, so the constant moves into the engine. + + In : X = state ptr (16-bit, bank $7E implied) + Out: scroll_anim_offset / scroll_remaining advanced, HDMA shadow + + sprite / BG2 VRAM uploads driven by the per-menu trampolines. + """ + + + { + php + rep #0x10 + sep #0x20 + lda.l 0x7E0000 + RollingBufferState.scroll_direction, x + bpl _frame_positive + rep #0x20 + lda.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + sec + sbc.w #8 + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + bra _frame_update_cursor + + _frame_positive: + rep #0x20 + lda.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + clc + adc.w #8 + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + + _frame_update_cursor: + sep #0x20 + lda.l 0x7E1B19 ; vanilla cursor-row marker + beq _frame_no_cursor + lda.l 0x7E0000 + RollingBufferState.scroll_direction, x + bpl _frame_cursor_down + inc.w 0x0311 + inc.w 0x0311 + bra _frame_no_cursor + + _frame_cursor_down: + dec.w 0x0311 + dec.w 0x0311 + + _frame_no_cursor: + lda.l 0x7E0000 + RollingBufferState.scroll_remaining, x + sec + sbc.b #8 + sta.l 0x7E0000 + RollingBufferState.scroll_remaining, x + ; Dispatch per-menu update_scroll_hdma via menu_id branch. The four + ; profiles' update_*_scroll_hdma functions live in bank-20 alongside + ; this engine, so jsr.w (3-byte) reaches them cleanly. + lda.l 0x7E0000 + RollingBufferState.menu_id, x + beq _frame_hdma_field + cmp.b #ROLLING_MENU_ID_TREASURE + beq _frame_hdma_treasure + cmp.b #ROLLING_MENU_ID_DROPS + beq _frame_hdma_drops + jsr.w update_key_item_scroll_hdma + bra _frame_hdma_done + + _frame_hdma_field: + jsr.w update_menu_scroll_hdma + bra _frame_hdma_done + + _frame_hdma_treasure: + jsr.w update_treasure_scroll_hdma + bra _frame_hdma_done + + _frame_hdma_drops: + jsr.w update_drops_scroll_hdma + + _frame_hdma_done: + jsr.l tfr_sprites_vblank_trampoline + jsr.l tfr_bg2_tiles_vblank_trampoline + plp + rtl + } + + rolling_engine_start_scroll_down: + """ + Kick off a non-blocking scroll-down animation. Advances buffer_pos + with wrap, pre-renders the new bottom slot via fn_render_slot, configures + the scroll FSM (state = 1, remaining = 16 pixels, direction = +8 = + positive = down, anim_offset = -16), stamps transfer_pending, and + dispatches the per-menu update_scroll_hdma to seed the first frame's + HDMA shadow. + + In : X = state ptr, A = current scroll_pos (8-bit) + Out: scroll state machine armed for the next 2 frames of animation. + """ + + + { + php + rep #0x10 + sep #0x20 + sta.l 0x001F88 ; stash scroll_pos + ldy.w #RollingBufferState.fn_update_hdma + jsr.w _engine_call_hook + ; buffer_slots = visible_rows + 1 + lda.l 0x7E0000 + RollingBufferState.visible_rows, x + inc + sta.l 0x001F89 + ; inc buffer_pos with wrap + lda.l 0x7E0000 + RollingBufferState.buffer_pos, x + inc + cmp.l 0x001F89 + bcc _start_dn_buf_ok + lda.b #0x00 + + _start_dn_buf_ok: + sta.l 0x7E0000 + RollingBufferState.buffer_pos, x + ; slot_index = (buffer_pos + visible_rows - 1) % buffer_slots + clc + adc.l 0x7E0000 + RollingBufferState.visible_rows, x + dec + + _start_dn_mod: + cmp.l 0x001F89 + bcc _start_dn_mod_done + sec + sbc.l 0x001F89 + bra _start_dn_mod + + _start_dn_mod_done: + sta.l 0x7E0000 + RollingBufferState.slot_index, x + ; edge_row = scroll_pos + visible_rows - 1 + lda.l 0x001F88 + clc + adc.l 0x7E0000 + RollingBufferState.visible_rows, x + dec + sta.l 0x7E0000 + RollingBufferState.edge_row, x + ldy.w #RollingBufferState.fn_render_slot + jsr.w _engine_call_hook + ; FSM : scroll_state = 1, remaining = 16, direction = +8 (positive = down) + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.scroll_state, x + lda.b #16 + sta.l 0x7E0000 + RollingBufferState.scroll_remaining, x + lda.b #8 + sta.l 0x7E0000 + RollingBufferState.scroll_direction, x + rep #0x20 + lda.w #0xFFF0 + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + sep #0x20 + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + jsr.w _engine_dispatch_update_scroll_hdma + plp + rtl + } + + rolling_engine_start_scroll_up: + """ + Kick off a non-blocking scroll-up animation. Walks buffer_pos backwards + (wrap to visible_rows = buffer_slots - 1), pre-renders the new top + slot, configures the FSM (state = 1, remaining = 16, direction = -2 = + $FE = negative = up, anim_offset = +16), stamps transfer_pending, + dispatches update_scroll_hdma. + + In : X = state ptr, A = current scroll_pos (8-bit, already decremented + by the caller) + Out: scroll state machine armed for next 2 frames of animation. + """ + + + { + php + rep #0x10 + sep #0x20 + sta.l 0x001F88 ; stash scroll_pos + ldy.w #RollingBufferState.fn_update_hdma + jsr.w _engine_call_hook + lda.l 0x7E0000 + RollingBufferState.buffer_pos, x + beq _start_up_wrap + dec + bra _start_up_wrap_done + + _start_up_wrap: + ; buffer_slots - 1 == visible_rows. + lda.l 0x7E0000 + RollingBufferState.visible_rows, x + + _start_up_wrap_done: + sta.l 0x7E0000 + RollingBufferState.buffer_pos, x + sta.l 0x7E0000 + RollingBufferState.slot_index, x + lda.l 0x001F88 ; scroll_pos + sta.l 0x7E0000 + RollingBufferState.edge_row, x + ldy.w #RollingBufferState.fn_render_slot + jsr.w _engine_call_hook + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.scroll_state, x + lda.b #16 + sta.l 0x7E0000 + RollingBufferState.scroll_remaining, x + lda.b #0xFE ; -2 (negative = up direction) + sta.l 0x7E0000 + RollingBufferState.scroll_direction, x + rep #0x20 + lda.w #0x0010 + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + sep #0x20 + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + jsr.w _engine_dispatch_update_scroll_hdma + plp + rtl + } + + rolling_engine_finish_scroll: + """ + End-of-animation : pre-render the next-direction edge slot (the one + that becomes the new prefetch after the current animation lands), + reset scroll_state + anim_offset, refresh the HDMA shadow, fire the + vanilla cursor + post-scroll cleanup trampolines. + + For scroll-down (direction = +N): slot_index = buffer_pos - 1 (with + wrap to buffer_slots - 1) ; edge_row = scroll_pos + visible_rows ; + skip render when scroll_pos + visible_rows >= item_count (past tail). + + For scroll-up (direction < 0, $FE) : slot_index = (buffer_pos + visible) + mod buffer_slots ; edge_row = scroll_pos - 1 ; skip render when + scroll_pos == 0 (past head). + + In : X = state ptr, A = current scroll_pos (8-bit) + Out: scroll FSM cleared, prefetch slot rendered, HDMA refreshed. + """ + + + { + php + rep #0x10 + sep #0x20 + sta.l 0x001F88 ; stash scroll_pos + ; buffer_slots = visible_rows + 1 + lda.l 0x7E0000 + RollingBufferState.visible_rows, x + inc + sta.l 0x001F89 + lda.l 0x7E0000 + RollingBufferState.scroll_direction, x + bmi _finish_was_up + ; --- scroll-down post-anim --- + lda.l 0x7E0000 + RollingBufferState.buffer_pos, x + beq _finish_dn_wrap + dec + bra _finish_dn_slot_ok + + _finish_dn_wrap: + ; buffer_slots - 1 == visible_rows + lda.l 0x7E0000 + RollingBufferState.visible_rows, x + + _finish_dn_slot_ok: + sta.l 0x7E0000 + RollingBufferState.slot_index, x + lda.l 0x001F88 ; scroll_pos + clc + adc.l 0x7E0000 + RollingBufferState.visible_rows, x + cmp.l 0x7E0000 + RollingBufferState.item_count, x + bcs _finish_skip + sta.l 0x7E0000 + RollingBufferState.edge_row, x + ldy.w #RollingBufferState.fn_render_slot + jsr.w _engine_call_hook + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + bra _finish_skip + + _finish_was_up: + ; --- scroll-up post-anim --- + lda.l 0x7E0000 + RollingBufferState.buffer_pos, x + clc + adc.l 0x7E0000 + RollingBufferState.visible_rows, x + + _finish_up_mod: + cmp.l 0x001F89 + bcc _finish_up_slot_ok + sec + sbc.l 0x001F89 + bra _finish_up_mod + + _finish_up_slot_ok: + sta.l 0x7E0000 + RollingBufferState.slot_index, x + lda.l 0x001F88 ; scroll_pos + beq _finish_skip + dec + sta.l 0x7E0000 + RollingBufferState.edge_row, x + ldy.w #RollingBufferState.fn_render_slot + jsr.w _engine_call_hook + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + + _finish_skip: + lda.b #0x00 + sta.l 0x7E0000 + RollingBufferState.scroll_state, x + rep #0x20 + lda.w #0x0000 + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + sep #0x20 + jsr.w _engine_dispatch_update_scroll_hdma + jsr.l draw_item_cursors_trampoline + jsr.l update_ctrl_after_scroll_trampoline + plp + rtl + } + + rolling_engine_swap_redraw: + """ + Re-render all `visible_rows + 1` slots from current buffer_pos after + an item swap. Slots whose item index >= item_count are cleared via + the built-in field-items clear path (other profiles' clear stubs were + RTS no-ops, so we skip the call entirely for them). Resets scroll + state so a mid-animation swap doesn't trigger spurious finish_scroll + re-renders. + + In : X = state ptr, A = current scroll_pos (8-bit) + Out: all `visible_rows + 1` slots refreshed, transfer_pending = 1, + HDMA shadow rebuilt via per-menu update_scroll_hdma. + """ + + + { + php + rep #0x10 + sep #0x20 + sta.l 0x001F88 ; stash scroll_pos + ldy.w #RollingBufferState.fn_update_hdma + jsr.w _engine_call_hook + lda.b #0x00 + sta.l 0x7E0000 + RollingBufferState.scroll_state, x + sta.l 0x7E0000 + RollingBufferState.scroll_remaining, x + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset, x + sta.l 0x7E0000 + RollingBufferState.scroll_anim_offset + 1, x + ; buffer_slots = visible_rows + 1 + lda.l 0x7E0000 + RollingBufferState.visible_rows, x + inc + sta.l 0x001F89 ; stash buffer_slots + lda.b #0x00 + + _swap_loop: + pha + clc + adc.l 0x7E0000 + RollingBufferState.buffer_pos, x + + _swap_mod: + cmp.l 0x001F89 + bcc _swap_mod_done + sec + sbc.l 0x001F89 + bra _swap_mod + + _swap_mod_done: + sta.l 0x7E0000 + RollingBufferState.slot_index, x + pla + pha + clc + adc.l 0x001F88 + cmp.l 0x7E0000 + RollingBufferState.item_count, x + bcs _swap_clear + sta.l 0x7E0000 + RollingBufferState.edge_row, x + ldy.w #RollingBufferState.fn_render_slot + jsr.w _engine_call_hook + bra _swap_next + + _swap_clear: + ; Only field-items has a real clear ; other menus' clear stubs were + ; RTS no-ops, so skip the call entirely unless menu_id == 0. + lda.l 0x7E0000 + RollingBufferState.menu_id, x + bne _swap_next + jsr.w clear_inventory_slot + + _swap_next: + pla + inc + cmp.l 0x001F89 + bcc _swap_loop + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + jsr.w _engine_dispatch_update_scroll_hdma + plp + rtl + } + + _engine_dispatch_update_scroll_hdma: + """ + Branches to the per-menu update_*_scroll_hdma function based on + state.menu_id. All four functions live in bank-20 alongside the engine + so a 16-bit jsr.w is enough. + """ + + + { + lda.l 0x7E0000 + RollingBufferState.menu_id, x + beq _dispatch_field + cmp.b #ROLLING_MENU_ID_TREASURE + beq _dispatch_treasure + cmp.b #ROLLING_MENU_ID_DROPS + beq _dispatch_drops + jsr.w update_key_item_scroll_hdma + rts + + _dispatch_field: + jsr.w update_menu_scroll_hdma + rts + + _dispatch_treasure: + jsr.w update_treasure_scroll_hdma + rts + + _dispatch_drops: + jsr.w update_drops_scroll_hdma + rts + } + + _engine_call_hook: + """ + Internal helper. JSLs through a hook far-ptr stored at + `state[X + Y]`. + + In : X = state ptr (16-bit, bank $7E implied) + Y = struct offset of the 3-byte hook field + M = 8 (caller-managed) + Out : hook RTLs back to caller of _engine_call_hook. A clobbered. + Hook is skipped if all three bytes are zero ; null-safe. + """ + + + { + phx + phy + ; Stash hook-field offset at $00:1F83 (next to the $1F80-82 jmp-vec + ; cell). Direct-page `sty.b 0x90` looked tempting but FF4's menu code + ; sets DP=$0100 before calling our patch, so the store actually + ; landed at $0190 = the BG1HOFS shadow ; vanilla NMI then scrolled + ; BG1 by the hook offset (26 px for fn_render_slot) on every render. + rep #0x20 + tya + sta.l 0x001F83 + txa + clc + adc.l 0x001F83 + tax ; X now points at the hook field's first byte + sep #0x20 + lda.l 0x7E0000, x + sta.l 0x001F80 + lda.l 0x7E0001, x + sta.l 0x001F81 + lda.l 0x7E0002, x + sta.l 0x001F82 + ora.l 0x001F80 + ora.l 0x001F81 + beq _engine_call_hook_null + phk + rep #0x20 + pea.w ( _engine_call_hook_return - 1 ) & 0xFFFF + sep #0x20 + jmp [0x001F80] + + _engine_call_hook_return: + _engine_call_hook_null: + ply + plx + rts + } + + rolling_engine_tick: + """ + Per-frame state-machine tick. + + Advances the scroll animation by one frame when scroll_state != 0 : + decrements scroll_remaining and, when it hits zero, clears + scroll_state and stamps transfer_pending = 1 so the next vblank + flush picks up the final frame. Returns immediately when + scroll_state is idle. + + Phase 1.3 scope. Phase 2 will additionally drive scroll_anim_offset, + read the pad-down bits in A.b to start fresh scrolls, and route the + new edge slot through fn_render_slot once that hook lands in the + struct. + + In : X = state ptr + Out : scroll_remaining decremented (if scrolling), scroll_state + cleared + transfer_pending set when animation completes. + """ + + + { + php + sep #0x20 + lda.l 0x7E0000 + RollingBufferState.scroll_state, x + beq _tick_idle + lda.l 0x7E0000 + RollingBufferState.scroll_remaining, x + sec + sbc.b #0x01 + sta.l 0x7E0000 + RollingBufferState.scroll_remaining, x + bne _tick_still_scrolling + lda.b #0x00 + sta.l 0x7E0000 + RollingBufferState.scroll_state, x + lda.b #0x01 + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + + _tick_still_scrolling: + _tick_idle: + plp + rtl + } + + rolling_engine_vblank_flush: + """ + NMI-time flush. + + Phase 1.4 scope : reads `state.dirty_mask`, returns immediately on + $00, otherwise clears the mask so the next vblank starts clean. + Phase 2 will wedge the real DMA-from-VWF-stage-to-VRAM dance in + between, driven by the `vwf_cfg_ptr` config and a small bank-01 + trampoline ; the dirty-mask gate + clear stays valid regardless of + how the DMA path lands. + + Must run inside force-blank or the HDMA gap window so the DMA + doesn't tear visible scanlines (phase 2 concern). + + In : X = state ptr + Out : state.dirty_mask = 0 if it was non-zero, A clobbered. + """ + + + { + php + sep #0x20 + lda.l 0x7E0000 + RollingBufferState.dirty_mask, x + beq _flush_clean + lda.b #0x00 + sta.l 0x7E0000 + RollingBufferState.dirty_mask, x + + _flush_clean: + plp + rtl + } + + rolling_engine_cursor_up: + """ + Decrement slot_index, wrapping to (visible_rows - 1) at zero. + + In : X = state ptr (bank $7E implied) + Out : state.slot_index updated, A clobbered + """ + + + { + php + sep #0x20 + lda.l 0x7E0000 + RollingBufferState.slot_index, x + bne _cursor_up_dec + lda.l 0x7E0000 + RollingBufferState.visible_rows, x + + _cursor_up_dec: + dec + sta.l 0x7E0000 + RollingBufferState.slot_index, x + plp + rtl + } + + rolling_engine_cursor_down: + """ + Increment slot_index, wrapping to 0 when it would equal visible_rows. + + In : X = state ptr + Out : state.slot_index updated, A clobbered + """ + + + { + php + sep #0x20 + lda.l 0x7E0000 + RollingBufferState.slot_index, x + inc + cmp.l 0x7E0000 + RollingBufferState.visible_rows, x + bcc _cursor_dn_store + lda.b #0x00 + + _cursor_dn_store: + sta.l 0x7E0000 + RollingBufferState.slot_index, x + plp + rtl + } + + rolling_engine_invalidate_slot: + """ + Mark one slot dirty for the next vblank flush. + + In : X = state ptr, A.b = slot index 0..visible_rows-1 + Out : state.dirty_mask gets bit (1 << A) set + """ + + + { + php + sep #0x20 + pha + ; X stays state ptr ; use Y for the shift-left tally so we can keep + ; X bound to the struct base for the dirty_mask write below. + phx + tay + lda.b #0x01 + + _inv_slot_shift: + cpy.w #0x0000 + beq _inv_slot_done + asl + dey + bra _inv_slot_shift + + _inv_slot_done: + plx + ora.l 0x7E0000 + RollingBufferState.dirty_mask, x + sta.l 0x7E0000 + RollingBufferState.dirty_mask, x + pla + plp + rtl + } + + rolling_engine_invalidate_all: + """ + Mark every visible slot dirty by setting dirty_mask = $FF. + + In : X = state ptr + Out : state.dirty_mask = $FF, A clobbered + """ + + + { + php + sep #0x20 + lda.b #0xFF + sta.l 0x7E0000 + RollingBufferState.dirty_mask, x + plp + rtl + } + + rolling_engine_shutdown: + """ + Tear down state on menu close. + + Zeroes `state.hdma_enable` + `state.dirty_mask` and resets + `state.base_scroll` to the $FFFF sentinel so the next cold-init + ensure_hdma_hook re-arms cleanly. Does not touch HDMAEN ($420C) : + the per-menu exit hook owns that bit (it knows which channel mask + to clear via `hdma_channel`). + + In : X = state ptr + Out : engine state half-reset, A clobbered. + """ + + + { + php + sep #0x20 + rep #0x10 + lda.b #0x00 + sta.l 0x7E0000 + RollingBufferState.hdma_enable, x + sta.l 0x7E0000 + RollingBufferState.dirty_mask, x + sta.l 0x7E0000 + RollingBufferState.scroll_state, x + sta.l 0x7E0000 + RollingBufferState.transfer_pending, x + sta.l 0x7E0000 + RollingBufferState.hdma_copy_pending, x + lda.b #0xFF + sta.l 0x7E0000 + RollingBufferState.base_scroll, x + sta.l 0x7E0000 + RollingBufferState.base_scroll + 1, x + plp + rtl + } + } +} diff --git a/src/libmz.s b/src/libmz.s index edcfd10..cfced55 100644 --- a/src/libmz.s +++ b/src/libmz.s @@ -1,237 +1,241 @@ """Shared low-level helpers: vblank spinner + DMA transfer routines for VRAM / palette + small bank-trampolines.""" .include "libmz.i" +.include "bank20.i" -wait_for_vblank: -"""Spin until the next vblank edge: wait for $4212 to go low, then high.""" -{ - pha -_negative: - lda.l 0x004212 - bmi _negative +.alloc libmz_block in bank20_reloc { + wait_for_vblank: + """Spin until the next vblank edge: wait for $4212 to go low, then high.""" + { + pha -_positive: - lda.l 0x004212 - bpl _positive - pla - rts -} + _negative: + lda.l 0x004212 + bmi _negative -wait_for_vblank_long: -"""RTL trampoline around `wait_for_vblank` for cross-bank callers.""" - jsr.w wait_for_vblank - rtl - -dma_transfer_to_vram: -""" -DMA-transfer a block from ROM/RAM into VRAM via channel 7. -Stack args (caller pushes in order): source_offset, source_bank, -vram_pointer, count, dma_mode. All five args are pulled before RTS. -""" -{ -; on the stack: -; return address -; source offset -; source bank -; vram pointer -; count -; mode - arg_count = 5 - stack_ptr = arg_count * 2 - 1 - source_offset = stack_ptr - source_bank = stack_ptr - 2 - vram_pointer = stack_ptr - 4 - count = stack_ptr - 6 - dma_mode = stack_ptr - 8 - channel = 7 - rep #0x20 - sep #0x10 - ldx #0x80 - stx 0x2115 - lda.b source_offset, s - sta.w 0x4302 + ( channel << 4 ) - sep #0x10 - lda.b source_bank, s - sta.w 0x4304 + ( channel << 4 ) - rep #0x20 - lda.b vram_pointer, s - sta.w 0x2116 - lda.b count, s - sta.w 0x4305 + ( channel << 4 ) - lda.b dma_mode, s - sta.w 0x4300 + ( channel << 4 ) - ldx.b #1 << channel - stx 0x420B - nop - nop - pla - pla - pla - pla - pla - rts -} + _positive: + lda.l 0x004212 + bpl _positive + pla + rts + } + wait_for_vblank_long: + """RTL trampoline around `wait_for_vblank` for cross-bank callers.""" + jsr.w wait_for_vblank + rtl + + dma_transfer_to_vram: + """ + DMA-transfer a block from ROM/RAM into VRAM via channel 7. + Stack args (caller pushes in order): source_offset, source_bank, + vram_pointer, count, dma_mode. All five args are pulled before RTS. + """ + { + ; on the stack: + ; return address + ; source offset + ; source bank + ; vram pointer + ; count + ; mode + arg_count = 5 + stack_ptr = arg_count * 2 - 1 + source_offset = stack_ptr + source_bank = stack_ptr - 2 + vram_pointer = stack_ptr - 4 + count = stack_ptr - 6 + dma_mode = stack_ptr - 8 + channel = 7 + rep #0x20 + sep #0x10 + ldx #0x80 + stx 0x2115 + lda.b source_offset, s + sta.w 0x4302 + ( channel << 4 ) + sep #0x10 + lda.b source_bank, s + sta.w 0x4304 + ( channel << 4 ) + rep #0x20 + lda.b vram_pointer, s + sta.w 0x2116 + lda.b count, s + sta.w 0x4305 + ( channel << 4 ) + lda.b dma_mode, s + sta.w 0x4300 + ( channel << 4 ) + ldx.b #1 << channel + stx 0x420B + nop + nop + pla + pla + pla + pla + pla + rts + } + + + dma_transfer_to_palette: + """ + DMA-transfer a block into CGRAM (palette) via channel 7. + Stack args (caller-pushed): source_offset, source_bank, count. + All three pulled before RTS. + """ + { + ; on the stack: + ; return address + ; source offset + ; source bank + ; count + arg_count = 3 + stack_ptr = arg_count * 2 - 1 + source_offset = stack_ptr + source_bank = stack_ptr - 2 + count = stack_ptr - 4 + rep #0x20 + sep #0x10 + ldx #0x80 + stx 0x2115 + lda.b source_offset, s + sta 0x4372 + lda.b source_bank, s + sta 0x4374 + lda.b count, s + sta 0x4375 + lda #0x2200 + sta 0x4370 + ldx #1 << 7 + stx 0x420B + nop + nop + pla + pla + pla + rts + } -dma_transfer_to_palette: -""" -DMA-transfer a block into CGRAM (palette) via channel 7. -Stack args (caller-pushed): source_offset, source_bank, count. -All three pulled before RTS. -""" -{ -; on the stack: -; return address -; source offset -; source bank -; count - arg_count = 3 - stack_ptr = arg_count * 2 - 1 - source_offset = stack_ptr - source_bank = stack_ptr - 2 - count = stack_ptr - 4 - rep #0x20 - sep #0x10 - ldx #0x80 - stx 0x2115 - lda.b source_offset, s - sta 0x4372 - lda.b source_bank, s - sta 0x4374 - lda.b count, s - sta 0x4375 - lda #0x2200 - sta 0x4370 - ldx #1 << 7 - stx 0x420B - nop - nop - pla - pla - pla - rts -} + _enable_display: + pha + lda #0x00 ; enable screen, full brightness + sta 0x2100 + pla + rts -_enable_display: - pha - lda #0x00 ; enable screen, full brightness - sta 0x2100 - pla - rts - -enable_gamepad: -"""Enable auto-joypad-read + vblank NMI by writing 1 to $4200.""" - pha - lda #0x01 - sta 0x4200 - pla - rts - - -disable_gamepad: -"""Disable auto-joypad-read and all NMI/IRQ sources ($4200 := 0).""" - stz 0x4200 - rts - - -initialize_snes: -""" -Reset PPU, DMA, and CPU registers to a known idle state at boot. -8-bit M/X ; clears OBJ/BG registers, neutralises mode-7 matrix to identity, -disables NMI/HDMA/joypad, leaves screen forced-blank. -""" - sep #0x30 ; make X, Y, A all 8-bits - lda #0x80 ; screen off, no brightness - sta 0x2100 ; brightness & screen enable register - lda #0x00 - sta 0x2101 ; sprite register (size & address in VRAM) - sta 0x2102 ; sprite registers (address of sprite memory [OAM]) - sta 0x2103 ; sprite registers (address of sprite memory [OAM]) - sta 0x2105 ; graphic mode register - sta 0x2106 ; mosaic register - sta 0x2107 ; plane 0 map VRAM location - sta 0x2108 ; plane 1 map VRAM location - sta 0x2109 ; plane 2 map VRAM location - sta 0x210A ; plane 3 map VRAM location - sta 0x210B ; plane 0 & 1 Tile data location - sta 0x210C ; plane 2 & 3 Tile data location - sta 0x210D ; plane 0 scroll x (first 8 bits) - sta 0x210D ; plane 0 scroll x (last 3 bits) - sta 0x210E ; plane 0 scroll y (first 8 bits) - sta 0x210E ; plane 0 scroll y (last 3 bits) - sta 0x210F ; plane 1 scroll x (first 8 bits) - sta 0x210F ; plane 1 scroll x (last 3 bits) - sta 0x2110 ; plane 1 scroll y (first 8 bits) - sta 0x2110 ; plane 1 scroll y (last 3 bits) - sta 0x2111 ; plane 2 scroll x (first 8 bits) - sta 0x2111 ; plane 2 scroll x (last 3 bits) - sta 0x2112 ; plane 2 scroll y (first 8 bits) - sta 0x2112 ; plane 2 scroll y (last 3 bits) - sta 0x2113 ; plane 3 scroll x (first 8 bits) - sta 0x2113 ; plane 3 scroll x (last 3 bits) - sta 0x2114 ; plane 3 scroll y (first 8 bits) - sta 0x2114 ; plane 3 scroll y (last 3 bits) - lda #0x80 ; increase VRAM address after writing to 0x2119 - sta 0x2115 ; VRAM address increment register - lda #0x00 - sta 0x2116 ; VRAM address low - sta 0x2117 ; VRAM address high - sta 0x211A ; initial mode 7 setting register - sta 0x211B ; mode 7 matrix parameter A register (low) - lda #0x01 - sta 0x211B ; mode 7 matrix parameter A register (high) - lda #0x00 - sta 0x211C ; mode 7 matrix parameter B register (low) - sta 0x211C ; mode 7 matrix parameter B register (high) - sta 0x211D ; mode 7 matrix parameter C register (low) - sta 0x211D ; mode 7 matrix parameter C register (high) - sta 0x211E ; mode 7 matrix parameter D register (low) - lda #0x01 - sta 0x211E ; mode 7 matrix parameter D register (high) - lda #0x00 - sta 0x211F ; mode 7 center position X register (low) - sta 0x211F ; mode 7 center position X register (high) - sta 0x2120 ; mode 7 center position Y register (low) - sta 0x2120 ; mode 7 center position Y register (high) - sta 0x2121 ; color number register (0x00-0xff) - sta 0x2123 ; bg1 & bg2 window mask setting register - sta 0x2124 ; bg3 & bg4 window mask setting register - sta 0x2125 ; obj & color window mask setting register - sta 0x2126 ; window 1 left position register - sta 0x2127 ; window 2 left position register - sta 0x2128 ; window 3 left position register - sta 0x2129 ; window 4 left position register - sta 0x212A ; bg1, bg2, bg3, bg4 window logic register - sta 0x212B ; obj, color window logic register (or, and, xor, xnor) - lda #0x01 - sta 0x212C ; main screen designation (planes, sprites enable) - lda #0x00 - sta 0x212D ; sub screen designation - sta 0x212E ; window mask for main screen - sta 0x212F ; window mask for sub screen - lda #0x30 - sta 0x2130 ; color addition & screen addition init setting - lda #0x00 - sta 0x2131 ; add/sub sub designation for screen, sprite, color - lda #0xE0 - sta 0x2132 ; color data for addition/subtraction - stz 0x2133 ; screen setting (interlace x,y/enable SFX data) - stz 0x4200 ; disable v-blank, interrupt, joypad register - lda #0xFF - sta 0x4201 ; programmable I/O port - lda #0x00 - sta 0x4202 ; multiplicand A - sta 0x4203 ; multiplier B - sta 0x4204 ; multiplier C - sta 0x4205 ; multiplicand C - sta 0x4206 ; divisor B - sta 0x4207 ; horizontal count timer - sta 0x4208 ; horizontal count timer MSB - sta 0x4209 ; vertical count timer - sta 0x420A ; vertical count timer MSB - sta 0x420B ; general DMA enable (bits 0-7) - sta 0x420C ; horizontal DMA (HDMA) enable (bits 0-7) - sta 0x420D ; access cycle designation (slow/fast rom) - rts + enable_gamepad: + """Enable auto-joypad-read + vblank NMI by writing 1 to $4200.""" + pha + lda #0x01 + sta 0x4200 + pla + rts + + + disable_gamepad: + """Disable auto-joypad-read and all NMI/IRQ sources ($4200 := 0).""" + stz 0x4200 + rts + + + initialize_snes: + """ + Reset PPU, DMA, and CPU registers to a known idle state at boot. + 8-bit M/X ; clears OBJ/BG registers, neutralises mode-7 matrix to identity, + disables NMI/HDMA/joypad, leaves screen forced-blank. + """ + sep #0x30 ; make X, Y, A all 8-bits + lda #0x80 ; screen off, no brightness + sta 0x2100 ; brightness & screen enable register + lda #0x00 + sta 0x2101 ; sprite register (size & address in VRAM) + sta 0x2102 ; sprite registers (address of sprite memory [OAM]) + sta 0x2103 ; sprite registers (address of sprite memory [OAM]) + sta 0x2105 ; graphic mode register + sta 0x2106 ; mosaic register + sta 0x2107 ; plane 0 map VRAM location + sta 0x2108 ; plane 1 map VRAM location + sta 0x2109 ; plane 2 map VRAM location + sta 0x210A ; plane 3 map VRAM location + sta 0x210B ; plane 0 & 1 Tile data location + sta 0x210C ; plane 2 & 3 Tile data location + sta 0x210D ; plane 0 scroll x (first 8 bits) + sta 0x210D ; plane 0 scroll x (last 3 bits) + sta 0x210E ; plane 0 scroll y (first 8 bits) + sta 0x210E ; plane 0 scroll y (last 3 bits) + sta 0x210F ; plane 1 scroll x (first 8 bits) + sta 0x210F ; plane 1 scroll x (last 3 bits) + sta 0x2110 ; plane 1 scroll y (first 8 bits) + sta 0x2110 ; plane 1 scroll y (last 3 bits) + sta 0x2111 ; plane 2 scroll x (first 8 bits) + sta 0x2111 ; plane 2 scroll x (last 3 bits) + sta 0x2112 ; plane 2 scroll y (first 8 bits) + sta 0x2112 ; plane 2 scroll y (last 3 bits) + sta 0x2113 ; plane 3 scroll x (first 8 bits) + sta 0x2113 ; plane 3 scroll x (last 3 bits) + sta 0x2114 ; plane 3 scroll y (first 8 bits) + sta 0x2114 ; plane 3 scroll y (last 3 bits) + lda #0x80 ; increase VRAM address after writing to 0x2119 + sta 0x2115 ; VRAM address increment register + lda #0x00 + sta 0x2116 ; VRAM address low + sta 0x2117 ; VRAM address high + sta 0x211A ; initial mode 7 setting register + sta 0x211B ; mode 7 matrix parameter A register (low) + lda #0x01 + sta 0x211B ; mode 7 matrix parameter A register (high) + lda #0x00 + sta 0x211C ; mode 7 matrix parameter B register (low) + sta 0x211C ; mode 7 matrix parameter B register (high) + sta 0x211D ; mode 7 matrix parameter C register (low) + sta 0x211D ; mode 7 matrix parameter C register (high) + sta 0x211E ; mode 7 matrix parameter D register (low) + lda #0x01 + sta 0x211E ; mode 7 matrix parameter D register (high) + lda #0x00 + sta 0x211F ; mode 7 center position X register (low) + sta 0x211F ; mode 7 center position X register (high) + sta 0x2120 ; mode 7 center position Y register (low) + sta 0x2120 ; mode 7 center position Y register (high) + sta 0x2121 ; color number register (0x00-0xff) + sta 0x2123 ; bg1 & bg2 window mask setting register + sta 0x2124 ; bg3 & bg4 window mask setting register + sta 0x2125 ; obj & color window mask setting register + sta 0x2126 ; window 1 left position register + sta 0x2127 ; window 2 left position register + sta 0x2128 ; window 3 left position register + sta 0x2129 ; window 4 left position register + sta 0x212A ; bg1, bg2, bg3, bg4 window logic register + sta 0x212B ; obj, color window logic register (or, and, xor, xnor) + lda #0x01 + sta 0x212C ; main screen designation (planes, sprites enable) + lda #0x00 + sta 0x212D ; sub screen designation + sta 0x212E ; window mask for main screen + sta 0x212F ; window mask for sub screen + lda #0x30 + sta 0x2130 ; color addition & screen addition init setting + lda #0x00 + sta 0x2131 ; add/sub sub designation for screen, sprite, color + lda #0xE0 + sta 0x2132 ; color data for addition/subtraction + stz 0x2133 ; screen setting (interlace x,y/enable SFX data) + stz 0x4200 ; disable v-blank, interrupt, joypad register + lda #0xFF + sta 0x4201 ; programmable I/O port + lda #0x00 + sta 0x4202 ; multiplicand A + sta 0x4203 ; multiplier B + sta 0x4204 ; multiplier C + sta 0x4205 ; multiplicand C + sta 0x4206 ; divisor B + sta 0x4207 ; horizontal count timer + sta 0x4208 ; horizontal count timer MSB + sta 0x4209 ; vertical count timer + sta 0x420A ; vertical count timer MSB + sta 0x420B ; general DMA enable (bits 0-7) + sta 0x420C ; horizontal DMA (HDMA) enable (bits 0-7) + sta 0x420D ; access cycle designation (slow/fast rom) + rts +} diff --git a/src/menus/in_game_text.s b/src/menus/in_game_text.s index 707ea2a..91c6034 100644 --- a/src/menus/in_game_text.s +++ b/src/menus/in_game_text.s @@ -1,357 +1,361 @@ """French translated text data for the in-game menus.""" .include "src/ingame/macros.i" +.include "../bank20.i" .table "text/ff4_menus.tbl" .extern lookup_dakuten -.scope in_game_menu { - """Main pause-menu strings.""" -cant_fight: - .text "KO" - .db 0 -menu: -; window - menu_window(23, 0, 7, 17) -; position - .dw 0x0070 - .text "Objets" - .db 0x01 - .dw 0x00F0 - .text "Magie" - .db 0x01 - .dw 0x0170 - .text "Equiper" - .db 0x01 - .dw 0x01F0 - .text "Statut" - .db 0x01 - .dw 0x0270 - .text "Placer" - .db 0x01 - .dw 0x02F0 - .text "Changer" - .db 0x01 - .dw 0x0370 - .text "Options" - .db 0x01 - .dw 0x03F0 - .text "Sauver" - .db 0 -gils: - .text "Gils" - .db 0 -time: - .text "Temps" - .db 0 -} -.scope items_menu { - """Items submenu strings.""" -items_menu_right: - menu_window(22, 0, 7, 3) -item: - menu_window(0, 0, 7, 3) - .dw 0x0044 - .text "Objets" - .db 0 -notuse: - .dw 0x0052 - .text "Impossible à utiliser." - .db 0 -} +.alloc in_game_text_block in bank20_reloc { + .scope in_game_menu { + """Main pause-menu strings.""" + cant_fight: + .text "KO" + .db 0 + menu: + ; window + menu_window(23, 0, 7, 17) + ; position + .dw 0x0070 + .text "Objets" + .db 0x01 + .dw 0x00F0 + .text "Magie" + .db 0x01 + .dw 0x0170 + .text "Equiper" + .db 0x01 + .dw 0x01F0 + .text "Statut" + .db 0x01 + .dw 0x0270 + .text "Placer" + .db 0x01 + .dw 0x02F0 + .text "Changer" + .db 0x01 + .dw 0x0370 + .text "Options" + .db 0x01 + .dw 0x03F0 + .text "Sauver" + .db 0 + gils: + .text "Gils" + .db 0 + time: + .text "Temps" + .db 0 + } + .scope items_menu { + """Items submenu strings.""" + items_menu_right: + menu_window(22, 0, 7, 3) + item: + menu_window(0, 0, 7, 3) + .dw 0x0044 + .text "Objets" + .db 0 + notuse: + .dw 0x0052 + .text "Impossible à utiliser." + .db 0 + } -.scope spells { - """Spell-list headers.""" -white: - move_to(24, 3) - .text "Magie" - .db 0 -black: - move_to(24, 5) - .text "Rituel" - .db 0 -summon: - move_to(24, 7) - .text "Chimere" - .db 0 -ninja: - move_to(24, 5) - .text "Ninja " - .db 0 -kokan: - move_to(24, 3) - .text "Echange" - .db 0 -mp_needed: - .text "Coût PM" - .db 0 -} -.scope status { - """Status screen labels.""" -status: - .dw 0x01F0 - .text "Statut" - .db 0 -exp_for_next_level: - .dw 0x0260 - .text "Niveau suivant" - .db 0 -char_stats: - .dw 0x0114 - 0x80 - .text "Niveau" - .db 1 - .dw 0x01A0 - .text "Expérience" - .db 1 - .dw 0x0206 - .text "PV" - .db 1 - .dw 0x0286 - .text "PM" - .db 1 - .dw 0x0344 - .text "Talents" - .db 1 - .dw 0x03C2 - .text "Vigueur" - .db 1 - .dw 0x0442 - .text "Agilité" - .db 1 - .dw 0x04C2 - .text "Vitesse" - .db 1 - .dw 0x0542 - .text "Esprit" - .db 1 - .dw 0x05C2 - .text "Volonté" - .db 1 -;att/def/mag: - .dw 0x035A - .text "Attaque" - .db 1 - .dw 0x03DA - .text "Attaque%" - .db 1 - .dw 0x045A - .text "Défense" - .db 1 - .dw 0x04DA - .text "Défense%" - .db 1 - .dw 0x055A - .text "Déf Mag" - .db 1 - .dw 0x05DA - .text "Déf Mag%" - .db 0 -} + .scope spells { + """Spell-list headers.""" + white: + move_to(24, 3) + .text "Magie" + .db 0 + black: + move_to(24, 5) + .text "Rituel" + .db 0 + summon: + move_to(24, 7) + .text "Chimere" + .db 0 + ninja: + move_to(24, 5) + .text "Ninja " + .db 0 + kokan: + move_to(24, 3) + .text "Echange" + .db 0 + mp_needed: + .text "Coût PM" + .db 0 + } -.scope options { - """Options screen text.""" -title: - .dw 0x0096 - .text "Options" - .db 0 -config: - .dw 0x0102, 0x141C - .dw 0x0144 - .text "Mode Combat" - .db 0x01 - .dw 0x015E - .text "Actif Pause" - .db 0x01 - .dw 0x01C4 - .text "Vit. Combat" - .db 0x01 - .dw 0x021E - .text "Vite Lent" - .db 0x01 - .dw 0x0244 - .text "Vit. Texte" - .db 0x01 - .dw 0x02C4 - .text "Audio" - .db 0x01 - .dw 0x02DE - .text "Stéréo Mono" - .db 0x01 - .dw 0x0344 - .text "Contrôle" - .db 0x01 - .dw 0x035E - .text "Normal Perso." - .db 0x01 - .dw 0x03DE - .text "Seul Multiple" - .db 0x01 - .dw 0x0444 - .text "Curseur" - .db 0x01 - .dw 0x045E - .text "Reset Mémoire" - .db 0x01 - .dw 0x04C4 - .text "Couleur" - .db 0 -controls: - move_to(5, 1) - .text "Contrôles Personalisés" - .db 0x01 - .dw 0x0204 - .text "Action" - .db 0x01 - .dw 0x0284 - .text "Annuler" - .db 0x01 - .dw 0x0304 - .text "Menu" - .db 0x01 - .dw 0x0384 - .text "Left Button" - .db 0x01 - .dw 0x0404 - .text "Start" - .db 1 - .dw 0x0484 - .text "Fin" - .db 0 -} + .scope status { + """Status screen labels.""" + status: + .dw 0x01F0 + .text "Statut" + .db 0 + exp_for_next_level: + .dw 0x0260 + .text "Niveau suivant" + .db 0 + char_stats: + .dw 0x0114 - 0x80 + .text "Niveau" + .db 1 + .dw 0x01A0 + .text "Expérience" + .db 1 + .dw 0x0206 + .text "PV" + .db 1 + .dw 0x0286 + .text "PM" + .db 1 + .dw 0x0344 + .text "Talents" + .db 1 + .dw 0x03C2 + .text "Vigueur" + .db 1 + .dw 0x0442 + .text "Agilité" + .db 1 + .dw 0x04C2 + .text "Vitesse" + .db 1 + .dw 0x0542 + .text "Esprit" + .db 1 + .dw 0x05C2 + .text "Volonté" + .db 1 + ;att/def/mag: + .dw 0x035A + .text "Attaque" + .db 1 + .dw 0x03DA + .text "Attaque%" + .db 1 + .dw 0x045A + .text "Défense" + .db 1 + .dw 0x04DA + .text "Défense%" + .db 1 + .dw 0x055A + .text "Déf Mag" + .db 1 + .dw 0x05DA + .text "Déf Mag%" + .db 0 + } -.scope equip { - """Equipment menu slot labels.""" -menu: - _text_y = 1 - menu_window(0, 0, 30, 11) - move_to(13, 0 + _text_y) - .text "M. Droite" - .db 0x01 - move_to(13, 2 + _text_y) - .text "M. Gauche" - .db 0x01 - move_to(13, 4 + _text_y) - .text "Tête" - .db 0x01 - move_to(13, 6 + _text_y) - .text "Corps" - .db 0x01 - move_to(13, 8 + _text_y) - .text "Mains" - .db 0 -} + .scope options { + """Options screen text.""" + title: + .dw 0x0096 + .text "Options" + .db 0 + config: + .dw 0x0102, 0x141C + .dw 0x0144 + .text "Mode Combat" + .db 0x01 + .dw 0x015E + .text "Actif Pause" + .db 0x01 + .dw 0x01C4 + .text "Vit. Combat" + .db 0x01 + .dw 0x021E + .text "Vite Lent" + .db 0x01 + .dw 0x0244 + .text "Vit. Texte" + .db 0x01 + .dw 0x02C4 + .text "Audio" + .db 0x01 + .dw 0x02DE + .text "Stéréo Mono" + .db 0x01 + .dw 0x0344 + .text "Contrôle" + .db 0x01 + .dw 0x035E + .text "Normal Perso." + .db 0x01 + .dw 0x03DE + .text "Seul Multiple" + .db 0x01 + .dw 0x0444 + .text "Curseur" + .db 0x01 + .dw 0x045E + .text "Reset Mémoire" + .db 0x01 + .dw 0x04C4 + .text "Couleur" + .db 0 + controls: + move_to(5, 1) + .text "Contrôles Personalisés" + .db 0x01 + .dw 0x0204 + .text "Action" + .db 0x01 + .dw 0x0284 + .text "Annuler" + .db 0x01 + .dw 0x0304 + .text "Menu" + .db 0x01 + .dw 0x0384 + .text "Left Button" + .db 0x01 + .dw 0x0404 + .text "Start" + .db 1 + .dw 0x0484 + .text "Fin" + .db 0 + } + .scope equip { + """Equipment menu slot labels.""" + menu: + _text_y = 1 + menu_window(0, 0, 30, 11) + move_to(13, 0 + _text_y) + .text "M. Droite" + .db 0x01 + move_to(13, 2 + _text_y) + .text "M. Gauche" + .db 0x01 + move_to(13, 4 + _text_y) + .text "Tête" + .db 0x01 + move_to(13, 6 + _text_y) + .text "Corps" + .db 0x01 + move_to(13, 8 + _text_y) + .text "Mains" + .db 0 + } -.scope dextrality { - """Handedness labels.""" -hands: -string_0: -; ぶきよう - .text "String 0" - .db 0 -string_1: -; ひだりきき - .text "Gaucher" - .db 0 -string_2: -; みぎきき - .text "Droiter" - .db 0 -string_3: -; りょうきき - .text "Ambidextre" - .db 0 -} -.scope messages { - """Generic prompt strings.""" -use_on_whom: - move_to(10, 1) - .text "Utiliser sur qui ?" - .db 0 -cantuse: - move_to(10, 1) - .text "Cet objet ne peut être utilisé ici." - .db 0 -cant_use_magic: - menu_window_move_text(7, 12, 14, 1) - .text "Ne peut utiliser la magie" - .db 0 -} + .scope dextrality { + """Handedness labels.""" + hands: + string_0: + ; ぶきよう + .text "String 0" + .db 0 + string_1: + ; ひだりきき + .text "Gaucher" + .db 0 + string_2: + ; みぎきき + .text "Droiter" + .db 0 + string_3: + ; りょうきき + .text "Ambidextre" + .db 0 + } + .scope messages { + """Generic prompt strings.""" + use_on_whom: + move_to(10, 1) + .text "Utiliser sur qui ?" + .db 0 + cantuse: + move_to(10, 1) + .text "Cet objet ne peut être utilisé ici." + .db 0 + cant_use_magic: + menu_window_move_text(7, 12, 14, 1) + .text "Ne peut utiliser la magie" + .db 0 + } -.scope use_spell { - """Spell-cast prompt strings.""" -mp_cost: - move_to(1, 4) - .text "Requis" - .db 0 -; use_on_whom: -; move_to(1, 10) -; .text 'Sur qui ?' -; .db 0 -} -.scope treasure { - """Treasure-chest UI text + windows.""" -choice_window: -"""patch them in place""" - menu_window(8, 0, 22, 2) -items_window: - menu_window(0, 3, 30, 10) -header_window: - menu_window(0, 0, 6, 2) - move_to(1, 1) - .text "Butin" - .db 1 -exit: - move_to(24, 1) - .text "Quitter" - .db 1 -take_all: - move_to(10, 1) - .text "Tout prendre" - .db 0 -exchange: - move_to(10, 1) - .text "Échanger " - .db 0 -key_items_left_warning: - menu_window(5, 10, 19, 4) - move_to(6, 11) - .text " Il reste des " - .db 1 - move_to(6, 13) - .text "objets importants." - .db 0 -} + .scope use_spell { + """Spell-cast prompt strings.""" + mp_cost: + move_to(1, 4) + .text "Requis" + .db 0 + ; use_on_whom: + ; move_to(1, 10) + ; .text 'Sur qui ?' + ; .db 0 + } + + .scope treasure { + """Treasure-chest UI text + windows.""" + choice_window: + """patch them in place""" + menu_window(8, 0, 22, 2) + items_window: + menu_window(0, 3, 30, 10) + header_window: + menu_window(0, 0, 6, 2) + move_to(1, 1) + .text "Butin" + .db 1 + exit: + move_to(24, 1) + .text "Quitter" + .db 1 + take_all: + move_to(10, 1) + .text "Tout prendre" + .db 0 + exchange: + move_to(10, 1) + .text "Échanger " + .db 0 + key_items_left_warning: + menu_window(5, 10, 19, 4) + move_to(6, 11) + .text " Il reste des " + .db 1 + move_to(6, 13) + .text "objets importants." + .db 0 + } -copy_text_with_dakuten_far: -"""Far-callable text copier with dakuten composite lookup.""" -{ - phb - phk - plb - rep #0x20 - txa - clc - adc 0x29 - tax - sep #0x20 -_loop: - lda.w 0x0000, y - beq _exit - jsr.l lookup_dakuten - sta 0x7e0000, x - xba - sta 0x7e0040, x - inx - inx - iny - bra _loop -_exit: - plb - rtl + copy_text_with_dakuten_far: + """Far-callable text copier with dakuten composite lookup.""" + { + phb + phk + plb + rep #0x20 + txa + clc + adc 0x29 + tax + sep #0x20 + _loop: + lda.w 0x0000, y + beq _exit + jsr.l lookup_dakuten + sta 0x7e0000, x + xba + sta 0x7e0040, x + inx + inx + iny + bra _loop + _exit: + plb + rtl + } } diff --git a/src/menus/start_screen_text.s b/src/menus/start_screen_text.s index b5af106..f3f33be 100644 --- a/src/menus/start_screen_text.s +++ b/src/menus/start_screen_text.s @@ -1,58 +1,63 @@ +.include "config.i" """French translated text data for the start screen / save selection.""" .include "src/ingame/macros.i" +.include "../bank20.i" .table "text/ff4_menus.tbl" -.scope newgame { - """Title-screen + save-slot strings.""" -new_game: - move_to(1, 1) - .text "Nouvelle partie" - .db 0 - .if DEBUG { -build_number: - VERSION := 'v1.0.0a0' - .text "${BUILD_DATE} ${VERSION}" - .db 0 + +.alloc start_screen_text_block in bank20_reloc { + .scope newgame { + """Title-screen + save-slot strings.""" + new_game: + move_to(1, 1) + .text "Nouvelle partie" + .db 0 + .if DEBUG { + build_number: + VERSION := 'v1.0.0a0' + .text "${BUILD_DATE} ${VERSION}" + .db 0 + } + time_load_save: + .dw 0x046E + 2 + .text "Temps" + .db 0 + gils_load_game: + .text "Gils" + .db 0 + save: + .text "Partie" + .db 0 + load_this_save: + .dw 0x006E + 2 + .text "Cette" + .db 1 + .dw 0x00EE + 2 + .text "partie?" + .db 0 + yes_no: + .dw 0x0172 + .text "Oui" + .db 1 + .dw 0x01F2 + .text "Non" + .db 0 + empty_save: + .dw 0x0090 + 2 + .text "VIDE" + .db 0 + saves: + move_to(1, 1) + .text "Sauvegardes" + .db 0 + save_completed: + move_to(8, 11) + .text "Sauvegarde terminée" + .db 0 + did_not_save: + move_to(1, 1) + .text "Annulation " + ; extra space at the end to clear the previous title. + .db 0 } -time_load_save: - .dw 0x046E + 2 - .text "Temps" - .db 0 -gils_load_game: - .text "Gils" - .db 0 -save: - .text "Partie" - .db 0 -load_this_save: - .dw 0x006E + 2 - .text "Cette" - .db 1 - .dw 0x00EE + 2 - .text "partie?" - .db 0 -yes_no: - .dw 0x0172 - .text "Oui" - .db 1 - .dw 0x01F2 - .text "Non" - .db 0 -empty_save: - .dw 0x0090 + 2 - .text "VIDE" - .db 0 -saves: - move_to(1, 1) - .text "Sauvegardes" - .db 0 -save_completed: - move_to(8, 11) - .text "Sauvegarde terminée" - .db 0 -did_not_save: - move_to(1, 1) - .text "Annulation " -; extra space at the end to clear the previous title. - .db 0 } diff --git a/src/menus/start_screen_text_en.s b/src/menus/start_screen_text_en.s index cef4f58..a37de43 100644 --- a/src/menus/start_screen_text_en.s +++ b/src/menus/start_screen_text_en.s @@ -1,3 +1,4 @@ +.include "config.i" """English-language counterpart of `start_screen_text.s`.""" .include "src/ingame/macros.i" diff --git a/src/menus/system_menus_text.i b/src/menus/system_menus_text.i index cdebc76..a32b74d 100644 --- a/src/menus/system_menus_text.i +++ b/src/menus/system_menus_text.i @@ -8,15 +8,16 @@ routines. } { - *=0x018301 - jmp.l display_text_in_menus - - *=0x0182CD - jmp.l load_text_with_destination_in_x - - *=0x0180D9 - jmp.l display_window_with_text - - *=0x018798 - jmp.l display_time +.alloc at 0x018301 { + jmp.l display_text_in_menus +} +.alloc at 0x0182CD { + jmp.l load_text_with_destination_in_x +} +.alloc at 0x0180D9 { + jmp.l display_window_with_text +} +.alloc at 0x018798 { + jmp.l display_time +} } diff --git a/src/menus/system_menus_text.s b/src/menus/system_menus_text.s index cf77f9d..c17c9fb 100644 --- a/src/menus/system_menus_text.s +++ b/src/menus/system_menus_text.s @@ -1,128 +1,133 @@ """Relocated implementations of the original system-menu text routines.""" -.extern assets_classes_ptr - -.macro _bank_switch() { - cpy.w #0x8000 - bmi _moved_text - lda.b #0x01 -; this assumes we shoud load data from bank 1 - pha - bra _jump_to_original -_moved_text: - pha - rep #0x20 - tya - adc.w #0x8000 - tay - sep #0x20 - pla - phk -_jump_to_original: -"""this assumes that the text lives in the same bank as this routine.""" - plb -} - -.macro _bank_switch_with_jump(jump_to) { - _bank_switch() - jmp.l jump_to -} +.include "../bank20.i" +.extern assets_classes_ptr -display_text_in_menus: -"""Trampoline into the original menu text-display routine at $01:830B, preserving DBR/D/X via stack.""" -{ - phb - phd - phx - ldx.w #0x100 - phx - pld - _bank_switch_with_jump(0x01830B) +.alloc system_menus_text_block in bank20_reloc { + .macro _bank_switch() { + cpy.w #0x8000 + bmi _moved_text + lda.b #0x01 + ; this assumes we shoud load data from bank 1 + pha + bra _jump_to_original + _moved_text: + pha + rep #0x20 + tya + adc.w #0x8000 + tay + sep #0x20 + pla + phk + _jump_to_original: + """this assumes that the text lives in the same bank as this routine.""" + plb + } + + .macro _bank_switch_with_jump(jump_to) { + _bank_switch() + jmp.l jump_to + } + + + display_text_in_menus: + """Trampoline into the original menu text-display routine at $01:830B, preserving DBR/D/X via stack.""" + { + phb + phd + phx + ldx.w #0x100 + phx + pld + + _bank_switch_with_jump(0x01830B) + } + + load_text_with_destination_in_x: + """Adjust X by the offset in $29 and trampoline into the original load-text routine at $01:8318.""" + + phb + phd + phx + phx + ldx.w #0x0100 + phx + pld + plx + + rep #0x20 + + txa + clc + adc 0x29 + tax + sep #0x20 + + _bank_switch_with_jump(0x018318) + + + display_window_with_text: + """Trampoline into the original window+text drawer at $01:80DD.""" + phy + phb + _bank_switch_with_jump(0x0180DD) + + display_time: + """Trampoline into the original play-time display routine at $01:879D.""" + phb + _bank_switch() + rep #0x20 + jmp.l 0x01879D + + disable_save: + """ + Overwrite the 'Sauver' menu label with $24 (space tile) glyphs to grey out the save option, then continue at + $01:8947. + """ + lda.b #0x24 + sta 0xCA31 ; S + sta 0xCA33 ; a + sta 0xCA35 ; u + sta 0xCA37 ; v + sta 0xCA39 ; e + sta 0xCA3B ; r + jmp.l 0x018947 + + load_classes_pointer: + """Resolve a class-name pointer from `assets_classes_ptr[A*2]` into A (24-bit).""" + phx + rep #0x20 + + and.w #0x00FF + asl + tax + + lda.l assets_classes_ptr, x + sep #0x20 + + plx + rtl + + load_dextrelity_pointer: + """ + Read the high two bits of (DP),$60 to pick a dexterity-text pointer from the table at $01:E2D9 ; returns + 24-bit pointer in Y. + """ + rep #0x20 + lda (0x60) + and.w #0x00c0 + lsr + lsr + lsr + lsr + lsr + phx + tax + lda.l 0x01e2d9, x + plx + tay + sep #0x20 + rtl } - -load_text_with_destination_in_x: -"""Adjust X by the offset in $29 and trampoline into the original load-text routine at $01:8318.""" - - phb - phd - phx - phx - ldx.w #0x0100 - phx - pld - plx - - rep #0x20 - - txa - clc - adc 0x29 - tax - sep #0x20 - - _bank_switch_with_jump(0x018318) - - -display_window_with_text: -"""Trampoline into the original window+text drawer at $01:80DD.""" - phy - phb - _bank_switch_with_jump(0x0180DD) - -display_time: -"""Trampoline into the original play-time display routine at $01:879D.""" - phb - _bank_switch() - rep #0x20 - jmp.l 0x01879D - -disable_save: -""" -Overwrite the 'Sauver' menu label with $24 (space tile) glyphs to grey out the save option, then continue at -$01:8947. -""" - lda.b #0x24 - sta 0xCA31 ; S - sta 0xCA33 ; a - sta 0xCA35 ; u - sta 0xCA37 ; v - sta 0xCA39 ; e - sta 0xCA3B ; r - jmp.l 0x018947 - -load_classes_pointer: -"""Resolve a class-name pointer from `assets_classes_ptr[A*2]` into A (24-bit).""" - phx - rep #0x20 - - and.w #0x00FF - asl - tax - - lda.l assets_classes_ptr, x - sep #0x20 - - plx - rtl - -load_dextrelity_pointer: -""" -Read the high two bits of (DP),$60 to pick a dexterity-text pointer from the table at $01:E2D9 ; returns -24-bit pointer in Y. -""" - rep #0x20 - lda (0x60) - and.w #0x00c0 - lsr - lsr - lsr - lsr - lsr - phx - tax - lda.l 0x01e2d9, x - plx - tay - sep #0x20 - rtl diff --git a/src/menus/tools_shop_text.s b/src/menus/tools_shop_text.s index 3946ac4..7fad818 100644 --- a/src/menus/tools_shop_text.s +++ b/src/menus/tools_shop_text.s @@ -1,78 +1,92 @@ """French translated text data for the tools/weapon/armor shop UI.""" .include "src/ingame/macros.i" +.include "../bank20.i" .table "text/ff4_menus.tbl" -.scope shops { - """Shop UI strings.""" -gils: - move_to(27, 6) - .text "Gils" - .db 0 -welcome_and_actions: - .dw 0x0054 - 2 - .text "Puis-je vous aider ?" -;.text 'いらっしゃい! どんなごようけんで?' - .db 1 - .dw 0x0148 - 4 - .text "Achat Vente Sortir" - .db 0 -quantity: -""".text 'かう うる でる'""" - .dw 0x0052 - .text "Que désirez vous ? " - .db 1 - .dw 0x0144 - .text "Quantité" - .db 1 - .dw 0x0146 + 14 * 2 - .text "1" - .db 0 -thank_you_window: - menu_window(5, 10, 11, 2) -thank_you_text: - move_to(6, 11) - .text "Merci !" - .db 0 -inventory_full: - menu_window(1, 10, 20, 4) - move_to(2, 11) - .text "L\'inventaire est" - .db 1 - move_to(2, 13) - .text "plein." - .db 0 -not_enough_gils: - menu_window(5, 10, 16, 4) - move_to(6, 11) - .text "Vous n\'avez pas" - .db 1 - move_to(6, 13) - .text "de Gils." - .db 0 -sell_window: - menu_window(8, 10, 14, 13) - move_to(13, 13) - .text " Unités" - .db 1 - move_to(19, 15) - .text "Gils" - .db 1 - move_to(10, 17) - .text "Êtes-vous" - .db 1 - move_to(11, 19) - .text "d\'accord ?" - .db 1 - move_to(12, 21) - .text "Oui Non" - .db 0 -weapons_title: - .text "Armes" - .db 0 -armor_title: - .text "Armures" - .db 0 -items_title: - .text "Objets" - .db 0 + +.alloc tools_shop_text_block in bank20_reloc { + .scope shops { + """Shop UI strings.""" + gils: + move_to(27, 6) + .text "Gils" + .db 0 + puis_je_vous_aider: + """Owner welcome greeting ; rendered through the small-VWF description region.""" + .dw 0x0054 - 2 + .text "Puis-je vous aider ?" + ;.text 'いらっしゃい! どんなごようけんで?' + .db 0 + welcome_and_actions: + .dw 0x0148 - 4 + .text "Achat Vente Sortir" + .db 0 + que_desirez_vous: + """Owner welcome prompt ; rendered through the small-VWF description region.""" + .dw 0x0052 + .text "Que désirez vous ? " + .db 0 + quantity: + """.text 'かう うる でる'""" + .dw 0x0144 + .text "Quantité" + .db 1 + .dw 0x0146 + 14 * 2 + .text "1" + .db 0 + thank_you_window: + menu_window(5, 10, 11, 2) + ; Trailing empty-text block keeps the vanilla `$82FB` (draw window + text) + ; happy when called via shop_thanks_text_hook ; the actual "Merci !" copy + ; is rendered separately through the small-VWF description region. + .dw 0x0000 + .db 0 + merci: + """Owner thank-you message ; rendered through the small-VWF description region.""" + move_to(6, 11) + .text "Merci !" + .db 0 + inventory_full: + menu_window(1, 10, 20, 4) + move_to(2, 11) + .text "L\'inventaire est" + .db 1 + move_to(2, 13) + .text "plein." + .db 0 + not_enough_gils: + menu_window(5, 10, 16, 4) + move_to(6, 11) + .text "Vous n\'avez pas" + .db 1 + move_to(6, 13) + .text "de Gils." + .db 0 + sell_window: + menu_window(8, 10, 14, 13) + move_to(13, 13) + .text " Unités" + .db 1 + move_to(19, 15) + .text "Gils" + .db 1 + move_to(10, 17) + .text "Êtes-vous" + .db 1 + move_to(11, 19) + .text "d\'accord ?" + .db 1 + move_to(12, 21) + .text "Oui Non" + .db 0 + weapons_title: + .text "Armes" + .db 0 + armor_title: + .text "Armures" + .db 0 + items_title: + .text "Objets" + .db 0 + } } diff --git a/src/menus/tools_shop_text_en.s b/src/menus/tools_shop_text_en.s index 8b19481..5f0ac37 100644 --- a/src/menus/tools_shop_text_en.s +++ b/src/menus/tools_shop_text_en.s @@ -7,19 +7,23 @@ gils: move_to(27, 6) .text "GP" .db 0 -welcome_and_actions: +puis_je_vous_aider: +"""Owner welcome greeting ; rendered through the small-VWF description region.""" .dw 0x0054 - 2 .text "Welcome!" ;.text 'いらっしゃい! どんなごようけんで?' - .db 1 + .db 0 +welcome_and_actions: .dw 0x0148 - 4 .text "Buy Sell Exit" .db 0 -quantity: -""".text 'かう うる でる'""" +que_desirez_vous: +"""Owner welcome prompt ; rendered through the small-VWF description region.""" .dw 0x0052 .text "What do you want? " - .db 1 + .db 0 +quantity: +""".text 'かう うる でる'""" .dw 0x0144 .text "Quantity" .db 1 @@ -28,7 +32,10 @@ quantity: .db 0 thank_you_window: menu_window(5, 10, 11, 2) -thank_you_text: + .dw 0x0000 + .db 0 +merci: +"""Owner thank-you message ; rendered through the small-VWF description region.""" move_to(6, 11) .text "Thank you!" .db 0 diff --git a/src/minimal_vwf_patches.s b/src/minimal_vwf_patches.s index c15737e..0daf555 100644 --- a/src/minimal_vwf_patches.s +++ b/src/minimal_vwf_patches.s @@ -10,229 +10,233 @@ the VWF layer kicks in without rewriting the message window. .import "dialog" .import "vwf" -*=0x00B404 - jsr.l compute_dialog_text_offset - jsr.l get_bank1_1_pointer - sta 0xDD - ldx.b dialog_ptr - stx 0x0772 - rts - -*=0x00B41D - jsr.l compute_dialog_text_offset - jsr.l get_bank1_2_pointer - sta 0xDD - ldx.b dialog_ptr - stx 0x0772 - rts - -*=0x00B436 - jsr.l compute_dialog_text_offset - jsr.l get_bank3_pointer - sta 0xDD - ldx.b dialog_ptr - stx 0x0772 - rts - -*=0x00B3BB - lda 0x1702 - sta.b dialog_ptr - lda 0x1701 - sta.b dialog_ptr + 1 - jsr.l get_bank2_pointer - rts - -; nukes the first call of display_script because the text -; has to be rendered before animating the window display - -*=0x00B32C - jsr.w _first_window - -; Replace the display_script function by the vwfed one. - -*=0x00B463 - jsr.l vwfstart - rts - -_first_window: - lda 0x01 - sta 0xDE - sta 0xED - jsr.l vwfinit - rts - -_animation_wait_route: - wait_for_nmi_end = 0x912F - jsr.w wait_for_nmi_end - -_wait_for_open_animation: - lda 0x7F - cmp #0x02 - bne _wait_for_open_animation - inc 0xDF - lda 0xDF - cmp #0x08 - bne _animation_wait_route - -; restore tileset position - lda 0x210C - and #0xF0 - clc - adc #0x02 - sta 0x210C - - jmp.w _end_of_animation - -; do not scroll between text blocks - -*=0x00B370 - lda #0x00 - sta 0x07 - jmp.w 0xB398 - -*=0x00B335 - jmp.w _animation_wait_route - -_end_of_animation: - jmp.w 0xB369 ; skip_wait_for_action_button +.alloc at 0x00B404 { + jsr.l compute_dialog_text_offset + jsr.l get_bank1_1_pointer + sta 0xDD + ldx.b dialog_ptr + stx 0x0772 + rts +} +.alloc at 0x00B41D { + jsr.l compute_dialog_text_offset + jsr.l get_bank1_2_pointer + sta 0xDD + ldx.b dialog_ptr + stx 0x0772 + rts +} +.alloc at 0x00B436 { + jsr.l compute_dialog_text_offset + jsr.l get_bank3_pointer + sta 0xDD + ldx.b dialog_ptr + stx 0x0772 + rts +} +.alloc at 0x00B3BB { + lda 0x1702 + sta.b dialog_ptr + lda 0x1701 + sta.b dialog_ptr + 1 + jsr.l get_bank2_pointer + rts + + ; nukes the first call of display_script because the text + ; has to be rendered before animating the window display +} +.alloc at 0x00B32C { + jsr.w first_window + ; Replace the display_script function by the vwfed one. +} +.alloc at 0x00B463 { + jsr.l vwfstart + rts + + first_window: + lda 0x01 + sta 0xDE + sta 0xED + jsr.l vwfinit + rts + + animation_wait_route: + wait_for_nmi_end = 0x912F + jsr.w wait_for_nmi_end + + _wait_for_open_animation: + lda 0x7F + cmp #0x02 + bne _wait_for_open_animation + inc 0xDF + lda 0xDF + cmp #0x08 + bne animation_wait_route + + ; restore tileset position + lda 0x210C + and #0xF0 + clc + adc #0x02 + sta 0x210C + + jmp.w end_of_animation + + ; do not scroll between text blocks +} +.alloc at 0x00B370 { + lda #0x00 + sta 0x07 + jmp.w 0xB398 +} +.alloc at 0x00B335 { + jmp.w animation_wait_route + end_of_animation: + jmp.w 0xB369 ; skip_wait_for_action_button +} { ; Oui .table "text/ff4_menus.tbl" - *=0x14F656 - .dw 0x2016 - fill_value(0x2017, 5) - .dw 0x2018 - .dw 0x0000 - *=0x14F666 - .dw 0x2019 - fill_value(0x20ff, 5) - .dw 0x201a - .dw 0x0000 - .dw 0x0000 - *=0x14f676 - .dw 0x2019 - .dw 0x2014 - .text "O" - .db 0x20 - .text "u" - .db 0x20 - .text "i" - .db 0x20 - .dw 0x20ff - .dw 0x201a - .dw 0x0000 -; Non - *=0x14F686 - .dw 0x2019 - fill_value(0x20ff, 5) - .dw 0x201a - .dw 0x0000 - .dw 0x0000 - *=0x14F696 - .dw 0x2019 - .dw 0x20ff - .text "N" - .db 0x20 - .text "o" - .db 0x20 - .text "n" - .db 0x20 - .dw 0x20ff - .dw 0x201a - .dw 0x0000 - *=0x14F6A6 - .dw 0x201b - fill_value(0x201c, 5) - .dw 0x201d - .dw 0x0000 - fill_value(0x0000, 8) +.alloc at 0x14F656 { + .dw 0x2016 + fill_value(0x2017, 5) + .dw 0x2018 + .dw 0x0000 +} +.alloc at 0x14F666 { + .dw 0x2019 + fill_value(0x20ff, 5) + .dw 0x201a + .dw 0x0000 + .dw 0x0000 +} +.alloc at 0x14f676 { + .dw 0x2019 + .dw 0x2014 + .text "O" + .db 0x20 + .text "u" + .db 0x20 + .text "i" + .db 0x20 + .dw 0x20ff + .dw 0x201a + .dw 0x0000 + ; Non +} +.alloc at 0x14F686 { + .dw 0x2019 + fill_value(0x20ff, 5) + .dw 0x201a + .dw 0x0000 + .dw 0x0000 +} +.alloc at 0x14F696 { + .dw 0x2019 + .dw 0x20ff + .text "N" + .db 0x20 + .text "o" + .db 0x20 + .text "n" + .db 0x20 + .dw 0x20ff + .dw 0x201a + .dw 0x0000 +} +.alloc at 0x14F6A6 { + .dw 0x201b + fill_value(0x201c, 5) + .dw 0x201d + .dw 0x0000 + fill_value(0x0000, 8) +} } ; yes/no cursor positions ; 0x28c4 0x2904 -*=0x00ae38 - ldx.w #0x28c4 - 1 - -*=0x00ae51 - ldx.w #0x2904 - 1 - -; Palette changes for allowing shadows - -*=0x15c229 - ; 15/C229: BF D0 87 0D LDA $0D87D0,X ; window palette - lda.l window_palette, x - -*=0x15c236 - jsr.l update_palette - nop - nop - -; Update secondary palette window background - -*=0x00823B - jsr.l update_palette - nop - nop - -;AE AA 16 LDX $16AA -;00823E 8E DD 0C STX $0CDD -;008241 20 13 8A JSR $8A13 - -; update the text palette used in the intro - -*=0x8ff04 - .dw 0x0463 - - -*=0x00B363 - jsr.l wait_for_action_button - nop - nop - -; Make gils window bigger - gils_line_size = 0x18 - -*=0x00ad28 - lda.b #gils_window_tilemap_1 >> 16 - ;00/AD26: A9 14 LDA #$14 - -*=0x00ad2d - ldx.w #gils_window_tilemap_1 & 0xffff - -*=0x00ad4b - ldx.w #gils_window_tilemap_2 & 0xffff - - -*=0x00ad39 - ldx.w #gils_line_size - -*=0x00ad63 - ldx.w #gils_window_tilemap_3 - -*=0x00ad69 - ldx.w #gils_line_size +.alloc at 0x00ae38 { + ldx.w #0x28c4 - 1 +} +.alloc at 0x00ae51 { + ldx.w #0x2904 - 1 -*=0x00ad51 - ldx.w #gils_line_size + ; Palette changes for allowing shadows +} +.alloc at 0x15c229 { + ; 15/C229: BF D0 87 0D LDA $0D87D0,X ; window palette + lda.l window_palette, x +} +.alloc at 0x15c236 { + jsr.l update_palette + nop + nop -*=0x00ad7b - ldx.w #gils_window_tilemap_4 & 0xffff + ; Update secondary palette window background +} +.alloc at 0x00823B { + jsr.l update_palette + nop + nop -*=0x00ad81 - ldx.w #( gils_window_tilemap_4_end - gils_window_tilemap_4 ) & 0xffff + ;AE AA 16 LDX $16AA + ;00823E 8E DD 0C STX $0CDD + ;008241 20 13 8A JSR $8A13 -; clear up longer gils window + ; update the text palette used in the intro +} +.alloc at 0x8ff04 { + .dw 0x0463 +} +.alloc at 0x00B363 { + jsr.l wait_for_action_button + nop + nop -*=0x00addd - ldx.w #0x100 + 0x80 + ; Make gils window bigger + gils_line_size = 0x18 +} +.alloc at 0x00ad28 { + lda.b #gils_window_tilemap_1 >> 16 + ;00/AD26: A9 14 LDA #$14 +} +.alloc at 0x00ad2d { + ldx.w #gils_window_tilemap_1 & 0xffff +} +.alloc at 0x00ad4b { + ldx.w #gils_window_tilemap_2 & 0xffff +} +.alloc at 0x00ad39 { + ldx.w #gils_line_size +} +.alloc at 0x00ad63 { + ldx.w #gils_window_tilemap_3 +} +.alloc at 0x00ad69 { + ldx.w #gils_line_size +} +.alloc at 0x00ad51 { + ldx.w #gils_line_size +} +.alloc at 0x00ad7b { + ldx.w #gils_window_tilemap_4 & 0xffff +} +.alloc at 0x00ad81 { + ldx.w #( gils_window_tilemap_4_end - gils_window_tilemap_4 ) & 0xffff -; gils value vram position + ; clear up longer gils window +} +.alloc at 0x00addd { + ldx.w #0x100 + 0x80 -*=0x00ad8a - ldx.w #0x28d3 + 2 + ; gils value vram position +} +.alloc at 0x00ad8a { + ldx.w #0x28d3 + 2 +} diff --git a/src/small_vwf/init.s b/src/small_vwf/init.s index 9d8ccb3..11bfbc5 100644 --- a/src/small_vwf/init.s +++ b/src/small_vwf/init.s @@ -2,15 +2,22 @@ Small (8x8) VWF init routine: prepares the menu tile buffer, runs the renderer init and wires up the long-form RTL entry points for cross-bank callers. """ +.include "config.i" .include "src/vwf.i" +.include "../bank20.i" .include "src/libmz.i" .import "assets" .import "libmz" + +; root-scope externs: `.alloc` bodies open their own scope, so an extern +; declared inside never resolves at the use site. .if BATTLE_ENABLED { .extern battle_render .extern battle_render.clear_buffer } -.include "src/small_vwf/render.s" -.include "src/small_vwf/item_description.s" +.alloc small_vwf_init_block in bank20_reloc { + .include "src/small_vwf/render.s" + .include "src/small_vwf/item_description.s" +} diff --git a/src/small_vwf/item_description.s b/src/small_vwf/item_description.s index 4b7cef1..92e090d 100644 --- a/src/small_vwf/item_description.s +++ b/src/small_vwf/item_description.s @@ -1,4 +1,7 @@ """Small-VWF item-description renderer.""" +.include "src/vwf_state.i" +.include "src/items.i" + .scope items_description { """Small-VWF item-description renderer entry-points.""" draw_trampoline: @@ -49,7 +52,62 @@ draw: plx sep #0x20 draw_string: +; Region config: description CHR lives in tile_id range $180..$1FF +; (allocator base $80, with attr-byte OR $01 -> effective tile_id +; $180 ; VRAM byte $5800..$5FF0). Disjoint from the field-items +; window at $100..$169 -> VRAM $5000..$56A0 ; the two regions used +; to fight for VRAM $5000 (both DMAs targeted there) and the last +; render stomped the other. +; +; Region map (BG3 CHR at VRAM $4000) +; $00..$FF static menu font ($4000..$4FF0) +; $100..$169 field-menu items ($5000..$56A0) +; $180..$1FF item description ($5800..$5FF0) + rep #0x20 + lda.w #0x0080 + sta.l VWF_CONFIG_BASE + VwfConfig.tile_id_base + sep #0x20 + lda.b #0x80 + sta.l VWF_CONFIG_BASE + VwfConfig.slot_budget + lda.b #0x01 + sta.l VWF_CONFIG_BASE + VwfConfig.flags +; Description flush descriptor uses the SECONDARY slot so the +; description region ($5800..$5FF0) gets its own DMA independent of +; the primary descriptor that items_menu_vwf rewrites every per-slot +; call. The allocator base is $80 (`init_with_tile_id_wide(#$0080)`) +; with the $01 attr-byte OR turning it into effective tile_id $180 +; for tilemap entries ; the actual CHR bytes live at buffer offset +; $80 * 16 = $800 (NOT $1800). +; +; src offset = $80 * 16 = $0800 buffer bytes +; vram dest = ($5000 + $800) / 2 = $2C00 word +; size = $80 tiles * 16 = $0800 bytes +; +; DIRTY_B is set at the tail of draw_string ; display_char also sets +; primary DIRTY which the field-items renderer covers in its own +; flush, so no clearing is needed here. + rep #0x20 + lda.w #0x0800 + sta.l VWF_CHR_SRC_OFFSET_B + lda.w #0x2C00 + sta.l VWF_CHR_VRAM_WORD_B + lda.w #0x0800 + sta.l VWF_CHR_BYTE_COUNT_B + sep #0x20 +; Preserve Y across render.init: the per-region CHR clear loop +; tays the budget word count and lands Y=$0000 on exit, which +; collapsed _char_loop into an immediate _char_loop_exit (the loop +; reads `lda.w $0000,y` -> $20:0000 = $00 terminator) and the +; description never rendered a single glyph. + phy jsr.w render.init + rep #0x20 + lda.w #0x0080 + jsr.w render_allocator.init_with_tile_id_wide + sep #0x20 ; _char_loop reads bytes one at a time ; M=16 here +; would lda two source bytes per char and soft-lock +; in wait_for_vblank. + ply _char_loop: lda.w 0x0000, y beq _char_loop_exit @@ -67,6 +125,10 @@ _char_loop: _char_loop_exit: jsr.w _transfer_item_description jsr.w render.deinit +; Description finished writing to buffer ; raise the secondary +; dirty flag so the NMI flush DMAs $5800..$5FF0 next vblank. + lda.b #0x01 + sta.l VWF_CHR_DIRTY_B plx pld plb @@ -99,8 +161,10 @@ _reset_render: bra _char_loop _transfer_item_description: jsr.w wait_for_vblank -; we could reduce the transfer size to the size of the string - dma_transfer_to_vram_call(render.buffer_ptr, 0x5000 >> 1, render.buffer_size, 0x1801) -; use of the menu engine built in dma transfer in NMI +; DMA description's CHR slice only: SRAM $703800 (= buffer + $80*16 +; for tile_id_base $80) to VRAM word $2C00 (= byte $5800), $800 +; bytes covering 128 tile_ids. Keeps the upload disjoint from the +; field-items DMA at byte $5000 / word $2800. + dma_transfer_to_vram_call(render.buffer_ptr + 0x800, 0x5800 >> 1, 0x800, 0x1801) rts } diff --git a/src/small_vwf/render.s b/src/small_vwf/render.s index 73448f8..62492c1 100644 --- a/src/small_vwf/render.s +++ b/src/small_vwf/render.s @@ -1,4 +1,7 @@ +.include "config.i" """Small (8x8) menu VWF renderer.""" +.include "src/vwf_state.i" + VARS_BUFFER = 0x710000 .macro initialize(var) { @@ -31,7 +34,10 @@ VARS_BUFFER = 0x710000 } .scope _vram_copy { - buffer = 0x704000 +; Moved from $704000 to $705000 so the CHR buffer at $703000 can +; grow to $1000 bytes (cover tile_ids $00..$FF) without trampling +; the vram-save staging. + buffer = 0x705000 save_dialog_vram_far: jsr.l 0x14fd0f ; original save @@ -107,16 +113,75 @@ _transfer_to_vram: } .scope render_allocator { - """Tile-id allocator for the small-VWF.""" + """ + Tile-id allocator for the small-VWF. Stored as a 16-bit word at + $702F00..$702F01 so callers can reach the full 10-bit BG3 + tile_id range (0..0x3FF) instead of being capped at 8 bits. + All accessors operate in 16-bit M and write both bytes ; callers + that only need the low byte can still do an 8-bit lda.l at the + same address. + """ allocated_tile_id = 0x702F00 + slot_limit_low = 0x702F02 +""" +Inventory clamp. When the allocator's low byte reaches this value, +`increment` freezes it ; the blitter happily keeps reading the same +tile_id and overwrites the slot's last tile instead of bleeding into +the next slot's CHR. Set to 0xFF (no clamp) by every non-inventory +init. Inventory slot init writes slot_base + K-1 here. +""" + + init_with_tile_id: -"""tile_id in A""" +""" +8-bit caller convention: A.lo = tile_id (0..0xFF), M = 8. +Zero-extends to 16-bit so the high byte at $702F01 is clean. +tilemap_write_no_inc adds the hardcoded +0x100 bit shift. 16-bit +callers needing tile_ids outside the 0x100-0x1FF window should use +init_with_tile_id_wide. Also resets slot_limit_low to 0xFF so the +clamp is a no-op for non-inventory regions. +""" + + sta.l allocated_tile_id + pha + lda.b #0xFF + sta.l slot_limit_low + pla + php + rep #0x20 + pha + lda.l allocated_tile_id + and.w #0x00ff + sta.l allocated_tile_id + pla + plp + rts +init_with_tile_id_wide: +""" +16-bit caller convention: A = full tile_id (0..0x3FF), M = 16. +Stores both bytes verbatim. Use this for inventory / future regions +that go past tile_id 0xFF. Also resets slot_limit_low to 0xFF so the +clamp is a no-op until the inventory slot init opts in. +""" + + + sta.l allocated_tile_id + pha + sep #0x20 + lda.b #0xFF + sta.l slot_limit_low + rep #0x20 + pla rts init: pha - lda.b #0x00 + rep #0x20 + lda.w #0x0000 sta.l allocated_tile_id + sep #0x20 + lda.b #0xFF + sta.l slot_limit_low pla rts .if BATTLE_ENABLED { @@ -127,14 +192,32 @@ init_battle_far: rtl } increment: +""" +Advance the 16-bit allocator by 1. Preserves M / A state. +Clamps the low byte at `slot_limit_low` so an inventory slot whose +name overflows K tiles keeps overwriting its last tile instead of +bleeding into the next slot's tile_id range. Non-inventory regions +set slot_limit_low = 0xFF so the clamp never fires. +""" + + + php + sep #0x20 pha lda.l allocated_tile_id + cmp.l slot_limit_low + bcs _inc_clamped + rep #0x20 + lda.l allocated_tile_id inc - and #0xff sta.l allocated_tile_id + sep #0x20 +_inc_clamped: pla + plp rts get: +"""Return current 16-bit allocator value in A (caller sets M).""" lda.l allocated_tile_id rts } @@ -149,14 +232,24 @@ get: prev_char = counter + 2 current_char = prev_char + 1 tilemap_offset = 0x1d - buffer_ptr = 0x703000 - buffer_size = 0x300 + buffer_ptr = VWF_CHR_BUFFER + buffer_size = VWF_CHR_BUFFER_SIZE last_drawn_text_ptr = buffer_ptr + buffer_size + 2 init: -"""font_ptr = assets_menu_font_dat ; moved to direct use of assets_menu_font_dat""" -; Initialize the renderer -; clear a chunk of ram -; resets variables +""" +font_ptr = assets_menu_font_dat ; moved to direct use of assets_menu_font_dat. + +Resets the per-render scratch (bits_left_on_tile / temp / counter) +and zeros the allocator. Clears ONLY the CHR slice owned by the +current region (`VwfConfig.tile_id_base * 16` for +`VwfConfig.slot_budget * 16` bytes), mirror of the per-slot clear +in battle's `init_inventory_for_current_slot`. Wiping the whole +buffer here clobbered other regions' CHR mid-frame, so callers +populate config first and `init` confines its $FF/$00 fill to +their slot. +""" + + .if ENABLE_KERNING_MENU { stz.b prev_char } @@ -170,22 +263,347 @@ _brk_init_bits: initialize(temp) stz.b temp pha - lda #0x00 - phx - ldx.w #0 -_clear_loop: +; --- Per-region CHR clear from VwfConfig --- + php + rep #0x30 + lda.l VWF_CONFIG_BASE + VwfConfig.tile_id_base + and.w #0x01FF ; 9-bit tile_id_base + asl + asl + asl + asl ; * 16 + clc + adc.w #VWF_CHR_BUFFER & 0xFFFF + tax + lda.l VWF_CONFIG_BASE + VwfConfig.slot_budget + and.w #0x00FF + asl + asl + asl ; budget * 8 word-pairs + tay + beq _init_skip_clear + sep #0x20 + +_init_clear_loop: lda.b #0xFF - sta.l buffer_ptr, x + sta.l VWF_CHR_BUFFER & 0xFF0000, x + inx lda.b #0x00 - sta.l buffer_ptr + 1, x + sta.l VWF_CHR_BUFFER & 0xFF0000, x inx + dey + bne _init_clear_loop + +_init_skip_clear: + plp + pla ; balances the `pha` after `stz.b temp` ; without this the +; `rts` popped the saved A byte as the high byte of the +; return address and item_description.draw_trampoline ran +; off into a BRK opcode on every menu open. + initialize(counter) + rts +flush_chr_to_vram: +""" +NMI-callable VWF CHR flush. Gates on `VWF_CHR_DIRTY` ; if set, +reads `VwfConfig.chr_src_offset` / `chr_vram_word` / `chr_byte_count` +from the config struct and DMAs the slice from `VWF_CHR_BUFFER + +chr_src_offset` to VRAM word `chr_vram_word`. Clears the dirty +byte on the way out. + +Designed to hang off the field NMI hook ; running in vblank means +the DMA does not tear visible scanlines and we drop the explicit +`wait_for_vblank` the synchronous tail-of-render flush needed. +Battle-side equivalent of the partial CHR DMA in +`messages_vwf.dma_transfer`. +""" + + + { + php + sep #0x20 + rep #0x10 + lda.l VWF_CHR_DIRTY + beq _flush_skip_a + lda.b #0x00 + sta.l VWF_CHR_DIRTY +; CRITICAL: clear HDMAEN bit 6 before retargeting ch6 for a one-shot +; DMA. The treasure-popup rolling buffer also arms ch6 (BG3VOFS HDMA) +; via $1BAE ; if we rewrite $4360-$4365 with the VWF flush descriptor +; without disabling HDMA bit 6 first, the per-scanline HDMA keeps +; running off the rewritten registers and writes VWF_CHR_BUFFER bytes +; to VMDATAL on every visible scanline for the rest of the frame. +; That trashes VRAM, kills BG3 scroll modulation, and made the +; treasure-popup band display garbage stride after the first frame. +; Push HDMAEN on the stack so we restore the prior mask exactly +; (treasure ch6 + drops ch4 + anything else vanilla armed). + lda.l 0x00420C + pha + and.b #0xBF + sta.l 0x00420C +; Channel 6: free per the FF4 DMA audit (vanilla btlgfx + menu +; never touch $4360..$436F ; ch7 already carries battle's +; `_sram_dma_transfer_7`, small_vwf's libmz transfers and the +; field NMI's tilemap upload). Source = VWF_CHR_BUFFER + +; VWF_CHR_FLUSH_OFFSET (engine constant, identical for battle + +; field). Dest VRAM word + size come from VwfConfig so each caller +; targets its own CHR slot without forking the upload path. + lda.b #0x01 ; DMAP: word transfer (2 byte regs, alt low/high) + sta.l 0x004360 + lda.b #0x18 ; BBAD: $2118 VMDATAL + sta.l 0x004361 + rep #0x20 + lda.w #( VWF_CHR_BUFFER + VWF_CHR_FLUSH_OFFSET ) & 0xFFFF + sta.l 0x004362 ; A1T low+mid + sep #0x20 + lda.b #( VWF_CHR_BUFFER + VWF_CHR_FLUSH_OFFSET ) >> 16 + sta.l 0x004364 ; A1B source bank + rep #0x20 + lda.l VWF_CONFIG_BASE + VwfConfig.chr_vram_word + sta.l 0x002116 ; VMADD + lda.l VWF_CONFIG_BASE + VwfConfig.chr_byte_count + sta.l 0x004365 ; DAS + sep #0x20 + lda.b #0x80 + sta.l 0x002115 ; VMAIN: increment on $2119, +1 word + lda.b #0x40 + sta.l 0x00420B ; MDMAEN ch6 +; Restore prior HDMAEN bits (treasure ch6, drops ch4, etc) ; one-shot +; DMA on ch6 has completed by now and HDMA can resume reading the +; treasure HDMA table for per-scanline BG3VOFS on the next frame. + pla + sta.l 0x00420C + +_flush_skip_a: +; --- Secondary descriptor flush (region 1B, drops in the treasure +; popup). Shares DMA channel 6 with the primary flush ; runs after +; the primary descriptor's DMA finishes so we never re-arm a busy +; channel. Each descriptor covers a disjoint slice of VRAM so two +; sequential DMAs land at distinct dest addresses without conflict. +; Secondary source offset is its OWN word so drops can DMA only its +; CHR slice ; primary keeps starting at VWF_CHR_FLUSH_OFFSET. +; +; Guard against savestates captured before the secondary descriptor +; existed : their SRAM at $7070C4..$7070CA carries random bytes, so +; before firing the DMA we whitelist VWF_CHR_VRAM_WORD_B against the +; two known arming targets ($2B70 drops, $2C00 description) and +; reject anything else as uninitialised. Stale SRAM almost certainly +; will not match either constant, so the secondary DMA stays dormant +; until items_menu_vwf or item_description explicitly arms it. Stops +; the save-selection sprite CHR from being trashed by random garbage +; on the very first NMI after a stale load. + sep #0x20 + lda.l VWF_CHR_DIRTY_B + beq _flush_skip_b + lda.b #0x00 + sta.l VWF_CHR_DIRTY_B + rep #0x20 + lda.l VWF_CHR_VRAM_WORD_B + cmp.w #0x2B70 + beq _vram_word_b_ok + cmp.w #0x2C00 + bne _flush_skip_b_late + +_vram_word_b_ok: + sep #0x20 +; Same HDMAEN-bit-6 save/restore dance as the primary flush ; ch6 is +; shared with treasure BG3VOFS HDMA and must not stay armed while +; we hijack its registers for the secondary VRAM blit. + lda.l 0x00420C + pha + and.b #0xBF + sta.l 0x00420C + lda.b #0x01 + sta.l 0x004360 + lda.b #0x18 + sta.l 0x004361 + rep #0x20 + lda.l VWF_CHR_SRC_OFFSET_B + clc + adc.w #VWF_CHR_BUFFER & 0xFFFF + sta.l 0x004362 + sep #0x20 + lda.b #VWF_CHR_BUFFER >> 16 + sta.l 0x004364 + rep #0x20 + lda.l VWF_CHR_VRAM_WORD_B + sta.l 0x002116 + lda.l VWF_CHR_BYTE_COUNT_B + sta.l 0x004365 + sep #0x20 + lda.b #0x80 + sta.l 0x002115 + lda.b #0x40 + sta.l 0x00420B + pla + sta.l 0x00420C + +_flush_skip_b_late: + sep #0x20 + +_flush_skip_b: + plp + rts + } +render_with_config: +""" +Config-driven VWF entry: reads `VwfConfig` at `VWF_CONFIG_BASE`, +sets up the allocator + render state from it, then walks +VWF_TEXT_BUFFER through `draw_text_buffer`. Single call site +replaces the bespoke init / display_char loops that the field- +items helper and the battle inventory text walker each carry. + +VwfConfig fields consumed (matches `src/vwf_state.i`): + tile_id_base first tile_id this slot owns + slot_budget +K-1 clamp ; $FF = no clamp + tilemap_base absolute WRAM byte index of the destination + tilemap row ; display_char advances tilemap_offset + (DP $1D) by 2 per blit + +`font_ptr` / `kerning_ptr` are reserved in the struct but +display_char still hardcodes `assets_menu_font_dat` for this +phase ; swapping the font fetch to indirect-through-config is the +next refactor and lets battle keep its own font without forking +the engine. + +M=8, X=16 on entry. Stack-balanced, RTS. +""" + + + { + php + sep #0x20 + rep #0x10 +; Reset per-glyph carry state so each slot's first char starts fresh. +; prev_char carries over from the previous slot's last char ; without +; a reset the new slot's first kerning lookup pairs against an +; unrelated glyph and shifts the allocator several tiles. Same goes +; for bits_left_on_tile : if a prior render ended with the value +; pinned by the $09 clamp at _end (kerning overshoot path), the new +; slot would start with bits_left=0 and burn its first tile. +; Symptom before this reset : treasure-inventory slot 0 rendered as +; "le d'or" (Aigu chopped) right after drops finished rendering. + .if ENABLE_KERNING_MENU { + stz.b prev_char + } + lda.b #0x08 + sta.b bits_left_on_tile +; Allocator base + clamp via the 16-bit `init_with_tile_id_wide` +; entry. tile_id_base is a 9-bit value (0..$1FF) so we need the +; full word ; the 8-bit init zero-extends and caps at $FF which +; would have wrapped field-menu slots 6..10 (base $C0 + 10*10 = +; $124) back into the menu font CHR range. + rep #0x20 + lda.l VWF_CONFIG_BASE + VwfConfig.tile_id_base + jsr.w render_allocator.init_with_tile_id_wide +; Belt + braces: re-stash allocated_tile_id verbatim so any +; interleaved render.init can not zero it on us before display_char +; reads it back. + lda.l VWF_CONFIG_BASE + VwfConfig.tile_id_base + sta.l render_allocator.allocated_tile_id + sep #0x20 + lda.l VWF_CONFIG_BASE + VwfConfig.tile_id_base + clc + adc.l VWF_CONFIG_BASE + VwfConfig.slot_budget + sec + sbc.b #0x01 + sta.l render_allocator.slot_limit_low +; --- Clear this slot's CHR slice to $FF $00 (blank 2bpp / 4bpp +; tile) so the new blit does not pile pixels onto the previous +; tenant. Mirror of `messages_vwf.init_inventory_for_current_slot` +; in `src/battle/message.s` ; clear loop spans +; (slot_budget * 16) bytes at VWF_CHR_BUFFER + tile_id_base * 16. + php + rep #0x30 + lda.l VWF_CONFIG_BASE + VwfConfig.tile_id_base + and.w #0x01FF ; 9-bit tile_id_base + asl + asl + asl + asl ; * 16 + clc + adc.w #VWF_CHR_BUFFER & 0xFFFF + tax + lda.l VWF_CONFIG_BASE + VwfConfig.slot_budget + and.w #0x00FF + asl + asl + asl ; words to clear = budget * 8 (16 bytes/tile, written as words) + tay + sep #0x20 + +_chr_clear_loop: + lda.b #0xFF + sta.l VWF_CHR_BUFFER & 0xFF0000, x + inx + lda.b #0x00 + sta.l VWF_CHR_BUFFER & 0xFF0000, x + inx + dey + bne _chr_clear_loop + plp +; Render scratch state. + lda.b #0x08 + sta.b bits_left_on_tile + stz.b temp + stz.b counter +; tilemap_offset = config.tilemap_base (16-bit). + rep #0x20 + lda.l VWF_CONFIG_BASE + VwfConfig.tilemap_base + sta.b tilemap_offset + sep #0x20 + jsr.w draw_text_buffer +; Tell the NMI flush hook this slot needs a VRAM upload. Mirror of +; `battle_render.dma_dirty_slots` ; engine-side so every consumer +; (field items, item descriptions, treasure list, ...) signals dirty +; without knowing about VRAM addresses. + lda.b #0x01 + sta.l VWF_CHR_DIRTY + plp + rts + } + +draw_text_buffer: +""" +Unified entry: walk a null-terminated string at VWF_TEXT_BUFFER +and blit each byte via display_char. + +Caller responsibilities (read from VwfConfig at VWF_CONFIG_BASE in +the next phase ; for now the field-items helper hand-sets these +directly): + - render_allocator.allocated_tile_id set via init_with_tile_id + - render_allocator.slot_limit_low set per slot budget + - render.bits_left_on_tile set to 8 + - render.temp, render.counter cleared + - render.tilemap_offset (= DP $1D) absolute WRAM byte index + of the bottom tilemap row + start ; display_char auto- + increments by 2 per blit + - X, Y free for caller use + +Each iteration reads the next byte from `VWF_TEXT_BUFFER + Y` (Y +caller-zeroed on entry), exits on $00. display_char writes both +the CHR slice and the tile_id at tilemap_offset, and increments +the allocator (clamped at slot_limit_low). +""" + + + { + phx + ldx.w #0x0000 + +_dtb_loop: + lda.l VWF_TEXT_BUFFER, x + beq _dtb_done inx - cpx.w #buffer_size + 2 - bne _clear_loop + phx + jsr.w display_char + plx + bra _dtb_loop + +_dtb_done: plx - pla - initialize(counter) rts + } deinit: { _restore_long(buffer_ptr) @@ -216,14 +634,16 @@ make_pointers: lda.w #0x0000 sep #0x20 - lda.l render_allocator.allocated_tile_id - - sta.b oldtilepos +; Read full 16-bit allocated_tile_id so 9-bit tile_ids ($100+) keep +; their high bit in oldtilepos. The previous 8-bit read masked bit +; 8 and field-menu VWF blits (tile_id_base $100) landed at SRAM +; offset 0, stomping the static font region. rep #0x20 + lda.l render_allocator.allocated_tile_id asl asl asl - asl + asl ; tile_id * 16, 16-bit result sta.b oldtilepos tay sep #0x20 @@ -443,7 +863,20 @@ _no_adjustment: pla _end: +; Clamp temp to 0..8 before writing bits_left_on_tile : kerning +; adjustments can push the value past 8 (e.g. when a kerning entry +; subtracts more than the current tile has left), and the downstream +; dispatch at $97FC indexes a 9-entry jump table by bits_left * 2. +; Out-of-range values land off the table end and execute random +; bytes (BRK $00 on the held-DOWN field-items scroll). Treat any +; value > 8 as "no bits left in this tile" so the next loop pass +; allocates a fresh tile_id instead of crashing. lda.b temp + cmp.b #0x09 + bcc _bits_left_in_range + lda.b #0x00 + +_bits_left_in_range: sta.b bits_left_on_tile _overflow: pha @@ -571,12 +1004,26 @@ small_vwf_kerning_binary_ext: rtl } tilemap_write_no_inc: +""" +Write `allocated_tile_id` low byte to `(tilemap_offset)` and OR +`VwfConfig.flags` into the next (palette/attr) byte. The flag +mask used to be hard-coded `$01` (the 9-bit tile_id bit for the +512-tile dialog window) but the field-menu items live in 2bpp +BG3 where bit 0 of the attr byte is tile_id bit 8 ; an always-on +OR pushed all our tile_ids into the +256 half of CHR and the +high inventory slots wrapped past $FF in unpredictable ways. +Letting the caller pick the OR value via the config struct keeps +dialog (set `flags = $01`) working without forcing the menu items +to honour a bit they do not own. +""" + + _base_addr = 0x7e0000 lda.l render_allocator.allocated_tile_id ldx.b tilemap_offset sta.l _base_addr, x lda.l _base_addr + 1, x - ora.b #0x01 + ora.l VWF_CONFIG_BASE + VwfConfig.flags sta.l _base_addr + 1, x rts tilemap_write: diff --git a/src/sram_management.s b/src/sram_management.s index 7c3d8ec..33adcca 100644 --- a/src/sram_management.s +++ b/src/sram_management.s @@ -18,9 +18,54 @@ ROM-header SRAM-size byte at $00:FFD8 (clamped to 7 → 128KB max). jsr.l 0x15C9AA lda.b #1 jsr.w _clear_ram - + jsr.w _zero_vwf_description_region rtl +_zero_vwf_description_region: +""" +Belt-and-suspenders boot init : zero the menu VWF VRAM union + +$5000-$5FF0 ($1000 bytes from VRAM word $2800). Covers : + Region 1 $5000-$56A0 (field / treasure item-name CHR) + Region 1B $56E0-$5AA0 (drops item-name CHR, treasure popup) + Region D $5800-$5FF0 (item description CHR) + +Without this, the menu save/restore round-trips whatever leftover +bytes happened to be at these VRAM addresses at the very first +SaveDlgGfx, which then persist forever through the SRAM save buffer +as glyph residue in the VRAM viewer (overworld never reads these +slices). Overworld re-uploads its own map CHR on every map transition, +so this one-time zero does not erase live overworld data ; it only +breaks the carry-forward of stale boot-VRAM. + +SRAM $70:0000+ was just zeroed by `_clear_ram` above, so it works as +a fixed-source zero-fill DMA. +""" + + + php + sep #0x20 + lda.b #0x80 + sta.l 0x002100 ; PPU force blank + lda.b #0x80 + sta.l 0x002115 ; VMAIN : increment on $2119, +1 word + rep #0x20 + lda.w #0x2800 ; VMADD : VRAM word $2800 (= byte $5000) + sta.l 0x002116 + lda.w #0x1811 ; $4300 = $11 (mode 1, fixed src) | $4301 = $18 (VMDATAL) + sta.l 0x004300 + lda.w #0x0000 ; A1T : SRAM byte $00 (freshly zeroed by _clear_ram) + sta.l 0x004302 + lda.w #0x1000 ; DAS : $1000 byte transfer = $5000..$5FFF + sta.l 0x004305 + sep #0x20 + lda.b #0x70 ; A1B : SRAM bank $70 + sta.l 0x004304 + lda.b #0x01 + sta.l 0x00420B ; MDMAEN ch0 + plp + rts + _clear_ram: { ; Get RAM size from ROM header diff --git a/src/vwf.s b/src/vwf.s index a13a894..b9e05c4 100644 --- a/src/vwf.s +++ b/src/vwf.s @@ -2,7 +2,9 @@ Dialog/text VWF rendering engine: control-code parser (`parse`), per-glyph blit (`vwf_putchar`), tile-position tracking (`TILEPOS`/`BITSLEFT`), button-glyph + ending-symbol drawing, and the gil-window tilemaps. """ +.include "config.i" .include "src/definitions.s" +.include "bank20.i" .include "src/libmz.i" .table "text/ff4_menus.tbl" .import "libmz" @@ -11,1190 +13,1196 @@ tracking (`TILEPOS`/`BITSLEFT`), button-glyph + ending-symbol drawing, and the g .import "assets" .import "kerning" -wait_for_action_button: -"""RTL trampoline that polls the gamepad until any action button is pressed (delegates to original `_waitpad`).""" - jsr.w _waitpad - rtl +; `.import` links kerning but doesn't surface its alloc-body GLOBALs by name; +; declare the ones used here at root scope. +.extern dialog_get_kerning_adjustment_binary_search -.if ENABLE_BUTTON_DISPLAY { -_get_action_button_id: - lda.l 0x0016A9 - bne _custom_mapping - lda.b #0x00 - xba - lda.b #0x00 - rts -_custom_mapping: - lda.b #0x00 - xba - lda.l 0x001A37 ; action button id location - rts -} +.alloc vwf_block in bank20_reloc { + wait_for_action_button: + """RTL trampoline that polls the gamepad until any action button is pressed (delegates to original `_waitpad`).""" + jsr.w _waitpad + rtl -.macro _make_color(r, g, b) { - red := ( r >> 3 ) & 0b11111 - blue := ( b >> 3 ) & 0xb11111 - green := ( g >> 3 ) & 0xb11111 - .dw ( blue << 10 ) + ( green << 5 ) + red -} + .if ENABLE_BUTTON_DISPLAY { + _get_action_button_id: + lda.l 0x0016A9 + bne _custom_mapping + lda.b #0x00 + xba + lda.b #0x00 + rts + _custom_mapping: + lda.b #0x00 + xba + lda.l 0x001A37 ; action button id location + rts + } -.macro hex_color(color) { - """Emit a 15-bit BGR SNES palette word from a 24-bit literal.""" - _make_color(color & 0xff, ( color >> 8 ) & 0xff, ( color >> 16 )) -} + .macro _make_color(r, g, b) { + red := ( r >> 3 ) & 0b11111 + blue := ( b >> 3 ) & 0xb11111 + green := ( g >> 3 ) & 0xb11111 + .dw ( blue << 10 ) + ( green << 5 ) + red + } + .macro hex_color(color) { + """Emit a 15-bit BGR SNES palette word from a 24-bit literal.""" + _make_color(color & 0xff, ( color >> 8 ) & 0xff, ( color >> 16 )) + } -_button_colors: - ; 0 - ; A Button - _make_color(0xeb, 0x1a, 0x1d) - _make_color(120, 10, 12) - ; 1 - ; B Button - _make_color(0xfe, 0xce, 0x15) - _make_color(136, 108, 0) - ; 2 - ; X Button - _make_color(0x07, 0x49, 0xb4) - _make_color(0, 53, 144) - ; 3 - ; Y Button - _make_color(0x00, 0x8d, 0x45) - _make_color(0, 70, 34) - ; 4 - ; Select Buttons - .dw 0b0011110111101111 - .dw 0b0001110011100111 - - -update_palette: -""" -Refresh menu/dialog palette entries from the configured colour at $16AA. -With ENABLE_BUTTON_DISPLAY, also pulls the action-button id and writes -the matching colour + shadow pair from `_button_colors` into $0CEF/$0CF1. -""" - ldx.w 0x16AA - stx.w 0x0CDD - stx.w 0x0CE5 -.if ENABLE_BUTTON_DISPLAY { - stx.w 0x0CED -; black for the shadow but it could be a darker version of the color - ldx.w #0x0000 - stx.w 0x0CEF - pha - jsr.w _get_action_button_id - - cmp #0x04 - bcc _proceed - ldx.w #5 -_proceed: - asl - asl - tax - - rep #0x20 - lda.l _button_colors, x ; color - sta.w 0x0CF1 - lda.l _button_colors + 2, x ; shadow color - sta.w 0x0CEF - sep #0x20 - lda #0x00 - xba - pla -} -rtl - -window_palette: -"""4 dialog/menu sub-palettes (4 colours each, BGR15).""" - ; Menu color palette I probably need to add one color - ; Palette 1, normal text - .db 0x00, 0x00 - .db 0x00, 0x40 - .db 0xCE, 0x39 - .db 0xFF, 0x7F - -; Palette 2, greyed out text patched replaced the grey with black - .db 0x00, 0x00 - .db 0x00, 0x40 - .db 0x00, 0x00 - .db 0xff, 0x7f - -; Palette 3, yellow - .db 0x00, 0x00 - .db 0x00, 0x40 - .db 0x80, 0x02 - .db 0x7F, 0x03 - -; Palette 4, red - .db 0x00, 0x00 - .db 0x00, 0x40 - .db 0xFF, 0x40 - .db 0x7F, 0x2E - -;current_pos = 0x3d -;current_text_pointer = 0x0772 -; 0x07 used for loop counter - -; ****************** -; ** Declarations ** -; ****************** - vram_tile_set_pointer = 0x6800 - vram_tile_map_pointer = 0x2C00 - WRAM = field_vwf.tile_buffer - - WRAMPTR = 0x2108 - -vwfinit: -""" -One-shot dialog VWF initialisation: clear the WRAM scratch area, -DMA the VWF tile buffer + the original font tileset into VRAM, -and point BG3 at the new $6000 tile-set base. -""" - jsr.w _clr ; on efface un peu de Wram - - jsr.w wait_for_vblank - dma_transfer_to_vram_call(WRAM, vram_tile_set_pointer, 0x0690, 0x1801) - jsr.w wait_for_vblank - dma_transfer_to_vram_call(WRAM, vram_tile_set_pointer + 0x348, 0x0690, 0x1801) - -; copy the old font tileset - jsr.w wait_for_vblank - dma_transfer_to_vram_call(0x0AF000, 0x6000, 0x800, 0x1801) - jsr.w wait_for_vblank - dma_transfer_to_vram_call(0x0AF000 + 0x800, 0x6000 + 0x400, 0x800, 0x1801) - jsr.w wait_for_vblank - -; Sets the BG3 vram pointer to 0x6000 - lda 0x210C - and #0xF0 - clc - adc #0x06 - sta 0x210C + _button_colors: + ; 0 + ; A Button + _make_color(0xeb, 0x1a, 0x1d) + _make_color(120, 10, 12) + ; 1 + ; B Button + _make_color(0xfe, 0xce, 0x15) + _make_color(136, 108, 0) + ; 2 + ; X Button + _make_color(0x07, 0x49, 0xb4) + _make_color(0, 53, 144) + ; 3 + ; Y Button + _make_color(0x00, 0x8d, 0x45) + _make_color(0, 70, 34) + ; 4 + ; Select Buttons + .dw 0b0011110111101111 + .dw 0b0001110011100111 + + + update_palette: + """ + Refresh menu/dialog palette entries from the configured colour at $16AA. + With ENABLE_BUTTON_DISPLAY, also pulls the action-button id and writes + the matching colour + shadow pair from `_button_colors` into $0CEF/$0CF1. + """ + ldx.w 0x16AA + stx.w 0x0CDD + stx.w 0x0CE5 + .if ENABLE_BUTTON_DISPLAY { + stx.w 0x0CED + ; black for the shadow but it could be a darker version of the color + ldx.w #0x0000 + stx.w 0x0CEF + pha + jsr.w _get_action_button_id + + cmp #0x04 + bcc _proceed + ldx.w #5 + _proceed: + asl + asl + tax + + rep #0x20 + lda.l _button_colors, x ; color + sta.w 0x0CF1 + lda.l _button_colors + 2, x ; shadow color + sta.w 0x0CEF + sep #0x20 + lda #0x00 + xba + pla + } rtl - ;** routine principale -vwfstart: -""" -Main entry for dialog VWF rendering: sets 8-bit A / 16-bit X-Y, enables HDMA, and falls into the parser loop -that consumes the dialog text stream. -""" - sep #0x20 - rep #0x10 - - lda.b #0x01 - sta.w 0x420D - -; 0x04-0x4F - var_base = 0x23 - CNTR = var_base - CURRENT_C = var_base + 2 - BITSLEFT = var_base + 4 - font_addr = var_base + 6 ; 7 8 - oldtilepos = var_base + 9 - TILEPOS = var_base + 11 - dialog_ptr = 0x20 - - no_wait_for_action = 0xcb - - lda #0 - jsr.w setup_font - - php - sep #0x20 -.macro _clear_16_bit(var) { - stz.b var - stz.b var + 1 -} -_clear_16_bit(CNTR) -_clear_16_bit(CURRENT_C) -_clear_16_bit(BITSLEFT) -_clear_16_bit(oldtilepos) -_clear_16_bit(TILEPOS) -plp + window_palette: + """4 dialog/menu sub-palettes (4 colours each, BGR15).""" + ; Menu color palette I probably need to add one color + ; Palette 1, normal text + .db 0x00, 0x00 + .db 0x00, 0x40 + .db 0xCE, 0x39 + .db 0xFF, 0x7F + + ; Palette 2, greyed out text patched replaced the grey with black + .db 0x00, 0x00 + .db 0x00, 0x40 + .db 0x00, 0x00 + .db 0xff, 0x7f + + ; Palette 3, yellow + .db 0x00, 0x00 + .db 0x00, 0x40 + .db 0x80, 0x02 + .db 0x7F, 0x03 + + ; Palette 4, red + .db 0x00, 0x00 + .db 0x00, 0x40 + .db 0xFF, 0x40 + .db 0x7F, 0x2E + + ;current_pos = 0x3d + ;current_text_pointer = 0x0772 + ; 0x07 used for loop counter + + ; ****************** + ; ** Declarations ** + ; ****************** + vram_tile_set_pointer = 0x6800 + vram_tile_map_pointer = 0x2C00 + WRAM = field_vwf.tile_buffer + + WRAMPTR = 0x2108 + + vwfinit: + """ + One-shot dialog VWF initialisation: clear the WRAM scratch area, + DMA the VWF tile buffer + the original font tileset into VRAM, + and point BG3 at the new $6000 tile-set base. + """ + jsr.w _clr ; on efface un peu de Wram + + jsr.w wait_for_vblank + dma_transfer_to_vram_call(WRAM, vram_tile_set_pointer, 0x0690, 0x1801) + jsr.w wait_for_vblank + dma_transfer_to_vram_call(WRAM, vram_tile_set_pointer + 0x348, 0x0690, 0x1801) + + ; copy the old font tileset + jsr.w wait_for_vblank + dma_transfer_to_vram_call(0x0AF000, 0x6000, 0x800, 0x1801) + jsr.w wait_for_vblank + dma_transfer_to_vram_call(0x0AF000 + 0x800, 0x6000 + 0x400, 0x800, 0x1801) + jsr.w wait_for_vblank + + ; Sets the BG3 vram pointer to 0x6000 + lda 0x210C + and #0xF0 + clc + adc #0x06 + sta 0x210C + + rtl + ;** routine principale + + vwfstart: + """ + Main entry for dialog VWF rendering: sets 8-bit A / 16-bit X-Y, enables HDMA, and falls into the parser loop + that consumes the dialog text stream. + """ + sep #0x20 + rep #0x10 + + lda.b #0x01 + sta.w 0x420D + + ; 0x04-0x4F + var_base = 0x23 + CNTR = var_base + CURRENT_C = var_base + 2 + BITSLEFT = var_base + 4 + font_addr = var_base + 6 ; 7 8 + oldtilepos = var_base + 9 + TILEPOS = var_base + 11 + dialog_ptr = 0x20 + + no_wait_for_action = 0xcb + + lda #0 + jsr.w setup_font + + php + sep #0x20 + .macro _clear_16_bit(var) { + stz.b var + stz.b var + 1 + } + _clear_16_bit(CNTR) + _clear_16_bit(CURRENT_C) + _clear_16_bit(BITSLEFT) + _clear_16_bit(oldtilepos) + _clear_16_bit(TILEPOS) + plp -lda.b #0x08 -sta.b BITSLEFT + lda.b #0x08 + sta.b BITSLEFT -jsr.l vwfinit + jsr.l vwfinit -jsr.w load_letter -bra _firstrun + jsr.w load_letter + bra _firstrun -main: -"""Per-character main loop.""" - jsr.w load_letter_inc + main: + """Per-character main loop.""" + jsr.w load_letter_inc -_firstrun: - jmp.w parse - bra main + _firstrun: + jmp.w parse + bra main -_fin: - ; this fixes a subtle issue where NPCs sprites positions and their collision table would be corrupted - xba - lda.b #0 - xba - rtl + _fin: + ; this fixes a subtle issue where NPCs sprites positions and their collision table would be corrupted + xba + lda.b #0 + xba + rtl -;****************** -;** Parsing code ** -;****************** + ;****************** + ;** Parsing code ** + ;****************** -parse: -"""Dialog control-code dispatcher.""" + parse: + """Dialog control-code dispatcher.""" -; Message Break - cmp #0x00 - bne _nxt1 - lda #0x01 - sta 0xDE -.if ENABLE_BUTTON_DISPLAY { - jsr.w _draw_button -} -jmp.w _fin - -_nxt1: - cmp #0x01 - bne _nxt2 - jmp.w newline - -_nxt2: - cmp #0x02 - bne _nxt3 - jmp.w space - -_nxt3: - ;Changement de Musique - cmp #0x03 - bne _nxt4 - jmp.w _musique - -_nxt4: - ; Nom des personages - cmp #0x04 - bne _nxt5 - jmp.w _display_character_name - -_nxt5: - -; wait - cmp #0x05 - bne _nxt6 - jmp.w _code05 - -_nxt6: - ; Close window after dialog end - cmp #0x06 - bne _nxt7 - lda #0x02 - sta 0xde + ; Message Break + cmp #0x00 + bne _nxt1 + lda #0x01 + sta 0xDE + .if ENABLE_BUTTON_DISPLAY { + jsr.w _draw_button + } jmp.w _fin - ; Display item - -_nxt7: - cmp #0x07 - bne _nxt8 - jmp.w _code07 - -; display gils count - -_nxt8: - cmp #0x08 - bne _nxt_fb - jmp.w _code08 - -_nxt_fb: - cmp #0xFC - bne _nxt_fc - jmp.w _suit3 - -_nxt_fc: - cmp #0xFE - bne _nxt_fe - jsr.w load_letter_inc - jsr.w setup_font - jmp.w main - -_nxt_fe: - - jsr.w _makeptr - jsr.w _shift_new - jsr.w _wdisplay - - jmp.w main - - -;*********** -;** Space ** -;*********** -space: -"""Space control code ($02): advance tile cursor.""" - jsr.w load_letter_inc - clc - adc.b TILEPOS - jmp.w main + _nxt1: + cmp #0x01 + bne _nxt2 + jmp.w newline + + _nxt2: + cmp #0x02 + bne _nxt3 + jmp.w space + + _nxt3: + ;Changement de Musique + cmp #0x03 + bne _nxt4 + jmp.w _musique + + _nxt4: + ; Nom des personages + cmp #0x04 + bne _nxt5 + jmp.w _display_character_name + + _nxt5: + + ; wait + cmp #0x05 + bne _nxt6 + jmp.w _code05 + + _nxt6: + ; Close window after dialog end + cmp #0x06 + bne _nxt7 + lda #0x02 + sta 0xde + jmp.w _fin + ; Display item + + _nxt7: + cmp #0x07 + bne _nxt8 + jmp.w _code07 + + ; display gils count + + _nxt8: + cmp #0x08 + bne _nxt_fb + jmp.w _code08 + + _nxt_fb: + cmp #0xFC + bne _nxt_fc + jmp.w _suit3 + + _nxt_fc: + cmp #0xFE + bne _nxt_fe + jsr.w load_letter_inc + jsr.w setup_font + jmp.w main + + _nxt_fe: + + jsr.w _makeptr + jsr.w _shift_new + jsr.w _wdisplay + + jmp.w main + + + ;*********** + ;** Space ** + ;*********** + + space: + """Space control code ($02): advance tile cursor.""" + jsr.w load_letter_inc + clc + adc.b TILEPOS + jmp.w main + + ;****************** + ;Cout en gils + ;****************** + + _code08: + lda.w 0x08F8 + sta.b 0x30 + lda.w 0x08F9 + sta.b 0x31 + lda.w 0x08FA + sta.b 0x32 + jsr.l 0x15C324 + + ldx.w #0x0000 + + _loop_b5c3: + lda.b 0x36, x + cmp #0x80 + bne _loop_b5d2 + inx + cpx.w #0x0005 + beq _loop_b5d2 + jmp.w _loop_b5c3 + + _loop_b5d2: + lda #0x00 + xba + lda.b 0x36, x + ; Old code + ;00B5D4 STA 0x0774,Y + ;00B5D7 LDA #0xFF + ;00B5D9 STA 0x0834,Y + ;00B5DC INY + phx + phy + + sta.b CURRENT_C ; appel de la vwf + + jsr.w _makeptr + jsr.w _shift_new + jsr.w _wdisplay + + ply + plx + + inx + cpx.w #0x0006 + bne _loop_b5d2 + + jmp.w main + + ;**************** + ;** printfname ** + ;**************** + + _display_character_name: + { + jsr.w load_letter_inc + xba + lda #0x00 + xba + + asl + pha + asl + clc + adc 1, s + tax + pla + + ldy.w #0x0000 + next: + lda #0x00 + xba + lda 0x1500, x + sta.b CURRENT_C + cmp #0xFF + beq exit + + phx + phy + jsr.w _makeptr + jsr.w _shift_new + jsr.w _wdisplay + ply + plx + + suite: + inx + + iny + cpy.w #0x0006 + beq exit + jmp.w next + exit: + jmp.w main + } + ;******************** + ;** Nouvelle ligne ** + ;******************** -;****************** -;Cout en gils -;****************** - -_code08: - lda.w 0x08F8 - sta.b 0x30 - lda.w 0x08F9 - sta.b 0x31 - lda.w 0x08FA - sta.b 0x32 - jsr.l 0x15C324 - - ldx.w #0x0000 - -_loop_b5c3: - lda.b 0x36, x - cmp #0x80 - bne _loop_b5d2 - inx - cpx.w #0x0005 - beq _loop_b5d2 - jmp.w _loop_b5c3 - -_loop_b5d2: - lda #0x00 - xba - lda.b 0x36, x - ; Old code - ;00B5D4 STA 0x0774,Y - ;00B5D7 LDA #0xFF - ;00B5D9 STA 0x0834,Y - ;00B5DC INY - phx - phy + newline: + """Newline control code ($01): reset BITSLEFT and snap TILEPOS.""" + rep #0x20 + lda.w #8 + sep #0x20 - sta.b CURRENT_C ; appel de la vwf + sta.b BITSLEFT - jsr.w _makeptr - jsr.w _shift_new - jsr.w _wdisplay + lda.b TILEPOS + clc + ;Second line + cmp #0x1A + 1 + bcs suit - ply - plx + lda.b #0x1A + sta.b TILEPOS + bra end - inx - cpx.w #0x0006 - bne _loop_b5d2 + suit: + """Snap TILEPOS to line 3 ($34).""" - jmp.w main + ;Third Line + cmp #0x34 + 1 + bcs _suit2 + lda.b #0x34 + sta.b TILEPOS + bra end -;**************** -;** printfname ** -;**************** + _suit2: -_display_character_name: -{ - jsr.w load_letter_inc - xba - lda #0x00 - xba + ;Forth Line + cmp #0x4E + 1 + bcs _suit3 - asl - pha - asl - clc - adc 1, s - tax - pla + lda.b #0x4E + sta.b TILEPOS + bra end - ldy.w #0x0000 -next: - lda #0x00 - xba - lda 0x1500, x - sta.b CURRENT_C - cmp #0xFF - beq exit - - phx - phy - jsr.w _makeptr - jsr.w _shift_new - jsr.w _wdisplay - ply - plx - -suite: - inx - - iny - cpy.w #0x0006 - beq exit - jmp.w next -exit: - jmp.w main -} -;******************** -;** Nouvelle ligne ** -;******************** - -newline: -"""Newline control code ($01): reset BITSLEFT and snap TILEPOS.""" - rep #0x20 - lda.w #8 - sep #0x20 + _suit3: + ; that's where new ends up + .if ENABLE_BUTTON_DISPLAY { + jsr.w _draw_button + } + lda.b #0x08 sta.b BITSLEFT - lda.b TILEPOS - clc - ;Second line - cmp #0x1A + 1 - bcs suit - - lda.b #0x1A - sta.b TILEPOS - bra end - -suit: -"""Snap TILEPOS to line 3 ($34).""" - -;Third Line - cmp #0x34 + 1 - bcs _suit2 - lda.b #0x34 - sta.b TILEPOS - bra end - -_suit2: - -;Forth Line - cmp #0x4E + 1 - bcs _suit3 - - lda.b #0x4E - sta.b TILEPOS - bra end - -_suit3: - ; that's where new ends up + stz.b CURRENT_C + stz.b TILEPOS + jsr.w _clr + jsr.w _waitpad + jsr.w _wdisplay -.if ENABLE_BUTTON_DISPLAY { - jsr.w _draw_button -} -lda.b #0x08 -sta.b BITSLEFT + end: + """Common newline tail.""" -stz.b CURRENT_C -stz.b TILEPOS -jsr.w _clr -jsr.w _waitpad -jsr.w _wdisplay + jmp.w main -end: -"""Common newline tail.""" + _draw_button: + { + jsr.w _get_action_button_id + cmp #4 ; select + bne _normal_button + lda #0xa5 + bra _store + _normal_button: + lda #0xa4 + _store: + sta.b CURRENT_C + jsr.w _draw_ending_symbol + + rts + } + _draw_ending_symbol: + pha + + lda #0x08 + sta.b BITSLEFT + lda #0x4E + 24 + sta.b oldtilepos + + lda #0x4E + 25 + sta.b TILEPOS + + + ldx.w #0xa2 * 17 + ldy.w #0xcc0 + jsr.w _makeptr + jsr.w _shift_new + jsr.w _wdisplay + + pla + rts + + + ;************* + ;** Musique ** + ;************* + + _musique: + jsr.w load_letter_inc + sta 0x1E01 + lda.b #0x01 + sta 0x1E00 + + jsr.l 0x048004 + jmp.w main + + _code05: + jsr.w load_letter_inc + xba + lda #0x00 + xba + asl + asl + asl + tax + stx 0x08F4 + ldx 0x0000 + stx 0x08F6 + { + ldx 0x08f4 + beq skip + loop: + cpx 0x08f6 + + bne loop + skip: + ldx.w #0x0000 + stx 0x08f4 + } jmp.w main -_draw_button: -{ - jsr.w _get_action_button_id - cmp #4 ; select - bne _normal_button - lda #0xa5 - bra _store -_normal_button: - lda #0xa4 -_store: - sta.b CURRENT_C - jsr.w _draw_ending_symbol - rts -} + .macro _vwf_putchar() { + phx + phy + sta.b CURRENT_C + jsr.w _makeptr + jsr.w _shift_new + jsr.w _wdisplay + ply + plx + } -_draw_ending_symbol: - pha + ; display item or magic - lda #0x08 - sta.b BITSLEFT - lda #0x4E + 24 - sta.b oldtilepos + _code07: + { + lda 0x08FB + + rep #0x20 + and.w #0x00FF + pha + clc + adc 0x01, s ; x2 + adc 0x01, s ; x3 + asl + asl ; x12 + tax + pla + sep #0x20 + + ; skip first char (usually a space or a symbol.) + inx + lda #0x0b ; 11 characters + + loop: + pha + lda.b #0x00 + xba + lda.l assets_items_dat, x + cmp #0xFF + beq cleanup + _vwf_putchar() + + inx + + pla + dec + bne loop + bra end + cleanup: + pla + end: + lda #0x00 + jmp.w main + } - lda #0x4E + 25 - sta.b TILEPOS + ;******************* + ;** Shift Routine ** + ;******************* + + _shift_new: + rep #0x20 + lda.w #0x0010 + sta.b CNTR + sep #0x20 + + _boucle2: + rep #0x20 + lda.w #0x0000 + sep #0x20 + phx + lda.b BITSLEFT + + cmp #0x08 + bne _shift + plx + ; LDA.L assets_font_dat,X + phy + txy + lda.b [font_addr], y + tyx + ply + inx + xba + bra _store + + _shift: + .if 1 { + plx + phy + txy + lda.b [font_addr], y + tyx + ply + + xba + lda #0x00 + xba + + ; make jump_table_pointer + phx + pha + lda.b BITSLEFT + asl + tax + pla + + rep #0x20 + jmp.w (_mul_table, x) + _mul_table: + .dw _mul_0 + .dw _mul_1 + .dw _mul_2 + .dw _mul_3 + .dw _mul_4 + .dw _mul_5 + .dw _mul_6 + .dw _mul_7 + .dw _mul_8 + + _mul_8: + _mul_7: + asl ; 1 + _mul_6: + asl ; 2 + _mul_5: + asl ; 3 + _mul_4: + asl ; 4 + _mul_3: + asl ; 5 + _mul_2: + asl ; 6 + _mul_1: + asl ; 7 + _mul_0: + plx + sep #0x20 + inx + } else { + tax ; using math multiplication + lda.l vwf_shift_table, x + sta.l 0x004202 ; MULTPILIER - ldx.w #0xa2 * 17 - ldy.w #0xcc0 - jsr.w _makeptr - jsr.w _shift_new - jsr.w _wdisplay + plx - pla - rts + phy + txy + lda.b [font_addr], y + tyx + ply + inx + sta.l 0x004203 ; MULTIPLICAND -;************* -;** Musique ** -;************* + rep #0x20 + nop + nop + nop + nop + lda.l 0x004216 ; the result is stored in 0x4216-0x4217 + sep #0x20 + } -_musique: - jsr.w load_letter_inc - sta 0x1E01 - lda.b #0x01 - sta 0x1E00 + _store: - jsr.l 0x048004 - jmp.w main + ; ff + ; notre bidule + ; 0b11111111 A + ; 0b00000001 B + ; roll B and B xor A ? + ; 0b11 = white + ; 0b10 = black + ; 0b01 = window background + ; 0b00 = transparent + TEXT_SHADOW := 1 -_code05: - jsr.w load_letter_inc xba - lda #0x00 - xba - asl - asl - asl - tax - stx 0x08F4 - ldx 0x0000 - stx 0x08F6 -{ - ldx 0x08f4 - beq skip -loop: - cpx 0x08f6 - - bne loop -skip: - ldx.w #0x0000 - stx 0x08f4 -} -jmp.w main - - -.macro _vwf_putchar() { phx - phy - sta.b CURRENT_C - jsr.w _makeptr - jsr.w _shift_new - jsr.w _wdisplay - ply - plx -} - -; display item or magic + tyx -_code07: -{ - lda 0x08FB - rep #0x20 - and.w #0x00FF pha - clc - adc 0x01, s ; x2 - adc 0x01, s ; x3 - asl - asl ; x12 - tax + ora.l WRAM, x + sta.l WRAM, x pla - sep #0x20 -; skip first char (usually a space or a symbol.) - inx - lda #0x0b ; 11 characters - -loop: pha - lda.b #0x00 - xba - lda.l assets_items_dat, x - cmp #0xFF - beq cleanup - _vwf_putchar() + ora.l WRAM + 1, x + sta.l WRAM + 1, x + pla - inx + .if TEXT_SHADOW { + pha + eor.l WRAM + 3, x + sta.l WRAM + 3, x + pla + pha + eor.l WRAM + 2, x + sta.l WRAM + 2, x + pla + } + xba + pha + ora.l WRAM + 0x20, x + sta.l WRAM + 0x20, x pla - dec - bne loop - bra end -cleanup: - pla -end: - lda #0x00 - jmp.w main -} -;******************* -;** Shift Routine ** -;******************* - -_shift_new: - rep #0x20 - lda.w #0x0010 - sta.b CNTR - sep #0x20 + pha + ora.l WRAM + 0x20 + 1, x + sta.l WRAM + 0x20 + 1, x + pla -_boucle2: - rep #0x20 - lda.w #0x0000 - sep #0x20 - phx - lda.b BITSLEFT + .if TEXT_SHADOW { + pha + eor.l WRAM + 3 + 0x20, x + sta.l WRAM + 3 + 0x20, x + pla - cmp #0x08 - bne _shift - plx - ; LDA.L assets_font_dat,X - phy + pha + eor.l WRAM + 2 + 0x20, x + sta.l WRAM + 2 + 0x20, x + pla + } txy - lda.b [font_addr], y - tyx - ply - inx - xba - bra _store - -_shift: -.if 1 { plx - phy - txy - lda.b [font_addr], y - tyx - ply - xba - lda #0x00 - xba - -; make jump_table_pointer - phx - pha - lda.b BITSLEFT - asl - tax - pla + iny + iny - rep #0x20 - jmp.w (_mul_table, x) -_mul_table: - .dw _mul_0 - .dw _mul_1 - .dw _mul_2 - .dw _mul_3 - .dw _mul_4 - .dw _mul_5 - .dw _mul_6 - .dw _mul_7 - .dw _mul_8 - -_mul_8: -_mul_7: - asl ; 1 -_mul_6: - asl ; 2 -_mul_5: - asl ; 3 -_mul_4: - asl ; 4 -_mul_3: - asl ; 5 -_mul_2: - asl ; 6 -_mul_1: - asl ; 7 -_mul_0: - plx - sep #0x20 - inx -} else { - tax ; using math multiplication - lda.l vwf_shift_table, x - sta.l 0x004202 ; MULTPILIER + dec.b CNTR + beq _exit + jmp.w _boucle2 + _exit: - plx + rep #0x20 + .if ENABLE_KERNING { + ; Routine reads the pair from CURRENT_C (DP), no need to load A. + jsr.w dialog_get_kerning_adjustment_binary_search + pha + } else { + lda.w #0x0000 + pha + } - phy txy lda.b [font_addr], y tyx - ply - inx - sta.l 0x004203 ; MULTIPLICAND - rep #0x20 - nop - nop - nop - nop - lda.l 0x004216 ; the result is stored in 0x4216-0x4217 sep #0x20 -} - -_store: - -; ff -; notre bidule -; 0b11111111 A -; 0b00000001 B -; roll B and B xor A ? -; 0b11 = white -; 0b10 = black -; 0b01 = window background -; 0b00 = transparent -TEXT_SHADOW := 1 - -xba -phx -tyx - - -pha -ora.l WRAM, x -sta.l WRAM, x -pla - -pha -ora.l WRAM + 1, x -sta.l WRAM + 1, x -pla - -.if TEXT_SHADOW { - pha - eor.l WRAM + 3, x - sta.l WRAM + 3, x - pla - pha - eor.l WRAM + 2, x - sta.l WRAM + 2, x - pla -} -xba - -pha -ora.l WRAM + 0x20, x -sta.l WRAM + 0x20, x -pla - -pha -ora.l WRAM + 0x20 + 1, x -sta.l WRAM + 0x20 + 1, x -pla - -.if TEXT_SHADOW { pha - eor.l WRAM + 3 + 0x20, x - sta.l WRAM + 3 + 0x20, x - pla - - pha - eor.l WRAM + 2 + 0x20, x - sta.l WRAM + 2 + 0x20, x - pla -} -txy -plx - -iny -iny - -dec.b CNTR -beq _exit -jmp.w _boucle2 - -_exit: - - rep #0x20 -.if ENABLE_KERNING { -; Routine reads the pair from CURRENT_C (DP), no need to load A. - jsr.w dialog_get_kerning_adjustment_binary_search - pha -} else { - lda.w #0x0000 - pha -} - -txy -lda.b [font_addr], y -tyx + lda.b BITSLEFT -sep #0x20 -pha - -lda.b BITSLEFT - -clc -sbc.b 0x01, s - -clc -adc.b 0x02, s - -loopdec: -"""Tail of the per-glyph blit.""" - cmp #0x00 - bmi coupe - beq coupe - - sta.b BITSLEFT - pla - pla - pla - rts - -coupe: -"""Glyph crossed a tile boundary: bump TILEPOS.""" clc - adc #0x08 - inc.b TILEPOS - bra loopdec - - -vwf_shift_table: -"""8-bit shift mask table indexed by BITSLEFT.""" - .db 0b00000000 ; dummy entrie =0 - .db 0b00000010 ; 1 - .db 0b00000100 ; 2 - .db 0b00001000 ; 3 - .db 0b00010000 ; 4 - .db 0b00100000 ; 5 - .db 0b01000000 ; 6 - .db 0b10000000 ; 7 - .db 0b10000000 ; 8 - -; Long-form (RTL) wrapper for cross-bank callers and Python tests. - -setup_font_ext: -"""RTL wrapper around setup_font for cross-bank callers.""" - jsr.w setup_font - rtl + sbc.b 0x01, s -setup_font: -"""Load the 24-bit address of font A from font_table.""" - ; A: font index -{ - phx - pha - asl clc - adc 0x01, s - xba - lda #0 - xba - tax - pla - - rep #0x20 - lda.l font_table, x - sta.b font_addr - sep #0x20 - - lda.l font_table + 2, x - sta.b font_addr + 2 - plx - rts -} - -;************************ -;** build font pointer ** -;************************ - -_makeptr: - pha - - ldx.w #0x0000 - ldy.w #0x0000 - lda.b CURRENT_C - xba - lda #0x00 - xba - rep #0x20 - pha - asl - asl - asl - asl - adc.b 0x01, s - tax - pla - lda.w #0x0000 - sep #0x20 - - lda.b TILEPOS - sta.b oldtilepos - rep #0x20 - asl - asl - asl - asl - asl - sta.b oldtilepos - tay - sep #0x20 - pla - rts - - -;=================================== -;Clear Wram -; -;=================================== - -_clr: -{ - phx - ldx.w #0x0000 -solid_bg_loop: - lda.b #0xff - sta.l WRAM, x - inx - - lda.b #0x00 - sta.l WRAM, x - inx - cpx.w #0x0D10 - - bne solid_bg_loop - - -transparent_bg_loop: - lda.b #0x00 - sta.l WRAM, x - inx - cpx.w #0x0D20 - bne transparent_bg_loop - - plx - rts -} - -_wait_key_up: - lda.l 0x000602 - bne _wait_key_up - lda.l 0x000603 - bne _wait_key_up - rts - -_wait_key_down: -{ - ACTION_BUTTON := 0x80 - lda.l 0x000602 - bit #ACTION_BUTTON ; Action button - bne exit - wai - bra _wait_key_down -exit: - rts -} + adc.b 0x02, s + + loopdec: + """Tail of the per-glyph blit.""" + cmp #0x00 + bmi coupe + beq coupe + + sta.b BITSLEFT + pla + pla + pla + rts + + coupe: + """Glyph crossed a tile boundary: bump TILEPOS.""" + clc + adc #0x08 + inc.b TILEPOS + bra loopdec + + + vwf_shift_table: + """8-bit shift mask table indexed by BITSLEFT.""" + .db 0b00000000 ; dummy entrie =0 + .db 0b00000010 ; 1 + .db 0b00000100 ; 2 + .db 0b00001000 ; 3 + .db 0b00010000 ; 4 + .db 0b00100000 ; 5 + .db 0b01000000 ; 6 + .db 0b10000000 ; 7 + .db 0b10000000 ; 8 + + ; Long-form (RTL) wrapper for cross-bank callers and Python tests. + + setup_font_ext: + """RTL wrapper around setup_font for cross-bank callers.""" + jsr.w setup_font + rtl + + setup_font: + """Load the 24-bit address of font A from font_table.""" + ; A: font index + { + phx + pha + asl + clc + adc 0x01, s + xba + lda #0 + xba + tax + pla + + rep #0x20 + lda.l font_table, x + sta.b font_addr + sep #0x20 + + lda.l font_table + 2, x + sta.b font_addr + 2 + plx + rts + } -;Wait for joypad 1 + ;************************ + ;** build font pointer ** + ;************************ + + _makeptr: + pha + + ldx.w #0x0000 + ldy.w #0x0000 + lda.b CURRENT_C + xba + lda #0x00 + xba + rep #0x20 + pha + asl + asl + asl + asl + adc.b 0x01, s + tax + pla + lda.w #0x0000 + sep #0x20 + + lda.b TILEPOS + sta.b oldtilepos + rep #0x20 + asl + asl + asl + asl + asl + sta.b oldtilepos + tay + sep #0x20 + pla + rts + + + ;=================================== + ;Clear Wram + ; + ;=================================== + + _clr: + { + phx + ldx.w #0x0000 + solid_bg_loop: + lda.b #0xff + sta.l WRAM, x + inx + + lda.b #0x00 + sta.l WRAM, x + inx + cpx.w #0x0D10 + + bne solid_bg_loop + + + transparent_bg_loop: + lda.b #0x00 + sta.l WRAM, x + inx + cpx.w #0x0D20 + bne transparent_bg_loop + + plx + rts + } -_waitpad: -{ - pha - lda.b no_wait_for_action - bne nowaitpad + _wait_key_up: + lda.l 0x000602 + bne _wait_key_up + lda.l 0x000603 + bne _wait_key_up + rts - .if ENABLE_DIALOG_SKIP { - jsr.w _wait_key_up + _wait_key_down: + { + ACTION_BUTTON := 0x80 + lda.l 0x000602 + bit #ACTION_BUTTON ; Action button + bne exit + wai + bra _wait_key_down + exit: + rts } - jsr.w _wait_key_down - bra end + ;Wait for joypad 1 -nowaitpad: - lda.b #0x30 + _waitpad: { -loop: - jsr.w wait_for_vblank - dec - bne loop + pha + lda.b no_wait_for_action + bne nowaitpad + + .if ENABLE_DIALOG_SKIP { + jsr.w _wait_key_up + } + jsr.w _wait_key_down + bra end + + + nowaitpad: + lda.b #0x30 + { + loop: + jsr.w wait_for_vblank + dec + bne loop + } + end: + pla + jsr.w _clr + jsr.w wait_for_vblank + dma_transfer_to_vram_call(WRAM, vram_tile_set_pointer, 0x0690, 0x1801) + jsr.w wait_for_vblank + dma_transfer_to_vram_call(WRAM + 0x348, vram_tile_set_pointer + 0x348, 0x0690, 0x1801) + rts } -end: - pla - jsr.w _clr - jsr.w wait_for_vblank - dma_transfer_to_vram_call(WRAM, vram_tile_set_pointer, 0x0690, 0x1801) - jsr.w wait_for_vblank - dma_transfer_to_vram_call(WRAM + 0x348, vram_tile_set_pointer + 0x348, 0x0690, 0x1801) - rts -} -_wdisplay: - ; wait for vblank to transfer - jsr.w wait_for_vblank + _wdisplay: + ; wait for vblank to transfer + jsr.w wait_for_vblank - sep #0x20 + sep #0x20 -;macro expansion + ;macro expansion - php - pha - phx + php + pha + phx - lda.b #0x80 - sta.w 0x2115 + lda.b #0x80 + sta.w 0x2115 - rep #0x20 - pha + rep #0x20 + pha - lda.b oldtilepos - lsr ; addresse vram /2 - clc - adc.w #vram_tile_set_pointer - sta.w 0x2116 + lda.b oldtilepos + lsr ; addresse vram /2 + clc + adc.w #vram_tile_set_pointer + sta.w 0x2116 - lda.b oldtilepos - clc - adc.w #WRAM & 0xFFFF - sta.w 0x4372 + lda.b oldtilepos + clc + adc.w #WRAM & 0xFFFF + sta.w 0x4372 - pla - sep #0x20 + pla + sep #0x20 - channel = 7 + channel = 7 - ldx.w #0x1801 - stx.w 0x4370 - lda.b #0xFF & ( WRAM >> 16 ) - sta.w 0x4374 + ldx.w #0x1801 + stx.w 0x4370 + lda.b #0xFF & ( WRAM >> 16 ) + sta.w 0x4374 - ldx.w #0x0040 - stx.w 0x4375 - lda.b #0x01 << 7 - sta 0x420B + ldx.w #0x0040 + stx.w 0x4375 + lda.b #0x01 << 7 + sta 0x420B - nop - nop + nop + nop - plx - pla - plp + plx + pla + plp - lda.b no_wait_for_action - bne _nowindow + lda.b no_wait_for_action + bne _nowindow - dma_transfer_to_vram_call(_winmap, vram_tile_map_pointer, _endwinmap - _winmap, 0x1801) - bra window + dma_transfer_to_vram_call(_winmap, vram_tile_map_pointer, _endwinmap - _winmap, 0x1801) + bra window -_nowindow: - dma_transfer_to_vram_call(_intromap, vram_tile_map_pointer, _endintromap - _intromap, 0x1801) + _nowindow: + dma_transfer_to_vram_call(_intromap, vram_tile_map_pointer, _endintromap - _intromap, 0x1801) -window: -"""Post-DMA window-display tail.""" -{ - lda.b CURRENT_C - cmp #0xFF - beq no_char_wait - wai ; wait for interrupts - lda.b no_wait_for_action ; add extra wait when in "story telling". - beq no_char_wait - jsr.w wait_for_vblank - jsr.w wait_for_vblank -no_char_wait: -} -rts + window: + """Post-DMA window-display tail.""" + { + lda.b CURRENT_C + cmp #0xFF + beq no_char_wait + wai ; wait for interrupts + lda.b no_wait_for_action ; add extra wait when in "story telling". + beq no_char_wait + jsr.w wait_for_vblank + jsr.w wait_for_vblank + no_char_wait: + } + rts -_winmap: -.if TEXT_SHADOW { - .if ENABLE_BUTTON_DISPLAY { - last_character_palette := 2 + _winmap: + .if TEXT_SHADOW { + .if ENABLE_BUTTON_DISPLAY { + last_character_palette := 2 + } else { + last_character_palette := 1 + } + .dw 0x2000, 0x2000, 0x2019, 0x2500, 0x2502, 0x2504, 0x2506, 0x2508, 0x250A, 0x250C, 0x250E, 0x2510, 0x2512, 0x2514, 0x2516, 0x2518, 0x251A, 0x251C, 0x251E, 0x2520, 0x2522, 0x2524, 0x2526, 0x2528, 0x252A, 0x252C, 0x252E, 0x2530, 0x2532, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2501, 0x2503, 0x2505, 0x2507, 0x2509, 0x250B, 0x250D, 0x250F, 0x2511, 0x2513, 0x2515, 0x2517, 0x2519, 0x251B, 0x251D, 0x251F, 0x2521, 0x2523, 0x2525, 0x2527, 0x2529, 0x252B, 0x252D, 0x252F, 0x2531, 0x2533, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2534, 0x2536, 0x2538, 0x253A, 0x253C, 0x253E, 0x2540, 0x2542, 0x2544, 0x2546, 0x2548, 0x254A, 0x254C, 0x254E, 0x2550, 0x2552, 0x2554, 0x2556, 0x2558, 0x255A, 0x255C, 0x255E, 0x2560, 0x2562, 0x2564, 0x2566, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2535, 0x2537, 0x2539, 0x253B, 0x253D, 0x253F, 0x2541, 0x2543, 0x2545, 0x2547, 0x2549, 0x254B, 0x254D, 0x254F, 0x2551, 0x2553, 0x2555, 0x2557, 0x2559, 0x255B, 0x255D, 0x255F, 0x2561, 0x2563, 0x2565, 0x2567, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2568, 0x256A, 0x256C, 0x256E, 0x2570, 0x2572, 0x2574, 0x2576, 0x2578, 0x257A, 0x257C, 0x257E, 0x2580, 0x2582, 0x2584, 0x2586, 0x2588, 0x258A, 0x258C, 0x258E, 0x2590, 0x2592, 0x2594, 0x2596, 0x2598, 0x259A, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2569, 0x256B, 0x256D, 0x256F, 0x2571, 0x2573, 0x2575, 0x2577, 0x2579, 0x257B, 0x257D, 0x257F, 0x2581, 0x2583, 0x2585, 0x2587, 0x2589, 0x258B, 0x258D, 0x258F, 0x2591, 0x2593, 0x2595, 0x2597, 0x2599, 0x259B, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x259C, 0x259E, 0x25A0, 0x25A2, 0x25A4, 0x25A6, 0x25A8, 0x25AA, 0x25AC, 0x25AE, 0x25B0, 0x25B2, 0x25B4, 0x25B6, 0x25B8, 0x25BA, 0x25BC, 0x25BE, 0x25C0, 0x25C2, 0x25C4, 0x25C6, 0x25C8, 0x25CA, 0x25CC, ( 0x21CE + ( last_character_palette << 10 ) ), 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x259D, 0x259F, 0x25A1, 0x25A3, 0x25A5, 0x25A7, 0x25A9, 0x25AB, 0x25AD, 0x25AF, 0x25B1, 0x25B3, 0x25B5, 0x25B7, 0x25B9, 0x25BB, 0x25BD, 0x25BF, 0x25C1, 0x25C3, 0x25C5, 0x25C7, 0x25C9, 0x25CB, 0x25CD, ( 0x21CF + ( last_character_palette << 10 ) ), 0x201A, 0x2000, 0x2000 ; noqa: E501 } else { - last_character_palette := 1 + .dw 0x2000, 0x2000, 0x2019, 0x2100, 0x2102, 0x2104, 0x2106, 0x2108, 0x210A, 0x210C, 0x210E, 0x2110, 0x2112, 0x2114, 0x2116, 0x2118, 0x211A, 0x211C, 0x211E, 0x2120, 0x2122, 0x2124, 0x2126, 0x2128, 0x212A, 0x212C, 0x212E, 0x2130, 0x2132, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2101, 0x2103, 0x2105, 0x2107, 0x2109, 0x210B, 0x210D, 0x210F, 0x2111, 0x2113, 0x2115, 0x2117, 0x2119, 0x211B, 0x211D, 0x211F, 0x2121, 0x2123, 0x2125, 0x2127, 0x2129, 0x212B, 0x212D, 0x212F, 0x2131, 0x2133, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2134, 0x2136, 0x2138, 0x213A, 0x213C, 0x213E, 0x2140, 0x2142, 0x2144, 0x2146, 0x2148, 0x214A, 0x214C, 0x214E, 0x2150, 0x2152, 0x2154, 0x2156, 0x2158, 0x215A, 0x215C, 0x215E, 0x2160, 0x2162, 0x2164, 0x2166, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2135, 0x2137, 0x2139, 0x213B, 0x213D, 0x213F, 0x2141, 0x2143, 0x2145, 0x2147, 0x2149, 0x214B, 0x214D, 0x214F, 0x2151, 0x2153, 0x2155, 0x2157, 0x2159, 0x215B, 0x215D, 0x215F, 0x2161, 0x2163, 0x2165, 0x2167, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2168, 0x216A, 0x216C, 0x216E, 0x2170, 0x2172, 0x2174, 0x2176, 0x2178, 0x217A, 0x217C, 0x217E, 0x2180, 0x2182, 0x2184, 0x2186, 0x2188, 0x218A, 0x218C, 0x218E, 0x2190, 0x2192, 0x2194, 0x2196, 0x2198, 0x219A, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x2169, 0x216B, 0x216D, 0x216F, 0x2171, 0x2173, 0x2175, 0x2177, 0x2179, 0x217B, 0x217D, 0x217F, 0x2181, 0x2183, 0x2185, 0x2187, 0x2189, 0x218B, 0x218D, 0x218F, 0x2191, 0x2193, 0x2195, 0x2197, 0x2199, 0x219B, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x219C, 0x219E, 0x21A0, 0x21A2, 0x21A4, 0x21A6, 0x21A8, 0x21AA, 0x21AC, 0x21AE, 0x21B0, 0x21B2, 0x21B4, 0x21B6, 0x21B8, 0x21BA, 0x21BC, 0x21BE, 0x21C0, 0x21C2, 0x21C4, 0x21C6, 0x21C8, 0x21CA, 0x21CC, 0x21CE, 0x201A, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2019, 0x219D, 0x219F, 0x21A1, 0x21A3, 0x21A5, 0x21A7, 0x21A9, 0x21AB, 0x21AD, 0x21AF, 0x21B1, 0x21B3, 0x21B5, 0x21B7, 0x21B9, 0x21BB, 0x21BD, 0x21BF, 0x21C1, 0x21C3, 0x21C5, 0x21C7, 0x21C9, 0x21CB, 0x21CD, 0x21CF, 0x201A, 0x2000, 0x2000 ; noqa: E501 } - .dw 0x2000, 0x2000, 0x2019, 0x2500, 0x2502, 0x2504, 0x2506, 0x2508, 0x250A, 0x250C, 0x250E, 0x2510, 0x2512, 0x2514, 0x2516, 0x2518, 0x251A, 0x251C, 0x251E, 0x2520, 0x2522, 0x2524, 0x2526, 0x2528, 0x252A, 0x252C, 0x252E, 0x2530, 0x2532, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2501, 0x2503, 0x2505, 0x2507, 0x2509, 0x250B, 0x250D, 0x250F, 0x2511, 0x2513, 0x2515, 0x2517, 0x2519, 0x251B, 0x251D, 0x251F, 0x2521, 0x2523, 0x2525, 0x2527, 0x2529, 0x252B, 0x252D, 0x252F, 0x2531, 0x2533, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2534, 0x2536, 0x2538, 0x253A, 0x253C, 0x253E, 0x2540, 0x2542, 0x2544, 0x2546, 0x2548, 0x254A, 0x254C, 0x254E, 0x2550, 0x2552, 0x2554, 0x2556, 0x2558, 0x255A, 0x255C, 0x255E, 0x2560, 0x2562, 0x2564, 0x2566, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2535, 0x2537, 0x2539, 0x253B, 0x253D, 0x253F, 0x2541, 0x2543, 0x2545, 0x2547, 0x2549, 0x254B, 0x254D, 0x254F, 0x2551, 0x2553, 0x2555, 0x2557, 0x2559, 0x255B, 0x255D, 0x255F, 0x2561, 0x2563, 0x2565, 0x2567, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2568, 0x256A, 0x256C, 0x256E, 0x2570, 0x2572, 0x2574, 0x2576, 0x2578, 0x257A, 0x257C, 0x257E, 0x2580, 0x2582, 0x2584, 0x2586, 0x2588, 0x258A, 0x258C, 0x258E, 0x2590, 0x2592, 0x2594, 0x2596, 0x2598, 0x259A, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2569, 0x256B, 0x256D, 0x256F, 0x2571, 0x2573, 0x2575, 0x2577, 0x2579, 0x257B, 0x257D, 0x257F, 0x2581, 0x2583, 0x2585, 0x2587, 0x2589, 0x258B, 0x258D, 0x258F, 0x2591, 0x2593, 0x2595, 0x2597, 0x2599, 0x259B, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x259C, 0x259E, 0x25A0, 0x25A2, 0x25A4, 0x25A6, 0x25A8, 0x25AA, 0x25AC, 0x25AE, 0x25B0, 0x25B2, 0x25B4, 0x25B6, 0x25B8, 0x25BA, 0x25BC, 0x25BE, 0x25C0, 0x25C2, 0x25C4, 0x25C6, 0x25C8, 0x25CA, 0x25CC, ( 0x21CE + ( last_character_palette << 10 ) ), 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x259D, 0x259F, 0x25A1, 0x25A3, 0x25A5, 0x25A7, 0x25A9, 0x25AB, 0x25AD, 0x25AF, 0x25B1, 0x25B3, 0x25B5, 0x25B7, 0x25B9, 0x25BB, 0x25BD, 0x25BF, 0x25C1, 0x25C3, 0x25C5, 0x25C7, 0x25C9, 0x25CB, 0x25CD, ( 0x21CF + ( last_character_palette << 10 ) ), 0x201A, 0x2000, 0x2000 ; noqa: E501 -} else { - .dw 0x2000, 0x2000, 0x2019, 0x2100, 0x2102, 0x2104, 0x2106, 0x2108, 0x210A, 0x210C, 0x210E, 0x2110, 0x2112, 0x2114, 0x2116, 0x2118, 0x211A, 0x211C, 0x211E, 0x2120, 0x2122, 0x2124, 0x2126, 0x2128, 0x212A, 0x212C, 0x212E, 0x2130, 0x2132, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2101, 0x2103, 0x2105, 0x2107, 0x2109, 0x210B, 0x210D, 0x210F, 0x2111, 0x2113, 0x2115, 0x2117, 0x2119, 0x211B, 0x211D, 0x211F, 0x2121, 0x2123, 0x2125, 0x2127, 0x2129, 0x212B, 0x212D, 0x212F, 0x2131, 0x2133, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2134, 0x2136, 0x2138, 0x213A, 0x213C, 0x213E, 0x2140, 0x2142, 0x2144, 0x2146, 0x2148, 0x214A, 0x214C, 0x214E, 0x2150, 0x2152, 0x2154, 0x2156, 0x2158, 0x215A, 0x215C, 0x215E, 0x2160, 0x2162, 0x2164, 0x2166, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2135, 0x2137, 0x2139, 0x213B, 0x213D, 0x213F, 0x2141, 0x2143, 0x2145, 0x2147, 0x2149, 0x214B, 0x214D, 0x214F, 0x2151, 0x2153, 0x2155, 0x2157, 0x2159, 0x215B, 0x215D, 0x215F, 0x2161, 0x2163, 0x2165, 0x2167, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2168, 0x216A, 0x216C, 0x216E, 0x2170, 0x2172, 0x2174, 0x2176, 0x2178, 0x217A, 0x217C, 0x217E, 0x2180, 0x2182, 0x2184, 0x2186, 0x2188, 0x218A, 0x218C, 0x218E, 0x2190, 0x2192, 0x2194, 0x2196, 0x2198, 0x219A, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x2169, 0x216B, 0x216D, 0x216F, 0x2171, 0x2173, 0x2175, 0x2177, 0x2179, 0x217B, 0x217D, 0x217F, 0x2181, 0x2183, 0x2185, 0x2187, 0x2189, 0x218B, 0x218D, 0x218F, 0x2191, 0x2193, 0x2195, 0x2197, 0x2199, 0x219B, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x219C, 0x219E, 0x21A0, 0x21A2, 0x21A4, 0x21A6, 0x21A8, 0x21AA, 0x21AC, 0x21AE, 0x21B0, 0x21B2, 0x21B4, 0x21B6, 0x21B8, 0x21BA, 0x21BC, 0x21BE, 0x21C0, 0x21C2, 0x21C4, 0x21C6, 0x21C8, 0x21CA, 0x21CC, 0x21CE, 0x201A, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2019, 0x219D, 0x219F, 0x21A1, 0x21A3, 0x21A5, 0x21A7, 0x21A9, 0x21AB, 0x21AD, 0x21AF, 0x21B1, 0x21B3, 0x21B5, 0x21B7, 0x21B9, 0x21BB, 0x21BD, 0x21BF, 0x21C1, 0x21C3, 0x21C5, 0x21C7, 0x21C9, 0x21CB, 0x21CD, 0x21CF, 0x201A, 0x2000, 0x2000 ; noqa: E501 -} -_endwinmap: - -_intromap: - .dw 0x2000, 0x2000, 0x2000, 0x2100, 0x2102, 0x2104, 0x2106, 0x2108, 0x210A, 0x210C, 0x210E, 0x2110, 0x2112, 0x2114, 0x2116, 0x2118, 0x211A, 0x211C, 0x211E, 0x2120, 0x2122, 0x2124, 0x2126, 0x2128, 0x212A, 0x212C, 0x212E, 0x2130, 0x2132, 0x2000, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2000, 0x2101, 0x2103, 0x2105, 0x2107, 0x2109, 0x210B, 0x210D, 0x210F, 0x2111, 0x2113, 0x2115, 0x2117, 0x2119, 0x211B, 0x211D, 0x211F, 0x2121, 0x2123, 0x2125, 0x2127, 0x2129, 0x212B, 0x212D, 0x212F, 0x2131, 0x2133, 0x2000, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2000, 0x2134, 0x2136, 0x2138, 0x213A, 0x213C, 0x213E, 0x2140, 0x2142, 0x2144, 0x2146, 0x2148, 0x214A, 0x214C, 0x214E, 0x2150, 0x2152, 0x2154, 0x2156, 0x2158, 0x215A, 0x215C, 0x215E, 0x2160, 0x2162, 0x2164, 0x2166, 0x2000, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2000, 0x2135, 0x2137, 0x2139, 0x213B, 0x213D, 0x213F, 0x2141, 0x2143, 0x2145, 0x2147, 0x2149, 0x214B, 0x214D, 0x214F, 0x2151, 0x2153, 0x2155, 0x2157, 0x2159, 0x215B, 0x215D, 0x215F, 0x2161, 0x2163, 0x2165, 0x2167, 0x2000, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2000, 0x2168, 0x216A, 0x216C, 0x216E, 0x2170, 0x2172, 0x2174, 0x2176, 0x2178, 0x217A, 0x217C, 0x217E, 0x2180, 0x2182, 0x2184, 0x2186, 0x2188, 0x218A, 0x218C, 0x218E, 0x2190, 0x2192, 0x2194, 0x2196, 0x2198, 0x219A, 0x2000, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2000, 0x2169, 0x216B, 0x216D, 0x216F, 0x2171, 0x2173, 0x2175, 0x2177, 0x2179, 0x217B, 0x217D, 0x217F, 0x2181, 0x2183, 0x2185, 0x2187, 0x2189, 0x218B, 0x218D, 0x218F, 0x2191, 0x2193, 0x2195, 0x2197, 0x2199, 0x219B, 0x2000, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2000, 0x219C, 0x219E, 0x21A0, 0x21A2, 0x21A4, 0x21A6, 0x21A8, 0x21AA, 0x21AC, 0x21AE, 0x21B0, 0x21B2, 0x21B4, 0x21B6, 0x21B8, 0x21BA, 0x21BC, 0x21BE, 0x21C0, 0x21C2, 0x21C4, 0x21C6, 0x21C8, 0x21CA, 0x21CC, 0x21CE, 0x2000, 0x2000, 0x2000 ; noqa: E501 - .dw 0x2000, 0x2000, 0x2000, 0x219D, 0x219F, 0x21A1, 0x21A3, 0x21A5, 0x21A7, 0x21A9, 0x21AB, 0x21AD, 0x21AF, 0x21B1, 0x21B3, 0x21B5, 0x21B7, 0x21B9, 0x21BB, 0x21BD, 0x21BF, 0x21C1, 0x21C3, 0x21C5, 0x21C7, 0x21C9, 0x21CB, 0x21CD, 0x21CF, 0x2000, 0x2000, 0x2000 ; noqa: E501 - -_endintromap: - - -gils_window_size := 10 - -gils_window_tilemap_1: -"""Top-edge tilemap row for the gil-count window.""" - .dw 0, 0 - .db 0x16, 0x20 - fill_value(0x2017, gils_window_size - 2) - .db 0x18, 0x20 - -gils_window_tilemap_2: -"""Filler row for the gil-count window interior.""" - .dw 0, 0 - - .db 0x19, 0x20 - fill_value(0x20ff, gils_window_size - 2) - .db 0x1a, 0x20 - -gils_window_tilemap_3: -"""Second filler row for the gil-count window.""" - .dw 0, 0 - - .db 0x19, 0x20 - fill_value(0x20ff, gils_window_size - 2) - .db 0x1a, 0x20 - -gils_window_tilemap_4: -"""Gils caption row for the gil-count window.""" - .dw 0, 0 - - .db 0x19, 0x20 - fill_value(0x20ff, gils_window_size - 2) - .db 0x1a, 0x20 - - fill_value(0x0000, 0x16) - - .db 0x19, 0x20 - fill_value(0x20ff, gils_window_size - 2 - 4) - .text "G" - .db 0x20 - .text "i" - .db 0x20 - .text "l" - .db 0x20 - .text "s" - .db 0x20 - - .db 0x1a, 0x20 ; "gil" - fill_value(0x0000, 0x16) - .db 0x1b, 0x20 - fill_value(0x201c, gils_window_size - 2) - .db 0x1d, 0x20 - -gils_window_tilemap_4_end: -"""End-of-data marker for gils_window_tilemap_4.""" + _endwinmap: + + _intromap: + .dw 0x2000, 0x2000, 0x2000, 0x2100, 0x2102, 0x2104, 0x2106, 0x2108, 0x210A, 0x210C, 0x210E, 0x2110, 0x2112, 0x2114, 0x2116, 0x2118, 0x211A, 0x211C, 0x211E, 0x2120, 0x2122, 0x2124, 0x2126, 0x2128, 0x212A, 0x212C, 0x212E, 0x2130, 0x2132, 0x2000, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2000, 0x2101, 0x2103, 0x2105, 0x2107, 0x2109, 0x210B, 0x210D, 0x210F, 0x2111, 0x2113, 0x2115, 0x2117, 0x2119, 0x211B, 0x211D, 0x211F, 0x2121, 0x2123, 0x2125, 0x2127, 0x2129, 0x212B, 0x212D, 0x212F, 0x2131, 0x2133, 0x2000, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2000, 0x2134, 0x2136, 0x2138, 0x213A, 0x213C, 0x213E, 0x2140, 0x2142, 0x2144, 0x2146, 0x2148, 0x214A, 0x214C, 0x214E, 0x2150, 0x2152, 0x2154, 0x2156, 0x2158, 0x215A, 0x215C, 0x215E, 0x2160, 0x2162, 0x2164, 0x2166, 0x2000, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2000, 0x2135, 0x2137, 0x2139, 0x213B, 0x213D, 0x213F, 0x2141, 0x2143, 0x2145, 0x2147, 0x2149, 0x214B, 0x214D, 0x214F, 0x2151, 0x2153, 0x2155, 0x2157, 0x2159, 0x215B, 0x215D, 0x215F, 0x2161, 0x2163, 0x2165, 0x2167, 0x2000, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2000, 0x2168, 0x216A, 0x216C, 0x216E, 0x2170, 0x2172, 0x2174, 0x2176, 0x2178, 0x217A, 0x217C, 0x217E, 0x2180, 0x2182, 0x2184, 0x2186, 0x2188, 0x218A, 0x218C, 0x218E, 0x2190, 0x2192, 0x2194, 0x2196, 0x2198, 0x219A, 0x2000, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2000, 0x2169, 0x216B, 0x216D, 0x216F, 0x2171, 0x2173, 0x2175, 0x2177, 0x2179, 0x217B, 0x217D, 0x217F, 0x2181, 0x2183, 0x2185, 0x2187, 0x2189, 0x218B, 0x218D, 0x218F, 0x2191, 0x2193, 0x2195, 0x2197, 0x2199, 0x219B, 0x2000, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2000, 0x219C, 0x219E, 0x21A0, 0x21A2, 0x21A4, 0x21A6, 0x21A8, 0x21AA, 0x21AC, 0x21AE, 0x21B0, 0x21B2, 0x21B4, 0x21B6, 0x21B8, 0x21BA, 0x21BC, 0x21BE, 0x21C0, 0x21C2, 0x21C4, 0x21C6, 0x21C8, 0x21CA, 0x21CC, 0x21CE, 0x2000, 0x2000, 0x2000 ; noqa: E501 + .dw 0x2000, 0x2000, 0x2000, 0x219D, 0x219F, 0x21A1, 0x21A3, 0x21A5, 0x21A7, 0x21A9, 0x21AB, 0x21AD, 0x21AF, 0x21B1, 0x21B3, 0x21B5, 0x21B7, 0x21B9, 0x21BB, 0x21BD, 0x21BF, 0x21C1, 0x21C3, 0x21C5, 0x21C7, 0x21C9, 0x21CB, 0x21CD, 0x21CF, 0x2000, 0x2000, 0x2000 ; noqa: E501 + + _endintromap: + + + gils_window_size := 10 + + gils_window_tilemap_1: + """Top-edge tilemap row for the gil-count window.""" + .dw 0, 0 + .db 0x16, 0x20 + fill_value(0x2017, gils_window_size - 2) + .db 0x18, 0x20 + + gils_window_tilemap_2: + """Filler row for the gil-count window interior.""" + .dw 0, 0 + + .db 0x19, 0x20 + fill_value(0x20ff, gils_window_size - 2) + .db 0x1a, 0x20 + + gils_window_tilemap_3: + """Second filler row for the gil-count window.""" + .dw 0, 0 + + .db 0x19, 0x20 + fill_value(0x20ff, gils_window_size - 2) + .db 0x1a, 0x20 + + gils_window_tilemap_4: + """Gils caption row for the gil-count window.""" + .dw 0, 0 + + .db 0x19, 0x20 + fill_value(0x20ff, gils_window_size - 2) + .db 0x1a, 0x20 + + fill_value(0x0000, 0x16) + + .db 0x19, 0x20 + fill_value(0x20ff, gils_window_size - 2 - 4) + .text "G" + .db 0x20 + .text "i" + .db 0x20 + .text "l" + .db 0x20 + .text "s" + .db 0x20 + + .db 0x1a, 0x20 ; "gil" + fill_value(0x0000, 0x16) + .db 0x1b, 0x20 + fill_value(0x201c, gils_window_size - 2) + .db 0x1d, 0x20 + + gils_window_tilemap_4_end: + """End-of-data marker for gils_window_tilemap_4.""" +} diff --git a/src/vwf_state.i b/src/vwf_state.i new file mode 100644 index 0000000..3786a45 --- /dev/null +++ b/src/vwf_state.i @@ -0,0 +1,125 @@ +""" +Shared VWF engine state addresses + future-config-struct slots. + +Both the battle messages renderer (`battle_render` in +`src/battle/message.s`) and the menu small-VWF renderer +(`render` in `src/small_vwf/render.s`) build glyph CHR into the same +SRAM tile buffer at $70:3000. They cohabit by never running +concurrently: battle owns the buffer during battle scenes, small_vwf +owns it during field menus. + +Define the shared address as a single symbol so future engines +(field-menu items, treasure-list VWF, drops-list VWF, ...) all +point at the same place automatically. Bumping the size only +needs to happen here ; `_vram_copy.buffer` already sits at $70:5000 +past the buffer so the VRAM-save staging is decoupled from the +CHR-buffer size. + +Forward-compat reservation: + VWF_CONFIG_BASE points at a planned per-context state struct + (font_ptr, kerning_ptr, tile_id_base, slot_budget, palette, + tilemap_base, flags). When the engines start reading from this + block, callers will write the struct once before invoking + `vwf_render_string` and the engine drops its hardcoded font + pointer. The struct lives in unused SRAM space ($70:F800+, same + region we already use for the items_menu_vwf debug breadcrumb) + so adding it does not perturb battle / menu live state. +""" + + +; --- VWF CHR buffer (shared across battle + menu renderers) ----------- + +VWF_CHR_BUFFER := 0x703000 +; Sized to cover tile_ids $00..$1FF (512 * 16 bytes). Field menu +; rolling buffer has 11 slots * K=10 tiles starting at base $C0, +; so the high slots end up at tile_id $160+ ; the previous $1000 +; sizing capped at $FF and the high-slot CHR landed in +; $704000-$704FFF which was outside any blit / DMA reach. +VWF_CHR_BUFFER_SIZE := 0x2000 + +; --- Null-terminated text-staging buffer ------------------------------ +; Callers copy the source string (from items_unleashed, monster names, +; magic list, ...) into this buffer + write $00 terminator, then call +; `vwf_render_string` with just a pointer. Lets the engine drop the +; explicit char-count argument that battle / item-description / field +; helpers each carry today, and lets us swap the source layout +; (fixed-stride table vs null-terminated table vs RAM-resident string) +; without touching the renderer. Sized for the longest field-menu +; item slot in `assets_items_unleashed_dat` + 1 terminator + headroom. +VWF_TEXT_BUFFER := 0x707000 +VWF_TEXT_BUFFER_SIZE := 0x40 + +; --- VWF config struct (forward-compat ; not consumed yet) ------------ +; Field offsets so future readers can `lda.l VWF_CONFIG_BASE + .field` +; without manual byte math. Phase n of the unification will start +; populating + consuming these. +VWF_CONFIG_BASE := 0x707080 + +; --- Engine scratch in SRAM (no DP collisions) --------------------------- +; Long-addressable scratch for VWF callers that need a counter / pointer +; without stealing direct-page bytes from the menu loop. The field-items +; helper uses VWF_SRC_OFFSET as the 16-bit source index into +; assets_items_unleashed_dat while X holds the destination index in +; VWF_TEXT_BUFFER (only sta.l abs,x is encoded by a816). +VWF_SRC_OFFSET := 0x7070C0 + +; --- Dirty flag for NMI-side CHR flush ---------------------------------- +; Set by `render.display_char` (or `render.render_with_config`) after a +; blit lands in `VWF_CHR_BUFFER`. The NMI flush hook reads this byte, +; fires the DMA from `VWF_CHR_BUFFER + $C00` to VRAM $AC00 ($400 bytes) +; when set, then clears it. Gates the upload exactly the same way the +; battle inventory's `dma_dirty_slots` gates the per-slot DMA. +VWF_CHR_DIRTY := 0x7070C2 + +; --- Secondary descriptor for two-region simultaneous flush ------------- +; Drops + treasure-inventory coexist in the treasure popup and render +; through the same `items_menu_vwf.draw_field_item_name` JSL hook. +; Region 1 = $100..$13B (treasure) flushes via the primary descriptor +; at VWF_CONFIG_BASE ; region 1B = $16E..$1A9 (drops) needs its own +; flush dest + size so each panel's CHR lands in VRAM without the +; other's stale buffer bytes leaking through one combined DMA. +; +; VWF_CALLER_CTX is a one-byte hint set by drops_rolling around the +; vanilla JSR chain ; items_menu_vwf reads it to decide whether to +; write the primary or the secondary descriptor + dirty flag. +; 0 = primary (treasure / field-items / default) +; 1 = drops (secondary) +VWF_CALLER_CTX := 0x7070C3 +VWF_CHR_DIRTY_B := 0x7070C4 +VWF_CHR_VRAM_WORD_B := 0x7070C5 +VWF_CHR_BYTE_COUNT_B := 0x7070C7 +VWF_CHR_SRC_OFFSET_B := 0x7070C9 + +.struct VwfConfig { + word tile_id_base + byte slot_budget + word tilemap_base + byte palette_byte + byte flags + word chr_vram_word + word chr_byte_count +} + +; Engine-shared CHR-flush source offset. Every VWF caller writes +; glyph CHR at `VWF_CHR_BUFFER + tile_id_base * 16` ; both battle +; inventory and field menu items happen to anchor at tile_id base +; $C0, so the flush always starts $C00 bytes into the buffer. +; (If a future client uses a different base, this constant moves to +; the engine and we still avoid forking the DMA source per caller.) +VWF_CHR_FLUSH_OFFSET := 0x100 * 0x10 + +; --- VRAM save / restore range --- +; The dialog VWF saves the VRAM CHR window before dialog opens and +; restores it on close. With field-menu items + item-description +; each carving out their own slice of CHR ($5000..$5FFF), the save +; range must cover that whole 4KB block so menu close puts the +; menu CHR back the way the field renderer expects it. +; +; Word $2800 .. $37FF = byte $5000 .. $6FFF (8KB) +; +; Generous on the upper end so a future region 3 / region 4 (status, +; equipment, ...) lands inside the save window without bumping the +; SRAM scratch. +VRAM_SAVE_BASE_WORD := 0x2800 +VRAM_SAVE_BYTE_COUNT := 0x2000 +VRAM_SAVE_SRAM_BASE := 0x705000 diff --git a/tests/_profile/bench_nmi.py b/tests/_profile/bench_nmi.py index 45a0edb..38fbc6e 100644 --- a/tests/_profile/bench_nmi.py +++ b/tests/_profile/bench_nmi.py @@ -19,7 +19,7 @@ 0x028302: "SetFlyingHDMA (added NMI hook)", } -e = load_emu_from_kss(kss_path("ff4-battle-ext.kss")) +e = load_emu_from_kss(kss_path("ff4-battle.kss")) e.run_frames(60) e.profile_start(lo=0x000000, hi=0xFFFFFF) diff --git a/tests/_profile/dump_inv_sram.py b/tests/_profile/dump_inv_sram.py new file mode 100644 index 0000000..b7383cc --- /dev/null +++ b/tests/_profile/dump_inv_sram.py @@ -0,0 +1,54 @@ +"""Dump WRAM tile buffer + inventory slot tilemap-buffer to text for +visual inspection. Loads battle-ext, opens inventory, snapshots.""" +from __future__ import annotations +import sys +from pathlib import Path + +sys.path.insert(0, "tests") +from kintsuki import Button +from _ff4kintsuki import kss_path, load_emu_from_kss, tap + +OUT = Path(__file__).parent / "dumps" / "inv_sram.hex" +OUT.parent.mkdir(exist_ok=True) + + +def dump_region(emu, label: str, start: int, length: int) -> str: + rows = [] + rows.append(f"=== {label} ${start:06x}..${start+length-1:06x} ({length}B) ===") + for off in range(0, length, 16): + addr = start + off + bytes_ = " ".join(f"{emu.read(addr + i):02x}" for i in range(16)) + ascii_ = "".join( + chr(b) if 32 <= b < 127 else "." + for b in (emu.read(addr + i) for i in range(16)) + ) + rows.append(f"{addr:06x}: {bytes_} {ascii_}") + return "\n".join(rows) + + +def main() -> None: + emu = load_emu_from_kss(kss_path("ff4-battle.kss"), settle_frames=600) + tap(emu, Button.DOWN, gap=20) + tap(emu, Button.DOWN, gap=20) + tap(emu, Button.A, gap=60) + + out = [] + # Inventory rolling-buffer tilemap entries (6 slots * 60 bytes) + out.append(dump_region(emu, "rolling slots ($7E97A6)", 0x7E97A6, 6 * 60)) + # VWF tile CHR staging + out.append(dump_region(emu, "vwf buffer head (msg/mon/names)", 0x703000, 0x600)) + out.append(dump_region(emu, "vwf buffer mid (cmd)", 0x703900, 0x300)) + out.append(dump_region(emu, "vwf buffer tail (inventory)", 0x703C00, 0x300)) + out.append(dump_region(emu, "vwf gate state", 0x703F00, 0x10)) + # Allocator state + out.append(dump_region(emu, "allocator", 0x702F00, 0x10)) + # Format buffer used by inventory render + out.append(dump_region(emu, "inv_format_buffer ($7E9E66)", 0x7E9E66, 0x40)) + # Mode flag DP $37 + out.append(dump_region(emu, "mode flag $37", 0x7E0030, 0x10)) + OUT.write_text("\n\n".join(out)) + print(f"wrote {OUT} ({OUT.stat().st_size} bytes)") + + +if __name__ == "__main__": + main() diff --git a/tests/_profile/dump_inv_tilemap.py b/tests/_profile/dump_inv_tilemap.py new file mode 100644 index 0000000..52305d6 --- /dev/null +++ b/tests/_profile/dump_inv_tilemap.py @@ -0,0 +1,122 @@ +"""Snapshot the battle inventory rolling-buffer tilemap + allocator +state across consecutive frames. Diffs each frame's snapshot against +the prior frame so we can pin down whether the tile_id refs in the +tilemap drift (allocator races), whether the WRAM CHR buffer drifts +(buffer reuse race), or both stay stable while VRAM stays correct. + +Usage: + python3 tests/_profile/dump_inv_tilemap.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, "tests") +from kintsuki import Button +from _ff4kintsuki import kss_path, load_emu_from_kss, tap + +REPO = Path(__file__).resolve().parents[2] +SYM_PATH = REPO / "build" / "ff4.sym" + +# Inventory rolling-buffer tilemap entries live in WRAM around the +# vanilla DrawInventoryItemText slot. The rolling layout stores 6 +# slots * 60 bytes starting at $97A6. +SLOT_BASE = 0x7E97A6 +SLOT_STRIDE = 60 +SLOT_COUNT = 6 + +# Inventory CHR region in the shared VWF buffer (16 bytes per tile, +# tile_id 0xC0..0xEF -> buffer + $C00..$EF0). +BUFFER_CHR_BASE = 0x703C00 +BUFFER_CHR_LEN = 0x300 # 48 tiles * 16 + +# Allocator state (cross-bank). +ALLOCATED_TILE_ID = 0x702F00 + +# Gate state (recently moved past inventory tiles). +PENDING_TRANSFER_MASK = 0x703F00 +REGION_DIRTY_BITS = 0x703F01 +RENDER_SKIPPED = 0x703F02 +TILEMAP_PENDING_MASK = 0x703F03 + +# Rolling-buffer state. +ROLLING_TOP_ROW = 0x7EEF84 +ROLLING_BUFFER_POS = 0x7EEF80 +ROLLING_SLOT_INDEX = 0x7EEF82 + + +def snapshot(emu) -> dict: + snap = { + "tile_id": emu.read(ALLOCATED_TILE_ID), + "pending": emu.read(PENDING_TRANSFER_MASK), + "region_dirty": emu.read(REGION_DIRTY_BITS), + "render_skipped": emu.read(RENDER_SKIPPED), + "tilemap_pending": emu.read(TILEMAP_PENDING_MASK), + "rolling_top": emu.read(ROLLING_TOP_ROW), + "rolling_pos": emu.read(ROLLING_BUFFER_POS), + "rolling_slot": emu.read(ROLLING_SLOT_INDEX), + "slots": [ + bytes(emu.read(SLOT_BASE + i * SLOT_STRIDE + j) for j in range(SLOT_STRIDE)) + for i in range(SLOT_COUNT) + ], + "buffer_chr": bytes(emu.read(BUFFER_CHR_BASE + i) for i in range(BUFFER_CHR_LEN)), + } + return snap + + +def diff(prev: dict, cur: dict, frame: int) -> None: + scalars = [ + "tile_id", "pending", "region_dirty", "render_skipped", + "tilemap_pending", "rolling_top", "rolling_pos", "rolling_slot", + ] + changes = [] + for k in scalars: + if prev[k] != cur[k]: + changes.append(f"{k}: {prev[k]:02X} -> {cur[k]:02X}") + for i in range(SLOT_COUNT): + if prev["slots"][i] != cur["slots"][i]: + differing = sum(1 for a, b in zip(prev["slots"][i], cur["slots"][i]) if a != b) + changes.append(f"slot{i}: {differing} bytes differ") + if prev["buffer_chr"] != cur["buffer_chr"]: + differing = sum(1 for a, b in zip(prev["buffer_chr"], cur["buffer_chr"]) if a != b) + changes.append(f"buffer_chr: {differing} / {BUFFER_CHR_LEN} bytes differ") + print(f"[frame {frame}] " + (", ".join(changes) if changes else "no change")) + + +def main() -> None: + emu = load_emu_from_kss(kss_path("ff4-battle.kss"), settle_frames=600) + # Settle until Cecil's ATB fills + cmd menu opens, then drive the + # input sequence the user mapped out: DOWN, DOWN, A -> item menu. + tap(emu, Button.DOWN, gap=20) + tap(emu, Button.DOWN, gap=20) + tap(emu, Button.A, gap=60) + print(f"Initial state after settle + nav:") + s0 = snapshot(emu) + print(f" tile_id={s0['tile_id']:02X} pending={s0['pending']:02X} " + f"region_dirty={s0['region_dirty']:02X} rolling_slot={s0['rolling_slot']:02X}") + for i, slot in enumerate(s0["slots"]): + first16 = slot[:16].hex(" ") + print(f" slot{i}[:16]: {first16}") + print() + + prev = s0 + for f in range(1, 11): + emu.run_frames(1) + cur = snapshot(emu) + diff(prev, cur, f) + prev = cur + + # Capture the visible framebuffer so we can eyeball the "scrolling" + # symptom alongside the byte deltas. + from kintsuki.visual import golden + out = REPO / "tests" / "goldens" / "battle_init" / "inv_open.png" + out.parent.mkdir(parents=True, exist_ok=True) + if out.exists(): + out.unlink() + golden(emu, out, threshold=0.1, max_diff_pixels=0) + print(f"\nframebuffer saved to {out}") + + +if __name__ == "__main__": + main() diff --git a/tests/_profile/dumps/inv_sram.hex b/tests/_profile/dumps/inv_sram.hex new file mode 100644 index 0000000..fb280c0 --- /dev/null +++ b/tests/_profile/dumps/inv_sram.hex @@ -0,0 +1,237 @@ +=== rolling slots ($7E97A6) $7e97a6..$7e990d (360B) === +7e97a6: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7e97b6: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7e97c6: c0 01 c1 01 c2 01 c3 01 c4 01 c5 01 ff 00 ff 00 ................ +7e97d6: ff 00 ff 00 ff 00 c8 00 80 00 85 00 ff 00 ff 00 ................ +7e97e6: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7e97f6: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ca 01 cb 01 ................ +7e9806: cc 01 cd 01 ce 01 cf 01 ff 00 ff 00 ff 00 ff 00 ................ +7e9816: ff 00 c8 00 80 00 86 00 ff 04 ff 04 ff 04 ff 04 ................ +7e9826: ff 04 ff 04 ff 04 ff 04 ff 00 ff 00 ff 00 ff 00 ................ +7e9836: ff 04 ff 04 ff 04 2a 04 d4 05 d5 05 d6 05 d7 05 ......*......... +7e9846: d8 05 d9 05 da 05 ff 00 ff 00 ff 00 ff 00 c8 04 ................ +7e9856: ff 04 81 04 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7e9866: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7e9876: ff 00 ff 00 de 01 df 01 e0 01 e1 01 e2 01 e3 01 ................ +7e9886: ff 00 ff 00 ff 00 ff 00 ff 00 e3 01 e4 01 ff 00 ................ +7e9896: ff 04 ff 04 ff 04 ff 04 ff 04 ff 04 ff 04 ff 04 ................ +7e98a6: ff 00 ff 00 ff 00 ff 00 ff 04 ff 04 ff 04 2a 04 ..............*. +7e98b6: e8 05 e9 05 ea 05 eb 05 ec 05 ed 05 ee 05 ff 00 ................ +7e98c6: ff 00 ff 00 ff 00 c8 04 ff 04 81 04 ff 00 ff 00 ................ +7e98d6: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7e98e6: ff 00 ff 00 ff 00 ff 00 ff 00 2a 00 f2 01 f3 01 ..........*..... +7e98f6: f4 01 f5 01 f6 01 f7 01 f8 01 ff 00 ff 00 ff 00 ................ +7e9906: ff 00 c8 00 ff 00 81 00 00 00 00 00 00 00 00 00 ................ + +=== vwf buffer head (msg/mon/names) $703000..$7035ff (1536B) === +703000: ff 00 ff 62 ff 92 ff 97 ff f2 ff 92 ff 92 ff 91 ...b............ +703010: ff 00 ff 20 ff 20 ff 76 ff 21 ff 27 ff 29 ff 17 ... . .v.!.'.).. +703020: ff 00 ff 00 ff 00 ff 32 ff 4a ff 4a ff 3a ff 09 .......2.J.J.:.. +703030: ff 00 ff 00 ff 00 ff 4c ff 52 ff 5e ff 50 ff 8c .......L.R.^.P.. +703040: ff 00 ff 00 ff 00 ff 0e ff 09 ff 09 ff 0e ff 08 ................ +703050: ff 01 ff 02 ff 00 ff 53 ff 64 ff 47 ff 44 ff 43 .......S.d.G.D.C +703060: ff 00 ff 00 ff 00 ff 24 ff a5 ff a9 ff 31 ff 20 .......$.....1. +703070: ff 00 ff 00 ff 00 ff ce ff 29 ff e9 ff 09 ff c9 .........)...... +703080: ff 00 ff 48 ff 40 ff ea ff 4a ff 4a ff 4b ff 2a ...H.@...J.J.K.* +703090: ff 00 ff 00 ff 00 ff 4c ff 52 ff 9e ff 10 ff 0c .......L.R...... +7030a0: ff 00 ff 08 ff 08 ff 08 ff 08 ff 08 ff 00 ff 08 ................ +7030b0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7030c0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7030d0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7030e0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7030f0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703100: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703110: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703120: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703130: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703140: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703150: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703160: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703170: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703180: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703190: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7031a0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7031b0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7031c0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7031d0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7031e0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7031f0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703200: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703210: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703220: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703230: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703240: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703250: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703260: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703270: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703280: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703290: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7032a0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7032b0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7032c0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7032d0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7032e0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7032f0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703300: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703310: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703320: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703330: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703340: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703350: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703360: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703370: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703380: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703390: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7033a0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7033b0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7033c0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7033d0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7033e0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7033f0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703400: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703410: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703420: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703430: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703440: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703450: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703460: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703470: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703480: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703490: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7034a0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7034b0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7034c0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7034d0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7034e0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7034f0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703500: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703510: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703520: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703530: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703540: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703550: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703560: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703570: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703580: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703590: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7035a0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7035b0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7035c0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7035d0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7035e0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7035f0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ + +=== vwf buffer mid (cmd) $703900..$703bff (768B) === +703900: ff 00 ff 62 ff 92 ff 97 ff f2 ff 92 ff 92 ff 91 ...b............ +703910: ff 00 ff 20 ff 20 ff 76 ff 21 ff 27 ff 29 ff 17 ... . .v.!.'.).. +703920: ff 00 ff 00 ff 00 ff 32 ff 4a ff 4a ff 3a ff 09 .......2.J.J.:.. +703930: ff 00 ff 00 ff 00 ff 4c ff 52 ff 5e ff 50 ff 8c .......L.R.^.P.. +703940: ff 00 ff 00 ff 00 ff a0 ff c0 ff 80 ff 80 ff 80 ................ +703950: ff 00 ff 64 ff 94 ff 97 ff 94 ff 94 ff 94 ff 67 ...d...........g +703960: ff 00 ff 00 ff 00 ff 18 ff a1 ff 99 ff 85 ff 38 ...............8 +703970: ff 00 ff 00 ff 00 ff c9 ff 29 ff 09 ff 29 ff c6 .........)...).. +703980: ff 00 ff 04 ff 00 ff 55 ff 64 ff 44 ff 44 ff 44 .......U.d.D.D.D +703990: ff 04 ff 88 ff 80 ff cc ff 92 ff 9e ff 90 ff 4c ...............L +7039a0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +7039b0: ff 00 ff 64 ff 94 ff 97 ff 94 ff 94 ff 94 ff 67 ...d...........g +7039c0: ff 00 ff 10 ff 00 ff 13 ff 94 ff 97 ff 94 ff 23 ...............# +7039d0: ff 00 ff 10 ff 10 ff 39 ff 92 ff 91 ff 10 ff 0b .......9........ +7039e0: ff 00 ff 00 ff 00 ff 80 ff 00 ff 80 ff 40 ff 80 .............@.. +7039f0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a00: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a10: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a20: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a30: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a40: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a50: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a60: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a70: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a80: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703a90: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703aa0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ab0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ac0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ad0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ae0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703af0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b00: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b10: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b20: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b30: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b40: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b50: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b60: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b70: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b80: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703b90: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ba0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703bb0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703bc0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703bd0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703be0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703bf0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ + +=== vwf buffer tail (inventory) $703c00..$703eff (768B) === +703c00: ff 00 ff 64 ff 90 ff 94 ff f5 ff 94 ff 94 ff 95 ...d............ +703c10: ff 00 ff 00 ff 00 ff e9 ff 29 ff e9 ff 29 ff c6 .........)...).. +703c20: ff 00 ff 54 ff 14 ff 54 ff 55 ff 55 ff 55 ff 54 ...T...T.U.U.U.T +703c30: ff 00 ff 00 ff 00 ff c0 ff 20 ff e0 ff 00 ff c0 ......... ...... +703c40: ff 00 ff 60 ff 90 ff 95 ff 96 ff 94 ff 94 ff 64 ...`...........d +703c50: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703c60: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703c70: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703c80: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703c90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +703ca0: ff 00 ff f2 ff 82 ff 87 ff e2 ff 82 ff 82 ff f1 ................ +703cb0: ff 00 ff 40 ff 40 ff 71 ff 4a ff 4b ff 4a ff 49 ...@.@.q.J.K.J.I +703cc0: ff 00 ff 00 ff 00 ff 94 ff 58 ff d0 ff 10 ff 90 .........X...... +703cd0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ce0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703cf0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703d00: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703d10: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703d20: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703d30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +703d40: ff 00 ff e0 ff 90 ff 93 ff e0 ff 93 ff 94 ff e3 ................ +703d50: ff 00 ff 00 ff 00 ff 1d ff a5 ff 9d ff 85 ff b8 ................ +703d60: ff 00 ff 00 ff 00 ff 26 ff 29 ff 2f ff 28 ff c6 .......&.)./.(.. +703d70: ff 00 ff 22 ff 22 ff 77 ff 22 ff 22 ff 22 ff 11 ...".".w.".".".. +703d80: ff 00 ff 00 ff 00 ff 30 ff 48 ff 78 ff 40 ff 30 .......0.H.x.@.0 +703d90: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703da0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703db0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703dc0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703dd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +703de0: ff 00 ff 60 ff 90 ff 83 ff 84 ff 84 ff 94 ff 63 ...`...........c +703df0: ff 00 ff 28 ff 28 ff 2a ff aa ff a9 ff a8 ff 2b ...(.(.*.......+ +703e00: ff 00 ff 00 ff 00 ff 55 ff 5a ff d3 ff 52 ff 91 .......U.Z...R.. +703e10: ff 00 ff 00 ff 00 ff 80 ff 40 ff c0 ff 00 ff 80 .........@...... +703e20: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703e30: ff 00 ff 18 ff 21 ff a0 ff 38 ff 24 ff a4 ff 19 .....!...8.$.... +703e40: ff 00 ff 80 ff 80 ff 80 ff 80 ff 80 ff 80 ff c0 ................ +703e50: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703e60: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703e70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +703e80: ff 00 ff e0 ff 90 ff 93 ff e0 ff 93 ff 94 ff e3 ................ +703e90: ff 00 ff 00 ff 00 ff 1d ff a5 ff 9d ff 85 ff b8 ................ +703ea0: ff 00 ff 00 ff 00 ff 26 ff 29 ff 2f ff 28 ff c6 .......&.)./.(.. +703eb0: ff 00 ff 22 ff 22 ff 77 ff 22 ff 22 ff 22 ff 11 ...".".w.".".".. +703ec0: ff 00 ff 00 ff 00 ff 30 ff 48 ff 78 ff 40 ff 30 .......0.H.x.@.0 +703ed0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ee0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ +703ef0: ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ + +=== vwf gate state $703f00..$703f0f (16B) === +703f00: e8 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ff 00 ................ + +=== allocator $702f00..$702f0f (16B) === +702f00: ee 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ + +=== inv_format_buffer ($7E9E66) $7e9e66..$7e9ea5 (64B) === +7e9e66: 0e 04 2a fe 0e 04 43 5c 62 70 60 6f 6f 60 ff ff ..*...C\bp`oo`.. +7e9e76: ff fe fc 0c c8 ff 81 00 00 00 00 00 00 00 00 00 ................ +7e9e86: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ +7e9e96: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ + +=== mode flag $37 $7e0030..$7e003f (16B) === +7e0030: 05 75 10 d8 50 d8 20 00 00 00 00 00 00 00 00 c2 .u..P. ......... \ No newline at end of file diff --git a/tests/_profile/profile_battle_vwf.py b/tests/_profile/profile_battle_vwf.py index b403fc4..530189e 100644 --- a/tests/_profile/profile_battle_vwf.py +++ b/tests/_profile/profile_battle_vwf.py @@ -1,6 +1,6 @@ """Profile the battle VWF blit path with all redraw gates forced dirty. -Loads ff4-battle-ext.kss at the Battle_ext seed point, pokes every +Loads ff4-battle.kss at the Battle_ext seed point, pokes every redraw-gate dirty bit so DrawCharNames + DrawMonsterNames + DrawCmd fire on every profiled frame, then runs `profile_start` scoped to the display_char neighbourhood in bank $20 and reports per-function call @@ -54,11 +54,11 @@ def label(pc: int) -> str: def force_redraw(emu): emu.write(0x7EEF9A, 0xFF) # battle_menu_dirty: chars + cmd + status emu.write(0x7EEF9B, 0xFF) # battle_monster_dirty: all monster slots - emu.write(0x703C01, 0xFF) # region_dirty_bits: slice-2 queue + emu.write(0x703F01, 0xFF) # region_dirty_bits: slice-2 queue (moved past inventory tiles) def run(): - emu = load_emu_from_kss(kss_path("ff4-battle-ext.kss"), settle_frames=0) + emu = load_emu_from_kss(kss_path("ff4-battle.kss"), settle_frames=0) force_redraw(emu) emu.run_frames(WARMUP_FRAMES) force_redraw(emu) diff --git a/tests/_profile/trace_cmd_dma.py b/tests/_profile/trace_cmd_dma.py index 8ee789e..e26eaa4 100644 --- a/tests/_profile/trace_cmd_dma.py +++ b/tests/_profile/trace_cmd_dma.py @@ -3,7 +3,7 @@ sys.path.insert(0, "tests") from _ff4kintsuki import kss_path, load_emu_from_kss -e = load_emu_from_kss(kss_path("ff4-battle-ext.kss"), settle_frames=0) +e = load_emu_from_kss(kss_path("ff4-battle.kss"), settle_frames=0) def snap(label): cmd_dirty = e.read(0x7EEF9A) diff --git a/tests/_profile/trace_drift.py b/tests/_profile/trace_drift.py index 07a030b..baa82e8 100644 --- a/tests/_profile/trace_drift.py +++ b/tests/_profile/trace_drift.py @@ -2,11 +2,10 @@ import sys sys.path.insert(0, "tests") import ctypes -from pathlib import Path from kintsuki import Emu -from _ff4kintsuki import ROM +from _ff4kintsuki import ROM, kss_path -KSS = Path("/Users/manz/PyCharmProjects/ff4-modules/.claude/worktrees/equip-window-relayout/tests/savestates/ff4-battle-ext.kss") +KSS = kss_path("ff4-battle.kss") e = Emu(load_srm_sidecar=False) e.load_rom(str(ROM)) blob = KSS.read_bytes() diff --git a/tests/_profile/trace_flying.py b/tests/_profile/trace_flying.py index 3246453..57a985e 100644 --- a/tests/_profile/trace_flying.py +++ b/tests/_profile/trace_flying.py @@ -3,7 +3,7 @@ sys.path.insert(0, "tests") from _ff4kintsuki import kss_path, load_emu_from_kss -e = load_emu_from_kss(kss_path("ff4-battle-ext.kss"), settle_frames=0) +e = load_emu_from_kss(kss_path("ff4-battle.kss"), settle_frames=0) def snap(label): ed4e = e.read(0x7EED4E) diff --git a/tests/_profile/trace_init_highlight.py b/tests/_profile/trace_init_highlight.py index 03a4a71..11e4f1c 100644 --- a/tests/_profile/trace_init_highlight.py +++ b/tests/_profile/trace_init_highlight.py @@ -23,7 +23,7 @@ def slot_pal_bytes(slot): def snap(label): s1822 = e.read(0x7E1822) dirty = e.read(0x7EEF9A) - region = e.read(0x703C01) + region = e.read(0x703F01) pals = [slot_pal_bytes(s) for s in range(5)] print(f"[{label:>10}] $1822={s1822:02X} EF9A={dirty:02X} reg_dirty={region:02X}") for s, pal in enumerate(pals): diff --git a/tests/_profile/trace_names_layout.py b/tests/_profile/trace_names_layout.py index 26c1869..5346db6 100644 --- a/tests/_profile/trace_names_layout.py +++ b/tests/_profile/trace_names_layout.py @@ -3,7 +3,7 @@ sys.path.insert(0, "tests") from _ff4kintsuki import kss_path, load_emu_from_kss -e = load_emu_from_kss(kss_path("ff4-battle-ext.kss"), settle_frames=0) +e = load_emu_from_kss(kss_path("ff4-battle.kss"), settle_frames=0) e.run_frames(120) # $74FD = text buffer (input to VWF), built by UpdateCharNames diff --git a/tests/_profile/trace_palette.py b/tests/_profile/trace_palette.py index 3b3269a..a98afa8 100644 --- a/tests/_profile/trace_palette.py +++ b/tests/_profile/trace_palette.py @@ -3,7 +3,7 @@ sys.path.insert(0, "tests") from _ff4kintsuki import kss_path, load_emu_from_kss -e = load_emu_from_kss(kss_path("ff4-battle-ext.kss"), settle_frames=0) +e = load_emu_from_kss(kss_path("ff4-battle.kss"), settle_frames=0) e.run_frames(120) def dump_b966(): diff --git a/tests/goldens/battle_init/battle_init.png b/tests/goldens/battle_init/battle_init.png index c8f0955..b1b38d0 100644 Binary files a/tests/goldens/battle_init/battle_init.png and b/tests/goldens/battle_init/battle_init.png differ diff --git a/tests/goldens/battle_init/inv_open.png b/tests/goldens/battle_init/inv_open.png new file mode 100644 index 0000000..0b2d8aa Binary files /dev/null and b/tests/goldens/battle_init/inv_open.png differ diff --git a/tests/goldens/field_inventory/scroll_0.bin b/tests/goldens/field_inventory/scroll_0.bin index 858e02a..de37067 100644 Binary files a/tests/goldens/field_inventory/scroll_0.bin and b/tests/goldens/field_inventory/scroll_0.bin differ diff --git a/tests/goldens/field_inventory/scroll_0.png b/tests/goldens/field_inventory/scroll_0.png index 15d9d56..9b7459d 100644 Binary files a/tests/goldens/field_inventory/scroll_0.png and b/tests/goldens/field_inventory/scroll_0.png differ diff --git a/tests/goldens/field_inventory/scroll_1.bin b/tests/goldens/field_inventory/scroll_1.bin index 858e02a..de37067 100644 Binary files a/tests/goldens/field_inventory/scroll_1.bin and b/tests/goldens/field_inventory/scroll_1.bin differ diff --git a/tests/goldens/field_inventory/scroll_10.bin b/tests/goldens/field_inventory/scroll_10.bin index f33dd9e..6cff6a8 100644 Binary files a/tests/goldens/field_inventory/scroll_10.bin and b/tests/goldens/field_inventory/scroll_10.bin differ diff --git a/tests/goldens/field_inventory/scroll_10.png b/tests/goldens/field_inventory/scroll_10.png index dcb058e..7cd27b3 100644 Binary files a/tests/goldens/field_inventory/scroll_10.png and b/tests/goldens/field_inventory/scroll_10.png differ diff --git a/tests/goldens/field_inventory/scroll_15.bin b/tests/goldens/field_inventory/scroll_15.bin index 6ed5e8e..f61c3ee 100644 Binary files a/tests/goldens/field_inventory/scroll_15.bin and b/tests/goldens/field_inventory/scroll_15.bin differ diff --git a/tests/goldens/field_inventory/scroll_5.bin b/tests/goldens/field_inventory/scroll_5.bin index 858e02a..de37067 100644 Binary files a/tests/goldens/field_inventory/scroll_5.bin and b/tests/goldens/field_inventory/scroll_5.bin differ diff --git a/tests/goldens/field_inventory/scroll_5.png b/tests/goldens/field_inventory/scroll_5.png index 6f9521f..180d38d 100644 Binary files a/tests/goldens/field_inventory/scroll_5.png and b/tests/goldens/field_inventory/scroll_5.png differ diff --git a/tests/goldens/field_inventory/swap_0_to_10.bg1.bin b/tests/goldens/field_inventory/swap_0_to_10.bg1.bin index 344de30..3cba4e3 100644 Binary files a/tests/goldens/field_inventory/swap_0_to_10.bg1.bin and b/tests/goldens/field_inventory/swap_0_to_10.bg1.bin differ diff --git a/tests/goldens/field_inventory/swap_0_to_3.bg1.bin b/tests/goldens/field_inventory/swap_0_to_3.bg1.bin index 70989a3..1057f3a 100644 Binary files a/tests/goldens/field_inventory/swap_0_to_3.bg1.bin and b/tests/goldens/field_inventory/swap_0_to_3.bg1.bin differ diff --git a/tests/goldens/field_inventory/trash_at_row3.bin b/tests/goldens/field_inventory/trash_at_row3.bin index 858e02a..de37067 100644 Binary files a/tests/goldens/field_inventory/trash_at_row3.bin and b/tests/goldens/field_inventory/trash_at_row3.bin differ diff --git a/tests/savestates/ff4-before-field-inventory.kss b/tests/savestates/ff4-before-field-inventory.kss new file mode 100644 index 0000000..12b863b Binary files /dev/null and b/tests/savestates/ff4-before-field-inventory.kss differ diff --git a/tests/savestates/ff4-battle-ext.kss b/tests/savestates/ff4-before-inventory-opens.kss similarity index 65% rename from tests/savestates/ff4-battle-ext.kss rename to tests/savestates/ff4-before-inventory-opens.kss index 28f8fae..40ae7e6 100644 Binary files a/tests/savestates/ff4-battle-ext.kss and b/tests/savestates/ff4-before-inventory-opens.kss differ diff --git a/tests/savestates/ff4-before-mayhem2.kss b/tests/savestates/ff4-before-mayhem2.kss new file mode 100644 index 0000000..7c95131 Binary files /dev/null and b/tests/savestates/ff4-before-mayhem2.kss differ diff --git a/tests/savestates/ff4-before-treasure-mayhem.kss b/tests/savestates/ff4-before-treasure-mayhem.kss new file mode 100644 index 0000000..16293ee Binary files /dev/null and b/tests/savestates/ff4-before-treasure-mayhem.kss differ diff --git a/tests/test_battle_init.py b/tests/test_battle_init.py index a8765e4..1be873a 100644 --- a/tests/test_battle_init.py +++ b/tests/test_battle_init.py @@ -24,7 +24,7 @@ ) GOLDENS = Path(__file__).parent / "goldens" / "battle_init" -KSS = kss_path("ff4-battle-ext.kss") +KSS = kss_path("ff4-battle.kss") @pytest.fixture @@ -37,7 +37,7 @@ def battle_emu(): # or HP delta) and the golden captures an empty header strip. emu.write(0x7EEF9A, 0xFF) # battle_menu_dirty: all chars + cmd + status emu.write(0x7EEF9B, 0xFF) # battle_monster_dirty: all monster slots - emu.write(0x703C01, 0xFF) # region_dirty_bits: slice-2 queue + emu.write(0x703F01, 0xFF) # region_dirty_bits: slice-2 queue (moved past inventory tiles) emu.run_frames(600) yield emu emu.close() diff --git a/tests/test_field_inventory_rolling.py b/tests/test_field_inventory_rolling.py index 7464005..9d71121 100644 --- a/tests/test_field_inventory_rolling.py +++ b/tests/test_field_inventory_rolling.py @@ -40,14 +40,28 @@ ) GOLDENS = Path(__file__).parent / "goldens" / "field_inventory" -KSS = kss_path("ff4-field-inventory-open.kss") +# Pre-open kss : the legacy `ff4-field-inventory-open.kss` froze +# VRAM from before the items_menu_vwf flush split + bottom-row blank +# pre-fill landed, so loading it short-circuits the new render path +# and the post-fix CHR never reaches VRAM. Use the before-open state +# and drive a fresh open through the current ROM so every render hook +# lands the way it does in-game. +KSS = kss_path("ff4-before-inventory-opens.kss") @pytest.fixture def field_emu(): - e = load_emu_from_kss(KSS) - # Settle so the menu is fully open and HDMA shadows are populated. + e = load_emu_from_kss(KSS, settle_frames=60) + # Open the items submenu. + tap(e, Button.A) e.run_frames(60) + # Cursor wiggle nudges check_if_description_was_rendered off its + # initial "last_drawn == 0 / autocounter pending" state so the + # description redraw actually fires for the highlighted slot. + tap(e, Button.DOWN) + e.run_frames(15) + tap(e, Button.UP) + e.run_frames(120) yield e e.close() diff --git a/tests/test_menu_exit_cleanup.py b/tests/test_menu_exit_cleanup.py index b14af0c..553d1d8 100644 --- a/tests/test_menu_exit_cleanup.py +++ b/tests/test_menu_exit_cleanup.py @@ -14,7 +14,7 @@ ) FIELD_STATE_BASE = 0x7E1BA8 -TREASURE_STATE_BASE = 0x7E1BD0 +TREASURE_STATE_BASE = 0x7E9C00 HDMA_SHADOW = 0x7E1BAE TREASURE_IN_MENU_FLAG = 0x7E1BC6 HDMA5_BASE = 0x004350 diff --git a/tests/test_rolling_engine_integration.py b/tests/test_rolling_engine_integration.py new file mode 100644 index 0000000..c73fbf2 --- /dev/null +++ b/tests/test_rolling_engine_integration.py @@ -0,0 +1,115 @@ +"""Integration test : drive the bank-20 rolling-inventory engine against +real in-game state, not synthetic SCRATCH_STATE. + +Opens the field-menu items submenu (state base = $7E:1BA8) and the +treasure popup state (base = $7E:1BD0), then JSLs through +`rolling_engine_invalidate_all` + `rolling_engine_vblank_flush` to +verify the dirty_mask gate works on live WRAM. + +Phase 1 entries are profile-agnostic ; this confirms they don't trip +on the existing macro-driven scroll FSM that owns the same struct. +""" +from __future__ import annotations + +import pytest + +from kintsuki import Button + +from _ff4kintsuki import load_emu_from_kss, kss_path, tap + + +FIELD_MENU_ROLLING_BASE = 0x7E1BA8 +TREASURE_ROLLING_BASE = 0x7E1BD0 + +# RollingBufferState offsets (mirrors src/items.i). +DIRTY_MASK_OFFSET = 25 + + +@pytest.fixture +def field_menu(): + """Field menu Items submenu open, cursor on Aiguille d'or.""" + e = load_emu_from_kss(kss_path("ff4-before-inventory-opens.kss"), + settle_frames=60) + tap(e, Button.A) + e.run_frames(60) + yield e + e.close() + + +def test_invalidate_all_lights_live_field_menu_state(field_menu) -> None: + """`rolling_engine_invalidate_all` on the live field-menu state base + sets dirty_mask = $FF without disturbing the rest of the struct.""" + e = field_menu + # Snapshot the engine-scratch bytes (top_row..hdma_copy_pending) so + # we can confirm the engine entry leaves them alone. + pre = bytes(e.read(FIELD_MENU_ROLLING_BASE + i) for i in range(15)) + + fn = e.lookup_symbol_addr("rolling_engine.rolling_engine_invalidate_all") + assert fn is not None + e.call(fn, x=FIELD_MENU_ROLLING_BASE & 0xFFFF) + + dirty = e.read(FIELD_MENU_ROLLING_BASE + DIRTY_MASK_OFFSET) + assert dirty == 0xFF, f"dirty_mask expected $FF, got ${dirty:02X}" + + post = bytes(e.read(FIELD_MENU_ROLLING_BASE + i) for i in range(15)) + assert pre == post, ( + "engine-scratch bytes were touched by invalidate_all " + f"(pre={pre.hex()} post={post.hex()})") + + +def test_vblank_flush_clears_live_dirty_mask(field_menu) -> None: + """Plant dirty_mask = $FF directly (no preceding engine call so the + field-menu NMI hooks can't intercept), then call vblank_flush and + verify the mask is cleared on live state.""" + e = field_menu + e.write(FIELD_MENU_ROLLING_BASE + DIRTY_MASK_OFFSET, 0xFF) + assert e.read(FIELD_MENU_ROLLING_BASE + DIRTY_MASK_OFFSET) == 0xFF + + flush = e.lookup_symbol_addr("rolling_engine.rolling_engine_vblank_flush") + e.call(flush, x=FIELD_MENU_ROLLING_BASE & 0xFFFF) + assert e.read(FIELD_MENU_ROLLING_BASE + DIRTY_MASK_OFFSET) == 0x00 + + +def test_field_menu_init_populates_engine_config(field_menu) -> None: + """init_menu_rolling_buffer_impl writes the phase-2 engine config + + hook far-ptrs into the field-menu state struct. + + Confirms the per-menu init now arms the bank-20 engine entries with + everything they need (visible_rows, item_list_ptr, hdma_channel, + vwf_cfg_ptr, render/hdma/draw hooks) even though the macro is still + driving the actual scroll machinery. + """ + e = field_menu + base = FIELD_MENU_ROLLING_BASE + assert e.read(base + 15) == 0x0A # visible_rows = 10 + assert e.read(base + 16) == 0x02 # slot_height_tiles = 2 + assert e.read(base + 17) == 0x40 # item_list_ptr low + assert e.read(base + 18) == 0x14 # item_list_ptr mid + assert e.read(base + 19) == 0x7E # item_list_ptr bank + assert e.read(base + 20) == 0x30 # item_count = 48 + assert e.read(base + 21) == 0x05 # hdma_channel = 5 + assert e.read(base + 22) == 0x80 # vwf_cfg_ptr low + assert e.read(base + 23) == 0x70 # vwf_cfg_ptr mid + assert e.read(base + 24) == 0x70 # vwf_cfg_ptr bank + # Hook far-ptrs land in bank-20 reloc region. + for hook_off in (26, 29, 32): + bank = e.read(base + hook_off + 2) + assert bank == 0x20, ( + f"hook at +{hook_off} bank=${bank:02X}, expected $20") + + +def test_cursor_down_on_field_menu_advances_slot_index(field_menu) -> None: + """`rolling_engine_cursor_down` advances slot_index on live state. + + Plants visible_rows = 11 (MENU_BUFFER_SLOTS), slot_index = 3 ; calls + cursor_down ; expects 4. visible_rows + slot_index live in the + engine-extended portion of the struct so we can poke them + independently of the live engine state.""" + e = field_menu + e.write(FIELD_MENU_ROLLING_BASE + 3, 3) # slot_index + e.write(FIELD_MENU_ROLLING_BASE + 15, 11) # visible_rows + + fn = e.lookup_symbol_addr("rolling_engine.rolling_engine_cursor_down") + e.call(fn, x=FIELD_MENU_ROLLING_BASE & 0xFFFF) + + assert e.read(FIELD_MENU_ROLLING_BASE + 3) == 4 diff --git a/tests/test_rolling_engine_skeleton.py b/tests/test_rolling_engine_skeleton.py new file mode 100644 index 0000000..6e3fbcd --- /dev/null +++ b/tests/test_rolling_engine_skeleton.py @@ -0,0 +1,388 @@ +"""Phase-1 smoke for the unified rolling-inventory engine. + +Each entry currently RTLs immediately (plans/rolling_inventory_engine.md +phase 1 stub) so the harness only verifies: + +* All eight bank-20 symbols resolve via the .adbg debug info. +* Each entry, when JSL'd from a fresh emulator with X = a sentinel + state-ptr, returns cleanly without crashing or trashing X/Y. +* `RollingBufferState` struct accepts the phase-1 extended fields + (visible_rows, slot_height_tiles, item_list_ptr, item_count, + hdma_channel, vwf_cfg_ptr, dirty_mask) without breaking the + existing field-items / treasure rolling consumers. + +Phase 2+ replaces the stubs with real implementations ; the same +harness expands to drive a fake inventory and assert scroll-state +transitions, dirty-mask flush ordering, and HDMA channel arming. +""" + +from __future__ import annotations + +import pytest + +from _ff4kintsuki import load_emu_from_kss, kss_path + +# RollingBufferState field offsets (mirrors src/items.i `.struct +# RollingBufferState`). Keep in sync if the struct evolves. +RBS_SLOT_INDEX = 3 +RBS_VISIBLE_ROWS = 15 +RBS_DIRTY_MASK = 25 + +# WRAM scratch region for engine state during unit tests. Sits in the +# vanilla "$AAAA filler" area at $7E:0500 -- never touched by the rest +# of the field-menu / treasure code paths during the menu settle so +# reads + writes stay deterministic. +SCRATCH_STATE = 0x7E0500 + + +ENGINE_ENTRIES = [ + "rolling_engine.rolling_engine_init", + "rolling_engine.rolling_engine_tick", + "rolling_engine.rolling_engine_vblank_flush", + "rolling_engine.rolling_engine_cursor_up", + "rolling_engine.rolling_engine_cursor_down", + "rolling_engine.rolling_engine_invalidate_slot", + "rolling_engine.rolling_engine_invalidate_all", + "rolling_engine.rolling_engine_shutdown", +] + + +@pytest.fixture +def emu(): + e = load_emu_from_kss(kss_path("ff4-before-inventory-opens.kss"), + settle_frames=10) + yield e + e.close() + + +def test_engine_entries_resolve(emu) -> None: + """Every phase-1 entry point is exported via the .adbg file.""" + missing = [sym for sym in ENGINE_ENTRIES + if emu.lookup_symbol_addr(sym) is None] + assert not missing, f"missing engine symbols: {missing}" + + +def test_engine_entries_in_bank_20(emu) -> None: + """Phase-1 entries land in the bank-20 reloc region ($20:8000+).""" + for sym in ENGINE_ENTRIES: + addr = emu.lookup_symbol_addr(sym) + assert addr is not None, sym + assert (addr >> 16) == 0x20, ( + f"{sym} at ${addr:06X} should be in bank 20") + + +# --------------------------------------------------------------- Helpers + + +def _setup_state(emu, *, slot_index: int, visible_rows: int, + dirty_mask: int = 0) -> int: + """Plant a sentinel `RollingBufferState` at SCRATCH_STATE and return + the low-16 of its address (= the value that goes in X for engine + JSLs ; the bank-$7E implied addressing in the engine reads from + $7E: + struct-field offset). + """ + emu.write(SCRATCH_STATE + RBS_SLOT_INDEX, slot_index) + emu.write(SCRATCH_STATE + RBS_VISIBLE_ROWS, visible_rows) + emu.write(SCRATCH_STATE + RBS_DIRTY_MASK, dirty_mask) + return SCRATCH_STATE & 0xFFFF + + +def _read_slot_index(emu) -> int: + return emu.read(SCRATCH_STATE + RBS_SLOT_INDEX) + + +def _read_dirty_mask(emu) -> int: + return emu.read(SCRATCH_STATE + RBS_DIRTY_MASK) + + +# --------------------------------------------------------------- Cursor + + +def test_cursor_down_increments_slot_index(emu) -> None: + """cursor_down advances slot_index by 1 while inside the window.""" + x = _setup_state(emu, slot_index=2, visible_rows=10) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_cursor_down") + emu.call(fn, x=x) + assert _read_slot_index(emu) == 3 + + +def test_cursor_down_wraps_to_zero(emu) -> None: + """cursor_down on the last slot wraps back to 0.""" + x = _setup_state(emu, slot_index=9, visible_rows=10) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_cursor_down") + emu.call(fn, x=x) + assert _read_slot_index(emu) == 0 + + +def test_cursor_up_decrements_slot_index(emu) -> None: + """cursor_up advances slot_index backwards by 1.""" + x = _setup_state(emu, slot_index=4, visible_rows=10) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_cursor_up") + emu.call(fn, x=x) + assert _read_slot_index(emu) == 3 + + +def test_cursor_up_wraps_from_zero(emu) -> None: + """cursor_up at slot 0 wraps to (visible_rows - 1).""" + x = _setup_state(emu, slot_index=0, visible_rows=10) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_cursor_up") + emu.call(fn, x=x) + assert _read_slot_index(emu) == 9 + + +# --------------------------------------------------------------- Dirty + + +def test_invalidate_slot_sets_bit(emu) -> None: + """invalidate_slot sets bit (1 << A) in dirty_mask without disturbing + bits already lit.""" + x = _setup_state(emu, slot_index=0, visible_rows=10, dirty_mask=0x01) + fn = emu.lookup_symbol_addr( + "rolling_engine.rolling_engine_invalidate_slot") + emu.call(fn, a=4, x=x) + # bit 4 set, bit 0 from setup also set + assert _read_dirty_mask(emu) == 0x11 + + +def test_invalidate_slot_idempotent(emu) -> None: + """Setting an already-dirty bit leaves dirty_mask unchanged.""" + x = _setup_state(emu, slot_index=0, visible_rows=10, dirty_mask=0x20) + fn = emu.lookup_symbol_addr( + "rolling_engine.rolling_engine_invalidate_slot") + emu.call(fn, a=5, x=x) + assert _read_dirty_mask(emu) == 0x20 + + +def test_invalidate_all_lights_full_mask(emu) -> None: + """invalidate_all sets dirty_mask to $FF.""" + x = _setup_state(emu, slot_index=0, visible_rows=10, dirty_mask=0x00) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_invalidate_all") + emu.call(fn, x=x) + assert _read_dirty_mask(emu) == 0xFF + + +# --------------------------------------------------------------- Init + + +# Engine-scratch bytes that init must zero (offsets within state struct). +INIT_ZERO_BYTES = [ + 0, # top_row + 1, # buffer_pos + 2, # edge_row + 3, # slot_index + 6, # hdma_enable + 7, # _pad + 8, # scroll_state + 9, # scroll_remaining + 10, # scroll_direction + 11, # transfer_pending + 12, # scroll_anim_offset lo + 13, # scroll_anim_offset hi + 14, # hdma_copy_pending + 25, # dirty_mask +] + + +def _fill_state_with_pattern(emu, byte: int = 0xAA, length: int = 32) -> None: + """Pre-fill 32 bytes at SCRATCH_STATE with a known sentinel so init's + zero-out and base_scroll = $FFFF stamping is visible against the + pattern.""" + for i in range(length): + emu.write(SCRATCH_STATE + i, byte) + + +def test_init_zeroes_engine_scratch(emu) -> None: + """rolling_engine_init clears the 14 engine-scratch + dirty_mask bytes.""" + _fill_state_with_pattern(emu) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_init") + emu.call(fn, x=SCRATCH_STATE & 0xFFFF) + for off in INIT_ZERO_BYTES: + val = emu.read(SCRATCH_STATE + off) + assert val == 0x00, ( + f"offset {off} expected zero after init, got ${val:02X}") + + +def test_init_stamps_base_scroll_sentinel(emu) -> None: + """rolling_engine_init writes $FFFF into base_scroll (offsets 4 + 5).""" + _fill_state_with_pattern(emu) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_init") + emu.call(fn, x=SCRATCH_STATE & 0xFFFF) + assert emu.read(SCRATCH_STATE + 4) == 0xFF + assert emu.read(SCRATCH_STATE + 5) == 0xFF + + +def test_init_preserves_config_fields(emu) -> None: + """Caller-managed config fields (visible_rows + dirty_mask aside) + must NOT be touched by init : engine_init clears scratch only.""" + _fill_state_with_pattern(emu) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_init") + emu.call(fn, x=SCRATCH_STATE & 0xFFFF) + # visible_rows (offset 15) + item_list_ptr 17..19 + item_count 20 + + # hdma_channel 21 + vwf_cfg_ptr 22..24 should keep the pattern. + for off in [15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: + val = emu.read(SCRATCH_STATE + off) + assert val == 0xAA, ( + f"config offset {off} got clobbered by init: ${val:02X}") + + +# --------------------------------------------------------------- Shutdown + + +def test_shutdown_clears_runtime_flags(emu) -> None: + """rolling_engine_shutdown clears hdma_enable / dirty_mask / + scroll_state / transfer_pending / hdma_copy_pending and restamps + the base_scroll sentinel.""" + _fill_state_with_pattern(emu) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_shutdown") + emu.call(fn, x=SCRATCH_STATE & 0xFFFF) + assert emu.read(SCRATCH_STATE + 6) == 0x00 # hdma_enable + assert emu.read(SCRATCH_STATE + 8) == 0x00 # scroll_state + assert emu.read(SCRATCH_STATE + 11) == 0x00 # transfer_pending + assert emu.read(SCRATCH_STATE + 14) == 0x00 # hdma_copy_pending + assert emu.read(SCRATCH_STATE + 25) == 0x00 # dirty_mask + assert emu.read(SCRATCH_STATE + 4) == 0xFF # base_scroll lo + assert emu.read(SCRATCH_STATE + 5) == 0xFF # base_scroll hi + + +def test_shutdown_leaves_cursor_alone(emu) -> None: + """Shutdown does not nuke slot_index / buffer_pos / edge_row / + top_row ; those are caller's UI state.""" + _setup_state(emu, slot_index=4, visible_rows=10) + emu.write(SCRATCH_STATE + 0, 0x07) # top_row + emu.write(SCRATCH_STATE + 1, 0x03) # buffer_pos + emu.write(SCRATCH_STATE + 2, 0x02) # edge_row + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_shutdown") + emu.call(fn, x=SCRATCH_STATE & 0xFFFF) + assert emu.read(SCRATCH_STATE + 0) == 0x07 + assert emu.read(SCRATCH_STATE + 1) == 0x03 + assert emu.read(SCRATCH_STATE + 2) == 0x02 + assert emu.read(SCRATCH_STATE + 3) == 0x04 # slot_index + + +# --------------------------------------------------------------- Tick + + +# Scroll FSM byte offsets within RollingBufferState. +RBS_SCROLL_STATE = 8 +RBS_SCROLL_REMAINING = 9 +RBS_TRANSFER_PENDING = 11 + + +def _setup_scroll(emu, *, state: int, remaining: int) -> int: + emu.write(SCRATCH_STATE + RBS_SCROLL_STATE, state) + emu.write(SCRATCH_STATE + RBS_SCROLL_REMAINING, remaining) + emu.write(SCRATCH_STATE + RBS_TRANSFER_PENDING, 0) + return SCRATCH_STATE & 0xFFFF + + +def test_tick_idle_is_a_noop(emu) -> None: + """Tick with scroll_state == 0 leaves scroll_remaining untouched.""" + x = _setup_scroll(emu, state=0, remaining=8) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_tick") + emu.call(fn, x=x) + assert emu.read(SCRATCH_STATE + RBS_SCROLL_STATE) == 0 + assert emu.read(SCRATCH_STATE + RBS_SCROLL_REMAINING) == 8 + assert emu.read(SCRATCH_STATE + RBS_TRANSFER_PENDING) == 0 + + +def test_tick_decrements_scroll_remaining(emu) -> None: + """Tick with scroll_state != 0 drops scroll_remaining by 1.""" + x = _setup_scroll(emu, state=1, remaining=4) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_tick") + emu.call(fn, x=x) + assert emu.read(SCRATCH_STATE + RBS_SCROLL_REMAINING) == 3 + assert emu.read(SCRATCH_STATE + RBS_SCROLL_STATE) == 1 + assert emu.read(SCRATCH_STATE + RBS_TRANSFER_PENDING) == 0 + + +def test_tick_finishes_animation_on_last_frame(emu) -> None: + """When scroll_remaining hits zero, scroll_state clears and + transfer_pending stamps 1 so NMI flush picks up the final frame.""" + x = _setup_scroll(emu, state=1, remaining=1) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_tick") + emu.call(fn, x=x) + assert emu.read(SCRATCH_STATE + RBS_SCROLL_REMAINING) == 0 + assert emu.read(SCRATCH_STATE + RBS_SCROLL_STATE) == 0 + assert emu.read(SCRATCH_STATE + RBS_TRANSFER_PENDING) == 1 + + +# --------------------------------------------------------------- Flush + + +def test_vblank_flush_clears_dirty_mask(emu) -> None: + """vblank_flush clears state.dirty_mask after handling the dirty rows.""" + x = _setup_state(emu, slot_index=0, visible_rows=10, dirty_mask=0x35) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_vblank_flush") + emu.call(fn, x=x) + assert _read_dirty_mask(emu) == 0x00 + + +def test_vblank_flush_noop_when_clean(emu) -> None: + """vblank_flush is harmless when dirty_mask is already zero.""" + x = _setup_state(emu, slot_index=0, visible_rows=10, dirty_mask=0x00) + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_vblank_flush") + emu.call(fn, x=x) + assert _read_dirty_mask(emu) == 0x00 + + +def test_init_fires_fn_draw_window_when_armed(emu) -> None: + """When state.fn_draw_window is non-zero, rolling_engine_init JSLs + through it. + + Plant a small RTL stub at $7E:0600 that writes a sentinel ($A5) to a + witness byte ($7E:0700), point fn_draw_window at the stub, then + call init. The witness byte should flip if the hook actually fires. + Stub bytes : `lda #$A5 ; sta.l $7E:0700 ; rtl` = A9 A5 8F 00 07 7E 6B. + """ + stub_bytes = bytes([0xA9, 0xA5, 0x8F, 0x00, 0x07, 0x7E, 0x6B]) + for i, b in enumerate(stub_bytes): + emu.write(0x7E0600 + i, b) + emu.write(0x7E0700, 0x00) + + _fill_state_with_pattern(emu) + # fn_draw_window lives at struct offset 32..34 (after dirty_mask). + emu.write(SCRATCH_STATE + 32, 0x00) + emu.write(SCRATCH_STATE + 33, 0x06) + emu.write(SCRATCH_STATE + 34, 0x7E) + + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_init") + emu.call(fn, x=SCRATCH_STATE & 0xFFFF) + + witness = emu.read(0x7E0700) + assert witness == 0xA5, ( + f"fn_draw_window stub did not fire ; witness=${witness:02X}") + + +def test_init_loops_fn_render_slot_visible_rows_times(emu) -> None: + """rolling_engine_init calls fn_render_slot once per visible row, + planting slot_index = edge_row = iteration count before each call. + + Stub at $7E:0800 increments a counter at $7E:0900 then RTLs. + With visible_rows = 5, init should fire 5 callbacks (counter = 5) + and leave slot_index + edge_row pinned at 4 (last iteration). + """ + # Stub : lda.l $7E0900 ; inc ; sta.l $7E0900 ; rtl + # AF 00 09 7E ; 1A ; 8F 00 09 7E ; 6B + stub = bytes([0xAF, 0x00, 0x09, 0x7E, + 0x1A, + 0x8F, 0x00, 0x09, 0x7E, + 0x6B]) + for i, b in enumerate(stub): + emu.write(0x7E0800 + i, b) + emu.write(0x7E0900, 0x00) + + # Zero the whole scratch state then set just visible_rows + render hook. + for i in range(40): + emu.write(SCRATCH_STATE + i, 0x00) + emu.write(SCRATCH_STATE + RBS_VISIBLE_ROWS, 5) + emu.write(SCRATCH_STATE + 26, 0x00) # fn_render_slot low + emu.write(SCRATCH_STATE + 27, 0x08) # fn_render_slot mid + emu.write(SCRATCH_STATE + 28, 0x7E) # fn_render_slot bank + + fn = emu.lookup_symbol_addr("rolling_engine.rolling_engine_init") + emu.call(fn, x=SCRATCH_STATE & 0xFFFF) + + counter = emu.read(0x7E0900) + assert counter == 5, f"fn_render_slot fired {counter} times, expected 5" + assert emu.read(SCRATCH_STATE + 3) == 4 # slot_index + assert emu.read(SCRATCH_STATE + 2) == 4 # edge_row diff --git a/tests/test_vwf_asset_widths.py b/tests/test_vwf_asset_widths.py new file mode 100644 index 0000000..7cf281a --- /dev/null +++ b/tests/test_vwf_asset_widths.py @@ -0,0 +1,67 @@ +"""VWF asset width budget tests. + +For each fixed-list XML asset that gets rendered through the 8x8 menu +VWF blitter, measure every entry's rendered tile-width with the same +font + kerning tables the runtime uses, and assert it fits the slot +budget the rendering code reserves. + +Battle inventory uses 10 VWF tile_ids per slot (the symbol byte + +colon + 2 digits are fixed-width tiles outside the budget). The +allocator clamp freezes at slot_base+9 ; an 11+ tile name would +truncate at render time. +""" +from __future__ import annotations +import math +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from script import Table # noqa: E402 +from metrics import TextMetrics # noqa: E402 +from build import read_fixed_from_xml # noqa: E402 + + +def _load(asset_xml: Path) -> tuple[list, TextMetrics, Table]: + table = Table(str(ROOT / "text" / "ff4_menus.tbl")) + metrics = TextMetrics(table, [str(ROOT / "assets" / "menu_font.dat")], char_height=8) + items = read_fixed_from_xml( + str(asset_xml), + table, + formatter=lambda t: (t or "").strip() + "[end]", + ) + return items, metrics, table + + +@pytest.mark.parametrize( + "asset_xml, vwf_tile_budget, kind", + [ + ("text/fr/items.xml", 10, "battle inventory items"), + ("text/fr/items_unleashed.xml", 10, "battle inventory items_unleashed"), + ], +) +def test_vwf_asset_within_budget(asset_xml: str, vwf_tile_budget: int, kind: str) -> None: + items, metrics, table = _load(ROOT / asset_xml) + offenders = [] + for i, ptr in enumerate(items): + body = ptr.value + # First byte is the fixed-width symbol icon ; VWF starts at byte 1. + vwf_bytes = body[1:] if body else b"" + # Strip trailing [end] sentinel before measurement. + if vwf_bytes and vwf_bytes[-1] == 0: + vwf_bytes = vwf_bytes[:-1] + width_px = metrics.measure_bytes(vwf_bytes) + tiles = math.ceil(width_px / 8) + if tiles > vwf_tile_budget: + text = table.to_text(body).replace("[end]", "") + offenders.append((i, tiles, width_px, text)) + assert not offenders, ( + f"{kind}: {len(offenders)} entries exceed {vwf_tile_budget}-tile VWF budget:\n " + + "\n ".join( + f"id={i:>3} {tiles:>2} tiles {px:>3}px {text!r}" + for i, tiles, px, text in offenders + ) + ) diff --git a/text/fr/items_unleashed.xml b/text/fr/items_unleashed.xml new file mode 100644 index 0000000..809d9d1 --- /dev/null +++ b/text/fr/items_unleashed.xml @@ -0,0 +1,259 @@ + + + + [0x29]Griffe de feu + [0x29]Griffe de glace + [0x29]Griffe de foudre + [0x29]Griffe de fée + [0x29]Griffe d'enfer + [0x29]Griffe de chat + [0x2a]Baguette + [0x2a]Baguette glace + [0x2a]Baguette feu + [0x2a]Baguette foudre + [0x2a]Baguette de mue + [0x2a]Baguette de fée + [0x2a]Baguette étoiles + [0x2a]Baguette Lilith + [0x2b]Bâton + [0x2b]Bâton de soin + [0x2b]Bâton mithril + [0x2b]Bâton de force + [0x2b]Bâton pulsar + [0x2b]Bâton du sage + [0x2b]Bâton runique + [0x2c]Epée ténébreuse + [0x2c]Lame de l'ombre + [0x2c]Death Bringer + [0x2e]Epée légendaire + [0x2e]Epée de lumière + [0x2e]Excalibur + [0x2d]Epée de flammes + [0x2d]Lame de glace + [0x2d]Défenseur + [0x2d]Epée de sang + [0x2d]Epée ancienne + [0x2d]Epée du sommeil + [0x2d]Lame briseuse + [0x2f]Lance + [0x2f]Lance du vent + [0x2f]Lance de feu + [0x2f]Lance de glace + [0x2f]Lance du dragon + [0x2f]Lance sacrée + [0x2f]Lance de sang + [0x2f]Gungnir + [0x31]Kunai + [0x31]Ashura + [0x31]Kotetsu + [0x31]Kikuichimonji + [0x31]Murasame + [0x31]Masamune + [0x30]Dague Assassin + [0x30]Tueur de mages + [0x3a]Fouet + [0x3a]Fouet à chaîne + [0x3a]Fouet foudre + [0x3a]Fouet de feu + [0x3a]Crin de dragon + [0x34]Hachette + [0x34]Hache des nains + [0x34]Tueur d'ogres + [0x30]Dague mithril + [0x30]Dague dansante + [0x2d]Epée mithril + [0x30]Couperet + [0x2e]Ragnarok + [0x32]Shuriken + [0x32]Shuriken Fuma + [0x33]Boomerang + [0x33]Engetsurin + [0x36]Harpe des rêves + [0x36]Harpe de Lamia + + [0x34]Hache d'Idun + [0x34]Hache runique + [0x35]Marteau mithril + [0x35]Marteau Terre + [0x35]Maillet + [0x2d]Vengeur + [0x37]Arc + [0x37]Arbalète + [0x37]Grand arc + [0x37]Arc mortel + [0x37]Arc elfique + [0x37]Arc de Yoichi + [0x37]Arc d'Artémis + [0x38]Flèche de fer + [0x38]Flèche sacrée + [0x38]Flèche de feu + [0x38]Flèche de glace + [0x38]Flèche foudre + [0x38]Flèche ténèbres + [0x38]Flèche de poison + [0x38]Flèche muselière + [0x38]Flèche d'ange + [0x38]Flèche Yoichi + [0x38]Flèche Méduse + [0x38]Flèche Artémis + + [0x3b]Bouclier de fer + [0x3b]Bouclier noir + [0x3b]Bouclier démon + [0x3b]Bouclier sacré + [0x3b]Bouclier mithril + [0x3b]Bouclier flamme + [0x3b]Bouclier glace + [0x3b]Bouclier diamant + [0x3b]Bouclier d'Egide + [0x3b]Bouclier Genji + [0x3b]Bouclier dragon + [0x3b]Bouclier cristal + [0x3c]Heaume de fer + [0x3c]Heaume noir + [0x3c]Heaume Hadès + [0x3c]Heaume démon + [0x3c]Heaume sacré + [0x3c]Heaume mithril + [0x3c]Heaume diamant + [0x3c]Heaume Genji + [0x3c]Heaume dragon + [0x3c]Heaume cristal + [0x3c]Chapeau de cuir + [0x3c]Chapeau plumes + [0x3c]Tricorne + [0x3c]Mitre du prêtre + [0x3c]Bandeau d'or + [0x3c]Ruban + [0x3c]Bandana + [0x3c]Béret vert + [0x3c]Capuche noire + [0x3c]Masque de verre + [0x3d]Armure de fer + [0x3d]Armure noire + [0x3d]Armure Hadès + [0x3d]Armure démon + [0x3d]Armure Chevalier + [0x3d]Armure mithril + [0x3d]Cotte de flammes + [0x3d]Armure de glace + [0x3d]Armure diamant + [0x3d]Armure Genji + [0x3d]Cotte de dragon + [0x3d]Cotte de cristal + [0x3d]Vêtements + [0x3d]Tunique cuir + [0x3d]Robe de Gaïa + [0x3d]Robe du prêtre + [0x3d]Robe noire + [0x3d]Robe de lumière + [0x3d]Robe blanche + [0x3d]Ruban de force + [0x3d]Bustier Minerva + [0x3d]Habit bagnard + [0x3d]Habit poète + [0x3d]Tenue Kenpou + [0x3d]Ceinture noire + [0x3d]Armure adamant + [0x3d]Habit ninja + [0x3e]Gantelet fer + [0x3e]Gantelet noir + [0x3e]Gantelet Hadès + [0x3e]Gantelet démon + [0x3e]Gantelet + [0x3e]Gantelet mithril + [0x3e]Gantelet diamant + [0x3e]Gantelet géant + [0x3e]Gantelet Genji + [0x3e]Gantelet dragon + [0x3e]Gantelet cristal + [0x3e]Brassard de fer + [0x3e]Anneau rubis + [0x3e]Brassard argent + [0x3e]Brassard force + [0x3e]Brassard rune + [0x3e]Anneau cristal + [0x3e]Brassard diamant + [0x3e]Anneau garde + [0x3e]Anneau maudit + [0xff]Eclat de Bombe + [0xff]Bras de Bombe + [0xff]Vent du Sud + [0xff]Vent du Nord + [0xff]Foudre de Zeus + [0xff]Foudre divine + [0xff]Poussière étoile + [0xff]Baiser Lilith + [0xff]Croc vampire + [0xff]Vin de Bacchus + [0xff]Sandales Hermès + [0xff]Sablier cuivre + [0xff]Sablier argent + [0xff]Sablier d'or + [0xff]Toile d'araignée + [0xff]Bouc émissaire + [0xff]Croc rouge + [0xff]Croc blanc + [0xff]Croc bleu + [0xff]Rideau lumière + [0xff]Âme de Bombe + [0xff]Rideau lunaire + [0xff]Cloche silence + [0xff]Tambour terre + [0xff]Cristal + [0xff]Moustache Coeurl + [0xff]Grimoire + [0xff]Bestiaire + [0xff]Réveil + [0xff]Corne licorne + [0xff]Potion + [0xff]Hi-Potion + [0xff]Méga-Potion + [0xff]Ether + [0xff]Ether sec + [0xff]Elixir + [0xff]Plume de phénix + [0xff]Aiguille d'or + [0xff]Baiser de fée + [0xff]Marteau magique + [0xff]Aliment diète + [0xff]Herbe d'écho + [0xff]Collyre + [0xff]Antidote + [0xff]Crucifix + [0xff]Panacée + [0xff]Alarme + [0xff]Pomme d'or + [0xff]Pomme d'argent + [0xff]Goutte de Soma + [0xff]Tente + [0xff]Chalet + [0xff]Revue coquine + [0xff]Porte de secours + [0xff]Pain de nain + [0x41]Gobelin + [0x41]Bombe + [0x41]Cocatrix + [0x41]Flagelleur + [0xff]Légume Gysahl + [0xff]Laissez-passer + [0xff]Flûte Gysahl + [0xff]Anneau Bombe + [0xff]Clé de Baron + [0xff]Lumière désert + [0xff]Cristal terre + [0xff]Pierre de magma + [0xff]Collier de Luca + [0xff]Murmure + [0xff]Cristal ténèbres + [0xff]Queue de rat + [0xff]Adamantite + [0xff]Poêle d'amour + [0xff]Queue rose + [0xff]Clé de Lugae + [0xff]Matière sombre + [0xff]Scénario 0015 + [0xff]Scénario 0016 + [0xff]--Trier-- + [0xff]Poubelle + diff --git a/todo/nmi-side-anim.md b/todo/nmi-side-anim.md index c78cf75..df73ae9 100644 --- a/todo/nmi-side-anim.md +++ b/todo/nmi-side-anim.md @@ -99,7 +99,7 @@ update the first 8 bytes (HDMA tables often repeat). - `tests/_profile/profile_bank20.py` : measure dma_transfer inclusive cycle delta after each phase -- Visual smoke on `ff4-battle-ext.kss` : Zu wing flap, char float +- Visual smoke on `ff4-battle.kss` : Zu wing flap, char float state if any party member has Float ## Crack / risk