Illusion of Gaia/Notes

From Data Crystal
< Illusion of Gaia
Revision as of 17:09, 3 May 2020 by Raeven0 (talk | contribs) (sprite data format)
Jump to navigation Jump to search

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 CardinalDirToPlayer, 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 Arg1,Arg2,Arg3 Unknown, used by world map
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 = Arg
cop #$5C dw Arg Set $7F:2A &= 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 Diameter,Angle Orbit actor whose ID is stored at $0000; Angle=0 is north
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
.byte xOffsetMirror
.byte yOffset
.byte yOffsetMirror
.byte $F8,$F0,$01,$01,$F8,$10,$F0,$10    ; Situational modifiers, e.g. first 4 are for damage recoil
.byte numSprites
{ Sprite00 }                 ; e.g. a head
{ Sprite01 }                 ; e.g. an arm
; etc.
}

Sprite:
{
.byte isLargeSpriteBool       ; 8x8 small, 16x16 large
.byte xOffset
.byte xOffsetMirror
.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.