diff --git a/build.py b/build.py index 93816a5..dd98c4a 100755 --- a/build.py +++ b/build.py @@ -106,6 +106,7 @@ def build_patch(input, output, lang): symbols={"LANG": lang}, include_paths=[Path("src")], prelude_file=Path("config.i"), + overlap_mode="warn", ) if result.exit_code != 0: @@ -165,7 +166,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 +187,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 +330,7 @@ def add_custom_kernings(text: str, advance: int) -> None: known_pairs_to_kern = [ "Ya", "Pa", - "Po" - "Fa", + "PoFa", "Fe", "Fo", "Fu", @@ -489,6 +491,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 +536,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..bf319ae 100644 --- a/ff4.s +++ b/ff4.s @@ -53,42 +53,48 @@ conditional_bg1_vofs := 0x208000 dialog_bank_ptr_base = 0x218000 -*=0xFFC0 +.alloc at 0x00FFC0 { ; patch snes cartridge type - ; original PCB: SHVC-1A3B - ; target PCB: SHVC-1A5B + ; 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. - jmp.l brk_handler - *=0x00FFE6 - .dw 0xFFE0 - *=0x00FFFE - .dw 0xFFE0 + ; JML trampoline in vector-table padding; native/emu BRK vectors point here. + .alloc at 0x00FFE0 { + jmp.l brk_handler + } + .alloc at 0x00FFE6 { + .dw 0xFFE0 + } + .alloc at 0x00FFFE { + .dw 0xFFE0 + } } -*=0x008031 - ; déroutage pour ajouter le splash screen -.if ENABLE_INTRO { - jsr.l start_splash_screen -} else { - jsr.l clear_ram +; déroutage pour ajouter le splash screen +.alloc at 0x008031 { + .if ENABLE_INTRO { + jsr.l start_splash_screen + } 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. @@ -129,8 +135,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 +154,7 @@ $15C9AA. _loop: sta.l 0x702000, x inx - cpx.w #0x5000 + cpx.w #0x5100 bne _loop } rtl @@ -167,6 +179,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 +220,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,7 +282,6 @@ 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 --------------------------------------------------- .import "libmz" @@ -253,6 +311,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 +324,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,6 +335,7 @@ 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" @@ -310,4 +371,12 @@ signature byte sits at PB:(PC - 1). 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/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_reloc.s b/src/battle/commands_reloc.s index c4cda17..2bb0861 100644 --- a/src/battle/commands_reloc.s +++ b/src/battle/commands_reloc.s @@ -1,4 +1,6 @@ """ +.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. """ @@ -13,172 +15,175 @@ 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 -} +.alloc battle_commands_reloc_block in bank20_reloc { + .if BATTLE_CMD_VWF { + command_length = 6 + } else { + command_length = 10 + } -_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: + 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: { -_loop: - lda.l assets_battle_commands_nul_dat, x - cmp #0 - beq _exit_command_loop - sta.w 0x0000, y - - inx - iny - bra _loop + 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_command_loop: -_pad_loop: - - -_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/equip_window.s b/src/battle/equip_window.s index 36f3ced..d97b403 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,105 @@ 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 +.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..702e88b 100644 --- a/src/battle/graphics.s +++ b/src/battle/graphics.s @@ -1,14 +1,19 @@ """ +.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 + +.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/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..f46d73f 100644 --- a/src/battle/inventory_rolling.s +++ b/src/battle/inventory_rolling.s @@ -4,152 +4,181 @@ Battle inventory rolling-buffer engine (single column, 5 visible rows + 1 prefet runs the field-menu NMI DMA check. """ .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 -; ============================================================================ -; 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) +.alloc battle_inventory_rolling_block in bank20_reloc { + .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 + .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 + .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 + ; Queue VRAM transfer for initial render lda #0x03 ldy.w #0x0002 jsr.l load_menu_tfr_data_trampoline @@ -160,280 +189,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 - - pla ; Get quantity - tax - jsr.l hex_to_dec_trampoline ; Convert to decimal - jsr.l normalize_num_trampoline ; Format digits + _has_item: + lda #0xC8 + sta.w inv_format_buffer, y + iny -; Add tens digit - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180E ; Tens digit - sta.w inv_format_buffer, y - iny - -; Add ones digit - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.w 0x180F ; Ones digit - -_finish_format: - sta.w inv_format_buffer, y - iny + pla + tax + jsr.l hex_to_dec_trampoline + jsr.l normalize_num_trampoline -; Terminator - lda #0x00 - sta.w inv_format_buffer, y + lda.w 0x180E + sta.w inv_format_buffer, y + iny -; Call draw_text to render formatted text to text buffer - jsr.l draw_text_rolling_trampoline + lda.w 0x180F - 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 + _finish_format: + sta.w inv_format_buffer, y + iny -_circ_not_disabled: + lda #0x00 + sta.w inv_format_buffer, y -; 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 + ; Call draw_text to render formatted text to text buffer + jsr.l draw_text_rolling_trampoline -; Symbol tile - lda #0x03 - sta.w inv_format_buffer, y - iny - lda.l assets_items_dat, x - sta.w inv_format_buffer, y - iny + plb ; Restore data bank + rts -; 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 +476,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 +512,478 @@ _circ_name_loop: lda #0x03 bra _circ_finish -_circ_has_item: - lda #0xC8 ; Colon - 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 - -_circ_finish: - sta.w inv_format_buffer, y - iny - - lda #0x00 - sta.w inv_format_buffer, y - - jsr.l draw_text_rolling_trampoline - - plb - rts - -; ============================================================================ -; _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_has_item: + lda #0xC8 + sta.w inv_format_buffer, y + iny -_circ_copy_row2: - lda.w text_buffer_base, x - sta (0x00), y - inx - iny - cpy.w #0x005E - bne _circ_copy_row2 + pla + tax + jsr.l hex_to_dec_trampoline + jsr.l normalize_num_trampoline -; Clear remaining bytes of second row (0x5E-0x7F) + lda.w 0x180E + sta.w inv_format_buffer, y + iny -_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 + lda.w 0x180F -; Copy second tilemap row (30 bytes) -; Tilemap row 2 is at +$40 (64 bytes) from row 1 - ldy.w #0x0040 + _circ_finish: + sta.w inv_format_buffer, y + iny -_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 + lda #0x00 + sta.w inv_format_buffer, y -; ============================================================================ -; _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 + jsr.l draw_text_rolling_trampoline -_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 + plb + rts -; 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 +991,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 +1023,1595 @@ _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) + ; ============================================================================ + ; 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 := 0x9C3E ; drops_rolling + 0x0E (drops_rolling moved to $9C30) + + ; 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 + + _field_nmi_check_treasure: + .if TREASURE_INVENTORY_ROLLING { + ; === Tilemap DMA transfer (treasure menu = BG3) === + lda.l 0x7E0000 + 0x9C0B ; treasure_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..4ace658 100644 --- a/src/battle/inventory_rolling_patches.s +++ b/src/battle/inventory_rolling_patches.s @@ -16,6 +16,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) @@ -62,26 +69,35 @@ calls, JML hooks for the scroll animation, surgical NOPs / RTS overrides). } .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).""" @@ -113,26 +129,23 @@ _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 +; 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 +153,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 +163,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 +184,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 diff --git a/src/battle/magic_reloc.s b/src/battle/magic_reloc.s index 5d2f6dd..7708e1a 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,168 @@ 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 +.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 -left_column: -; Left column - rep #0x20 - lda.w #left_column_base - clc - adc.b current_row_offset -; add current row offset - sta 0x32 + 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 -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 + left_column: + ; Left column + rep #0x20 + lda.w #left_column_base + clc + adc.b current_row_offset + ; add current row offset + sta 0x32 -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 + 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 + letter_loop: + lda.l assets_magic_dat, x + jsr.l draw_letter_far + inx + dec 0x02 + bne letter_loop + lda #0x0A + sta 0x02 -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 + clear_loop: + lda.b #0xff + jsr.l draw_letter_far + dec 0x02 + bne clear_loop -same_row: - sep #0x20 - inc 0x0a -; next spell index - dec.b spell_counter -; decrement spell counter - beq exit - jmp.w spell_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 -exit: - rtl -} + same_row: + sep #0x20 + inc 0x0a + ; next spell index + dec.b spell_counter + ; decrement spell counter + beq exit + jmp.w spell_loop -magic_list_ptrs: -"""Per-magic-type spell-list base pointers (white, black, summon, ninja, kokan).""" - .dw 0x2c7a, 0x2d9a, 0x2eba, 0x2fda, 0x30fa + 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_reloc.s b/src/battle/math_reloc.s index ca15d01..615abe4 100644 --- a/src/battle/math_reloc.s +++ b/src/battle/math_reloc.s @@ -1,86 +1,91 @@ """ +.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() -} +.alloc battle_math_reloc_block in bank20_reloc { + .macro shorta() { + """Switch A to 8-bit (`SEP #$20`).""" + sep #0x20 + } -_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 + .macro shorta0() { + """Clear A and switch to 8-bit (TDC then `SEP #$20`).""" + tdc + shorta() + } -; 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. + _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 -; 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 + ; 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. -; 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 + ; 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 -; 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 + ; 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 -; 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 + ; 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 - shorta0() ; TDC + SEP #$20 - clear high byte of A, 8-bit mode - plp - rtl + ; 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..199340d 100644 --- a/src/battle/message.s +++ b/src/battle/message.s @@ -2,6 +2,8 @@ Battle-message tile renderer + VWF parser scopes (`battle_render` low-level blitter, `messages_vwf` high-level dialog-stream consumer). """ +.include "src/battle/inventory_budget.i" +.include "src/vwf_state.i" .if 0 { .scope _vwf_tile_ring { ; Ring buffer for VWF tile allocation @@ -148,13 +150,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 +; 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 ; --- 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_bits = 0x703f01 REGION_DIRTY_MESSAGES = 0x01 REGION_DIRTY_MONSTERS = 0x02 REGION_DIRTY_NAMES = 0x04 @@ -163,14 +174,14 @@ _not_found: ; 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 + 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 (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_mask = 0x703f03 TILEMAP_PENDING_COMMANDS = 0x01 TILEMAP_PENDING_MAIN = 0x02 bits_left_on_tile = 0xA9 @@ -191,6 +202,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 @@ -752,15 +794,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 @@ -829,6 +884,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 +1347,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 +1488,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 +1630,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/monsters_reloc.s b/src/battle/monsters_reloc.s index 348ae25..eb7c06f 100644 --- a/src/battle/monsters_reloc.s +++ b/src/battle/monsters_reloc.s @@ -1,50 +1,55 @@ """ +.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 + +.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..910b3f1 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,314 @@ 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) +.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 -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 + 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) -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 + 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 -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 + 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 -_gcnwb_skip: - 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 -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. -""" + _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 - 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 + _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 + + .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 + + _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..ceba5a1 100644 --- a/src/battle/redraw_writer_patches.s +++ b/src/battle/redraw_writer_patches.s @@ -92,112 +92,125 @@ follow-up patches. .pool bank02_battle_redraw_helpers { range 0x0297AB 0x029824 - fill 0 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 diff --git a/src/battle/sram.s b/src/battle/sram.s index 6512167..e7bba76 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,140 @@ 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 -} -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 +.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 } - 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 } - -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 -} + 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 + } -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 -} + ; 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 + } -_sink: - rtl + 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 + } -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/dakuten.s b/src/dakuten.s index 756dafc..30c24cc 100755 --- a/src/dakuten.s +++ b/src/dakuten.s @@ -1,59 +1,64 @@ """ +.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: -""" -input: A 8bit: current char -output: A 16bits: the resolved char -""" -{ - php - sep #0x20 - cmp.l assets_dakuten_bin - bmi _char_out_of_range - cmp.l assets_dakuten_bin + 2 - bpl _char_out_of_range - - phx - rep #0x20 - and.w #0x00ff - sec - sbc.l assets_dakuten_bin - asl - tax - - lda.l assets_dakuten_bin + 4, x - - sep #0x20 - plx - bra _exit -_char_out_of_range: - xba - lda #0xff - -_exit: - plp - rtl -} +.alloc dakuten_block in bank20_reloc { + .incbin "assets/dakuten.bin" + + lookup_dakuten: + """ + input: A 8bit: current char + output: A 16bits: the resolved char + """ + { + php + sep #0x20 + cmp.l assets_dakuten_bin + bmi _char_out_of_range + cmp.l assets_dakuten_bin + 2 + bpl _char_out_of_range + + phx + rep #0x20 + and.w #0x00ff + sec + sbc.l assets_dakuten_bin + asl + tax + + lda.l assets_dakuten_bin + 4, x + + sep #0x20 + plx + bra _exit + _char_out_of_range: + xba + lda #0xff + + _exit: + plp + rtl + } + + _store_char_with_dakuten: + { + jsr.l lookup_dakuten + cmp #0xff + beq _skip + sta.l 0x7E0000, x + _skip: + + sta.l 0x7E0040, x -_store_char_with_dakuten: -{ - jsr.l lookup_dakuten - cmp #0xff - beq _skip - sta.l 0x7E0000, x -_skip: - - sta.l 0x7E0040, x - -;0187A9 9F 00 00 7E STA $7E0000,X -;0187AD E8 INX -;0187AE E8 INX -;0187AF C8 INY - rtl + ;0187A9 9F 00 00 7E STA $7E0000,X + ;0187AD E8 INX + ;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/drops_rolling.s b/src/ingame/drops_rolling.s index 3e434c3..50769f1 100644 --- a/src/ingame/drops_rolling.s +++ b/src/ingame/drops_rolling.s @@ -47,7 +47,11 @@ DROPS_SCROLL_LIMIT := 3 DROPS_SCROLL_PIXELS_PER_FRAME := 8 DROPS_SCROLL_TOTAL_PIXELS := 16 -drops_rolling := 0x1BE0 +; 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 := 0x9C30 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 @@ -65,7 +69,7 @@ drops_hdma_copy_pending := drops_rolling + RollingBufferState.hdma_copy_pending ; 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. @@ -93,267 +97,478 @@ DROPS_SCROLL_STATE_SCROLLING := 1 ; --- Profile hooks (stubs, real implementations land alongside the ; drops geometry + tilemap layout work) ----------------------------------- -drops_ensure_hdma_initialized: -"""Lazy init: pin BG4VOFS shadow to 0 + configure ch4 driving BG4VOFS on first scroll.""" - rep #0x20 - lda.w drops_rolling_base_scroll - cmp.w #0xFFFF - bne _drops_hdma_already_init - -; treasure_drops_window is anchored at BG (0, 0): top border at BG -; 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 -; 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 - -; Configure HDMA channel 4: DIRECT mode 2 bytes, dest BG4VOFS ($2114), -; src $7E:9880 (drops active table). - sep #0x20 - lda #0x02 - sta.l DROPS_HDMA4_CTRL - lda #0x14 - sta.l DROPS_HDMA4_DEST - rep #0x20 - lda.w #DROPS_HDMA_TABLE_ADDR - sta.l DROPS_HDMA4_SRC_LO - sep #0x20 - lda #DROPS_HDMA_BANK - sta.l DROPS_HDMA4_SRC_BANK - -; 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 - 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. - jsr.w update_drops_scroll_hdma - rts - -_drops_hdma_already_init: - sep #0x20 - rts - -drops_render_item_to_slot: -"""Render one drops item from $7E:FF28 + edge_row*Item.__size into the BG3 buffer at $7E:D600 + slot_index*128 + 4.""" - php - phb - lda #0x7E - pha - plb - rep #0x30 - pha - phx - phy - lda.b 0x5a - pha - lda.b 0x29 - pha - lda.b 0x45 - pha - lda.b 0x33 - pha - sep #0x20 - lda.b 0x5d - pha - 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. - lda.w #0xC600 - sta.b 0x29 - sep #0x20 - lda.w drops_rolling_edge_row - asl - clc - adc #0x28 - sta.b 0x5a - lda #0xFF - adc #0x00 - sta.b 0x5b - rep #0x20 - lda.b 0x5a - tax - sep #0x20 - lda.l 0x7E0000 + Item.id, x - pha - lda.l 0x7E0000 + Item.qty, x - sta.b 0x5C - stz.b 0x34 - pla - jsr.l check_can_use_item_trampoline - lda.w drops_rolling_slot_index - sta.b 0x5d - rep #0x20 - lda.w drops_rolling_slot_index - and.w #0x00FF - xba - lsr - clc - adc.w #0x0044 - tay - sep #0x20 - jsr.l draw_item_slot_inner_trampoline - pla - sta.b 0xDB - pla - sta.b 0x5d - rep #0x20 - pla - sta.b 0x33 - pla - sta.b 0x45 - pla - sta.b 0x29 - pla - sta.b 0x5a - rep #0x10 - ply - plx - pla - plb - plp - rts - -clear_drops_slot: -"""Blank a single drops tilemap slot. STUB.""" - rts - -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 - -_drops_hdma_header: -"""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 - sta.l DROPS_HDMA_SHADOW, x - inx - inx - rts - -_drops_hdma_footer: -""" -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. - -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. -""" - - - 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. - sta.l DROPS_HDMA_SHADOW, x - inx - inx - sep #0x20 - lda #87 - sta.l DROPS_HDMA_SHADOW, x - inx - rep #0x20 - lda.w #0xFFE8 - ; -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 - rts - -_drops_hdma_signal: -"""NMI shadow-copy signal for the drops ch4 table.""" - sep #0x20 - lda #0x01 - sta.w drops_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. -""" - engine_init_rolling_buffer(drops_rolling, DROPS_VISIBLE_ITEMS, _drops_draw_window, drops_ensure_hdma_initialized, drops_render_item_to_slot) ; noqa: E501 - - -_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 - 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_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_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) - -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_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_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 +.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.""" + rep #0x20 + lda.w drops_rolling_base_scroll + cmp.w #0xFFFF + bne _drops_hdma_already_init + + ; treasure_drops_window is anchored at BG (0, 0): top border at BG + ; 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 + ; 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 + + ; Configure HDMA channel 4: DIRECT mode 2 bytes, dest BG4VOFS ($2114), + ; src $7E:9880 (drops active table). + sep #0x20 + lda #0x02 + sta.l DROPS_HDMA4_CTRL + lda #0x14 + sta.l DROPS_HDMA4_DEST + rep #0x20 + lda.w #DROPS_HDMA_TABLE_ADDR + sta.l DROPS_HDMA4_SRC_LO + sep #0x20 + lda #DROPS_HDMA_BANK + sta.l DROPS_HDMA4_SRC_BANK + + ; 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 + 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. + jsr.w update_drops_scroll_hdma + rts + + _drops_hdma_already_init: + sep #0x20 + rts + + drops_render_item_to_slot: + """Render one drops item from $7E:FF28 + edge_row*Item.__size into the BG3 buffer at $7E:D600 + slot_index*128 + 4.""" + php + phb + lda #0x7E + pha + plb + rep #0x30 + pha + phx + phy + lda.b 0x5a + pha + lda.b 0x29 + pha + lda.b 0x45 + pha + lda.b 0x33 + pha + sep #0x20 + lda.b 0x5d + pha + 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. + lda.w #0xC600 + sta.b 0x29 + sep #0x20 + lda.w drops_rolling_edge_row + asl + clc + adc #0x28 + sta.b 0x5a + lda #0xFF + adc #0x00 + sta.b 0x5b + rep #0x20 + lda.b 0x5a + tax + sep #0x20 + lda.l 0x7E0000 + Item.id, x + pha + lda.l 0x7E0000 + Item.qty, x + sta.b 0x5C + stz.b 0x34 + pla + jsr.l check_can_use_item_trampoline + ; --- 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_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 + and.w #0x00FF + xba + lsr + clc + adc.w #0x0044 + tay + sep #0x20 + jsr.l draw_item_slot_inner_trampoline + lda #0x00 + sta.l VWF_CALLER_CTX + pla + sta.b 0xDB + pla + sta.b 0x5d + rep #0x20 + pla + sta.b 0x33 + pla + sta.b 0x45 + pla + sta.b 0x29 + pla + sta.b 0x5a + rep #0x10 + ply + plx + pla + plb + plp + rts + + clear_drops_slot: + """Blank a single drops tilemap slot. STUB.""" + rts + + update_drops_scroll_hdma: + """Build the drops HDMA shadow table via the shared engine.""" + ; 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.""" + sep #0x20 + lda #32 + sta.l DROPS_HDMA_SHADOW, x + inx + rep #0x20 + lda.w drops_rolling_base_scroll + sta.l DROPS_HDMA_SHADOW, x + inx + inx + rts + + _drops_hdma_footer: + """ + 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. + + 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. + """ + + + 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. + sta.l DROPS_HDMA_SHADOW, x + inx + inx + sep #0x20 + lda #87 + sta.l DROPS_HDMA_SHADOW, x + inx + rep #0x20 + lda.w #0xFFE8 + ; -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 + rts + + _drops_hdma_signal: + """NMI shadow-copy signal for the drops ch4 table.""" + sep #0x20 + lda #0x01 + sta.w drops_hdma_copy_pending + rts + + + ; --- Engine instantiations ------------------------------------------------- + + drops_init_impl: + """ + 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. + """ + php + rep #0x30 + sep #0x20 + lda.b #DROPS_VISIBLE_ITEMS + sta.l 0x7E0000 + drops_rolling + RollingBufferState.visible_rows + lda.b #0x02 + sta.l 0x7E0000 + drops_rolling + RollingBufferState.slot_height_tiles + ; item_list_ptr = $7E:FF28 (treasure drops table) + lda.b #0x28 + sta.l 0x7E0000 + drops_rolling + RollingBufferState.item_list_ptr + lda.b #0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.item_list_ptr + 1 + lda.b #0x7E + sta.l 0x7E0000 + drops_rolling + RollingBufferState.item_list_ptr + 2 + lda.b #DROPS_TOTAL_ITEMS + sta.l 0x7E0000 + drops_rolling + RollingBufferState.item_count + lda.b #0x04 + sta.l 0x7E0000 + drops_rolling + RollingBufferState.hdma_channel + lda.b #0x80 + sta.l 0x7E0000 + drops_rolling + RollingBufferState.vwf_cfg_ptr + lda.b #0x70 + sta.l 0x7E0000 + drops_rolling + RollingBufferState.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l 0x7E0000 + drops_rolling + RollingBufferState.vwf_cfg_ptr + 2 + lda.b #drops_fn_render_slot_trampoline & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_render_slot + lda.b #( drops_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_render_slot + 1 + lda.b #( drops_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_render_slot + 2 + lda.b #drops_fn_update_hdma_trampoline & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_update_hdma + lda.b #( drops_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_update_hdma + 1 + lda.b #( drops_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_update_hdma + 2 + lda.b #drops_fn_draw_window_trampoline & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_draw_window + lda.b #( drops_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_draw_window + 1 + lda.b #( drops_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + drops_rolling + RollingBufferState.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_DROPS + sta.l 0x7E0000 + 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 + rep #0x10 + ldy.w #treasure_drops_window + jsr.l draw_window_trampoline + sep #0x10 + rts + + drops_start_scroll_down_impl: + """Drops profile: kick scroll-down state machine via the engine.""" + php + rep #0x10 + lda.l 0x7E0000 + 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 via the engine.""" + php + rep #0x10 + lda.l 0x7E0000 + 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.""" + 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 cleanup via the bank-20 engine.""" + php + rep #0x10 + lda.l 0x7E0000 + 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 6 slots via the bank-20 engine.""" + php + sep #0x20 + rep #0x10 + lda.l 0x7E0000 + 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 via the engine.""" + php + rep #0x10 + lda.l 0x7E0000 + drops_scroll_pos + ldx.w #drops_rolling + jsr.l rolling_engine.rolling_engine_swap_redraw + plp + rtl +} diff --git a/src/ingame/free_space.s b/src/ingame/free_space.s index 3b8d511..e5fd5f6 100644 --- a/src/ingame/free_space.s +++ b/src/ingame/free_space.s @@ -14,12 +14,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 @@ -54,8 +55,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 +76,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 +107,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 @@ -217,7 +222,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/inventory_rolling.s b/src/ingame/inventory_rolling.s index 31546ad..6542d98 100644 --- a/src/ingame/inventory_rolling.s +++ b/src/ingame/inventory_rolling.s @@ -90,895 +90,1120 @@ 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_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_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 0x7E0000 + menu_rolling + RollingBufferState.visible_rows + lda.b #0x02 ; 2 BG rows per slot + sta.l 0x7E0000 + menu_rolling + RollingBufferState.slot_height_tiles + ; item_list_ptr = $7E:1440 (vanilla inventory array) + lda.b #0x40 + sta.l 0x7E0000 + menu_rolling + RollingBufferState.item_list_ptr + lda.b #0x14 + sta.l 0x7E0000 + menu_rolling + RollingBufferState.item_list_ptr + 1 + lda.b #0x7E + sta.l 0x7E0000 + menu_rolling + RollingBufferState.item_list_ptr + 2 + ; item_count = 48 (vanilla field inventory) + lda.b #0x30 + sta.l 0x7E0000 + menu_rolling + RollingBufferState.item_count + ; hdma_channel = 5 (BG1VOFS HDMA used by field-items rolling buffer) + lda.b #0x05 + sta.l 0x7E0000 + menu_rolling + RollingBufferState.hdma_channel + ; vwf_cfg_ptr = $70:7080 (VWF_CONFIG_BASE) + lda.b #0x80 + sta.l 0x7E0000 + menu_rolling + RollingBufferState.vwf_cfg_ptr + lda.b #0x70 + sta.l 0x7E0000 + menu_rolling + RollingBufferState.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l 0x7E0000 + menu_rolling + RollingBufferState.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 0x7E0000 + menu_rolling + RollingBufferState.fn_render_slot + lda.b #( menu_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_render_slot + 1 + lda.b #( menu_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_render_slot + 2 + ; fn_update_hdma = menu_fn_update_hdma_trampoline + lda.b #menu_fn_update_hdma_trampoline & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_update_hdma + lda.b #( menu_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_update_hdma + 1 + lda.b #( menu_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_update_hdma + 2 + ; fn_draw_window = menu_fn_draw_window_trampoline + lda.b #menu_fn_draw_window_trampoline & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_draw_window + lda.b #( menu_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_draw_window + 1 + lda.b #( menu_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + menu_rolling + RollingBufferState.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_FIELD + sta.l 0x7E0000 + menu_rolling + RollingBufferState.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_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 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 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 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_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 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 + } } diff --git a/src/ingame/inventory_rolling_trampolines.s b/src/ingame/inventory_rolling_trampolines.s index 8463c9b..ae43c03 100644 --- a/src/ingame/inventory_rolling_trampolines.s +++ b/src/ingame/inventory_rolling_trampolines.s @@ -4,6 +4,8 @@ wrappers around original bank-$01 helpers used by the rolling code. """ .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 +19,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 @@ -136,8 +138,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 +169,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 @@ -195,8 +219,13 @@ treasure_scroll_down_trigger: """Treasure profile: input-driven scroll-down trigger.""" lda.w treasure_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 @@ -206,8 +235,13 @@ treasure_scroll_up_trigger: """Treasure profile: input-driven scroll-up trigger.""" lda.w treasure_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 @@ -271,6 +305,17 @@ by calling the original $82C0 so original per-frame work still runs. """ +; 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_scroll_state beq _treasure_main_check_drops_tick jsr.w _treasure_update_scroll_frame @@ -423,5 +468,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/items_menu.s b/src/ingame/items_menu.s index f9e60f4..413e849 100644 --- a/src/ingame/items_menu.s +++ b/src/ingame/items_menu.s @@ -1,16 +1,23 @@ """ -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 + lda #ITEM_UNLEASHED_TEXT_SIZE ; --- Patch multiply logic --- @@ -19,7 +26,7 @@ changes, $0F8000 → `assets_items_dat` pointer remaps. ; New: JSL to relocated routine (4 bytes) + 3 NOPs *=0x019023 - jsr.l multiply_item_index_12 + jsr.l multiply_item_index_17 nop nop nop @@ -31,26 +38,51 @@ changes, $0F8000 → `assets_items_dat` pointer remaps. ; Original: 01/902E: BF 00 80 0F LDA $0F8000,X *=0x01902E - lda.l assets_items_dat, x + lda.l assets_items_unleashed_dat, x ; --- menu: item name (in loop) --- ; Original: 01/9043: BF 00 80 0F LDA $0F8000,X *=0x019043 - lda.l assets_items_dat, x + lda.l assets_items_unleashed_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. + +; 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. + +*=0x019013 + lda (0x60), y + jsr.w draw_field_item_name_trampoline + rts + +; 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). + +*=0x019060 + jsr.w draw_field_item_name_trampoline + rts ; ===== COLON/QUANTITY POSITION PATCHES ===== -; Item names expanded from 9 to 12 bytes (+3 chars = +6 VRAM bytes) -; Change offset from $0052 to $0058 +; Item names expanded from 9 to 16 bytes (+7 chars = +14 VRAM bytes) +; Change offset from $0052 to $0060 ; --- DrawItemSlot: left column colon/qty position --- ; Original: 01/A1FC: 69 52 00 ADC #$0052 *=0x01A1FC - adc #0x0058 + adc #0x0060 ; --- DrawItemSlot: right column colon/qty position --- ; Original: 01/A236: 69 52 00 ADC #$0052 *=0x01A236 - adc #0x0058 + 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..94e2d05 100644 --- a/src/ingame/key_item_picker.s +++ b/src/ingame/key_item_picker.s @@ -52,7 +52,11 @@ KEY_ITEM_SCROLL_LIMIT := 42 KEY_ITEM_SCROLL_PIXELS_PER_FRAME := 8 KEY_ITEM_SCROLL_TOTAL_PIXELS := 16 -key_item_rolling := 0x1BF0 +; 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 := 0x9C60 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 @@ -66,7 +70,7 @@ key_item_transfer_pending := key_item_rolling + RollingBufferState.transfer_pend 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_scroll_pos := 0x9C8F KEY_ITEM_HDMA_TABLE_ADDR := 0x9900 KEY_ITEM_HDMA_TABLE := 0x7E9900 @@ -84,323 +88,503 @@ KEY_ITEM_HDMA4_SRC_LO := 0x4342 KEY_ITEM_HDMA4_SRC_BANK := 0x4344 -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. -""" - rep #0x20 - 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 - sep #0x20 - jsr.w _key_item_init_hdma_channel - lda #KEY_ITEM_HDMA_CHANNEL_BIT - sta.l 0x7E1BAE - sta.w key_item_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 - sep #0x20 - jsr.w update_key_item_scroll_hdma - lda #0x02 - sta.l KEY_ITEM_HDMA4_CTRL - lda #0x12 - sta.l KEY_ITEM_HDMA4_DEST - rep #0x20 - lda.w #KEY_ITEM_HDMA_TABLE_ADDR - sta.l KEY_ITEM_HDMA4_SRC_LO - sep #0x20 - lda #KEY_ITEM_HDMA_BANK - sta.l KEY_ITEM_HDMA4_SRC_BANK - 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 - phb - lda #0x7E - pha - plb - rep #0x30 - pha - phx - phy - lda.b 0x5a - pha - lda.b 0x29 - pha - lda.b 0x45 - pha - lda.b 0x33 - pha - sep #0x20 - lda.b 0x5d - pha - lda.b 0xDB - pha - rep #0x20 - lda.w #0xD600 - sta.b 0x29 - sep #0x20 - lda.w key_item_rolling_edge_row - asl - clc - adc #0x12 - sta.b 0x5a - lda #0x07 - adc #0x00 - sta.b 0x5b - rep #0x20 - lda.b 0x5a - tax - sep #0x20 - lda.l 0x7E0000 + Item.id, x - pha - lda.l 0x7E0000 + Item.qty, x - sta.b 0x5C - stz.b 0x34 - pla - jsr.l check_can_use_item_trampoline - lda.w key_item_rolling_slot_index - sta.b 0x5d - rep #0x20 - lda.w key_item_rolling_slot_index - and.w #0x00FF - xba - lsr - clc - adc.w #0x0444 - tay - sep #0x20 - jsr.l draw_item_slot_inner_trampoline - pla - sta.b 0xDB - pla - sta.b 0x5d - rep #0x20 - pla - sta.b 0x33 - pla - sta.b 0x45 - pla - sta.b 0x29 - pla - sta.b 0x5a - rep #0x10 - ply - plx - pla - plb - 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). -""" - 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). - lda #0x00 - sta.l 0x7E1BAE - lda #0x00 - sta.l 0x00420C - plp - rtl - - -clear_key_item_slot: -"""Blank one tilemap row at slot_index in the BG3 buffer.""" - php - phb - lda #0x7E - pha - plb - rep #0x30 - pha - phx - phy - lda.b 0x29 - pha - lda.w #0xD600 - sta.b 0x29 - lda.w key_item_rolling_slot_index - and.w #0x00FF - xba - lsr - clc - adc.w #0x0044 - tay - sep #0x20 - ldx.w #0x0000 - -_clear_key_loop: - lda #0x00 - sta (0x29), y - iny - inx - cpx.w #0x0040 - bne _clear_key_loop - rep #0x20 - pla - sta.b 0x29 - rep #0x10 - ply - plx - pla - plb - plp - rts - -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]. -""" - php - phb - sep #0x20 - rep #0x10 - lda #0x7E - pha - plb - ldx.w #0x0000 - -_filter_clear: - stz.w 0x0712, x - inx - cpx.w #0x0060 - bne _filter_clear - ldx.w #0x0000 - ldy.w #0x0000 - -_filter_walk: - lda.w 0x1440, x - cmp #0xCE - bcc _filter_next - cmp #0xE7 - bcc _filter_accept - cmp #0xEB - bcc _filter_next - cmp #0xFE - bcs _filter_next - -_filter_accept: - sta.w 0x0712, y - lda.w 0x1441, x - sta.w 0x0713, y - iny - iny - -_filter_next: - inx - inx - cpx.w #0x0060 - bne _filter_walk - plb - plp - rts - -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 - -_key_item_hdma_header: -"""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 - 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.""" - sep #0x20 - lda #16 - sta.l KEY_ITEM_HDMA_SHADOW, x - inx - rep #0x20 - lda.w key_item_rolling_base_scroll - clc - adc.w #16 - sta.l KEY_ITEM_HDMA_SHADOW, x - inx - inx - rts - -_key_item_hdma_signal: -"""NMI shadow-copy signal — set both picker copy_pending + shared $1BB6 mirror.""" - sep #0x20 - lda #0x01 - sta.w key_item_hdma_copy_pending - rts - - -key_item_init_impl: -"""Init key-item picker (filter $1440 -> $0712, then engine init).""" - 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 - - -_key_item_draw_window: -""" -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 - -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 - -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 - -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) - -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 - -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) +.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. + """ + rep #0x20 + 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 + sep #0x20 + jsr.w _key_item_init_hdma_channel + lda #KEY_ITEM_HDMA_CHANNEL_BIT + sta.l 0x7E1BAE + sta.w key_item_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 + sep #0x20 + jsr.w update_key_item_scroll_hdma + lda #0x02 + sta.l KEY_ITEM_HDMA4_CTRL + lda #0x12 + sta.l KEY_ITEM_HDMA4_DEST + rep #0x20 + lda.w #KEY_ITEM_HDMA_TABLE_ADDR + sta.l KEY_ITEM_HDMA4_SRC_LO + sep #0x20 + lda #KEY_ITEM_HDMA_BANK + sta.l KEY_ITEM_HDMA4_SRC_BANK + 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 + phb + lda #0x7E + pha + plb + rep #0x30 + pha + phx + phy + lda.b 0x5a + pha + lda.b 0x29 + pha + lda.b 0x45 + pha + lda.b 0x33 + pha + sep #0x20 + lda.b 0x5d + pha + lda.b 0xDB + pha + rep #0x20 + lda.w #0xD600 + sta.b 0x29 + sep #0x20 + lda.w key_item_rolling_edge_row + asl + clc + adc #0x12 + sta.b 0x5a + lda #0x07 + adc #0x00 + sta.b 0x5b + rep #0x20 + lda.b 0x5a + tax + sep #0x20 + lda.l 0x7E0000 + Item.id, x + pha + lda.l 0x7E0000 + Item.qty, x + sta.b 0x5C + stz.b 0x34 + pla + jsr.l check_can_use_item_trampoline + lda.w key_item_rolling_slot_index + sta.b 0x5d + rep #0x20 + lda.w key_item_rolling_slot_index + and.w #0x00FF + xba + lsr + clc + adc.w #0x0444 + tay + sep #0x20 + jsr.l draw_item_slot_inner_trampoline + pla + sta.b 0xDB + pla + sta.b 0x5d + rep #0x20 + pla + sta.b 0x33 + pla + sta.b 0x45 + pla + sta.b 0x29 + pla + sta.b 0x5a + rep #0x10 + ply + plx + pla + plb + 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). + """ + 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). + lda #0x00 + sta.l 0x7E1BAE + lda #0x00 + sta.l 0x00420C + plp + rtl + + + clear_key_item_slot: + """Blank one tilemap row at slot_index in the BG3 buffer.""" + php + phb + lda #0x7E + pha + plb + rep #0x30 + pha + phx + phy + lda.b 0x29 + pha + lda.w #0xD600 + sta.b 0x29 + lda.w key_item_rolling_slot_index + and.w #0x00FF + xba + lsr + clc + adc.w #0x0044 + tay + sep #0x20 + ldx.w #0x0000 + + _clear_key_loop: + lda #0x00 + sta (0x29), y + iny + inx + cpx.w #0x0040 + bne _clear_key_loop + rep #0x20 + pla + sta.b 0x29 + rep #0x10 + ply + plx + pla + plb + plp + rts + + 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]. + """ + php + phb + sep #0x20 + rep #0x10 + lda #0x7E + pha + plb + ldx.w #0x0000 + + _filter_clear: + stz.w 0x0712, x + inx + cpx.w #0x0060 + bne _filter_clear + ldx.w #0x0000 + ldy.w #0x0000 + + _filter_walk: + lda.w 0x1440, x + cmp #0xCE + bcc _filter_next + cmp #0xE7 + bcc _filter_accept + cmp #0xEB + bcc _filter_next + cmp #0xFE + bcs _filter_next + + _filter_accept: + sta.w 0x0712, y + lda.w 0x1441, x + sta.w 0x0713, y + iny + iny + + _filter_next: + inx + inx + cpx.w #0x0060 + bne _filter_walk + plb + plp + rts + + update_key_item_scroll_hdma: + """Build the key-item HDMA shadow table via the shared engine.""" + ; 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).""" + sep #0x20 + lda #112 + sta.l KEY_ITEM_HDMA_SHADOW, x + inx + rep #0x20 + 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.""" + sep #0x20 + lda #16 + sta.l KEY_ITEM_HDMA_SHADOW, x + inx + rep #0x20 + lda.w key_item_rolling_base_scroll + clc + adc.w #16 + sta.l KEY_ITEM_HDMA_SHADOW, x + inx + inx + rts + + _key_item_hdma_signal: + """NMI shadow-copy signal — set both picker copy_pending + shared $1BB6 mirror.""" + sep #0x20 + lda #0x01 + sta.w key_item_hdma_copy_pending + rts + + + key_item_init_impl: + """ + 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 + php + rep #0x30 + sep #0x20 + lda.b #KEY_ITEM_BUFFER_SLOTS + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.visible_rows + lda.b #0x02 + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.slot_height_tiles + ; item_list_ptr = $7E:0712 (filtered key-item array) + lda.b #0x12 + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.item_list_ptr + lda.b #0x07 + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.item_list_ptr + 1 + lda.b #0x7E + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.item_list_ptr + 2 + lda.b #KEY_ITEM_TOTAL_ITEMS + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.item_count + lda.b #0x04 + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.hdma_channel + lda.b #0x80 + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.vwf_cfg_ptr + lda.b #0x70 + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.vwf_cfg_ptr + 2 + lda.b #key_item_fn_render_slot_trampoline & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_render_slot + lda.b #( key_item_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_render_slot + 1 + lda.b #( key_item_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_render_slot + 2 + lda.b #key_item_fn_update_hdma_trampoline & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_update_hdma + lda.b #( key_item_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_update_hdma + 1 + lda.b #( key_item_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_update_hdma + 2 + lda.b #key_item_fn_draw_window_trampoline & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_draw_window + lda.b #( key_item_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_draw_window + 1 + lda.b #( key_item_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + key_item_rolling + RollingBufferState.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_KEY_ITEM + sta.l 0x7E0000 + 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. + """ + rts + + key_item_start_scroll_down_impl: + """Picker: kick scroll-down state machine via the engine.""" + php + rep #0x10 + lda.l 0x7E0000 + 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 via the engine.""" + php + rep #0x10 + lda.l 0x7E0000 + 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.""" + 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 cleanup via the bank-20 engine.""" + php + rep #0x10 + lda.l 0x7E0000 + 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 via the bank-20 engine.""" + php + sep #0x20 + rep #0x10 + lda.l 0x7E0000 + 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..608c8e0 100644 --- a/src/ingame/key_item_picker_patches.s +++ b/src/ingame/key_item_picker_patches.s @@ -31,6 +31,14 @@ Patched: .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. @@ -58,47 +66,51 @@ Patched: 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. +; 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. *=0x00B253 rep #0x10 - jsr.l multiply_by_12 + 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_NAME_TEXT_SIZE. +; (8 letters per name). Bump to ITEM_UNLEASHED_TEXT_SIZE. *=0x00B26F - lda #ITEM_NAME_TEXT_SIZE + lda #ITEM_UNLEASHED_TEXT_SIZE ; Item-name table source at $00:B273: original `lda.l $0F8000,x` -; (JP layout). Redirect to French assets_items_dat. +; (JP layout). Redirect to assets_items_unleashed_dat. *=0x00B273 - lda.l assets_items_dat, x + 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. +; 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. *=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): +; 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. *=0x00B28E - sta 0x0780, y + sta 0x0784, y *=0x00B2A4 - sta 0x0781, y + sta 0x0785, y *=0x00B2A9 - sta 0x0782, y + 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 diff --git a/src/ingame/main.s b/src/ingame/main.s index f401ccc..efcf29b 100644 --- a/src/ingame/main.s +++ b/src/ingame/main.s @@ -219,11 +219,22 @@ end: *=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 +; 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 + save_size = 0x2000 phb tdc pha 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..6c00557 100644 --- a/src/ingame/shop.s +++ b/src/ingame/shop.s @@ -47,18 +47,38 @@ small-VWF item descriptions. *=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. + +*=0x01C353 + jmp.w shop_welcome_text_hook *=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. + +*=0x01C442 + jsr.w shop_quantity_text_hook *=0x01c7e4 load_system_menu_text_pointer(shops.quantity) +*=0x01C7E7 + jsr.w shop_quantity_text_hook + *=0x01C568 load_system_menu_text_pointer(shops.gils + 2) *=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. + +*=0x01C751 + jsr.w shop_thanks_text_hook *=0x01c962 load_system_menu_text_pointer(shops.sell_window) diff --git a/src/ingame/treasure_rolling.s b/src/ingame/treasure_rolling.s index 5e7f4d2..984b23d 100644 --- a/src/ingame/treasure_rolling.s +++ b/src/ingame/treasure_rolling.s @@ -40,7 +40,12 @@ TREASURE_ITEM_LIST_HEIGHT := 80 ; 5 items × 16 pixels ; 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 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 := 0x9C00 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 @@ -53,6 +58,18 @@ treasure_scroll_direction := treasure_rolling + RollingBufferState.scroll_direct 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 +; 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 @@ -108,883 +125,1100 @@ TREASURE_HDMA6_SRC_BANK := 0x4364 TREASURE_HDMA6_IND_BANK := 0x4367 HDMAEN := 0x420C ; HDMA enable register -init_treasure_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_treasure_scroll_hdma - -; Configure HDMA channel 6 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 TREASURE_HDMA6_CTRL ; $004360 - - lda #0x12 ; BG3VOFS register ($2112) — treasure inventory is on BG3 - sta.l TREASURE_HDMA6_DEST ; $004361 - -; Source = HDMA table in WRAM at $7E9800 - rep #0x20 ; 16-bit A - lda.w #TREASURE_HDMA_TABLE_ADDR ; $9800 - sta.l TREASURE_HDMA6_SRC_LO ; $004362-$004363 - sep #0x20 ; 8-bit A - lda #TREASURE_HDMA_BANK ; $7E - sta.l TREASURE_HDMA6_SRC_BANK ; $004364 - -; HDMA channel 5 is now enabled via shadow variable (treasure_hdma_enable) -; The NMI hook at $8083 reads the shadow and writes to HDMAEN - - plp - rts - -disable_treasure_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_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) -""" - 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 TREASURE_HDMA_TABLE, x - inx - rep #0x20 ; 16-bit A for value - lda.w treasure_rolling_base_scroll - sta.l TREASURE_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 - -_t_init_item_rows: - sep #0x20 ; 8-bit A for count - lda #16 ; 16 scanlines per item row - sta.l TREASURE_HDMA_TABLE, x - inx - rep #0x20 ; 16-bit A for value - lda.w treasure_rolling_base_scroll - sta.l TREASURE_HDMA_TABLE, x - inx - inx - - inc.b 0x40 - lda.b 0x40 - - cmp.w #1 ; 10 rows - bcc _t_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 TREASURE_HDMA_TABLE, x - inx - rep #0x20 - lda.w treasure_rolling_base_scroll - clc - adc.w #16 ; Lock at base + 16 - sta.l TREASURE_HDMA_TABLE, x - inx - inx - -; End marker - sep #0x20 - lda #0x00 - sta.l TREASURE_HDMA_TABLE, x - -; Restore DP bytes - rep #0x20 - pla - sta.b 0x40 ; Restore $40-$41 - -; Restore registers - ply - plx - pla - plp - rts - -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 - -_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 - sec - sbc.w #16 - sta.l TREASURE_HDMA_SHADOW, x - inx - inx - sep #0x20 - lda #8 - sta.l TREASURE_HDMA_SHADOW, x - inx - rep #0x20 - lda.w treasure_rolling_base_scroll - sta.l TREASURE_HDMA_SHADOW, x - inx - inx - rts - -_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 - clc - adc.w #16 - sta.l TREASURE_HDMA_SHADOW, x - inx - inx - rts - -_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 - 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 -; 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 -; the state-machine bytes, so the cursor/scroll position the user -; 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) - -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 - -_treasure_draw_inventory_window: -"""Treasure menu draws a 5-row inventory window so the bottom border lands on the rolling-buffer footer scanlines.""" - rep #0x10 - ldy.w #treasure_inventory_window - jsr.l draw_window_trampoline - 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 -; -; Strategy: Set up $5d = slot_index (for Y position calculation), -; $5a = pointer to item data, then call game's DrawItemSlot. - -_treasure_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 #0xD600 ; BG3 screen buffer (treasure inventory lives on BG3) - sta.b 0x29 - sep #0x20 ; 8-bit A - -; Calculate item data pointer: $1440 + (edge_row * Item.__size) - lda.w treasure_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 treasure_rolling_slot_index - sta.b 0x5d - -; Calculate Y = slot_index * 128 + $44 -; Y is the tilemap offset for this 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 - 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 _t_not_trash_item - jsr.w draw_trash_treasure ; Draw trash icon - bra _t_skip_draw_item_slot - -_t_not_trash_item: - ; 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 -; Expects: Y = tilemap offset, ($5A) = item pointer, ($29) = tilemap base - jsr.l draw_item_slot_inner_trampoline ; bank-$01 trampoline for original @ $A1ED - -_t_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 - -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. -""" - -; Check if already initialized (base_scroll != 0xFFFF) - rep #0x20 ; 16-bit A - lda.w 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 - -; Initialize HDMA channel configuration - sep #0x20 ; Back to 8-bit for InitMenuInventoryHDMA - jsr.w init_treasure_inventory_hdma - -; 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 -; 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 -; `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 - ora #0x40 ; Channel 6 enable (treasure rolling buffer) - sta.l 0x7E1BAE - sta.w treasure_hdma_enable - rts - -_t_hdma_already_init: - sep #0x20 ; Restore 8-bit mode - rts - -; STATE MACHINE ROUTINES (FF6-style non-blocking scroll) - -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. - -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 treasure_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 - bne _t_scroll_still_active - -; Scroll finished - clean up and return to idle - jsr.l treasure_finish_scroll_impl - -_t_scroll_state_idle: - plp - clc ; Carry clear = process input - rts - -_t_scroll_still_active: - plp - sec ; Carry set = skip input - 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_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_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) - -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 - -; Inventory Rolling Buffer Patches - Relocated to Bank $20 -; These routines are called via JSL from bank $01 hooks. - -.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 -; Clear HDMA copy flag - stz.w treasure_scroll_anim_offset -; Clear low byte - stz.w treasure_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 - -treasure_menu_exit_hook_impl: -"""Treasure menu teardown: zero HDMA shadow, full 12-byte state, ch6 regs, original $1BC6 flag.""" - php - sep #0x20 - lda #0x00 - sta.l 0x7E1BAE -; treasure_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 - sta.l 0x004361 - sta.l 0x004362 - sta.l 0x004363 - sta.l 0x004364 -; HDMA6 ctrl/dest/src cleared - rep #0x20 - lda.w #0x0000 - sta.w treasure_rolling - sta.w treasure_rolling + 2 - sta.w treasure_rolling + 4 - sta.w treasure_rolling + 6 - sta.w treasure_rolling + 8 - sta.w treasure_rolling + 10 - plp - jsr.l reset_sprites_trampoline - rtl -; treasure_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. - -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 - - -; _clear_treasure_slot -; Clears a single inventory slot in the tilemap buffer. -; Input: treasure_rolling_slot_index = slot to clear (0-10) -; Used when item index is out of bounds (>= 48) - -_clear_treasure_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 #0xD600 ; BG3 screen buffer (treasure inventory lives on BG3) - sta.b 0x29 -; Calculate Y = slot_index * 128 + 70 - lda.w treasure_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) - -_t_clear_slot_loop: - lda #0xFF -; Blank tile - sta (0x29), y - iny - lda #0x04 -; Palette 4 (matches normal items) - sta (0x29), y - iny - dex - bne _t_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_treasure_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_treasure_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_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) -""" - - -; 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 - -_t_clear_row1: - lda #0xFF -; Blank tile - sta (0x29), y - iny - lda #0x00 -; Attribute - sta (0x29), y - iny - dex - bne _t_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 - -_t_clear_row2: - lda #0xFF -; Blank tile - sta (0x29), y - iny - lda #0x00 -; Attribute - sta (0x29), y - iny - dex - bne _t_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 -; treasure_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 - -treasure_check_and_clear_count_impl: -"""Treasure profile: drop the count column for empty/used slots (impl).""" - lda (0x5a) -; Load item ID - beq _t_clear_count -; If 0, clear and skip - cmp #0xFE -; Check for special item $FE - bne _t_normal_return -; If not $FE, return normally to draw count -; Item is $FE - skip count but don't clear - bra _t_skip_to_rts - -_t_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 - -_t_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 - -_t_normal_return: - rtl - -treasure_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) -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 - beq _t_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 + treasure_rolling_buffer_pos -; Add buffer_pos - -_t_circ_slot_mod: - cmp #TREASURE_BUFFER_SLOTS -; >= 11? - bcc _t_circ_slot_done - sec - sbc #TREASURE_BUFFER_SLOTS -; Subtract 11 to wrap - bra _t_circ_slot_mod - -_t_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 - -_t_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 - -treasure_circular_slot_calc_ext: -"""Trampoline to call CircularSlotCalc from bank $01 patch at $A1BA""" - jsr.w treasure_circular_slot_calc - rtl +.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 + + 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_treasure_scroll_hdma + + ; Configure HDMA channel 6 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 TREASURE_HDMA6_CTRL ; $004360 + + lda #0x12 ; BG3VOFS register ($2112) — treasure inventory is on BG3 + sta.l TREASURE_HDMA6_DEST ; $004361 + + ; Source = HDMA table in WRAM at $7E9800 + rep #0x20 ; 16-bit A + lda.w #TREASURE_HDMA_TABLE_ADDR ; $9800 + sta.l TREASURE_HDMA6_SRC_LO ; $004362-$004363 + sep #0x20 ; 8-bit A + lda #TREASURE_HDMA_BANK ; $7E + sta.l TREASURE_HDMA6_SRC_BANK ; $004364 + + ; HDMA channel 5 is now enabled via shadow variable (treasure_hdma_enable) + ; The NMI hook at $8083 reads the shadow and writes to HDMAEN + + plp + rts + + disable_treasure_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_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) + """ + 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 TREASURE_HDMA_TABLE, x + inx + rep #0x20 ; 16-bit A for value + lda.w treasure_rolling_base_scroll + sta.l TREASURE_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 + + _t_init_item_rows: + sep #0x20 ; 8-bit A for count + lda #16 ; 16 scanlines per item row + sta.l TREASURE_HDMA_TABLE, x + inx + rep #0x20 ; 16-bit A for value + lda.w treasure_rolling_base_scroll + sta.l TREASURE_HDMA_TABLE, x + inx + inx + + inc.b 0x40 + lda.b 0x40 + + cmp.w #1 ; 10 rows + bcc _t_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 TREASURE_HDMA_TABLE, x + inx + rep #0x20 + lda.w treasure_rolling_base_scroll + clc + adc.w #16 ; Lock at base + 16 + sta.l TREASURE_HDMA_TABLE, x + inx + inx + + ; End marker + sep #0x20 + lda #0x00 + sta.l TREASURE_HDMA_TABLE, x + + ; Restore DP bytes + rep #0x20 + pla + sta.b 0x40 ; Restore $40-$41 + + ; Restore registers + ply + plx + pla + plp + rts + + update_treasure_scroll_hdma: + """Build the treasure-menu HDMA scroll table via the shared engine.""" + ; 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 + sec + sbc.w #16 + sta.l TREASURE_HDMA_SHADOW, x + inx + inx + sep #0x20 + lda #8 + sta.l TREASURE_HDMA_SHADOW, x + inx + rep #0x20 + lda.w treasure_rolling_base_scroll + sta.l TREASURE_HDMA_SHADOW, x + inx + inx + rts + + _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 + clc + adc.w #16 + sta.l TREASURE_HDMA_SHADOW, x + inx + inx + rts + + _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 + 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 + ; 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 + ; the state-machine bytes, so the cursor/scroll position the user + ; was on before the swap survives. + + treasure_refresh_slots_impl: + """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) 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 0x7E0000 + treasure_rolling + RollingBufferState.visible_rows + lda.b #0x02 + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.slot_height_tiles + lda.b #0x40 + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.item_list_ptr + lda.b #0x14 + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.item_list_ptr + 1 + lda.b #0x7E + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.item_list_ptr + 2 + lda.b #TREASURE_TOTAL_ITEMS + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.item_count + lda.b #0x06 + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.hdma_channel + lda.b #0x80 + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.vwf_cfg_ptr + lda.b #0x70 + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.vwf_cfg_ptr + 1 + lda.b #0x70 + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.vwf_cfg_ptr + 2 + lda.b #treasure_fn_render_slot_trampoline & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_render_slot + lda.b #( treasure_fn_render_slot_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_render_slot + 1 + lda.b #( treasure_fn_render_slot_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_render_slot + 2 + lda.b #treasure_fn_update_hdma_trampoline & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_update_hdma + lda.b #( treasure_fn_update_hdma_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_update_hdma + 1 + lda.b #( treasure_fn_update_hdma_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_update_hdma + 2 + lda.b #treasure_fn_draw_window_trampoline & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_draw_window + lda.b #( treasure_fn_draw_window_trampoline >> 8 ) & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_draw_window + 1 + lda.b #( treasure_fn_draw_window_trampoline >> 16 ) & 0xFF + sta.l 0x7E0000 + treasure_rolling + RollingBufferState.fn_draw_window + 2 + lda.b #ROLLING_MENU_ID_TREASURE + sta.l 0x7E0000 + 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.""" + rep #0x10 + ldy.w #treasure_inventory_window + jsr.l draw_window_trampoline + sep #0x10 + rts + + ; _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 + ; + ; Strategy: Set up $5d = slot_index (for Y position calculation), + ; $5a = pointer to item data, then call game's DrawItemSlot. + + _treasure_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 #0xD600 ; BG3 screen buffer (treasure inventory lives on BG3) + sta.b 0x29 + sep #0x20 ; 8-bit A + + ; Calculate item data pointer: $1440 + (edge_row * Item.__size) + lda.w treasure_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 treasure_rolling_slot_index + sta.b 0x5d + + ; Calculate Y = slot_index * 128 + $44 + ; Y is the tilemap offset for this 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 + 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 _t_not_trash_item + jsr.w draw_trash_treasure ; Draw trash icon + bra _t_skip_draw_item_slot + + _t_not_trash_item: + ; 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 + ; Expects: Y = tilemap offset, ($5A) = item pointer, ($29) = tilemap base + jsr.l draw_item_slot_inner_trampoline ; bank-$01 trampoline for original @ $A1ED + + _t_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 + + 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. + """ + + ; Check if already initialized (base_scroll != 0xFFFF) + rep #0x20 ; 16-bit A + lda.w 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 + ; 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.w treasure_scroll_cooldown + rep #0x20 + + ; Initialize HDMA channel configuration + sep #0x20 ; Back to 8-bit for InitMenuInventoryHDMA + jsr.w init_treasure_inventory_hdma + + ; 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 + ; 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 + ; `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 + ora #0x40 ; Channel 6 enable (treasure rolling buffer) + sta.l 0x7E1BAE + sta.w treasure_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: + sep #0x20 ; Restore 8-bit mode + rts + + ; STATE MACHINE ROUTINES (FF6-style non-blocking scroll) + + 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. + + 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 + 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 + bne _t_scroll_still_active + + ; Scroll finished - clean up and return to idle + jsr.l treasure_finish_scroll_impl + + _t_scroll_state_idle: + plp + clc ; Carry clear = process input + rts + + _t_scroll_still_active: + plp + sec ; Carry set = skip input + rts + + treasure_start_scroll_down_impl: + """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 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.""" + 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 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 { + 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 + ; Clear HDMA copy flag + stz.w treasure_scroll_anim_offset + ; Clear low byte + stz.w treasure_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 + + treasure_menu_exit_hook_impl: + """Treasure menu teardown: zero HDMA shadow, full 12-byte state, ch6 regs, original $1BC6 flag.""" + php + sep #0x20 + lda #0x00 + sta.l 0x7E1BAE + ; treasure_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 + sta.l 0x004361 + sta.l 0x004362 + sta.l 0x004363 + sta.l 0x004364 + ; HDMA6 ctrl/dest/src cleared + rep #0x20 + lda.w #0x0000 + sta.w treasure_rolling + sta.w treasure_rolling + 2 + sta.w treasure_rolling + 4 + sta.w treasure_rolling + 6 + sta.w treasure_rolling + 8 + sta.w treasure_rolling + 10 + plp + jsr.l reset_sprites_trampoline + rtl + ; treasure_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. + + treasure_swap_redraw_hook_impl_body: + """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) + ; Used when item index is out of bounds (>= 48) + + _clear_treasure_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 #0xD600 ; BG3 screen buffer (treasure inventory lives on BG3) + sta.b 0x29 + ; Calculate Y = slot_index * 128 + 70 + lda.w treasure_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) + + _t_clear_slot_loop: + lda #0xFF + ; Blank tile + sta (0x29), y + iny + lda #0x04 + ; Palette 4 (matches normal items) + sta (0x29), y + iny + dex + bne _t_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_treasure_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_treasure_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_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) + """ + + + ; 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 + + _t_clear_row1: + lda #0xFF + ; Blank tile + sta (0x29), y + iny + lda #0x00 + ; Attribute + sta (0x29), y + iny + dex + bne _t_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 + + _t_clear_row2: + lda #0xFF + ; Blank tile + sta (0x29), y + iny + lda #0x00 + ; Attribute + sta (0x29), y + iny + dex + bne _t_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 + ; treasure_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 + + treasure_check_and_clear_count_impl: + """Treasure profile: drop the count column for empty/used slots (impl).""" + lda (0x5a) + ; Load item ID + beq _t_clear_count + ; If 0, clear and skip + cmp #0xFE + ; Check for special item $FE + bne _t_normal_return + ; If not $FE, return normally to draw count + ; Item is $FE - skip count but don't clear + bra _t_skip_to_rts + + _t_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 + + _t_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 + + _t_normal_return: + rtl + + treasure_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) + 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 + beq _t_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 + treasure_rolling_buffer_pos + ; Add buffer_pos + + _t_circ_slot_mod: + cmp #TREASURE_BUFFER_SLOTS + ; >= 11? + bcc _t_circ_slot_done + sec + sbc #TREASURE_BUFFER_SLOTS + ; Subtract 11 to wrap + bra _t_circ_slot_mod + + _t_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 + + _t_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 + + 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/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..6c00e25 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 @@ -52,4 +138,20 @@ 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 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..190fb44 --- /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..f325e4b 100644 --- a/src/menus/start_screen_text.s +++ b/src/menus/start_screen_text.s @@ -1,58 +1,62 @@ """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/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/small_vwf/init.s b/src/small_vwf/init.s index 9d8ccb3..fe396a6 100644 --- a/src/small_vwf/init.s +++ b/src/small_vwf/init.s @@ -3,14 +3,18 @@ Small (8x8) VWF init routine: prepares the menu tile buffer, runs the renderer i RTL entry points for cross-bank callers. """ .include "src/vwf.i" +.include "../bank20.i" .include "src/libmz.i" .import "assets" .import "libmz" -.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 { + .if BATTLE_ENABLED { + .extern battle_render + .extern battle_render.clear_buffer + } + + .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..4287e7a 100644 --- a/src/small_vwf/render.s +++ b/src/small_vwf/render.s @@ -1,4 +1,6 @@ """Small (8x8) menu VWF renderer.""" +.include "src/vwf_state.i" + VARS_BUFFER = 0x710000 .macro initialize(var) { @@ -31,7 +33,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 +112,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 +191,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 +231,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 +262,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 +633,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 +862,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 +1003,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..656f209 100644 --- a/src/vwf.s +++ b/src/vwf.s @@ -3,6 +3,7 @@ Dialog/text VWF rendering engine: control-code parser (`parse`), per-glyph blit tracking (`TILEPOS`/`BITSLEFT`), button-glyph + ending-symbol drawing, and the gil-window tilemaps. """ .include "src/definitions.s" +.include "bank20.i" .include "src/libmz.i" .table "text/ff4_menus.tbl" .import "libmz" @@ -11,1190 +12,1193 @@ 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 -.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