41 KiB
ZScream Custom Overworld (Overworld/ZSCustomOverworld.asm)
1. Overview
ZSCustomOverworld is a powerful and extensive system that replaces large parts of the vanilla A Link to the Past overworld engine. Its primary purpose is to remove hardcoded behaviors and replace them with a data-driven approach, allowing for a highly customizable overworld.
Instead of relying on hardcoded logic for palettes, graphics, and layouts, ZSCustomOverworld reads this information from a large pool of data tables located in expanded ROM space (starting at $288000). These tables are designed to be edited by the ZScream overworld editor.
2. Key Features
- Custom Palettes & Colors: Assign a unique main palette and background color to every overworld screen.
- Custom Graphics: Assign custom static tile graphics (GFX groups) and animated tile sets to each area.
- Custom Overlays: Add or remove subscreen overlays (like rain, fog, and clouds) on a per-area basis.
- Flexible Layouts: Fixes vanilla bugs related to screen transitions and adds support for new area sizes, such as 2x1 "wide" and 1x2 "tall" areas, in addition to the standard 1x1 and 2x2.
- Expanded Special Worlds: Allows the normally limited "special world" areas (like the Master Sword grove) to be used as full-featured overworld screens.
3. Core Architecture: Data Tables
The system's flexibility comes from a large data pool starting at org $288000. Key tables include:
.BGColorTable: A table of 16-bit color values for the background of each overworld screen..EnableTable: A series of flags to enable or disable specific features of ZSCustomOverworld, such as custom palettes or overlays..MainPaletteTable: An index ($00to$05) into the game's main overworld palette sets for each screen..MosaicTable: A bitfield for each screen to control mosaic transitions on a per-direction basis..AnimatedTable: The GFX sheet ID for animated tiles for each screen..OverlayTable: The overlay ID (e.g.,$9Ffor rain) for each screen.$FFmeans no overlay..OWGFXGroupTable: A large table defining the 8 GFX group sheets to be loaded for each overworld screen..Overworld_ActualScreenID_New: A table that defines the "parent" screen for multi-screen areas (e.g., for a 2x2 area, all four screens point to the top-left screen's ID)..ByScreen..._NewTables: Four tables (ByScreen1for right,2for left,3for down,4for up) that define the camera boundaries for screen transitions. These are crucial for supporting non-standard area sizes..Overworld_SpritePointers_state_..._NewTables: These tables define which sprite set to load for each overworld area based on the game state (state_0for the intro,state_1for post-Agahnim 1,state_2for post-Ganon). This allows for different enemy and NPC populations as the story progresses.
4. Key Hooks & Functions
ZSCustomOverworld replaces dozens of vanilla routines. Some of the most critical hooks are:
-
org $0283EE(PreOverworld_LoadProperties_Interupt):- Original:
Overworld_LoadProperties. This function loads music, palettes, and GFX when transitioning from a dungeon/house to the overworld. - New Logic: The ZS version is heavily modified to read from the custom data tables for palettes and GFX instead of using hardcoded logic. It also removes hardcoded music changes for certain exits.
- Original:
-
org $02C692(Overworld_LoadAreaPalettes):- Original: A routine to load overworld palettes.
- New Logic: Reads the main palette index from the
.MainPaletteTableinstead of using a hardcoded value.
-
org $02A9C4(OverworldHandleTransitions):- Original: The main logic for handling screen-to-screen transitions on the overworld.
- New Logic: This is one of the most heavily modified sections. The new logic uses the custom tables (
.ByScreen...,.Overworld_ActualScreenID_New, etc.) to handle transitions between areas of different sizes, fixing vanilla bugs and allowing for new layouts.
-
org $02AF58(Overworld_ReloadSubscreenOverlay_Interupt):- Original: Logic for loading subscreen overlays.
- New Logic: Reads the overlay ID from the
.OverlayTableinstead of using hardcoded checks for specific areas (like the Misery Mire rain).
-
org $09C4C7(LoadOverworldSprites_Interupt):- Original:
LoadOverworldSprites. This function determines which sprites to load for the current overworld screen. - New Logic: The ZS version reads from the
.Overworld_SpritePointers_state_..._Newtables based on the current game state ($7EF3C5) to get a pointer to the correct sprite set for the area. This allows for dynamic sprite populations.
- Original:
5. Configuration
!UseVanillaPool: A flag that, when set to 1, forces the system to use data tables that mimic the vanilla game's behavior. This is useful for debugging.!Func...Flags: A large set of individual flags that allow for enabling or disabling specific hooks. This provides granular control for debugging and compatibility testing.
6. Analysis & Future Work: Sprite Loading
The LoadOverworldSprites_Interupt hook at org $09C4C7 is a critical component that requires further investigation to support dynamic sprite sets, such as those needed for a day/night cycle.
-
Identified Conflict: The current ZScream implementation for sprite loading conflicts with external logic that attempts to swap sprite sets based on in-game conditions (e.g., time of day). The original hook's design, which calls
JSL.l Sprite_OverworldReloadAll, can lead to recursive loops and stack overflows if not handled carefully. -
Investigation Goal: The primary goal is to modify
LoadOverworldSprites_Interuptto accommodate multiple sprite sets for a single area. The system needs to be able to check a condition (like whether it is currently night) and then select the appropriate sprite pointer, rather than relying solely on the staticstate_..._Newtables. -
Technical Challenges: A previous attempt to integrate this functionality was reverted due to build system issues where labels from other modules (like
Oracle_CheckIfNight16Bit) were not visible toZSCustomOverworld.asm. A successful solution will require resolving these cross-module dependencies and carefully merging the day/night selection logic with ZScream's existing data-driven approach to sprite loading.
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).