Illusion of Gaia/Notes

From Data Crystal
Jump to navigation Jump to search

Chip tiny.png 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
db DestOffset
dl CharSrcAddr
db IsSprites
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
db DestOffset
dl PaletteSrcAddr
Load CGRAM data. Writes to the CGRAM buffer at $7f0a00.
db $05 db SrcOffset,SizeW
db DestOffset,Layers
dl MetaTileSrcAddr
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
dl TilemapSrcAddr
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
dl SprSetSrcAddr
Load metasprite / spriteset / sprite tile connectivity data. Data is written to $7e4000.
db $11 db MusicId,RoomGroup
dl MusicSrcAddr
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
db DirAndSave,NegCamBounds,PosCamBounds
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.