# ZScream Custom Overworld - Advanced Technical Documentation **Target Audience**: Developers modifying ZScream internals or integrating complex systems **Prerequisites**: Understanding of 65816 assembly, ALTTP memory architecture, and basic ZScream concepts **Last Updated**: October 3, 2025 --- ## Table of Contents 1. [Internal Hook Architecture](#1-internal-hook-architecture) 2. [Memory Management & State Tracking](#2-memory-management--state-tracking) 3. [Graphics Loading Pipeline](#3-graphics-loading-pipeline) 4. [Sprite Loading System Deep Dive](#4-sprite-loading-system-deep-dive) 5. [Cross-Namespace Integration](#5-cross-namespace-integration) 6. [Performance Considerations](#6-performance-considerations) 7. [Adding Custom Features](#7-adding-custom-features) 8. [Debugging & Troubleshooting](#8-debugging--troubleshooting) --- ## 1. Internal Hook Architecture ### 1.1 Hook Categories ZScream replaces **38+ vanilla routines** across multiple ROM banks. These hooks fall into distinct categories: | Category | Count | Purpose | |----------|-------|---------| | **Palette Loading** | 7 | Load custom palettes per area | | **Graphics Decompression** | 12 | Load custom static/animated GFX | | **Subscreen Overlays** | 8 | Control rain, fog, pyramid BG | | **Screen Transitions** | 9 | Handle camera, scrolling, mosaic | | **Sprite Loading** | 2 | Load sprites based on area+state | ### 1.2 Hook Execution Order During Transition When Link transitions between overworld screens, hooks fire in this precise order: ``` [TRANSITION START] ↓ 1. Overworld_OperateCameraScroll_Interupt ($02BC44) └─ Controls camera movement, checks for pyramid BG special scrolling ↓ 2. OverworldScrollTransition_Interupt ($02C02D) └─ Aligns BG layers during scroll, prevents BG1 flicker for pyramid ↓ 3. OverworldHandleTransitions ($02A9C4) [CRITICAL] └─ Calculates new screen ID using .ByScreen tables └─ Handles staggered layouts, 2x1/1x2 areas └─ Triggers mosaic if .MosaicTable has bit set ↓ 4. NewOverworld_FinishTransGfx (Custom Function) ├─ Frame 0: CheckForChangeGraphicsTransitionLoad │ └─ Reads .OWGFXGroupTable for new area ├─ Frame 0: LoadTransMainGFX │ └─ Decompresses 3 static sheets (if changed) ├─ Frame 0: PrepTransMainGFX │ └─ Stages GFX in buffer for DMA ├─ Frames 1-7: BlockGFXCheck │ └─ DMA's one 0x0600-byte block per frame └─ Frame 8: Complete, move to next module ↓ 5. NewLoadTransAuxGFX ($00D673) └─ Decompresses variable GFX sheets 3-6 (if changed) └─ Stages in $7E6000 buffer ↓ 6. NMI_UpdateChr_Bg2HalfAndAnimated (Custom NMI Handler) └─ DMA's variable sheets to VRAM during NMI ↓ 7. Overworld_ReloadSubscreenOverlay_Interupt ($02AF58) └─ Reads .OverlayTable, activates BG1 for subscreen if needed ↓ 8. Overworld_LoadAreaPalettes ($02C692) └─ Reads .MainPaletteTable, loads sprite/BG palettes ↓ 9. Palette_SetOwBgColor_Long ($0ED618) └─ Reads .BGColorTable, sets transparent color ↓ 10. LoadOverworldSprites_Interupt ($09C4C7) └─ Reads .Overworld_SpritePointers_state_X_New └─ Integrates day/night check (Oracle_ZSO_CheckIfNight) ↓ [TRANSITION COMPLETE] ``` ### 1.3 Critical Path: Transition GFX Loading The **most performance-sensitive** part of ZScream is the graphics decompression pipeline: ```asm ; Vanilla: Immediate 3BPP decompression, ~2 frames ; ZScream: Conditional decompression + DMA staging, ~1-8 frames NewOverworld_FinishTransGfx: { ; Frame 0: Decision + Decompression LDA.w TransGFXModuleFrame : BNE .notFirstFrame ; Read new area's GFX group (8 sheets) JSR.w CheckForChangeGraphicsTransitionLoad ; Decompress sheets 0-2 (static) if changed JSR.w LoadTransMainGFX ; If any sheets changed, prep for DMA LDA.b $04 : BEQ .dontPrep JSR.w PrepTransMainGFX .notFirstFrame ; Frames 1-7: DMA one block per frame (saves CPU time) LDA.b #$08 : STA.b $06 JSR.w BlockGFXCheck ; Frame 8: Complete CPY.b #$08 : BCC .return INC.b $11 ; Move to next submodule } ``` **Key Insight**: The `TransGFXModule_PriorSheets` array ($04CB[0x08]) caches the last loaded GFX group. If the new area uses the same sheets, decompression is **skipped entirely**, saving ~1-2 frames. --- ## 2. Memory Management & State Tracking ### 2.1 Free RAM Usage ZScream claims specific free RAM regions for state tracking: | Address | Size | Label | Purpose | |---------|------|-------|---------| | `$04CB` | 8 bytes | `TransGFXModule_PriorSheets` | Cache of last loaded GFX sheets (0-7) | | `$04D3` | 2 bytes | `NewNMITarget1` | VRAM target for NMI DMA (sheet 1) | | `$04D5` | 2 bytes | `NewNMISource1` | Source address for NMI DMA (sheet 1) | | `$04D7` | 2 bytes | `NewNMICount1` | Byte count for NMI DMA (sheet 1) | | `$04D9` | 2 bytes | `NewNMITarget2` | VRAM target for NMI DMA (sheet 2) | | `$04DB` | 2 bytes | `NewNMISource2` | Source address for NMI DMA (sheet 2) | | `$04DD` | 2 bytes | `NewNMICount2` | Byte count for NMI DMA (sheet 2) | | `$0716` | 2 bytes | `OWCameraBoundsS` | Custom camera bounds (south/left) | | `$0718` | 2 bytes | `OWCameraBoundsE` | Custom camera bounds (east/right) | | `$0CF3` | 1 byte | `TransGFXModuleFrame` | Frame counter for GFX loading | | `$0FC0` | 1 byte | `AnimatedTileGFXSet` | Current animated tile set ID | | `$7EFDC0` | 64 bytes | `ExpandedSpritePalArray` | Expanded sprite palette array | ### 2.2 Data Pool Memory Map (Bank $28) All custom data tables reside in a reserved block: ``` org $288000 ; PC $140000 Pool: { ; PALETTE DATA .BGColorTable ; [0x0180] - 16-bit BG colors per area .MainPaletteTable ; [0x00C0] - Palette set indices (0-5) ; FEATURE TOGGLES .EnableTable ; [0x00C0] - Enable/disable features .EnableTransitionGFXGroupLoad ; [0x0001] - Global GFX load toggle .MosaicTable ; [0x0180] - Mosaic bitfields per area ; GRAPHICS DATA .AnimatedTable ; [0x00C0] - Animated tile set IDs .OverlayTable ; [0x0180] - Overlay IDs ($9F=rain, $FF=none) .OWGFXGroupTable ; [0x0600] - 8 sheets per area (8*$C0) .OWGFXGroupTable_sheet0 ; [0x00C0] .OWGFXGroupTable_sheet1 ; [0x00C0] // ... sheets 2-7 ; LAYOUT & TRANSITION DATA .DefaultGFXGroups ; [0x0018] - 8 sheets * 3 worlds .Overworld_ActualScreenID_New ; [0x0180] - Parent screen for multi-tile ; CAMERA BOUNDARIES .ByScreen1_New ; [0x0180] - Right transition bounds .ByScreen2_New ; [0x0180] - Left transition bounds .ByScreen3_New ; [0x0180] - Down transition bounds .ByScreen4_New ; [0x0180] - Up transition bounds ; SPRITE POINTERS .Overworld_SpritePointers_state_0_New ; [0x0140] - Intro sprites .Overworld_SpritePointers_state_1_New ; [0x0140] - Post-Aga1 sprites .Overworld_SpritePointers_state_2_New ; [0x0140] - Post-Ganon sprites } assert pc() <= $289938 ; Must not exceed this boundary! ``` **⚠️ CRITICAL**: The pool ends at `$289938` (`$141938`). Exceeding this boundary will corrupt other code! ### 2.3 Table Index Calculation Most ZScream tables are indexed by area ID (`$8A`): ```asm ; BYTE TABLES (1 byte per area) LDA.b $8A ; Load current area TAX ; Use as index LDA.l Pool_MainPaletteTable, X ; Read palette index ; WORD TABLES (2 bytes per area) LDA.b $8A : ASL : TAX ; Area * 2 for 16-bit index LDA.l Pool_BGColorTable, X ; Read 16-bit color ; GFX GROUP TABLE (8 bytes per area) REP #$30 LDA.b $8A : AND.w #$00FF : ASL #3 : TAX ; Area * 8 SEP #$20 LDA.w Pool_OWGFXGroupTable_sheet0, X ; Read sheet 0 LDA.w Pool_OWGFXGroupTable_sheet1, X ; Read sheet 1 // etc. ``` --- ## 3. Graphics Loading Pipeline ### 3.1 Static GFX Sheets (0-2) Sheets 0-2 are **always loaded together** because they decompress quickly (~1 frame): ``` Sheet 0: Main tileset (walls, ground, trees) Sheet 1: Secondary tileset (decorative, objects) Sheet 2: Tertiary tileset (area-specific) ``` **Loading Process**: 1. `LoadTransMainGFX` reads `.OWGFXGroupTable_sheet0-2` for new area 2. Compares against `TransGFXModule_PriorSheets[0-2]` 3. If changed, calls `Decomp_bg_variable` for each sheet 4. `PrepTransMainGFX` stages decompressed data in `$7F0000` buffer 5. `BlockGFXCheck` DMA's one 0x0600-byte block per frame (8 frames total) ### 3.2 Variable GFX Sheets (3-6) Sheets 3-6 are **loaded separately** because they're larger (~2-3 frames total): ``` Sheet 3: Variable tileset slot 0 Sheet 4: Variable tileset slot 1 Sheet 5: Variable tileset slot 2 Sheet 6: Variable tileset slot 3 ``` **Loading Process**: 1. `NewLoadTransAuxGFX` reads `.OWGFXGroupTable_sheet3-6` for new area 2. Compares against `TransGFXModule_PriorSheets[3-6]` 3. If changed, calls `Decomp_bg_variableLONG` for each sheet 4. Decompressed data staged in `$7E6000` buffer 5. `NMI_UpdateChr_Bg2HalfAndAnimatedLONG` DMA's during NMI using: - `NewNMISource1/2` - Source addresses in `$7F` bank - `NewNMITarget1/2` - VRAM targets - `NewNMICount1/2` - Transfer sizes ### 3.3 Animated Tile GFX Animated tiles (waterfalls, lava, spinning tiles) use a **separate system**: ```asm ReadAnimatedTable: { PHB : PHK : PLB ; Index into .AnimatedTable (1 byte per area) LDA.b $8A : TAX LDA.w Pool_AnimatedTable, X ; Store in $0FC0 for game to use STA.w AnimatedTileGFXSet PLB RTL } ``` **Animated GFX Set IDs**: - `$58` - Light World water/waterfalls - `$59` - Dark World lava/skulls - `$5A` - Special World animations - `$FF` - No animated tiles **Decompression Timing**: - **On transition**: `ReadAnimatedTable : DEC : TAY` → `DecompOwAnimatedTiles` - **On mirror warp**: `AnimateMirrorWarp_DecompressAnimatedTiles` - **After bird travel**: Hook at `$0AB8F5` - **After map close**: Hook at `$0ABC5A` ### 3.4 GFX Sheet $FF - Skip Loading If any sheet in `.OWGFXGroupTable` is set to `$FF`, ZScream **skips** loading that sheet: ```asm ; Example: Area uses default LW sheet 0, custom sheets 1-2 .OWGFXGroupTable_sheet0: db $FF ; Don't change sheet 0 .OWGFXGroupTable_sheet1: db $12 ; Load custom sheet $12 .OWGFXGroupTable_sheet2: db $34 ; Load custom sheet $34 ``` This allows selective GFX changes without re-decompressing unchanged sheets. --- ## 4. Sprite Loading System Deep Dive ### 4.1 The Three-State System ZScream extends vanilla's 2-state sprite system to support **3 game states**: | State | SRAM `$7EF3C5` | Trigger | Typical Use | |-------|----------------|---------|-------------| | **0** | `$00` | Game start | Pre-Zelda rescue (intro) | | **1** | `$01-$02` | Uncle reached / Zelda rescued | Mid-game progression | | **2** | `$03+` | Agahnim defeated | Post-Agahnim world | **State Pointer Tables** (192 areas × 2 bytes each): ``` Pool_Overworld_SpritePointers_state_0_New ; $140 bytes Pool_Overworld_SpritePointers_state_1_New ; $140 bytes Pool_Overworld_SpritePointers_state_2_New ; $140 bytes ``` Each entry is a **16-bit pointer** to a sprite list in ROM. ### 4.2 LoadOverworldSprites_Interupt Implementation This is the **core sprite loading hook** at `$09C4C7`: ```asm LoadOverworldSprites_Interupt: { ; Get current area's size (1x1, 2x2, 2x1, 1x2) LDX.w $040A LDA.l Pool_BufferAndBuildMap16Stripes_overworldScreenSize, X : TAY ; Store X/Y boundaries for sprite loading LDA.w .xSize, Y : STA.w $0FB9 : STZ.w $0FB8 LDA.w .ySize, Y : STA.w $0FBB : STZ.w $0FBA ; === DAY/NIGHT CHECK === ; Check if it's night time JSL Oracle_ZSO_CheckIfNight ASL : TAY ; Y = 0 (day) or 2 (night) REP #$30 ; Calculate state table offset ; .phaseOffset: dw $0000 (state 0), $0140 (state 1), $0280 (state 2) TXA : ASL : CLC : ADC.w .phaseOffset, Y : TAX ; Get sprite pointer for (area, state) LDA.l Pool_Overworld_SpritePointers_state_0_New, X : STA.b $00 SEP #$20 ; Continue to vanilla sprite loading code... } ``` **Key Insight**: The `ASL : TAY` after day/night check doubles the state index, allowing the `.phaseOffset` table to select between **6 possible sprite sets** (3 states × 2 times of day). ### 4.3 Day/Night Integration: The Challenge The **original conflict** occurred because: 1. ZScream's `LoadOverworldSprites_Interupt` lives in **bank $09** (`$04C4C7`) 2. `Oracle_CheckIfNight16Bit` lives in **bank $34** (`Overworld/time_system.asm`) 3. ZScream is **outside** the `Oracle` namespace 4. The `Oracle_` prefix is **not visible** to ZScream during assembly **Failed Approach #1**: Direct JSL call ```asm ; In ZSCustomOverworld.asm (outside Oracle namespace) JSL Oracle_CheckIfNight16Bit ; ❌ Label not found during assembly ``` **Failed Approach #2**: Recursive loop ```asm ; Calling Sprite_OverworldReloadAll from within the sprite loading hook JSL.l Sprite_OverworldReloadAll ; ❌ This calls LoadOverworldSprites again! ; Stack overflows after ~200 recursions ``` **Working Solution**: Self-contained day/night check ```asm ; In time_system.asm (inside Oracle namespace) Oracle_ZSO_CheckIfNight: ; Exported with Oracle_ prefix { ; Self-contained logic that doesn't depend on other Oracle functions PHB : PHK : PLB ; Special area checks (Tail Palace, Zora Sanctuary) LDA $8A CMP.b #$2E : BEQ .tail_palace CMP.b #$2F : BEQ .tail_palace CMP.b #$1E : BEQ .zora_sanctuary JMP .continue_check .tail_palace LDA.l $7EF37A : AND #$10 : BNE .load_peacetime JMP .continue_check .zora_sanctuary LDA.l $7EF37A : AND #$20 : BNE .load_peacetime JMP .continue_check .load_peacetime ; Return original GameState LDA.l $7EF3C5 PLB RTL .continue_check REP #$30 ; Don't change during intro LDA.l $7EF3C5 : AND.w #$00FF : CMP.w #$0002 : BCC .day_time ; Check time ($7EE000 = hour) LDA.l $7EE000 : AND.w #$00FF CMP.w #$0012 : BCS .night_time ; >= 6 PM CMP.w #$0006 : BCC .night_time ; < 6 AM .day_time LDA.l $7EF3C5 BRA .done .night_time ; Return GameState + 1 (load next state's sprites) LDA.l $7EF3C5 : CLC : ADC #$0001 ; NOTE: Does NOT permanently modify SRAM! .done SEP #$30 PLB RTL } ``` **Why This Works**: 1. Function is **fully self-contained** - no dependencies on other Oracle functions 2. Uses only SRAM reads (`$7EF37A`, `$7EF3C5`, `$7EE000`) 3. Returns modified state **without writing to SRAM** (temporary for sprite loading only) 4. Can be called from **any** context, including outside Oracle namespace ### 4.4 Sprite Table Organization Strategy To support day/night cycles, organize sprite pointers like this: ``` ; Example: Area $03 (Lost Woods) ; State 0 (Intro): No day/night distinction Pool_Overworld_SpritePointers_state_0_New: dw LostWoods_Intro_Sprites ; State 1 (Mid-game): Day sprites Pool_Overworld_SpritePointers_state_1_New: dw LostWoods_Day_Sprites ; State 2 (Post-Ganon): Night sprites Pool_Overworld_SpritePointers_state_2_New: dw LostWoods_Night_Sprites ; Actual sprite data in expanded space LostWoods_Day_Sprites: db $05 ; 5 sprites db $04, $12, $34, $56 ; Moblin at (4, 12), Octorok at (34, 56) // ... more sprites LostWoods_Night_Sprites: db $06 ; 6 sprites db $04, $12, $78, $9A ; Stalfos at (4, 12), Poe at (78, 9A) // ... more sprites ``` **Advanced Technique**: Use the **same pointer** for areas that don't change: ```asm ; Area $10 (Hyrule Castle) - No day/night changes Pool_Overworld_SpritePointers_state_1_New: dw HyruleCastle_AllTimes_Sprites Pool_Overworld_SpritePointers_state_2_New: dw HyruleCastle_AllTimes_Sprites ; Same pointer, saves ROM space ``` --- ## 5. Cross-Namespace Integration ### 5.1 Understanding Asar Namespaces Asar's `namespace` directive creates label isolation: ```asm ; In Oracle_main.asm namespace Oracle { incsrc Core/ram.asm ; Inside namespace incsrc Sprites/all_sprites.asm incsrc Items/all_items.asm } ; ZScream is OUTSIDE the namespace incsrc Overworld/ZSCustomOverworld.asm ; Outside namespace ``` **Visibility Rules**: | Call Type | From Inside Namespace | From Outside Namespace | |-----------|----------------------|------------------------| | **Local Label** (`.label`) | ✅ Same function only | ✅ Same function only | | **Function Label** (no `.`) | ✅ Visible within namespace | ❌ **NOT VISIBLE** | | **Exported Label** (`Oracle_FunctionName`) | ✅ Visible | ✅ **VISIBLE** | ### 5.2 Calling Oracle Functions from ZScream **❌ WRONG** - This will fail during assembly: ```asm ; In ZSCustomOverworld.asm (outside namespace) LoadDayNightSprites: { JSL CheckIfNight16Bit ; ❌ ERROR: Label not found BCS .is_night // ... } ``` **✅ CORRECT** - Use the exported `Oracle_` prefix: ```asm ; In ZSCustomOverworld.asm (outside namespace) LoadDayNightSprites: { JSL Oracle_CheckIfNight16Bit ; ✅ Works! BCS .is_night ; Load day sprites BRA .done .is_night ; Load night sprites .done RTL } ``` ### 5.3 Exporting Functions for ZScream To make a function callable from ZScream, export it with the `Oracle_` prefix: ```asm ; In Core/symbols.asm or similar (inside Oracle namespace) namespace Oracle { CheckIfNight16Bit: { ; Implementation... RTL } ; Export with Oracle_ prefix for external callers Oracle_CheckIfNight16Bit: JML CheckIfNight16Bit } ``` **Alternative**: Use `autoclean namespace` to auto-export: ```asm autoclean namespace Oracle { ; All labels automatically get Oracle_ prefix CheckIfNight16Bit: ; Becomes Oracle_CheckIfNight16Bit externally { RTL } } ``` ### 5.4 Build Order Dependencies Asar processes files **sequentially**. If ZScream needs Oracle labels, Oracle must be included **first**: ```asm ; In Oracle_main.asm - CORRECT ORDER namespace Oracle { ; 1. Define all Oracle functions first incsrc Core/symbols.asm incsrc Core/patches.asm incsrc Overworld/time_system.asm ; Defines Oracle_ZSO_CheckIfNight } ; 2. THEN include ZScream (which references Oracle functions) incsrc Overworld/ZSCustomOverworld.asm ``` **❌ WRONG ORDER**: ```asm ; This will fail! incsrc Overworld/ZSCustomOverworld.asm ; References Oracle_ZSO_CheckIfNight namespace Oracle { incsrc Overworld/time_system.asm ; Too late! Already referenced } ``` ### 5.5 Data Access Patterns **Accessing Oracle RAM from ZScream**: ```asm ; Oracle defines custom WRAM ; In Core/ram.asm (inside namespace): MenuScrollLevelV = $7E0730 ; ZScream can access via full address: LDA.l $7E0730 ; ✅ Direct memory access works STA.w $0100 ; But NOT via label: LDA.w MenuScrollLevelV ; ❌ Label not visible ``` **Best Practice**: Define shared memory labels in **both** locations: ```asm ; In ZSCustomOverworld.asm OracleMenuScrollV = $7E0730 ; Local copy of label LoadMenuState: { LDA.l OracleMenuScrollV ; ✅ Use local label // ... } ``` --- ## 6. Performance Considerations ### 6.1 Frame Budget Analysis Overworld transitions have a strict **frame budget** before the player notices lag: | Operation | Frames | Cumulative | Notes | |-----------|--------|------------|-------| | Camera scroll | 1-16 | 1-16 | Depends on Link's speed | | GFX decompression (sheets 0-2) | 1-2 | 2-18 | Only if sheets changed | | GFX staging (PrepTransMainGFX) | 1 | 3-19 | Only if sheets changed | | Block DMA (8 blocks × 0x0600) | 8 | 11-27 | One per frame | | Variable GFX decompress (sheets 3-6) | 2-3 | 13-30 | Only if sheets changed | | Animated tile decompress | 1 | 14-31 | Always runs | | Sprite loading | 1 | 15-32 | Always runs | | **Total (worst case)** | **~32** | | Sheets 0-6 all changed | | **Total (best case)** | **~10** | | No GFX changes | **Optimization Strategy**: ZScream **caches** loaded sheets to avoid unnecessary decompression: ```asm ; Only decompress if sheet ID changed LDA.w Pool_OWGFXGroupTable_sheet0, X : CMP.b #$FF : BEQ .skip CMP.w TransGFXModule_PriorSheets+0 : BEQ .skip ; Sheet changed, decompress TAY JSL.l Decomp_bg_variableLONG .skip ``` **Result**: Areas using the same GFX group load **2-3 frames faster**. ### 6.2 Optional Feature Toggles ZScream provides **38 debug flags** (`!Func...`) to disable hooks: ```asm ; In ZSCustomOverworld.asm ; Disable GFX decompression (use vanilla) !Func00D585 = $00 ; Disables NewLoadTransAuxGFX ; Disable subscreen overlays (faster) !Func00DA63 = $00 ; Disables ActivateSubScreen ``` **When to disable**: - **During development**: Test if a specific hook is causing issues - **For speed hacks**: Remove overlays, custom palettes for faster transitions - **Compatibility testing**: Verify other mods don't conflict with specific hooks ### 6.3 DMA Optimization: Block Transfer Instead of DMA'ing all GFX at once (expensive), ZScream spreads it across **8 frames**: ```asm BlockGFXCheck: { ; DMA one 0x0600-byte block per frame LDA.b $06 : DEC : STA.b $06 ; Decrement frame counter ; Calculate source address ; Block N = $7F0000 + (N * 0x0600) LDX.b $06 LDA.l BlockSourceTable, X : STA.w $04D3 ; DMA during NMI LDA.b #$01 : STA.w SNES.DMAChannelEnable RTL } ``` **Trade-off**: Transitions take slightly longer, but **no frame drops**. ### 6.4 GFX Sheet Size Guidelines Keep custom GFX sheets **under 0x0600 bytes** (decompressed) to fit in buffer: ``` Compression Ratios (typical): - 3BPP tileset: 0x0600 bytes decompressed - Compressed size: ~0x0300-0x0400 bytes (50-66% ratio) - Compression time: ~15,000-20,000 cycles (~0.5 frames) ``` **Exceeding 0x0600 bytes**: Data will overflow buffer, corrupting WRAM! --- ## 7. Adding Custom Features ### 7.1 Adding a New Data Table **Example**: Add a "music override" table to force specific songs per area. **Step 1**: Reserve space in the data pool ```asm ; In ZSCustomOverworld.asm, Pool section org $288000 Pool: { ; ... existing tables ... ; NEW: Music override table (1 byte per area) .MusicOverrideTable db $02 ; Area $00 - Song $02 db $FF ; Area $01 - No override db $05 ; Area $02 - Song $05 // ... 189 more entries (192 total) } ``` **Step 2**: Create a function to read the table ```asm pullpc ; Save current org position ReadMusicOverrideTable: { PHB : PHK : PLB ; Use local bank LDA.b $8A : TAX ; Current area LDA.w Pool_MusicOverrideTable, X CMP.b #$FF : BEQ .noOverride ; Store in music queue STA.w $0132 .noOverride PLB RTL } pushpc ; Restore org position ``` **Step 3**: Hook into existing code ```asm ; Hook the music loading routine org $0283EE ; PreOverworld_LoadProperties JSL ReadMusicOverrideTable NOP : NOP ; Fill unused space ``` **Step 4**: Update ZScream data in editor In your level editor (ZScream tool): ```python # Add UI for music override music_override_table = [0xFF] * 192 # Default: no override music_override_table[0x00] = 0x02 # Area 0: Song 2 music_override_table[0x02] = 0x05 # Area 2: Song 5 # Save to ROM at $288000 + offset ``` ### 7.2 Adding a New Hook **Example**: Trigger custom code when entering specific areas. **Step 1**: Find injection point Use a debugger to find where vanilla code runs during area entry. Example: `$02AB08` (`Overworld_LoadMapProperties`). **Step 2**: Create your custom function ```asm pullpc OnAreaEntry_Custom: { ; Save context PHA : PHX : PHY ; Check if this is area $03 (Lost Woods) LDA.b $8A : CMP.b #$03 : BNE .notLostWoods ; Trigger custom event LDA.b #$01 : STA.l $7EF400 ; Set custom flag ; Play custom music LDA.b #$10 : STA.w $0132 .notLostWoods ; Restore context PLY : PLX : PLA RTL } pushpc ``` **Step 3**: Hook into vanilla code ```asm org $02AB08 JSL OnAreaEntry_Custom NOP ; Fill unused bytes if needed ``` **Step 4**: Add debug toggle (optional) ```asm ; At top of file with other !Func... flags !EnableAreaEntryHook = $01 ; In hook if !EnableAreaEntryHook == $01 org $02AB08 JSL OnAreaEntry_Custom NOP else org $02AB08 ; Original code db $A5, $8A, $29 endif ``` ### 7.3 Modifying Transition Behavior **Example**: Add diagonal screen transitions. **Challenge**: Vanilla only supports 4 directions (up, down, left, right). ZScream uses `.ByScreen1-4` tables for these. **Solution**: Create a 5th table for diagonal data. **Step 1**: Add diagonal data table ```asm Pool: { ; ... existing tables ... ; NEW: Diagonal transition table ; Format: %UDLR where U=up-right, D=down-right, L=down-left, R=up-left .DiagonalTransitionTable db %0000 ; Area $00 - No diagonal transitions db %0001 ; Area $01 - Up-left allowed db %1100 ; Area $02 - Up-right, down-right allowed // ... 189 more entries } ``` **Step 2**: Modify transition handler ```asm ; Hook into OverworldHandleTransitions ($02A9C4) org $02A9C4 JML HandleDiagonalTransitions pullpc HandleDiagonalTransitions: { ; Check if player is moving diagonally LDA.b $26 : BEQ .notMoving ; X velocity LDA.b $27 : BEQ .notMoving ; Y velocity ; Both non-zero = diagonal movement JSR.w CheckDiagonalAllowed BCS .allowDiagonal ; Not allowed, fall back to vanilla JML $02A9C8 ; Original code .allowDiagonal ; Calculate new screen ID for diagonal JSR.w CalculateDiagonalScreen STA.b $8A ; Set new area ; Trigger transition JML $02AA00 ; Continue transition code .notMoving JML $02A9C8 ; Original code } CheckDiagonalAllowed: { LDA.b $8A : TAX LDA.l Pool_DiagonalTransitionTable, X ; Check appropriate bit based on direction // ... implementation ... RTS } CalculateDiagonalScreen: { ; Calculate screen ID for diagonal move // ... implementation ... RTS } pushpc ``` --- ## 8. Debugging & Troubleshooting ### 8.1 Common Issues & Solutions #### Issue: "Screen turns black during transition" **Cause**: GFX decompression exceeded buffer size. **Debug Steps**: 1. Check `Pool_OWGFXGroupTable` for the affected area 2. Measure compressed size of each sheet (should be < 0x0400 bytes) 3. Check for sheet ID `> $7F` (invalid) **Solution**: ```asm ; Add bounds checking to decompression LDA.w Pool_OWGFXGroupTable_sheet0, X CMP.b #$80 : BCS .invalidSheet ; Sheet ID too high CMP.b #$FF : BEQ .skipSheet ; Skip marker ; Valid sheet, decompress TAY JSL.l Decomp_bg_variableLONG ``` #### Issue: "Sprites don't load in new area" **Cause**: Sprite pointer table points to invalid address. **Debug Steps**: 1. Check `Pool_Overworld_SpritePointers_state_X_New` for the area 2. Verify pointer points to valid ROM address (PC `$140000`+) 3. Check sprite list format (count byte, then sprite data) **Solution**: ```asm ; Add validation to sprite loader LDA.l Pool_Overworld_SpritePointers_state_0_New, X : STA.b $00 LDA.l Pool_Overworld_SpritePointers_state_0_New+1, X : STA.b $01 ; Validate pointer is in ROM range ($00-$7F banks) AND.b #$7F : CMP.b #$40 : BCC .invalidPointer ; Valid, continue BRA .loadSprites .invalidPointer ; Use default sprite list LDA.w #DefaultSpriteList : STA.b $00 LDA.w #DefaultSpriteList>>8 : STA.b $01 ``` #### Issue: "Day/night sprites don't switch" **Cause**: `Oracle_ZSO_CheckIfNight` not returning correct value. **Debug Steps**: 1. Check `$7EE000` (current hour) in RAM viewer 2. Verify `$7EF3C5` (GameState) is >= $02 3. Check sprite tables for states 2 and 3 **Solution**: ```asm ; Add debug output to ZSO_CheckIfNight .night_time ; Log to unused RAM for debugging LDA.l $7EE000 : STA.l $7F0000 ; Hour LDA.l $7EF3C5 : STA.l $7F0001 ; Original state ; Return state + 1 CLC : ADC #$0001 STA.l $7F0002 ; Modified state ``` ### 8.2 Emulator Debugging Tools **Mesen-S Debugger**: ``` 1. Set breakpoint: $09C4C7 (LoadOverworldSprites_Interupt) 2. Watch expressions: - $8A (current area) - $7EF3C5 (GameState) - $7EE000 (current hour) - $00-$01 (sprite pointer) 3. Memory viewer: $288000 (ZScream data pool) ``` **bsnes-plus Debugger**: ``` 1. Memory breakpoint: Write to $8A (area change) 2. Trace logger: Enable, filter for "JSL", search for ZScream functions 3. VRAM viewer: Check tile uploads after transition ``` ### 8.3 Assertion Failures ZScream uses `assert` directives to catch data overflow: ```asm assert pc() <= $289938 ; Must not exceed data pool boundary ``` **If this fails**: ``` Error: Assertion failed at ZSCustomOverworld.asm line 1393 PC: $289A00 (exceeds $289938 by $C8 bytes) ``` **Solution**: Reduce data table sizes or move tables to different bank. ### 8.4 Build System Troubleshooting **Issue**: "Label not found: Oracle_ZSO_CheckIfNight" **Cause**: Build order issue - ZScream assembled before Oracle functions defined. **Solution**: Check `Oracle_main.asm` include order: ```asm ; CORRECT: namespace Oracle { incsrc Overworld/time_system.asm ; Defines label } incsrc Overworld/ZSCustomOverworld.asm ; Uses label ; WRONG: incsrc Overworld/ZSCustomOverworld.asm ; Uses label namespace Oracle { incsrc Overworld/time_system.asm ; Too late! } ``` --- ## 9. Future Enhancement Possibilities ### 9.1 Multi-Layer Backgrounds **Concept**: Support BG3 parallax scrolling (like mountains in distance). **Implementation**: - Add `.BG3LayerTable` to data pool - Hook `Overworld_OperateCameraScroll` to update BG3 scroll - Modify `InitTilesets` to load BG3 graphics **Challenges**: - SNES Mode 1 supports BG1/BG2/BG3, but subscreen uses BG1 - Would need to disable overlays when BG3 parallax active ### 9.2 Weather System Integration **Concept**: Dynamic weather per area (rain, snow, wind). **Implementation**: - Add `.WeatherTable` (rain intensity, snow, wind direction) - Extend `RainAnimation` hook to support multiple weather types - Add particle systems for snow/leaves **Challenges**: - Performance impact (60 particles @ 60 FPS = 3600 calcs/sec) - Would need sprite optimization ### 9.3 Area-Specific Camera Boundaries **Concept**: Custom camera scroll limits per area (like Master Sword grove). **Implementation**: - Add `.CameraBoundsTable` (4 bytes per area: top, bottom, left, right) - Hook camera scroll functions to read table - Apply limits before updating `$E0-$E7` scroll positions **Already Partially Implemented**: `OWCameraBoundsS/E` at `$0716/$0718`. --- ## 10. Reference: Complete Hook List | Address | Bank | Function | Purpose | |---------|------|----------|---------| | `$00D585` | $00 | `Decomp_bg_variableLONG` | Decompress variable GFX sheets | | `$00D673` | $00 | `NewLoadTransAuxGFX` | Load variable sheets 3-6 | | `$00D8D5` | $00 | `AnimateMirrorWarp_DecompressAnimatedTiles` | Load animated tiles on mirror warp | | `$00DA63` | $00 | `AnimateMirrorWarp_LoadSubscreen` | Enable/disable subscreen overlay | | `$00E221` | $00 | `InitTilesetsLongCalls` | Load GFX groups from custom tables | | `$00EEBB` | $00 | `Palette_InitWhiteFilter_Interupt` | Zero BG color for pyramid area | | `$00FF7C` | $00 | `MirrorWarp_BuildDewavingHDMATable_Interupt` | BG scrolling for pyramid | | `$0283EE` | $02 | `PreOverworld_LoadProperties_Interupt` | Load area properties on dungeon exit | | `$028632` | $02 | `Credits_LoadScene_Overworld_PrepGFX_Interupt` | Load GFX for credits scenes | | `$029A37` | $02 | `Spotlight_ConfigureTableAndControl_Interupt` | Fixed color setup | | `$02A4CD` | $02 | `RainAnimation` | Rain overlay animation | | `$02A9C4` | $02 | `OverworldHandleTransitions` | Screen transition logic | | `$02ABBE` | $02 | `NewOverworld_FinishTransGfx` | Multi-frame GFX loading | | `$02AF58` | $02 | `Overworld_ReloadSubscreenOverlay_Interupt` | Load subscreen overlays | | `$02B391` | $02 | `MirrorWarp_LoadSpritesAndColors_Interupt` | Pyramid warp special handling | | `$02BC44` | $02 | `Overworld_OperateCameraScroll_Interupt` | Camera scroll control | | `$02C02D` | $02 | `OverworldScrollTransition_Interupt` | BG alignment during scroll | | `$02C692` | $02 | `Overworld_LoadAreaPalettes` | Load custom palettes | | `$09C4C7` | $09 | `LoadOverworldSprites_Interupt` | Load sprites with day/night support | | `$0AB8F5` | $0A | Bird travel animated tile reload | Load animated tiles after bird | | `$0ABC5A` | $0A | Map close animated tile reload | Load animated tiles after map | | `$0BFEB6` | $0B | `Overworld_SetFixedColorAndScroll` | Set overlay colors and scroll | | `$0ED627` | $0E | Custom BG color on warp | Set transparent color | | `$0ED8AE` | $0E | Reset area color after flash | Restore BG color post-warp | --- ## Appendix A: Memory Map Quick Reference ``` === WRAM === $04CB[8] - TransGFXModule_PriorSheets (cached GFX IDs) $04D3[2] - NewNMITarget1 (VRAM target for sheet 1) $04D5[2] - NewNMISource1 (Source for sheet 1) $04D7[2] - NewNMICount1 (Size for sheet 1) $04D9[2] - NewNMITarget2 (VRAM target for sheet 2) $04DB[2] - NewNMISource2 (Source for sheet 2) $04DD[2] - NewNMICount2 (Size for sheet 2) $0716[2] - OWCameraBoundsS (Camera south/left bounds) $0718[2] - OWCameraBoundsE (Camera east/right bounds) $0CF3[1] - TransGFXModuleFrame (GFX loading frame counter) $0FC0[1] - AnimatedTileGFXSet (Current animated set) === SRAM === $7EE000[1] - Current hour (0-23) $7EF3C5[1] - GameState (0=intro, 1-2=midgame, 3+=postgame) $7EF37A[1] - Crystals (dungeon completion flags) === ROM === $288000-$289938 - ZScream data pool (Bank $28) $289940+ - ZScream functions ``` --- ## Appendix B: Sprite Pointer Format Each sprite list in ROM follows this format: ``` [COUNT] [SPRITE_0] [SPRITE_1] ... [SPRITE_N] └─ 1 byte └──────── 4 bytes each ────────┘ Sprite Entry (4 bytes): Byte 0: Y coordinate (high 4 bits) + X coordinate (high 4 bits) Byte 1: Y coordinate (low 8 bits) Byte 2: X coordinate (low 8 bits) Byte 3: Sprite ID Example: db $03 ; 3 sprites db $00, $12, $34, $05 ; Sprite $05 at ($034, $012) db $01, $56, $78, $0A ; Sprite $0A at ($178, $156) db $00, $9A, $BC, $12 ; Sprite $12 at ($0BC, $09A) ``` --- **End of Advanced Documentation** For basic ZScream usage, see `ZSCustomOverworld.md`. For general overworld documentation, see `Overworld.md`. For troubleshooting ALTTP issues, see `Docs/General/Troubleshooting.md` (Task 4).