- Introduced comprehensive documentation covering internal hook architecture, memory management, graphics loading pipeline, sprite loading system, cross-namespace integration, performance considerations, and debugging strategies. - Detailed sections on adding custom features and modifying existing behaviors, including examples and code snippets. - Included a complete hook list and memory map quick reference for developers.
34 KiB
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
- Internal Hook Architecture
- Memory Management & State Tracking
- Graphics Loading Pipeline
- Sprite Loading System Deep Dive
- Cross-Namespace Integration
- Performance Considerations
- Adding Custom Features
- 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:
; 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):
; 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:
LoadTransMainGFXreads.OWGFXGroupTable_sheet0-2for new area- Compares against
TransGFXModule_PriorSheets[0-2] - If changed, calls
Decomp_bg_variablefor each sheet PrepTransMainGFXstages decompressed data in$7F0000bufferBlockGFXCheckDMA'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:
NewLoadTransAuxGFXreads.OWGFXGroupTable_sheet3-6for new area- Compares against
TransGFXModule_PriorSheets[3-6] - If changed, calls
Decomp_bg_variableLONGfor each sheet - Decompressed data staged in
$7E6000buffer NMI_UpdateChr_Bg2HalfAndAnimatedLONGDMA's during NMI using:NewNMISource1/2- Source addresses in$7FbankNewNMITarget1/2- VRAM targetsNewNMICount1/2- Transfer sizes
3.3 Animated Tile GFX
Animated tiles (waterfalls, lava, spinning tiles) use a separate system:
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:
; 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:
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:
- ZScream's
LoadOverworldSprites_Interuptlives in bank $09 ($04C4C7) Oracle_CheckIfNight16Bitlives in bank $34 (Overworld/time_system.asm)- ZScream is outside the
Oraclenamespace - The
Oracle_prefix is not visible to ZScream during assembly
Failed Approach #1: Direct JSL call
; In ZSCustomOverworld.asm (outside Oracle namespace)
JSL Oracle_CheckIfNight16Bit ; ❌ Label not found during assembly
Failed Approach #2: Recursive loop
; 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
; 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:
- Function is fully self-contained - no dependencies on other Oracle functions
- Uses only SRAM reads (
$7EF37A,$7EF3C5,$7EE000) - Returns modified state without writing to SRAM (temporary for sprite loading only)
- 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:
; 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:
; 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:
; In ZSCustomOverworld.asm (outside namespace)
LoadDayNightSprites:
{
JSL CheckIfNight16Bit ; ❌ ERROR: Label not found
BCS .is_night
// ...
}
✅ CORRECT - Use the exported Oracle_ prefix:
; 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:
; 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:
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:
; 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:
; 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:
; 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:
; 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:
; 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:
; 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:
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
; 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
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
; 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):
# 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
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
org $02AB08
JSL OnAreaEntry_Custom
NOP ; Fill unused bytes if needed
Step 4: Add debug toggle (optional)
; 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
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
; 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:
- Check
Pool_OWGFXGroupTablefor the affected area - Measure compressed size of each sheet (should be < 0x0400 bytes)
- Check for sheet ID
> $7F(invalid)
Solution:
; 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:
- Check
Pool_Overworld_SpritePointers_state_X_Newfor the area - Verify pointer points to valid ROM address (PC
$140000+) - Check sprite list format (count byte, then sprite data)
Solution:
; 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:
- Check
$7EE000(current hour) in RAM viewer - Verify
$7EF3C5(GameState) is >= $02 - Check sprite tables for states 2 and 3
Solution:
; 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:
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:
; 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
.BG3LayerTableto data pool - Hook
Overworld_OperateCameraScrollto update BG3 scroll - Modify
InitTilesetsto 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
RainAnimationhook 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-$E7scroll 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).