- 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.
30 KiB
Oracle of Secrets - Troubleshooting Guide
Version: 1.0
Last Updated: October 3, 2025
Audience: ALTTP ROM hack developers, ZScream users, Oracle of Secrets contributors
Table of Contents
- Introduction
- BRK Crashes
- Stack Corruption
- Processor Status Register Issues
- Cross-Namespace Calling
- Memory Conflicts
- Graphics and DMA Issues
- ZScream-Specific Issues
- Build Errors
- Debugging Tools and Techniques
Introduction
This guide covers common issues encountered when developing ALTTP ROM hacks, particularly for the Oracle of Secrets project which uses:
- 65816 Assembly (SNES processor)
- Asar v1.9+ (assembler)
- ZScream Custom Overworld System
- Mixed namespace architecture (Oracle{} and ZScream)
Understanding the root causes of these issues will help you debug faster and write more stable code.
BRK Crashes
What is a BRK Crash?
A BRK crash occurs when the SNES executes a BRK instruction ($00 opcode), triggering a software interrupt. In ALTTP, this is used as an error handler that:
- Halts game execution
- Displays a debug screen (if enabled)
- Shows the crash location in ROM
Common Causes
1. Jumping to Invalid Memory
; BAD: $00 is typically uninitialized ROM space
JMP $0000
; GOOD: Jump to known valid code
JMP $008000
2. RTL/RTS Without Matching JSL/JSR
SomeFunction:
{
; ... code ...
RTL ; ← Pops garbage from stack, jumps to random location
}
; Later, execution hits $00 (BRK) in uninitialized space
Solution: Always ensure subroutines are called before returning:
MainCode:
JSL SomeFunction ; ← Must call it!
SomeFunction:
; ... code ...
RTL ; ← Now safe
3. Incorrect Bank Byte
; BAD: Bank $00 doesn't contain code at $C000
JML $00C000
; GOOD: Use correct bank
JML $02C000 ; Bank $02 contains overworld code
4. Data Corruption
If RAM locations $00-$01 (used for indirect addressing) get corrupted:
LDA.b ($00) ; If $00/$01 = $0000, reads from $00:0000 (likely BRK)
How to Find the Crash Location
In Mesen-S (Recommended):
- Enable "Break on BRK": Debugger → Settings → Break on BRK
- When crash occurs, debugger shows:
- PC (Program Counter): Address where BRK was executed
- Stack contents: Shows return addresses (where you came from)
- Read the stack to trace back:
Stack at $01F0: 82 9A 02 ← Came from $029A82 Stack at $01ED: 45 C4 09 ← Came from $09C445
In BSNES-Plus:
- Tools → Debugger
- Set breakpoint on
$000000(BRK opcode) - When triggered, examine:
- S register (Stack Pointer)
- Memory viewer at
$7E0100 + Sto see stack contents
Manual Method:
- Note the last known good action (e.g., "crashed when entering area $2B")
- Search codebase for hooks in that area:
grep -r "0283EE\|02A9C4" Overworld/ - Add temporary tracking code:
LDA.b #$42 : STA.l $7F5000 ; Breadcrumb marker
Prevention Strategies
-
Initialize jump tables properly:
JumpTable: dw Function1 ; ← Must be valid addresses dw Function2 dw Function3 -
Validate indices before using jump tables:
LDA.b $00 ; Index CMP.b #$06 ; Max entries BCC .valid LDA.b #$00 ; Default to 0 .valid ASL A ; * 2 for word table TAX JMP (JumpTable, X) -
Use assertions (Asar):
assert pc() <= $09C50D ; Ensure code doesn't overflow
Stack Corruption
Understanding the 65816 Stack
The stack on SNES:
- Lives at
$7E0100-$7E01FF(256 bytes) - Grows downward (S register decrements)
- Used for:
- JSR/JSL (pushes return address)
- PHA/PHX/PHY (pushes registers)
- Interrupts (NMI pushes PC, Status)
Common Issues
1. Unbalanced Push/Pop
; BAD: 3 pushes, 2 pops
PHX
PHY
PHA
PLA
PLY
; ← Missing PLX! Stack is now corrupted (+1 byte)
RTL ; Returns to wrong address
Solution: Always balance:
PHX
PHY
PHA
; ... code ...
PLA
PLY
PLX ; ← Now balanced
RTL
2. JSR/JSL vs RTS/RTL Mismatch
; BAD: JSL pushes 3 bytes, RTS pops 2 bytes
MainFunction:
JSL SubFunction ; Pushes $02 $C4 $09 (3 bytes)
SubFunction:
; ... code ...
RTS ; ← Pops only 2 bytes! Stack corrupted
Solution: Match call/return types:
MainFunction:
JSL SubFunction ; JSL (long call)
SubFunction:
; ... code ...
RTL ; ← RTL (long return) - matches!
Memory Map:
- JSR: Pushes 2 bytes (PC - 1), within same bank
- JSL: Pushes 3 bytes (PBR, PC - 1), cross-bank
- RTS: Pops 2 bytes, increments, continues
- RTL: Pops 3 bytes (bank + address), increments, continues
3. Stack Overflow
If S register goes below $0100, stack wraps to page $00 (Direct Page), corrupting variables:
; Example: Deep recursion
RecursiveFunction:
JSL RecursiveFunction ; ← Each call uses 3 bytes
RTL
; Eventually: S < $0100, corrupts $7E0000+ variables
Solution:
- Limit recursion depth
- Use iteration instead of recursion when possible
- Check S register if suspicious:
TSC ; Transfer Stack to C (16-bit accumulator) CMP.w #$01C0 ; Check if below safe threshold BCS .safe ; Handle stack overflow .safe
Debugging Stack Issues
-
Set watchpoint on stack pointer:
- Mesen-S: Debugger → Add Breakpoint → Type: Execute → Address:
S < $01C0
- Mesen-S: Debugger → Add Breakpoint → Type: Execute → Address:
-
Track stack manually:
; Add to suspicious functions: PHP : PLA : STA.l $7F5000 ; Save processor status TSC : STA.l $7F5002 ; Save stack pointer -
Check stack balance in emulator:
- Note S register before JSL:
S = $01F3 - After RTL, S should be:
S = $01F3(same value) - If different, you have unbalanced stack operations
- Note S register before JSL:
Processor Status Register Issues
Understanding the P Register
The Processor Status Register (P) controls 65816 behavior:
P = [N V M X D I Z C]
│ │ │ │ │ │ │ └─ Carry flag
│ │ │ │ │ │ └─── Zero flag
│ │ │ │ │ └───── IRQ disable
│ │ │ │ └─────── Decimal mode
│ │ │ └───────── Index register size (X/Y): 0=16-bit, 1=8-bit
│ │ └─────────── Memory/Accumulator size (A): 0=16-bit, 1=8-bit
│ └───────────── Overflow flag
└─────────────── Negative flag
Most critical: M flag (bit 5) and X flag (bit 4)
Common Issues
1. Wrong Accumulator Size
REP #$20 ; A = 16-bit (M flag = 0)
LDA.w #$1234 ; A = $1234
SEP #$20 ; A = 8-bit (M flag = 1)
LDA.w #$1234 ; ← ERROR: Assembler generates LDA #$34, $12 becomes opcode!
Correct:
SEP #$20 ; A = 8-bit
LDA.b #$12 ; Load 8-bit value only
2. Mismatched Index Sizes
REP #$30 ; A=16-bit, X/Y=16-bit
LDX.w #$0120 ; X = $0120
SEP #$10 ; X/Y now 8-bit!
LDX.b #$20 ; X = $0020 (high byte cleared!)
LDA $7E0000, X ; ← Reads from $7E0020, not $7E0120!
Solution: Be explicit about sizes:
REP #$30 ; Both A and X/Y are 16-bit
LDX.w #$0120
LDA.l $7E0000, X ; Explicit long addressing
SEP #$30 ; Back to 8-bit for safety
3. Missing SEP/REP After JSL
MainFunction:
REP #$30 ; Set 16-bit mode
JSL SubFunction ; Call other function
; What mode are we in now? We don't know!
LDA.w $1234 ; ← May fail if SubFunction left us in 8-bit mode
Best Practice:
MainFunction:
REP #$30 ; Set 16-bit mode
JSL SubFunction
SEP #$30 ; ← Always reset to known state!
; Now we know we're in 8-bit mode
Or use calling conventions:
SubFunction:
; Save processor status on entry
PHP
; ... do work ...
PLP ; Restore processor status on exit
RTL
4. Cross-Bank Processor State
ZScream operates in different contexts than Oracle code:
namespace Oracle
{
MainCode:
REP #$20 ; Oracle code uses 16-bit
JSL ZScreamFunction ; ← Crosses namespace!
; ZScream may leave us in 8-bit mode
LDA.w $1234 ; ← Potential error
}
; In ZScream (no namespace)
ZScreamFunction:
SEP #$20 ; ZScream uses 8-bit
; ... work ...
RTL ; ← Doesn't restore Oracle's 16-bit mode
Solution: Use PHP/PLP or establish conventions:
namespace Oracle
{
MainCode:
PHP ; Save current mode
JSL ZScreamFunction
PLP ; Restore Oracle's mode
}
Prevention
-
Always initialize at function start:
MyFunction: PHP ; Save caller's state SEP #$30 ; Set to known 8-bit state ; ... code ... PLP ; Restore caller's state RTL -
Use Asar's rep/sep warnings:
rep #$20 ; Lowercase = warning if you use 8-bit operations next lda.w #$1234 -
Document function register sizes in comments:
; Function: CalculateOffset ; Inputs: A=16-bit (offset), X=8-bit (index) ; Outputs: A=16-bit (result) ; Clobbers: None ; Status: Returns with P unchanged (uses PHP/PLP)
Cross-Namespace Calling
Oracle of Secrets Namespace Architecture
namespace Oracle ; Most custom code
{
; Oracle code here
}
; ZScream code is NOT in a namespace (vanilla bank space)
The Visibility Problem
From inside Oracle{} namespace:
- ✅ Can call Oracle labels directly:
JSL OracleFunction - ❌ Cannot call ZScream labels directly:
JSL ZScreamFunctionfails - ✅ Must use
Oracle_prefix:JSL Oracle_ZScreamFunction
Why? Asar namespaces create separate symbol tables. ZScream labels must be explicitly exported to Oracle namespace.
Common Errors
1. Calling ZScream from Oracle Without Prefix
namespace Oracle
{
MainCode:
JSL LoadOverworldSprites_Interupt ; ← ERROR: Label not found
}
Error Message:
error: (MainCode.asm:45): label "LoadOverworldSprites_Interupt" not found
Solution:
namespace Oracle
{
MainCode:
JSL Oracle_LoadOverworldSprites_Interupt ; ← Use Oracle_ prefix
}
2. Forgetting to Export ZScream Label
In ZSCustomOverworld.asm, labels must be exported:
; Define ZScream function
LoadOverworldSprites_Interupt:
{
; ... code ...
RTL
}
; Export to Oracle namespace
namespace Oracle
{
Oracle_LoadOverworldSprites_Interupt = LoadOverworldSprites_Interupt
}
If missing the export, Oracle code can't call it!
3. Wrong Call Direction
; ZScream code (no namespace)
ZScreamFunction:
JSL Oracle_SomeFunction ; ← ERROR: ZScream isn't in Oracle namespace!
Solution: Exit Oracle namespace first:
namespace Oracle
{
SomeFunction:
; ... code ...
RTL
}
; Export Oracle function for ZScream
Oracle_SomeFunction = Oracle_SomeFunction ; Make visible outside namespace
; Now ZScream can call it
ZScreamFunction:
JSL Oracle_SomeFunction ; ← Works!
The Bridge Function Pattern
When you need ZScream to call Oracle code (e.g., day/night check for sprite loading):
namespace Oracle
{
CheckIfNight:
; Main implementation (uses Oracle WRAM variables)
LDA.l $7EE000 ; Custom time system
CMP.w #$0012
BCS .night
; ... logic ...
RTL
}
; Bridge function (no namespace = accessible to ZScream)
ZSO_CheckIfNight:
{
PHB
PHK
PLB
; Call Oracle function
JSL Oracle_CheckIfNight ; Can call INTO Oracle namespace
PLB
RTL
}
; Now ZScream hooks can use it
org $09C4C7
LoadOverworldSprites_Interupt:
{
; ... code ...
JSL Oracle_ZSO_CheckIfNight ; ← Bridge function name uses Oracle_ for exports
; ... code ...
}
Best Practices
-
Naming Convention:
Oracle_prefix = Exported from any namespace for Oracle to useZSO_prefix = Bridge function for ZScream-to-Oracle calls- Combine:
Oracle_ZSO_CheckIfNight= Bridge exported to Oracle
-
Export Block Pattern:
namespace Oracle { ; All Oracle code here } ; Export block at end of file namespace Oracle { Oracle_ExportedFunction1 = ExportedFunction1 Oracle_ExportedFunction2 = ExportedFunction2 Oracle_ZSO_BridgeFunction = ZSO_BridgeFunction } -
Check Build Order: In
Oracle_main.asmorMeadow_main.asm:incsrc "Core/symbols.asm" ; Define symbols first incsrc "Overworld/ZSCustomOverworld.asm" ; ZScream next incsrc "Items/all_items.asm" ; Oracle code afterIf Oracle code is included before ZScream defines exports, you'll get "label not found" errors!
Memory Conflicts
Bank Collisions
Understanding LoROM Mapping
ALTTP uses LoROM memory mapping:
$008000-$00FFFF→ ROM $000000-$007FFF (Bank $00)$018000-$01FFFF→ ROM $008000-$00FFFF (Bank $01)$028000-$02FFFF→ ROM $010000-$017FFF (Bank $02)- ...
$208000-$20FFFF→ ROM $100000-$107FFF (Bank $20 / Custom)
Oracle of Secrets uses banks $20-$41 for custom code (33 banks).
Common Collision Errors
1. Overlapping ORG Statements
; File1.asm
org $288000
CustomFunction1:
; 200 bytes of code
; File2.asm
org $288000 ; ← ERROR: Same address!
CustomFunction2:
; Overwrites CustomFunction1!
Asar Error:
warning: (File2.asm:10): overwrote some code here with org/pushpc command
Solution: Use assert to protect boundaries:
org $288000
CustomFunction1:
; ... code ...
assert pc() <= $288100 ; Reserve space
org $288100 ; Start next function safely
CustomFunction2:
; ... code ...
2. Freespace Conflicts
; File1.asm
freecode ; Asar auto-allocates at $208000
; File2.asm
freecode ; ← May allocate at same location if not careful!
Solution: Use explicit banks:
; File1.asm
org $208000 ; Explicit bank $20
; File2.asm
org $218000 ; Explicit bank $21
3. Data Pool Overflow
ZScream uses $288000-$289938 for data pool (6456 bytes). If you add too much data:
org $288000
Pool_SpritePointers:
dw SpritesArea00, SpritesArea01, ... ; 160 areas * 6 states * 2 bytes = 1920 bytes
Pool_MapData:
; 5000 bytes
; Total: 6920 bytes - OVERFLOW! Crosses into $289938
Check with:
org $288000
; ... all pool data ...
assert pc() <= $289938 ; Verify within bounds
WRAM Conflicts
Custom WRAM Region ($7E0730+)
Oracle of Secrets uses $7E0730-$7E078F (96 bytes in MAP16OVERFLOW region).
Common Issue: Variable Overlap
; Core/ram.asm
Oracle_FairyCounter = $7E0730 ; 1 byte
; Items/all_items.asm
Oracle_ItemEffect = $7E0730 ; ← ERROR: Same address!
Solution: Maintain central registry in Core/ram.asm:
; Custom WRAM Block
Oracle_FairyCounter = $7E0730 ; 1 byte - Fairy spawn counter
Oracle_ItemEffect = $7E0731 ; 1 byte - Current item effect
Oracle_BossHealth = $7E0732 ; 2 bytes - Boss HP (16-bit)
; ... continue registry ...
; Always check before adding new:
; Last used: $7E0733
; Available: $7E0734-$7E078F (91 bytes free)
SRAM Conflicts
Repurposed Blocks
Oracle of Secrets repurposes vanilla SRAM:
$7EF38A-$7EF3C4: Collectibles (59 bytes)$7EF410-$7EF41F: Dreams system (16 bytes)
Issue: Vanilla Code Still Writes
Some vanilla functions may write to repurposed SRAM. Example:
; Vanilla function writes to $7EF38A (old value)
; Now you use $7EF38A for masks collected
; Vanilla write corrupts your data!
Solution: Hook and redirect vanilla writes:
; Find vanilla write
org $01D234 ; Example address
JSL Oracle_RedirectedSave
NOP
; Your redirect
namespace Oracle
{
RedirectedSave:
; Don't write to $7EF38A anymore
; Or write to new location $7EF500
STA.l $7EF500
RTL
}
Build Errors from Memory Issues
"No freespace large enough"
error: no freespace large enough found
Cause: Tried to use freecode but all banks are full.
Solution:
- Check bank usage:
grep -r "org \$2[0-9]8000" . - Add new bank range in Asar (if using freespace manager)
- Use explicit
orgin unused bank
"PC out of bounds"
error: (file.asm:45): PC $29A000 is out of bounds
Cause: Code exceeded bank boundary ($xx8000-$xxFFFF in LoROM).
Solution:
; Check PC before running out of space
org $298000
LargeFunction:
; ... lots of code ...
assert pc() < $2A0000 ; Would fail and show where overflow happens
; Split across banks instead:
org $298000
LargeFunction_Part1:
; ... code ...
JML LargeFunction_Part2
org $2A8000
LargeFunction_Part2:
; ... more code ...
Graphics and DMA Issues
Blank Screen
Symptoms
- Game boots, music plays, but screen is black
- Or screen is garbled/corrupted
Common Causes
1. Force Blank Not Released
LDA.b #$80
STA $2100 ; Force blank (screen off)
; ... forgot to turn it back on ...
Solution: Always release force blank:
LDA.b #$80
STA $2100 ; Force blank ON
; ... do VRAM updates ...
LDA.b #$0F
STA $2100 ; Force blank OFF, brightness 15
2. VRAM Upload During Active Display
; BAD: Writing to VRAM while screen is on
LDA.b #$80
STA $2115 ; VRAM port control
LDA.b #$00
STA $2116 ; VRAM address low
LDA.b #$20
STA $2117 ; VRAM address high
LDA.b #$FF
STA $2118 ; ← CORRUPTS VRAM if screen is on!
Solution: Use NMI or force blank:
; Method 1: During NMI
NMI_Handler:
; Screen is in VBlank, safe to write
LDA.b #$FF
STA $2118
; Method 2: Force blank
LDA.b #$80 : STA $2100 ; Screen off
LDA.b #$FF : STA $2118 ; Write to VRAM
LDA.b #$0F : STA $2100 ; Screen on
3. Corrupted Stripe Data
ZScream uses stripe system for VRAM uploads. If stripe data is malformed:
; Stripe format: [Size] [Address Low] [Address High] [Data...] [00=End]
; BAD: Missing terminator
.stripe_data
db $04, $00, $20 ; Upload 4 bytes to $2000
db $FF, $FF, $FF, $FF
; ← Missing $00 terminator! Continues reading garbage
Solution:
.stripe_data
db $04, $00, $20 ; Upload 4 bytes to $2000
db $FF, $FF, $FF, $FF
db $00 ; ← Terminator
Flickering Graphics
Cause: DMA During Active Display
; BAD: DMA while screen is active causes flickering
LDA.b #$01
STA $4300 ; DMA mode
; ... setup DMA ...
LDA.b #$01
STA $420B ; ← Trigger DMA mid-frame = flicker
Solution: Only DMA during VBlank (NMI):
NMI_Handler:
; Safe: We're in VBlank period
LDA.b #$01
STA $420B ; Trigger DMA
RTI
Missing Tiles / Wrong Graphics
Symptoms
- Link appears as wrong sprite
- Enemies are garbled
- Tileset is corrupted
Common Causes
1. Wrong Graphics Slot
ZScream uses 7 graphics slots:
- Slot 0-2: Static (BG graphics)
- Slot 3-6: Variable (Sprite graphics)
; BAD: Loading Link graphics to wrong slot
LDA.b #$05 ; Slot 5
LDX.w #LinkGFX
JSL LoadGraphicsSlot ; ← Overwrites enemy sprites!
Solution: Follow slot conventions:
; Slot 3: Usually Link graphics
; Slot 4: Enemy set 1
; Slot 5: Enemy set 2
; Slot 6: Special sprites
LDA.b #$03 ; Slot 3 = Link
LDX.w #LinkGFX
JSL LoadGraphicsSlot
Refer to Docs/World/Overworld/ZSCustomOverworldAdvanced.md Section 3 for detailed slot assignments.
2. Sprite Pointer Table Corruption
; Sprite pointer table must be valid addresses
Pool_Overworld_SpritePointers_state_0_New:
dw Area00_Sprites, Area01_Sprites, ...
dw $0000 ; ← BAD: Null pointer causes crash
Solution:
Pool_Overworld_SpritePointers_state_0_New:
dw Area00_Sprites, Area01_Sprites, ...
dw EmptySprites ; ← Valid empty list
EmptySprites:
db $FF ; Terminator (no sprites)
Palette Issues
Wrong Colors
Cause: Palette Not Updated
; Changed area but didn't reload palettes
LDA.b #$2B ; New area
STA.w $040A
; ← Missing: JSL OverworldPalettesLoader
Solution:
LDA.b #$2B
STA.w $040A
JSL OverworldPalettesLoader ; Load palettes for area $2B
Day/Night Palette Mismatch
If day/night transition doesn't update palettes:
; In time system transition
.switch_to_night
LDA.b #$01
STA.l $7EE001 ; Set night flag
; ← Missing: Reload palettes
JSL Oracle_LoadTimeBasedPalettes
ZScream-Specific Issues
Sprite Loading Fails
Symptom
- Sprites don't appear in custom overworld area
- Error: "No sprites loaded"
Cause 1: Missing Sprite Pointer Entry
Pool_Overworld_SpritePointers_state_0_New:
dw Area00_Sprites, Area01_Sprites, ...
; Only 80 entries, but you added area $81
Solution: Extend table:
Pool_Overworld_SpritePointers_state_0_New:
dw Area00_Sprites, Area01_Sprites, ...
; ... (0x00-0x7F) ...
dw Area80_Sprites, Area81_Sprites ; Add new entries
Cause 2: Day/Night State Not Handled
; Sprite loading checks day/night with Oracle_ZSO_CheckIfNight
; If that function doesn't exist or returns wrong value:
ZSO_CheckIfNight:
LDA.l $7EF3C5 ; Always returns day state
RTL ; ← Forgets to check $7EE000 (time)
Solution: Verify ZSO_CheckIfNight implementation (see Troubleshooting Section 5).
Map16 Stripes Fail
Symptom
- Custom tiles don't appear
- Map16 changes aren't visible
Cause: Stripe Buffer Overflow
; ZScream uses 3 stripe buffers: $7EC800, $7ED800, $7EE800
; Each 2048 bytes
.generate_stripes
LDX.w #$0000
.loop
; Generate 1 tile worth of stripes (9 bytes)
; Loop 300 times = 2700 bytes
; ← OVERFLOWS! Corrupts other memory
Solution: Check buffer size:
.generate_stripes
LDX.w #$0000
.loop
; Generate stripe
INX #9
CPX.w #$0800 ; Check if buffer full (2048 bytes)
BCS .buffer_full
; Continue...
BRA .loop
.buffer_full
; Stop generating
Transition Hangs
Symptom
- Game freezes when transitioning between areas
- Music continues but screen is stuck
Cause 1: Infinite Loop in Hook
org $0283EE ; Overworld transition hook
Oracle_CustomTransition:
JSL SomeFunction
JMP Oracle_CustomTransition ; ← Infinite loop!
Solution:
org $0283EE
Oracle_CustomTransition:
JSL SomeFunction
JMP $0283F3 ; Jump to vanilla code after hook
Cause 2: Module Not Advanced
Oracle_TransitionHook:
; ... custom transition logic ...
; ← Forgot to increment $10 (module)
RTL
Solution:
Oracle_TransitionHook:
; ... custom transition logic ...
INC.b $10 ; Advance module
RTL
Missing Day/Night Graphics
Symptom
- Day sprites show at night
- Night enemies appear during day
Cause: Sprite Pointer Table Only Has 3 States
; You only defined 3 game states:
Pool_Overworld_SpritePointers_state_0_New:
dw Area00_Day, Area01_Day, ...
; But Oracle_ZSO_CheckIfNight returns state 4-5 for night!
Solution: Define all 6 states (day + night):
Pool_Overworld_SpritePointers_state_0_New:
; State 0 day (entries $00-$9F)
dw Area00_Day, Area01_Day, ...
; State 0 night (entries $A0-$13F)
dw Area00_Night, Area01_Night, ...
; State 1 day (entries $140-$1DF)
dw Area00_Day, Area01_Day, ...
; State 1 night (entries $1E0-$27F)
dw Area00_Night, Area01_Night, ...
; State 2 day/night (similar structure)
See Docs/World/Overworld/ZSCustomOverworldAdvanced.md Section 4.3 for phase offset calculation.
Build Errors
Asar Assembler Errors
"label not found"
error: (file.asm:42): label "CustomFunction" not found
Causes:
- Typo in label name
- Label defined after use (in some cases)
- Namespace issue (see Section 5)
- File not included in main assembly file
Solutions:
; 1. Check spelling
JMP CustomFunction ; ← Check this matches definition
CustomFunction: ; ← Must be exact match (case-sensitive)
; 2. Move definition before use (if forward reference fails)
; 3. Check namespace
namespace Oracle
{
JSL Oracle_CustomFunction ; ← Need prefix
}
; 4. Include file
; In Oracle_main.asm:
incsrc "Custom/file.asm" ; ← Make sure file is included
"redefined label"
error: (file.asm:100): label "CustomFunction" redefined
Cause: Same label defined twice.
Solution:
; BAD: Two definitions
CustomFunction:
RTL
CustomFunction: ; ← ERROR
RTL
; GOOD: Use sublabels
CustomFunction:
.entry_point1
RTL
.entry_point2
RTL
"org or pushpc/pullpc not at start of line"
error: (file.asm:50): org or pushpc/pullpc not at start of line
Cause: Asar requires org, pushpc, pullpc as first statement on line.
Solution:
; BAD
org $288000 ; ← Indented
; GOOD
org $288000 ; ← Column 1
"assertion failed"
error: (file.asm:200): assertion failed: pc() <= $09C50D
note: (file.asm:200): expanded from: 04C510 <= 04C50D
Meaning: Your code exceeded allowed space.
Solution:
org $09C4C7
CustomHook:
; ... 300 bytes of code ...
; PC is now at $09C55F
assert pc() <= $09C50D ; FAILS: $09C55F > $09C50D
; Fix: Move some code elsewhere
org $09C4C7
CustomHook:
; Minimal hook
JSL CustomHook_Main ; Jump to freespace
NOP : NOP
assert pc() <= $09C50D ; ← Now passes
org $288000
CustomHook_Main:
; Main implementation in freespace
; ... 300 bytes ...
RTL
Debugging Tools and Techniques
Emulator Debuggers
Mesen-S (Recommended)
Features:
- Powerful CPU debugger with execution breakpoints
- Memory viewer with live updates
- Conditional breakpoints:
A == #$42 - Stack viewer
- Event viewer (shows NMI, IRQ timing)
Usage:
- Tools → Debugger (F7)
- Set breakpoint: Click line number in disassembly
- Run until breakpoint hit
- Examine registers (A, X, Y, P, S, PC, DB, PB)
Advanced:
Conditional breakpoint examples:
- Break when A equals value: A == #$0F
- Break when memory changes: [W]$7E0730
- Break when PC in range: PC >= $288000 && PC < $289000
- Break on BRK: PC == $000000
BSNES-Plus
Features:
- Cycle-accurate emulation (best for timing-sensitive debugging)
- Memory editor with search
- Tilemap viewer
- VRAM viewer
Usage:
- Tools → Debugger
- Set breakpoint: Right-click address
- Memory search: Tools → Memory Editor → Search
Manual Debugging Techniques
1. Breadcrumb Tracking
; Add at suspected crash location
LDA.b #$01 : STA.l $7F5000 ; Breadcrumb 1
JSL SuspiciousFunction
LDA.b #$02 : STA.l $7F5000 ; Breadcrumb 2
; If crash occurs between breadcrumbs, $7F5000 = $01
; After crash, check $7F5000 in memory viewer
2. Register Logging
; Log registers to RAM
Oracle_DebugLog:
STA.l $7F5000 ; Save A
PHX
TXA
STA.l $7F5001 ; Save X
PLX
PHY
TYA
STA.l $7F5002 ; Save Y
PLY
RTL
; Use it:
JSL Oracle_DebugLog ; Snapshot registers
JSL ProblematicFunction
JSL Oracle_DebugLog ; Snapshot again - compare
3. Assertion Macros
; Define assertion macro
macro assert_equals(address, expected)
PHA
LDA.l <address>
CMP.b #<expected>
BEQ .ok
BRK ; Trigger crash if mismatch
.ok
PLA
endmacro
; Use it:
%assert_equals($7E0730, $05) ; Check variable is expected value
4. PC Tracking
; At critical points, save PC to RAM
LDA.b #$01 : STA.l $7F5010 ; Mark "entered function 1"
JSL Function1
LDA.b #$02 : STA.l $7F5010 ; Mark "entered function 2"
JSL Function2
; After crash, $7F5010 shows last function entered
Common Debug Checklist
When encountering an issue:
- ✅ Check error message carefully - Asar errors are usually precise
- ✅ Verify namespace - Is label prefixed correctly?
- ✅ Check stack balance - Equal push/pop counts?
- ✅ Verify processor state - REP/SEP correct for operation?
- ✅ Check memory bounds - Assertions in place?
- ✅ Test in Mesen-S first - Best debugger for SNES
- ✅ Use breadcrumbs - Narrow down crash location
- ✅ Check build order - Files included in correct order?
- ✅ Review recent changes - Compare with known working version
- ✅ Read vanilla code - Understand what you're hooking
Conclusion
This guide covers the most common issues encountered in ALTTP ROM hacking, particularly for Oracle of Secrets. Key takeaways:
- BRK crashes are usually caused by invalid jumps or corrupted return addresses
- Stack corruption comes from unbalanced push/pop or JSR/JSL vs RTS/RTL mismatches
- Processor status (M/X flags) must be carefully managed across function boundaries
- Namespace issues require proper exports and
Oracle_prefixes - Memory conflicts need assertions and careful space management
- Graphics issues stem from VRAM timing or corrupted data
- ZScream issues often relate to sprite loading tables and day/night state
When stuck:
- Use Mesen-S debugger
- Add breadcrumb tracking
- Verify processor state
- Check assertions
- Review vanilla code
For further help:
- Read
Docs/World/Overworld/ZSCustomOverworldAdvanced.mdfor ZScream details - Check
Docs/Core/Ram.mdfor memory map - Review
Docs/General/DevelopmentGuidelines.mdfor best practices
Happy debugging! 🎮