The current URL is datacrystal.tcrf.net.
Illusion of Gaia/Notes
The following article is a Notes Page for Illusion of Gaia.
Map assets
Assets for each map are loaded based on a table at $0d8000. The engine seeks through this table when a map is loaded, looking for the map's ID header.
The format of a map's entry in the table is
db MapNum : db $00
followed by one or more of the following header cards:
Card | Arguments | Notes |
---|---|---|
db $02
|
db PpuSettingsIndex
|
Indexes pointers to PPU register settings at $818000. |
db $03
|
db SrcOffset,SizeW
|
Load VRAM character data. Writes to VRAM after decompressing (if needed) to $7e7000. The base VRAM word-address is $2000 (byte-address $4000) if IsSprites=0, or $4000 (byte-address $8000) if IsSprites=1. |
db $04
|
db SrcOffset,SizeW
|
Load CGRAM data. Writes to the CGRAM buffer at $7f0a00. |
db $05
|
db SrcOffset,SizeW
|
Load BG metatile mappings, which join four 8x8 px VRAM characters as a 16x16 px metatile. Only 8 bits of the VRAM addresses are used; the four high bits in each tile set its collision type. Mappings are written to $7e2000 for MapLayer, $7e2800 for EffectLayer. Layers=1 for Map, 2 for Effect, 3 for both. |
db $06
|
db Layer
|
Load metatile tilemap for Layer=1 (MapLayer, conventionally BG1) or Layer=2 (EffectLayer, conventionally BG2). Tilemap is written to $7ea000 for MapLayer, $7ec000 for EffectLayer. |
db $10
|
dw SizeB : db Dummy
|
Load metasprite / spriteset / sprite tile connectivity data. Data is written to $7e4000. |
db $11
|
db MusicId,RoomGroup
|
Load music. MusicId sets which music should be resumed by actors that interrupt the normal music. If $7e06f6 is nonzero and not equal to RoomGroup, this card does nothing, and any pre-existing music continues without interruption; the Gold Ship uses this to preserve music between outside and inside. $7e06f6 is zeroed after this card is read. |
db $13
|
db FlagIndex,DestLabel
|
If game flag FlagIndex is set, jump to DestLabel. |
db $14
|
db Label
|
Set jump label here; jump here from another entry using $13 or $15. |
db $15
|
db DestLabel
|
Jump to DestLabel, unconditionally. |
Always uncompressed: palette data (card $04), music data (card $11). Other data can be compressed in Quintet's LZ format, in which case the Size bytes here are ignored. If the data has #$0000 for its "compressed size", it is taken as uncompressed and the Size bytes here are used.
Most of these can occur in any order and any number, and will override previous data in a logical way. However, $03 with IsSprites=0 must precede $05/$06 in order to set up VRAM and the decompression buffer, and $05 for each layer must precede $06 for that layer.
Map design tables
Actor lists
Actor lists per map start at $0c8200. Each valid map has an actor list, addressed by indexed pointers at $0c8000.
Entries in actor lists are of the form:
db PosX,PosY,Param : dl EntryPtr-3 : db StatsIndex[,MonsterId,DeathActionIndex]
PosX,PosY
- usually the spawn point, in tiles from the map's northwest corner. Some actors override and use for different purposes.
Param
- if odd, usually represents an actor-specific parameter, e.g. "contents of this Dark Space"; right-shifted 1 bit and stored in actor-page $0E. If even, bit-3 is cleared and Param becomes the high byte of the actor's sprite OAM override.
EntryPtr
- address where actor code execution will start. The list points 3 bytes earlier, to a byte+word header: starting sprite ID, then starting actor-page $10/$11 (physics and rendering flags).
StatsIndex
- monster's stat block, indexing a table of HP/STR/DEF/DP at $81abf0 + 4*StatsIndex. Actors not involved in combat should have StatsIndex=0, in which case MonsterId and DeathActionIndex are omitted and the next entry in the list begins.
MonsterId
- labels the monster to track its death through map transitions. If MonsterId is 0, it always respawns. Otherwise, it stays dead until the death tracking flag is cleared: usually by going to the world map, by player death, or manually.
DeathActionIndex
- indexes a map rearrangement (BG layer tile copy). When the monster dies, the map rearrangement is triggered automatically, as if by calling cop #$32 : db DeathActionIndex : cop #$33.
- The actor list ends with $FF, followed by the name of the map (if any), followed by $CA.
Thinker lists
Thinker lists per map start at $0ce7e5. Each valid map has a thinker list, addressed by indexed pointers at $0ce5e5.
Entries in thinker lists are of the form:
db Param : dl EntryPtr-2
Param
is a thinker-specific parameter, e.g. "palette bundle index". EntryPtr
is the code execution start point. The list points two bytes earlier, to a word header copied into $7f000e,x. Flag #$0004 in the header means execute-after-actors if set, before-actors if clear.
Actor code
Each actor has a base address / ID, which indexes $30 blocks of RAM at each of $7e0000,x, $7f0000,x, and $7f1000,x. The actor list is a doubly linked list. Each frame, the list is processed in forward order running actor code, then post-processed (e.g. collision detection) in reverse.
Each actor has a dynamic code entry point. The handler invokes code by the equivalent of jsl EntryPtr.
At entrancy, actor code should assume that the processor state is:
m=0, x=0, d=0, i=1 X = ActorID D = ActorID DBR = $81
Actor code must restore this state before it exits, except that X need not be restored. Actor code must exit via the equivalent of rtl at the stack level of entrancy.
At $7e0000,x (which is also the direct page) most memory is used by the engine:
$7e:00 long EntryPtr Pointer to code that will be run on this actor's turn. $7e:03 byte dummy03 Dummy byte, makes it easier to handle EntryPtr. $7e:04 word PrevId Pointer to actor (= actor ID) before this one in the list. $7e:06 word NextId Pointer to actor (= actor ID) after this one in the list. $7e:08 word WaitTime Frames to wait before running code again, e.g. animation timer. $7e:0a long ArgPtr While in a COP, pointer to COP argument or return address. $7e:0d byte dummy0d Dummy byte, makes it easier to handle ArgPtr. $7e:0e word OamXor XOR'd with OAM for palette changes etc. if actor has a sprite. $7e:10 word Flags10 Actor state flags. $7e:12 word Flags12 Actor state flags. $7e:14 word PosX Actor x position, pixels, from northwest corner of map. $7e:16 word PosY Actor y position, pixels, from northwest corner of map. $7e:18 word OffsX Offset of sprite from PosX, if actor has a sprite. $7e:1a word OffsY Offset of sprite from PosY, if actor has a sprite. $7e:1c word OffsXMir As OffsX, if the h-mirror flag in $0e were flipped. $7e:1e word OffsYMir As OffsY, if the v-mirror flag in $0e were flipped. $7e:20 byte HitboxW Hitbox size, West (-x) direction. If no hitbox, free. $7e:21 byte HitboxE Hitbox size, East (+x) direction. If no hitbox, free. $7e:22 byte HitboxN Hitbox size, North (-y) direction. If no hitbox, free. $7e:23 byte HitboxS Hitbox size, South (+y) direction. If no hitbox, free. $7e:24 word Free24 Free memory. $7e:26 word Free26 Free memory. $7e:28 word SprIdx Sprite (graphics) index, if actor has a sprite. $7e:2a word SprFrame Animation frame of sprite, if actor has a sprite. $7e:2c word MoveX Movement data or address of current move pattern, x direction. $7e:2e word MoveY Movement data or address of current move pattern, y direction.
At $7f0000,x is a mix of engine, subroutine, and free memory:
$7f:00 dwrd AnimScr1 Scratch bytes for sprite animation COPs. $7f:04 word RetPtr1 Return pointer set by some COPs. $7f:06 long SprSetPtr Pointer to spriteset (e.g. "Diamond Mine"), which $7e:28 indexes. $7f:09 byte dummy09 Dummy byte for SprSetPtr. $7f:0a word ChatPtr How actor interacts with player. Specifics depend on flags. $7f:0c word SprMetPtr Pointer to metasprite (i.e. sprite tile assembly data). $7f:0e word AnimScr2 Scratch bytes for sprite animation COPs. $7f:10 word OrbitAng Not reserved. Orbit routines use this as orbiting angle. $7f:12 word OrbitDia Not reserved. Orbit routines use this as orbiting diameter. $7f:14 word LoopCntr Not reserved. Some COPs use this as a loop counter. $7f:16 word SprTimer Timer for sprite animation COPs. $7f:18 word MoveXAlt Alternate MoveX, meaning depends on context. $7f:1a word MoveYAlt Alternate MoveY, meaning depends on context. $7f:1c word ParentId If set, and conditions are met, actor dies with its parent. $7f:1e word RetPtr2 Return pointer set by some COPs. $7f:20 word StatsPtr Pointer to monster's HP/STR/DEF/DP stats. $7f:22 word EnemyNum Dungeon-level enemy number for flagging killed enemies. $7f:24 word DeathIdx Index of map rearrangement script to perform on death. $7f:26 word CurrHp Current HP. $7f:28 word IframeCtr Abs. value is iframes; typically positive means stunned. $7f:2a word Flags7F2A Actor state flags. $7f:2c word MoveScr1 Scratch bytes for some movement COPs. $7f:2e word MoveScr2 Scratch bytes for some movement COPs.
Memory at $7f1000,x is mostly unreserved:
$7f1000 word HitPtr When hit by player, EntryPtr is set to HitPtr. $7f1002 word DodgePtr When player attacks, EntryPtr is set to DodgePtr. $7f1004 long DiePtr When actor dies, EntryPtr is set to DiePtr. $7f1007 byte dummy4 Dummy byte for DiePtr. $7f1008 word CollPtr When actor collides with player, EntryPtr is set to CollPtr if Flags7F2A is so set. $7f100a .... Free100A Through $7f100f, no use known; probably free. $7f1010 .... Scr1010 Through $7f1017, scratch bytes for some COPs, otherwise free. $7f1018 long SnapPtr COP #$43 stores caller here; no other use known. $7f101b byte dummy5 Dummy byte for SnapPtr. $7f101c word Free101C No use known; probably free. $7f101e word ChainDmg When colliding with an actor, damage to deal to it. $7f1020 .... Free1020 Through $7f102f, no use known; probably free.
To save ROM space, actor code uses the COP opcode ($02) and its label as a subroutine caller. Arguments follow the COP label. Example:
02 d0 cop #$D0 ; If game flag a9 00 db $a9, $00 ; $A9 is set to 0, ab cd dw JmpTarget ; go to JmpTarget = $cdab. Else, 02 e0 cop #$E0 ; Die.
Different COPs may return control to the actor after the arguments or at some other address; or may exit (return control to the engine) via pla : pla : rtl, which is equivalent to rtl at the stack level of actor entrancy.
It is always safe to call COP with the processor state as:
m=0, x=0, d=0, i=1 X = ActorID D = ActorID DBR = $81
Some COPs do not require this state.
Most COPs restore non-scratch state upon returning. A few COPs are explicitly state-altering. No COP has a state-altering side effect.
Registers A and Y as well as n,v,z,c are clobbered by the COP handler. Some COPs return useful values in them.
The valid COPs are:
COP | Arguments | Notes |
---|---|---|
cop #$00
|
Generate scaled 16-bit sine @ $7E8900-CFF for HDMA | |
cop #$01
|
dl SrcAddr : db Reg
|
Queue H/DMA to register Reg on available channel |
cop #$02
|
dl SrcAddr : db Reg
|
Queue DMA to register Reg on available channel |
cop #$03
|
db Channel : dl SrcAddr : db Reg
|
Queue H/DMA to Reg using Channel (1-7) |
cop #$04
|
db MusicId
|
Start music |
cop #$05
|
db MusicId
|
Fade out, then start music |
cop #$06
|
db SoundId
|
Play sound, second channel |
cop #$07
|
db SoundId
|
Play sound, first channel |
cop #$08
|
db SoundId1,SoundId2
|
Sound on both channels |
cop #$09
|
db Shift
|
Tempo modifier |
cop #$0A
|
db RawVal
|
Explicit write to $2140 for APU programming |
cop #$0B
|
Set BG tile solidity mask to $Fx (wall) here | |
cop #$0C
|
Set BG tile solidity mask to $0x (clear) here | |
cop #$0D
|
db OffsX,OffsY
|
Set BG tile solidity mask to $Fx, relative |
cop #$0E
|
db OffsX,OffsY
|
Set BG tile solidity mask to $0x, relative |
cop #$0F
|
db AbsX,AbsY
|
Set BG tile solidity mask to $Fx, absolute |
cop #$10
|
db AbsX,AbsY
|
Set BG tile solidity mask to $0x, absolute |
cop #$11
|
Set BG tile solidity mask to $00 on all tiles touched by actor | |
cop #$12
|
db AbsX,AbsY
|
Set BG tile solidity mask to $x0, absolute |
cop #$13
|
dw JmpAddr
|
Branch if this solid mask is not $00 |
cop #$14
|
db OffsX,OffsY : dw JmpAddr
|
Branch if relative solid mask is not $00 |
cop #$15
|
dw JmpAddr
|
Branch if north solid mask is not $00 |
cop #$16
|
dw JmpAddr
|
Branch if south solid mask is not $00 |
cop #$17
|
dw JmpAddr
|
Branch if west solid mask is not $00 |
cop #$18
|
dw JmpAddr
|
Branch if east solid mask is not $00 |
cop #$19
|
db MusicId : dl TextAddr
|
Music and text, similar to cop #$04 + cop #$BF |
cop #$1A
|
db Type : dw JmpAddr
|
Branch if this solid mask is Type |
cop #$1B
|
db Type : dw JmpAddr
|
Branch if north solid mask is Type |
cop #$1C
|
db Type : dw JmpAddr
|
Branch if south solid mask is Type |
cop #$1D
|
db Type : dw JmpAddr
|
Branch if west solid mask is Type |
cop #$1E
|
db Type : dw JmpAddr
|
Branch if east solid mask is Type |
cop #$1F
|
dw JmpAddr
|
Branch if not on gridline |
cop #$20
|
db AcNum,Dist : dw JmpAddr
|
Branch if actor number AcNum from the map's actor list is within Dist |
cop #$21
|
db Dist : dw JmpAddr
|
Branch if player is within Dist |
cop #$22
|
db SpriteId,Speed
|
Basic movement up to $FE pixels; write destination to $7F:18,$7F:1A before calling |
cop #$23
|
RNG, range 0..$FF, returns 8-bit result in A (warning: very expensive call) | |
cop #$24
|
db Max
|
RNG, range 0..Max, returns 8-bit result in $0420 (warning: very expensive call) |
cop #$25
|
db AbsX,AbsY
|
Set new position |
cop #$26
|
db MapNum : dw PosX,PosY
|
Queue map change at end of frame |
cop #$27
|
db Delay
|
If off-screen, wait for Delay frames, then check again |
cop #$28
|
dw PosX,PosY,JmpAddr
|
Branch if player is at Pos |
cop #$29
|
db AcNum : dw PosX,PosY,JmpAddr
|
Branch if actor number AcNum from the map's actor list is at Pos |
cop #$2A
|
dw Dist,WestAddr,HereAddr,EastAddr
|
Branch on whether PlayerX is within Dist, too far west, or too far east |
cop #$2B
|
dw Dist,NorthAddr,HereAddr,SouthAddr
|
Branch on whether PlayerY is within Dist, too far north, or too far south |
cop #$2C
|
dw NearYAddr,NearXAddr
|
Branch on whether Player is nearer in y or x dimension |
cop #$2D
|
Return A=DirToPlayer, 0/1/2 = N/NE/E etc. | |
cop #$2E
|
db OffsX,OffsY
|
Return A=DirToPlayer from relative location |
cop #$2F
|
db DirToPlayer : dw JmpAddr
|
Branch if DirToPlayer is... |
cop #$30
|
db OffsX,OffsY,DirToPlayer : dw JmpAddr
|
Branch if DirToPlayer from relative location is... |
cop #$31
|
dw SouthAddr,NorthAddr,WestAddr,EastAddr
|
Branch on Player's facing direction |
cop #$32
|
db BgChg
|
Stage BG tilemap change (e.g. opening door) from data at $81d3ce + 8*BgChg |
cop #$33
|
Perform staged BG tilemap change | |
cop #$34
|
Castoth door macro, equivalent to cop #$32 : db $7F:24 : cop #$08 : db $0f,$0f : cop #$33 | |
cop #$35
|
Return A=CardinalToPlayer, 0/1/2/3 = N/E/S/W | |
cop #$36
|
Palette handlers: Restart palette bundle | |
cop #$37
|
db PalBundleIndex
|
Palette handlers: Start new palette bundle |
cop #$38
|
db PalBundleIndex,Iters
|
Palette handlers: Start new palette bundle and prepare to loop Iters times |
cop #$39
|
Palette handlers: Advance palette bundle, exit if more palettes remain | |
cop #$3A
|
Palette handlers: Advance or restart palette bundle, exit if more palettes or Iters remain | |
cop #$3B
|
db Param : dl EntryPtr
|
Spawn new thinker running EntryPtr with parameter Param |
cop #$3C
|
dl EntryPtr
|
Spawn new thinker running EntryPtr |
cop #$3D
|
Thinker only: Mark for death after thinker returns this frame | |
cop #$3E
|
dw BtnMask
|
Exit if buttons in BtnMask are not pressed this frame; add 1 to BtnMask to include previous frame |
cop #$3F
|
dw BtnMask
|
Exit if buttons in BtnMask are pressed this frame; add 1 to BtnMask to include previous frame |
cop #$40
|
dw BtnMask,JmpAddr
|
Branch if buttons in BtnMask are pressed this frame; add 1 to BtnMask to include previous frame |
cop #$41
|
dw BtnMask,JmpAddr
|
Branch if buttons in BtnMask are not pressed this frame; add 1 to BtnMask to include previous frame |
cop #$42
|
db AbsX,AbsY,Type
|
Set BG tile solidity mask to Type at absolute location |
cop #$43
|
Snap self to grid | |
cop #$44
|
db XLeft,YUp,XRight,YDown : dw JmpAddr
|
Branch if Player is in signed relative tile area |
cop #$45
|
db XLeft,YTop,XRight,YBot : dw JmpAddr
|
Branch if Player is in absolute tile area |
cop #$46
|
Set position of previous actor (ID in $04) to here | |
cop #$47
|
Set position of next actor (ID in $06) to here | |
cop #$48
|
Return player facing direction, 0/1/2/3 = S/N/W/E | |
cop #$49
|
db PlayerBody : dw JmpAddr
|
Branch if Player's Body is not PlayerBody (0=Will, 1=Freedan, 2=Shadow) |
cop #$4A
|
Utility COP for #$43, probably no ad-hoc use | |
cop #$4B
|
db PosX,PosY,MetatileIndex
|
Draw metatile with collision during VBlank; hangs the actor for ~2 frames |
cop #$4C
|
db Arg1
|
Unknown, used by world map |
cop #$4D
|
db Arg1,Arg2
|
Unknown, used by world map |
cop #$4E
|
db Arg1,Arg2
|
Unknown, used by world map |
cop #$4F
|
dl SrcAddr : dw VramWord,XferSizeB
|
Queue ad hoc DMA of XferSizeB bytes to VRAM at VramWord |
cop #$50
|
dl SrcAddr : db OffsW,PalWord,XferSizeW
|
MVN of XferSizeW words from SrcAddr+2*OffsW to palette stage at $7F0A00+2*PalWord |
cop #$51
|
dl SrcAddr : dw DestAddr
|
Decompress data at SrcAddr into DestAddr in bank $7E |
cop #$52
|
db SpriteId,Speed,MaxTime
|
Stage movement; must write destination to $7F:18,$7F:1A before calling; MaxTime<0 means no limit |
cop #$53
|
Perform movement staged by cop #$52 | |
cop #$54
|
dl Arg
|
Utility function, sets $7F0000,x = Arg and $7F0003,x = $00 |
cop #$55
|
db Spr,New24,New25
|
Resets sprite (as cop #$80) and sets $24 and $25 |
cop #$56
|
Unknown use, advances sprite animation based on global state | |
cop #$57
|
dl OnDeath
|
Set OnDeath pointer |
cop #$58
|
dw OnHit
|
Set OnHit pointer |
cop #$59
|
dw Dodge
|
Set Dodge pointer |
cop #$5A
|
dw OnCollide
|
Set OnCollide pointer |
cop #$5B
|
dw Arg
|
Set $7F:2A = bitwise OR of $7F:2A with Arg |
cop #$5C
|
dw Arg
|
Set $7F:2A = bitwise AND of $7F:2A with Arg |
cop #$5D
|
dw JmpAddr
|
Branch if low-priority sprite and behind wall (i.e. priority bits in $0e are unset and BG tile solidity mask is $xE or $xF) |
cop #$5E
|
dw Arg
|
Set $7F1016,x |
cop #$5F
|
dw BaseAddr : db BytesPerPeriod
|
Initialize scaled sines for HDMA; must write 2*Amplitude to $7F:08 before calling; uses 2KB in bank $7E |
cop #$60
|
db Delay,ScrollLayer
|
Advance scaled sines, offset by PPU scroll value for BG1 (ScrollLayer=0) or BG2 (ScrollLayer=2) |
cop #$61
|
dl SrcAddr : db Reg
|
Queue HDMA to Reg, intended for use with cop #$5F : cop #$60 |
cop #$62
|
db MatchTile : dw JmpAddr
|
Duplicate of cop #$1A |
cop #$63
|
db InitSpeed,NegLogA,GndTilePos
|
Stage gravity |
cop #$64
|
Do gravity (must rtl to move) | |
cop #$65
|
dw PosX,PosY : db Dummy,WMapMoveId
|
Stage world map movement from pixels PosX,PosY using move script pointer indexed at $83ad77; follow with cop #$26 to perform transition |
cop #$66
|
dw PosX,PosY : db WMapOptsId
|
Stage world map choices from pixels PosX,PosY using text box code pointer indexed at $83b401; follow with cop #$26 to perform transition |
cop #$67
|
db Dummy,WMapMoveId
|
As #$65 without setting position; used when already on world map |
cop #$68
|
dw JmpAddr
|
Branch if off-screen |
cop #$69
|
dw Min
|
Exit if $00E4< Min |
cop #$6A
|
dw NewAddr
|
Set CodePtr of Actor06 |
cop #$6B
|
dw TextAddr
|
Text script (alt vers w/no screen refresh) |
cop #$6C
|
db New12,New10
|
Set $7F:12,10 = New12,New10 |
cop #$6D
|
db DiameterSpeed,AngleSpeed
|
Spiral about actor whose ID is stored at $0000 |
cop #$80
|
db Spr
|
Stage new sprite Spr, #$8x = HMirror; or Spr=$FF to reset current animation |
cop #$81
|
db Spr,XMove
|
Stage X movement |
cop #$82
|
db Spr,YMove
|
Stage Y movement |
cop #$83
|
db Spr,XMove,YMove
|
Stage X+Y movement |
cop #$84
|
db Spr,Iters
|
Stage Spr animation loop Iters times |
cop #$85
|
db Spr,Iters,XMove
|
Stage Spr loop and X movement for Iters |
cop #$86
|
db Spr,Iters,YMove
|
Stage Spr loop and Y movement for Iters |
cop #$87
|
db Spr,Iters,XMove,YMove
|
Stage Spr loop and X+Y movement for Iters |
cop #$88
|
dl MetaspriteAddr
|
Set new metasprite data address |
cop #$89
|
Animate and/or move sprite for one iteration, exiting each frame if unfinished | |
cop #$8A
|
Animate and/or move sprite, all staged iterations, exiting each frame if unfinished | |
cop #$8B
|
Animate and/or move one frame only, without exiting | |
cop #$8C
|
db SprFrame
|
Do sprite loops, but continue if at SprFrame |
cop #$8D
|
db Spr
|
Stage Spr as #$80, and update hitbox size if permitted |
cop #$8E
|
db PlayerSpr
|
Stage Player special sprite |
cop #$8F
|
db BodySpr
|
Stage Player normal sprite |
cop #$90
|
db BodySpr,XMove
|
Stage Player X movement |
cop #$91
|
db BodySpr,YMove
|
Stage Player Y movement |
cop #$92
|
db BodySpr,XMove,YMove
|
Stage Player X+Y movement |
cop #$93
|
Duplicate of #$89 | |
cop #$94
|
db BodySpr,XMove,YMove,WallType
|
(unused) As #$92 but would have set WallType for #$96-#$98 |
cop #$95
|
As #$8F but use value at $0000 for BodySpr | |
cop #$96
|
dw BtnMaskTrigger
|
(unused) After #$94, would animate and set a flag if this tile solid mask were WallType |
cop #$97
|
dw BtnMaskTrigger
|
(unused) After #$94, would animate and set a flag if north tile solid mask were WallType |
cop #$98
|
dw BtnMaskTrigger
|
(unused) After #$94, would animate and set a flag if south tile solid mask were WallType |
cop #$99
|
dl SpawnAddr
|
Spawn new actor, running at SpawnAddr, before This in list (ID in $04), returning new ID in Y |
cop #$9A
|
dl SpawnAddr : dw New10
|
As #$99, setting new actor's $10 = New10 |
cop #$9B
|
dl SpawnAddr
|
Spawn new actor, running at SpawnAddr, after This in list (ID in $06), returning new ID in Y |
cop #$9C
|
dl SpawnAddr : dw New10
|
As #$9B, setting new actor's $10 = New10 |
cop #$9D
|
dl SpawnAddr : dw OffsX,OffsY
|
As #$9B, spawning at relative position |
cop #$9E
|
dl SpawnAddr : dw OffsX,OffsY,New10
|
As #$9C and #$9D |
cop #$9F
|
dl SpawnAddr : dw AbsX,AbsY
|
As #$9B, spawning at absolute position |
cop #$A0
|
dl SpawnAddr : dw AbsX,AbsY,New10
|
As #$9C and #$9F |
cop #$A1
|
dl ChildAddr : dw New10
|
As #$9A, marking new actor as Child |
cop #$A2
|
dl ChildAddr : dw New10
|
As #$9C, marking new actor as Child |
cop #$A3
|
dl ChildAddr : dw AbsX,AbsY,New10
|
As #$A0, marking new actor as Child |
cop #$A4
|
dl ChildAddr : db OffsX,OffsY : dw New10
|
As #$9E, with 8-bit pixel offsets, marking new actor as Child |
cop #$A5
|
dl ChildAddr : db OffsX,OffsY : dw New10
|
As #$A4, placing child Last in execution order rather than Next |
cop #$A6
|
dl ChildAddr : db Spr,OffsX,OffsY : dw New10
|
Broken; would have been as #$A5, also setting Child's sprite |
cop #$A7
|
Mark actor for death after next return (and children, if so flagged) | |
cop #$A8
|
Kill Actor04 | |
cop #$A9
|
Kill Actor06 | |
cop #$AA
|
db XMove
|
Stage and save XMove |
cop #$AB
|
db YMove
|
Stage and save YMove |
cop #$AC
|
db XMove,YMove
|
Stage and save X/YMove |
cop #$AD
|
db ForceSW
|
Set/clear forced south/west movement |
cop #$AE
|
db ForceNE
|
Set/clear forced north/east movement |
cop #$AF
|
db ForceNeg
|
Set/clear both, to force negative movement |
cop #$B0
|
db XMoveL,YMoveL
|
Stage and save X/YMove for Last actor |
cop #$B1
|
Load saved movement | |
cop #$B2
|
Set max collision priority flag | |
cop #$B3
|
Set min collision priority flag | |
cop #$B4
|
Clear max collision priority flag | |
cop #$B5
|
Clear min collision priority flag | |
cop #$B6
|
db NewPriority
|
Update sprite priority bits (in $0F) |
cop #$B7
|
db NewPalette
|
Update sprite palette bits (in $0F) |
cop #$B8
|
Toggle HMirror | |
cop #$B9
|
Toggle VMirror | |
cop #$BA
|
Unset HMirror | |
cop #$BB
|
Set HMirror | |
cop #$BC
|
db OffsX,OffsY
|
Set new position immediate |
cop #$BD
|
dl Bg3ScriptAddr
|
Run BG3 script, e.g. drawing status bar or text |
cop #$BE
|
db OptCounts,SkipLines : dw OptionsAddr At OptionsAddr: dw CancelAddr[,ChoiceAddr1,...]
|
Dialogue box options; must print box and text with #$BF then call this |
cop #$BF
|
dw TextAddr
|
Text message |
cop #$C0
|
dw OnInteract
|
Set EntryPtr on player chat/pickup |
cop #$C1
|
Set EntryPtr here, and continue | |
cop #$C2
|
Set EntryPtr here, and exit | |
cop #$C3
|
dl NewPtr : dw Delay
|
Set EntryPtr there, exit, and wait Delay frames |
cop #$C4
|
dl NewPtr
|
Set EntryPtr there, and exit |
cop #$C5
|
Restore SavedPtr | |
cop #$C6
|
dw SavedPtr
|
Set SavedPtr |
cop #$C7
|
dl NewPtr
|
Like JML: set EntryPtr, and continue there |
cop #$C8
|
dw SubPtr
|
Like JSR: set SavedPtr here, EntryPtr at SubPtr, and continue there |
cop #$C9
|
dw SubPtr
|
Like delayed JSR: set SavedPtr here, EntryPtr at SubPtr, and exit |
cop #$CA
|
db Iters
|
Loop from here to next cop #$CB, Iters times |
cop #$CB
|
Loop end, exit if unfinished Iters, otherwise continue | |
cop #$CC
|
db SetFlag
|
Set game flag, range 0..$FF |
cop #$CD
|
dw SetFlagW
|
Set game flag, range 0..$FFFF |
cop #$CE
|
db ClearFlag
|
Clear flag |
cop #$CF
|
dw ClearFlagW
|
Clear flag |
cop #$D0
|
db Flag,Val : dw IfThenAddr
|
Branch if Flag is Val (0/1) |
cop #$D1
|
dw FlagW : db Val : dw IfThenAddr
|
Branch if Flag is Val (0/1) |
cop #$D2
|
db Flag,Val
|
Exit if Flag is not Val (0/1) |
cop #$D3
|
dw FlagW : db Val
|
Exit if Flag is not Val (0/1) |
cop #$D4
|
db AddItemId : dw FullInvAddr
|
Give item, branching if inventory is full |
cop #$D5
|
db RemoveItemId
|
Remove item |
cop #$D6
|
db ItemId : dw HasItemAddr
|
Branch if Player has item |
cop #$D7
|
db ItemId : dw EquippedItemAddr
|
Branch if item is equipped |
cop #$D8
|
Set dungeon-level monster killed flag | |
cop #$D9
|
dw IndexAddr,JmpListAddr
|
Switch-case statement, equivalent to ldx IndexAddr : jmp (JmpListAddr,x) |
cop #$DA
|
db Delay
|
Exit and wait for Delay frames, range 0..$FF |
cop #$DB
|
dw Delay
|
Exit and wait for Delay frames, range 0..$7FFF |
cop #$DC
|
Unclear, conditions on obscure globals | |
cop #$DD
|
Unclear, conditions on obscure globals | |
cop #$DE
|
Unclear, conditions on obscure globals | |
cop #$DF
|
Unclear, conditions on obscure globals | |
cop #$E0
|
Mark for death (with children, if flagged so) and return immediately | |
cop #$E1
|
Restore SavedPtr and set A=#$FFFF | |
cop #$E2
|
dl NewPtr
|
Set EntryPtr, but continue here this frame |
Flags are stored in $10, $12, and $7F:2A:
$10 & #$8000 Is Player $10 & #$4000 Offscreen (engine temp flag) $10 & #$2000 Disable rendering and collisions $10 & #$1000 Interactable/chattable/useable $10 & #$0800 Continue acting during dialogue $10 & #$0400 Is weapon of Player (e.g. Friar) $10 & #$0200 Disable collisions with weapons $10 & #$0100 Disable collisions with Player $10 & #$0080 Is hurt (engine temp flag) $10 & #$0040 Is dying (engine temp flag) $10 & #$0020 Is item pickup, e.g. DP (but other obscure uses too) $10 & #$0010 Invulnerable; make clanging sound on hit $10 & #$0008 If moving via COP #$8x, collide with walls $10 & #$0004 Is colliding with a wall (engine temp flag) $10 & #$0002 Maximum OAM priority $10 & #$0001 Minimum OAM priority $12 & #$8000 ? $12 & #$4000 If moving via COP #$8x, force movement to be west/south $12 & #$2000 If moving via COP #$8x, force movement to be east/north $12 & #$1000 ? $12 & #$0800 ? $12 & #$0400 ? $12 & #$0200 ? $12 & #$0100 Constant hitbox (even if sprite changes) $12 & #$0080 Cut bottom 8px of hitbox (to seem of higher elevation) $12 & #$0040 Remove children on death $12 & #$0020 Hide HP $12 & #$0010 Disable recoil on damage $12 & #$0008 ? $12 & #$0004 ? $12 & #$0002 Persistent h-mirror OAM flag (resist being changed by COPs) $12 & #$0001 Disable stun on damage $7F:2A & #$0080 Exclude from monster counter on radar $7F:2A & #$0010 Enable CollPtr $7F:2A & #$0002 Forced movement (e.g. recoil or cop #$22) in progress (engine temp flag) $7F:2A & #$xxxx Others unknown
Thinker code
To run code during VBlank, at end-of-frame, or while other processing is paused (e.g. during music loads), use a thinker. These are like lightweight, no-sprite actors. Most concepts about actor code apply to thinkers.
In thinker memory, if flag #$0004 in $7f000e,x is set, code will run after actor processing. If clear, code will run after the NMI handler. The NMI handler finishes during VBlank (unless you seriously abuse DMA), so thinkers can e.g. do ad hoc updates to CGRAM or MMIO registers.
Key points:
- Thinker memory is blocks of $10 bytes, instead of $30.
- Direct page $00 - $0d have the same meaning as for actors. $0e/f as well as $7f:0e/f contain different flags. Other memory is free.
- Don't use COPs that affect properties the thinker doesn't have, e.g. position, sprite, physics. Probably the only useful COPs are #$36 - #$3D and #$C1 - #$DB.
- To die, a thinker must call COP #$3D and subsequently rtl.
Sprite data format
VRAM characters for most actors are loaded by map header $03. It is possible to load VRAM ad hoc, e.g. with cop #$4f, but ad hoc changes are permanently lost if the player opens the inventory screen.
VRAM characters for the player are loaded from pointers in a table at $81d971:
{ .long ptrAnimationList00 ; Will .long ptrCharacterData00 ; Will .long ptrAnimationList01 ; Freedan .long ptrCharacterData01 ; Freedan .long ptrAnimationList02 ; Shadow .long ptrCharacterData02 ; Shadow ; etc. for transformations, abilities }
Actor sprites/animations are chosen from an AnimationList a.k.a. SpriteSet. Actor memory $7f0008,x is a pointer to an AnimationList. For monsters the AnimationList is map-specific, always at $7e4000, and loaded by map header $10.
The AnimationList/SpriteSet format is:
AnimationList: { .word ptrAnimationData00 ; e.g. Will facing south, or Viper fluttering .word ptrAnimationData01 ; e.g. Will facing north, or Viper fast-fluttering ; etc. } AnimationData: { .word frameDuration00 ; e.g. $77 frames of... .word ptrMetasprite00 ; e.g. Will staring, followed by .word frameDuration01 ; e.g. $04 frames of... .word ptrMetasprite01 ; e.g. Will blinking ; etc. .word $FFFF } Metasprite: { .byte xOffset ; Sprite and hurtbox start at ActorPos - Offset. .byte xOffsetMirror ; Hurtbox size is Offset + OffsetMirror. .byte yOffset ; Offset/OffsetMirror are swapped if actor is reflected. .byte yOffsetMirror ; Notes on customizing the hurtbox are below. .byte xRecoilHitboxOffset ; RecoilHitbox starts at ActorPos + Offset. .byte yRecoilHitboxOffset ; Used when actor is recoiling from .byte xRecoilHitboxSizeInTiles ; damage, for wall collisions .byte yRecoilHitboxSizeInTiles ; and bumping other actors. .byte xHostileHitboxOffset ; HostileHitbox starts at ActorPos + Offset. .byte xHostileHitboxSize ; Used when damaging other actors. .byte yHostileHitboxOffset ; Offsets in the Metasprite section .byte yHostileHitboxSize ; are signed, sizes are unsigned. .byte numSprites { Sprite00 } ; e.g. a head { Sprite01 } ; e.g. an arm ; etc. } Sprite: { .byte isLargeSpriteBool ; 8x8 small, 16x16 large .byte xOffset ; These offsets are unsigned. .byte xOffsetMirror ; Mirror offsets are used if the actor is reflected. .byte yOffset .byte yOffsetMirror .word propertiesAndAddress ; i.e. OAM data, vhoopppccccccccc }
For Player sprites, character data is pushed to VRAM from ROM CharacterData + ((propertiesAndAddress & $01FF) << 5). For all other sprites, propertiesAndAddress is the sprite's literal OAM data, so the Address portion is a VRAM word-address.
The sprite and hurtbox extend east/south from ActorPos - x/yOffset (or x/yOffsetMirror if reflected). To manually set hurtbox bounds, set flag #$0100 in actor memory $12 and manually write values for xOffset/xOffsetMirror/yOffset/yOffsetMirror at $20/$21/$22/$23.
Palette bundle data format
Ad hoc palette changes (i.e. CGRAM updates) are conventionally performed using scripted palette bundles. There are $80 palette bundles, all in bank $16 and indexed by pointers at $168000. A palette bundle contains one or more palette sequences. Sequences contains blocks of palette data that are sequentially written to the same region of CGRAM (via cop #$39
). Sequences are not interruptible, but after cycling through the entire sequence, the calling code may restart the same sequence (via cop #$36
) or switch to a different sequence in the bundle (via lda #SequenceNum : sta $0e
).
The indexed pointers in bank $16 point to palette bundle headers, which contain 6 bytes for every sequence in the bundle:
PaletteSequenceHeader: { .byte numPalettes .word dataSourceAddress .byte cgramTargetWord ; or $00 to write COLDATA instead of CGRAM .byte numBytesMinusOne ; or $02 to write COLDATA instead of CGRAM .byte delayFrames ; until next CGRAM write }
followed by a terminal $00.
Palette source data within a bundle can overlap. This is a common design for e.g. wave or flame effects, where a small number of colors are rolled cyclically through CGRAM.
A thinker-type actor is usually used to handle palette bundles; this has the advantage that (due to how thinker initialization is coded) the palette bundle can be efficiently spawned using cop #$3B
with Param
= the desired palette bundle index. Common palette bundle handler actors are $80b519 which runs a bundle's first sequence exactly once then dies, and $80b520 which runs a bundle's first sequence repeatedly forever.
Music data format
The music engine is based on N-SPC, with some modifications to streamline data loads. There are $3C BRR samples, stored consecutively starting at address $C50000, and wrapping around to the next bank at $xx8000. Each sample is preceded by 2 bytes indicating its length.
Samples $00 - $0d are used to construct sound effects; the remaining samples are for music. The following table describes the samples and the information that's needed to put them in a song:
SampleID | Approximate sound | Used in Areas | InstrumentDefinition | SampleLength | LoopStartOffset |
---|---|---|---|---|---|
$00 | SFX | N/A | $FF,$E0,$B8,$03,$90 | $065D | $0009 |
$01 | SFX | N/A | $8F,$E0,$B8,$04,$00 | $0063 | $003F |
$02 | SFX | N/A | $8F,$F4,$B8,$03,$C0 | $09E1 | $09E1 |
$03 | SFX | N/A | $8F,$E9,$B8,$06,$F0 | $0051 | $0012 |
$04 | SFX | N/A | $FF,$E0,$B8,$04,$80 | $034E | $034E |
$05 | SFX | N/A | $FF,$E0,$B8,$02,$80 | $022E | $022E |
$06 | SFX | N/A | $8F,$F6,$B8,$0F,$70 | $0492 | $0264 |
$07 | SFX | N/A | $8F,$E0,$B8,$04,$00 | $0063 | $003F |
$08 | SFX | N/A | $FF,$E0,$B8,$03,$D0 | $0585 | $0585 |
$09 | SFX | N/A | $FF,$E0,$B8,$03,$D0 | $0585 | $0585 |
$0A | SFX | N/A | $FF,$E4,$B8,$0A,$E0 | $0DD1 | $0438 |
$0B | SFX | N/A | $8F,$EB,$B8,$01,$00 | $001B | $0012 |
$0C | SFX | N/A | $FF,$8E,$B8,$01,$F0 | $0105 | $00F3 |
$0D | SFX | N/A | $FF,$91,$B8,$03,$D0 | $0585 | $0585 |
$0E | Electric guitar 1 | Raft, City | $FF,$E0,$B8,$0D,$A0 | $12EA | $0480 |
$0F | Strings 1 | Castle, Inca, City | $FB,$E0,$B8,$04,$90 | $19BC | $0009 |
$10 | Flute 1 | Raft, City, Great Wall | $FF,$88,$B8,$03,$F0 | $05A9 | $0585 |
$11 | Glockenspiel | Village, Dreams, Raft, City | $FF,$8E,$B8,$03,$F0 | $02BE | $029A |
$12 | Snare 1 | City | $FF,$E0,$B8,$05,$50 | $03DE | $03DE |
$13 | Jingle bell | City, Space flight chorus | $FF,$E0,$B8,$05,$B0 | $13CB | $13CB |
$14 | Acoustic guitar 1 | Village | $FF,$70,$B8,$0D,$00 | $0804 | $078F |
$15 | Pan flute | Silence, Village, Spooky, Inca, Flute songs | $F8,$8A,$B8,$08,$F0 | $0B37 | $0AE6 |
$16 | Woodblock 1 | Village | $FF,$E0,$B8,$07,$A0 | $07B3 | $07B3 |
$17 | Bongo | Village | $FF,$E0,$B8,$07,$A0 | $07B3 | $07B3 |
$18 | Chirp 1 | Village | $FF,$E0,$B8,$02,$A0 | $0747 | $0747 |
$19 | Chirp 2 | Village | $FF,$E0,$B8,$02,$80 | $0747 | $0747 |
$1A | Choir 1 | Spooky, Dreams | $F8,$E0,$B8,$02,$F0 | $141C | $0009 |
$1B | Pizzicato strings 1 | Spooky, Raft, Sky Garden, Mu, Angkor Wat, Pyramid | $BF,$6C,$B8,$04,$F0 | $046E | $0441 |
$1C | Strings 2 | Castle, MinorDungeon, Spooky, Inca, Sky Garden, Mu, Great Wall, Angkor Wat, Pyramid, Boss fight, Item fanfare, Comet | $F9,$E0,$B8,$08,$F0 | $10A1 | $039F |
$1D | Tom-tom 1 | Castle, MinorDungeon, Spooky, Inca, Sky Garden, Mu, Great Wall, Angkor Wat, Pyramid, Boss fight, Dark Space, Item fanfare, Comet | $FF,$F1,$B8,$03,$D0 | $15F9 | $15F9 |
$1E | Synth brass 1 | Castle | $FF,$A8,$B8,$03,$F0 | $0786 | $0762 |
$1F | Horn 1 | Castle, Sky Garden, Item fanfare, Comet | $FF,$A8,$B8,$04,$70 | $0615 | $0573 |
$20 | Strings 3 | MinorDungeon, Dreams, Raft, Sky Garden, Mu, Great Wall, Angkor Wat, Pyramid, Boss fight, Item fanfare, Comet | $FC,$E0,$B8,$04,$70 | $1E96 | $019E |
$21 | Trumpet 1 | MinorDungeon, Sky Garden, Mu, Pyramid | $AF,$88,$B8,$02,$F0 | $044A | $042F |
$22 | Piccolo | MinorDungeon, Dark Space, World map | $F8,$A8,$B8,$03,$F0 | $04E3 | $04BF |
$23 | Muted trumpet 1 | Inca, Great Wall, Angkor Wat, Pyramid | $FC,$AA,$B8,$05,$F0 | $042F | $03F9 |
$24 | Orchestra hit 1 | Inca, Mu, Angkor Wat | $FF,$E0,$B8,$02,$80 | $0D14 | $0D14 |
$25 | Koto | Great Wall | $CF,$52,$B8,$04,$70 | $040B | $03BA |
$26 | Orchestra hit 2 | Pyramid | $FF,$E0,$B8,$01,$F0 | $0B01 | $0B01 |
$27 | Choir 2 | Space flight chorus | $F9,$A8,$B8,$03,$50 | $2EB9 | $0009 |
$28 | Electric piano | Space flight chorus, Dark Space | $FF,$8C,$B8,$03,$F0 | $095A | $0936 |
$29 | Trumpet 2 | Boss fight, Comet | $AF,$88,$B8,$02,$F0 | $0465 | $044A |
$2A | Piano | Dreams | $FF,$8C,$B8,$04,$70 | $0DD1 | $0D80 |
$2B | Bass | World map | $9F,$68,$B8,$19,$00 | $049B | $03BA |
$2C | Acoustic guitar 2 | World map | $AF,$4C,$B8,$09,$00 | $0E10 | $0615 |
$2D | Cymbal 1 | Credits | $FF,$8D,$B8,$03,$D0 | $20FA | $20FA |
$2E | Electric guitar 2 | Credits, Unused fanfare | $F9,$E0,$B8,$08,$F0 | $10A1 | $039F |
$2F | Tom-tom 2 | Credits, Unused fanfare | $FF,$F1,$B8,$03,$D0 | $15F9 | $15F9 |
$30 | Cymbal 2 | Credits | $FF,$8D,$B8,$03,$D0 | $20FA | $20FA |
$31 | Snare 2 | Credits, Unused fanfare | $FF,$E0,$B8,$05,$50 | $03DE | $03DE |
$32 | Horn 2 | Credits, Unused fanfare | $FF,$A8,$B8,$04,$70 | $0615 | $0573 |
$33 | Synth brass 2 | Credits, Unused fanfare | $FF,$A8,$B8,$03,$F0 | $0786 | $0762 |
$34 | Muted Trumpet 2 | Credits | $FC,$AB,$B8,$05,$F0 | $042F | $03F9 |
$35 | Strings 4 | Credits, Unused fanfare | $FC,$E0,$B8,$04,$70 | $1E96 | $019E |
$36 | Pizzicato strings 2 | Credits | $BF,$6C,$B8,$04,$F0 | $046E | $0441 |
$37 | Flute 2 | Credits | $FF,$88,$B8,$03,$F0 | $05A9 | $0585 |
$38 | Choir 3 | Dark Space | $FF,$CA,$B8,$04,$70 | $138C | $0E46 |
$39 | Woodblock 2 | Dark Space | $FF,$FE,$B8,$07,$A0 | $026D | $026D |
$3A | Waterdrop | Dark Space | $FF,$E0,$B8,$03,$D0 | $0585 | $0585 |
$3B | Squawk | South Cape | $FF,$E0,$B8,$02,$40 | $09EA | $09EA |
Music is loaded via map header $11 with an address, or via cop #$04/#$05 with an index; the COP argument is 1-indexed and pulls from the table of music addresses at $81cba6 (so cop #$04 : db $01 selects the first song, not the second).
Music data contains two segments. The first segment consists of any number of blocks with the following structure:
dw PayloadSize dw SpcRamTargetAddr db Payload
PayloadSize is the number of bytes in Payload, and the bytes of Payload will be written to ARAM at SpcRamTargetAddr. To follow IOG convention, music data for a song using InstrumentCount instruments should contain the following block writes in the following order:
- At
$1254
, write 6 bytes per instrument in the song: one for the instrument number (starting from $0E), then 5 to define the ADSR envelope, gain, and pitch. The 5 definition bytes for each instrument are given in the table above. Regardless of which instruments you're actually using, the first instrument number is $0E, the second is $0F, etc. - At
$1300
, write these literal $0018 bytes:$32,$65,$7F,$98,$B2,$CB,$E5,$FC,$0A,$19,$28,$3C,$50,$64,$7D,$96,$AA,$B9,$C8,$D4,$E1,$EB,$F5,$FF
- At
$5532
, write the N-SPC song data. By IOG convention the song should contain a pickup segment, which plays once, followed by a looping segment, which plays forever. Songs should generally only use 6 channels, because the final two channels are used for sound effects. The basic conventional structure for this payload is as follows:
dw $5534 ; Pointer to start of song dw $553e ; Pointer to list of pointers to each channel's pickup segment dw $554e ; Pointer to list of pointers to each channel's looping segment dw $00ff : dw $5536 ; Return to start of looping segments when looping segment ends dw $0000 ; End song (unreachable due to the above loop jump) dw Track0Intro,Track1Intro,Track2Intro,Track3Intro,Track4Intro,Track5Intro,Track6Intro,Track7Intro dw Track0LoopStart,Track1LoopStart,Track2LoopStart,Track3LoopStart,Track4LoopStart,Track5LoopStart,Track6LoopStart,Track7LoopStart ; [For unused tracks, set TrackNIntro = $0000 and TrackNLoopStart = $0000.] Track0Intro: ; <track data> db $00 Track1Intro: ; <track data> db $00 ; etc.
- At
$1038
, write 4 bytes per instrument, as two word-size pointers. The first pointer points to the start of the instrument's sample data in ARAM, and the second points to its loop point. This populates the DSP DIR register. By IOG convention, sample data follows song data, so the pointers can be calculated as follows:- 1st instrument, 1st pointer, i.e. address of 1st instrument sample = $5532 + song length
- 1st instrument, 2nd pointer, i.e. 1st instrument loop point = $5532 + song length + 1st instrument's LoopStartOffset from table above
- Nth instrument, 1st pointer = (N-1)th instrument, 1st pointer + (N-1)th instrument's SampleLength from table above
- Nth instrument, 2nd pointer = Nth instrument, 1st pointer + Nth instrument's LoopStartOffset from table above
- At
$0ffc
, write the literal word $5532 - At
$0ffe
, write a 2-byte pointer to the byte after the last instrument sample, which can be calculated as $5532 + song length + sum of all SampleLengths of loaded instruments - At
$0fe0
, write the literal bytes $0E,$0F,...,($0E + InstrumentCount - 1)
Following all of those block writes comes the second segment of music data, which consists of:
dw $0000 dw SpcRamSampleAddr db InstrumentCount db InstrumentId1,InstrumentId2,...
InstrumentCount is the number of instrument samples to load for this song, and the InstrumentIds are their IDs. The samples for those instruments will be written to ARAM at SpcRamSampleAddr, which should be set immediately after the song data, i.e. at $5532 + song length.
The above convention arises because the sound effect samples (IDs $00 - $0d) are always in ARAM, while music samples are loaded only if the active song uses them -- we load music sample data and metadata into pre-existing structures that already contain sound effect data and metadata. For example, the list of all 6-byte instrument definitions starts at $1200 in ARAM. There are $0e static entries for the sound effect instruments, totalling $54 bytes; so definitions of additional instruments must start at $1254. From the SPC's perspective, the first instrument we load and add to the list is instrument $0E, the next is $0F, etc., because the SPC doesn't know how the samples are ordered or numbered in ROM.