Illusion of Gaia/Notes

From Data Crystal
< Illusion of Gaia
Revision as of 02:35, 25 May 2024 by Raeven0 (talk | contribs) (text engine notes)
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.

Drawing text and BG3 tiles

For drawing text and dialogue boxes, and otherwise painting the BG3 layer (e.g. the HUD and radar), the game has two different engines. One engine is used for general dialogue and interaction text; it draws double-height characters (8px by 16px), and has features that make it easier to program dialogue. The other engine is more general-purpose, being used for inventory screen text as well as most non-text elements like the HUD and radar; it draws single-height characters (8px by 8px), and has features that make it easier to draw arbitrary tile patterns.

Both engines have many features in common.

Instructions for painting BG3 are provided to the engines as a byte script. Some bytes represent "opcodes" that change engine parameters, like setting the screen position where tiles will be drawn. All other bytes represent tiles ("characters") for the engine to draw, by indexing the tile graphic address in VRAM. (When writing text, this is why the encoding is not ASCII: the bytes correspond to VRAM addresses, not Latin letters.)

An internal cursor stores the screen position where a character will be drawn. Drawing a character advances the cursor to the right, and from the right edge of the screen, the cursor advances to the left edge and lower by one tile. The internal cursor also has a starting position, set by some opcodes; this is important when drawing text. A "newline" opcode generally behaves intuitively, moving the cursor horizontally to line up with its starting position and then down one character. (The actual behavior is to keep a count of the number of newlines processed, and move the cursor down by that many characters. This detail doesn't matter in any normal text-writing scenario.)

Scripts may "include" other scripts via an opcode. This is mostly equivalent to copying the target script into the calling script. However, opcodes that reference word-size addresses will use the bank of the target script, not the parent script. (There is a special case related to the terminating character of a dialogue script, detailed in that section.)

A script ends with a terminating character, which returns processing to the calling code or script.

Dialogue

A dialogue script is called by cop #$BF during gameplay, or by cop #$6B on the main menu; the former routine runs extra code to prepare the game world to be frozen during the dialogue. The entry point of the script has to be in the bank of the caller, but via opcode $CD, nearly all of the script can be stored in any bank.

In addition to the common features, the dialogue engine supports a dialogue box. A dialogue box is drawn by opcodes, then the cursor's starting position is set at the first character position inside the box. A new-page opcode clears the contents of the dialogue box and returns the cursor to the starting position. If the cursor is on the last character line of the dialogue box, a newline scrolls the entire text upward by one line instead of advancing the cursor below of the box; the top line of text is cleared in this case.

It is possible to draw more than one dialogue box on the screen. However, only the most-recently-created box can be written in, interacted with, or even deleted; all previous boxes and text remain as fixed tiles. To remove previous boxes, you have to draw a new box over the old one and then delete the new box. (The Japanese release, however, supported swapping between two active dialogue boxes.)

Dialogue scripts generally halt game processing, freezing the player etc. until the text is fully drawn and, if the script requires player input, the player has provided that input. In actor memory, flag #$0800 in address $10 causes actor code to continue running through a dialogue box.

There are two terminating opcodes for dialogue scripts. Byte $C0 requires the player to press a button, then clears the screen and returns control to the caller; this is the normal way to end dialogue. Byte $CA immediately returns control to the caller, without awaiting input or clearing the screen. Therefore, scripts that will be included should generally end in $CA, and top-level scripts should generally end in $C0. A notable exception is that dialogue scripts for multiple-choice prompts (i.e. menus) should use the $CA terminator, so that the text remains on-screen during the menu interaction.

All byte values $C0 and above are treated as opcodes, though bytes greater than $D8 have no defined code and will probably crash the game if used. The dialogue box opcodes are:

Opcode Arguments Notes
C0 None Pauses awaiting input, then clears the screen and returns. The normal way to end a dialogue. Equivalent to $D0 + $C8 + $CA but without the flickering arrow.
C1 db PosX, PosY Creates a dialogue box with its upper-left corner at PosX,PosY and sets the cursor's initial position. The minimum is 1,1.
C2 db TemplateId Includes a template script. These template scripts are indexed by pointers at $81ca95 and set up the box and text in commonly-used ways. Specifically: templates 0-6 set the color and sound of Will/Kara/Lilly/Erik/Lance/Seth/Neil, and 7 sets a red color; templates 8/9/10/11 create a box near the top of the screen of height 1/2/3/4, and templates 12/13/14/15 do likewise near the bottom of the screen; 16 ($10) is a duplicate of 15 ($0F), both of which produce the default dialogue box size and position; $11 creates a wide 3-line box at the bottom of the screen headered with "Where do you go?"; and $12-$15 set the colors of Will/Kara/Lilly/Lance.
C3 db NewBG3PaletteId Changes the palette of subsequent text. These are the seven 4-color BG3 palettes. Guaranteed usable options are 0 for the default blue, and 4 for the alternative that's set by code $D4. In principle, any of 0-7 can be used, but palettes other than 0 and 4 are reserved for the HUD and radar.
C4 None Not usable; just hangs the game. (The functionality of this code was not ported from the Japanese release.)
C5 dw AddressOfPointerList, AddressContainingIndex Indexed include; given a list of pointers to text scripts, the value at AddressContainingIndex indexes the script to include. In other words, in the same bank as this code, includes the script at the address whose value is at AddressOfPointerList + twice the value at AddressContainingIndex.
C6 dw NumDigits, AddressContainingNumber Prints the hexadecimal numeric value at a memory address. The least significant NumDigits digits are drawn, omitting leading zeroes. To print the value in base 10, store it as BCD.
C7 db SizeX, SizeY Sets box size. Sizes the printable area of the text box to be "SizeX" letters wide and "SizeY" lines high. The border of the box is sized to fit around the printable area.
C8 None Deletes the dialogue box, clearing all of its BG3 tiles.
C9 db Delay Pauses printing for the specified number of frames.
CA None Ends the script, returning control to the caller. This is the normal way to end an included script, as well as top-level scripts that shouldn't clear the screen, such as menus.
CB None Starts a new line of text, as a carriage-return + line-feed.
CC db SkipSize Advances the cursor by SkipSize/2 letters.
CD dl IncludeAddr Long-include. Includes the script at the long address IncludeAddr.
CE None Page-advance. Clears the dialogue box and returns the cursor to its initial position.
CF None Pauses awaiting input from the player, then clears the dialogue box and continues. Equivalent to $D0 + $CE.
D0 None Pauses awaiting input from the player.
D1 dw JumpAddr Jumps to a different address in this bank. This is a true jump, not an include.
D2 db SoundId Sets the sound that will be played when a character (other than whitespace) is printed.
D3 None Creates the default dialogue box: four lines of 26 characters, near the bottom of the screen, blue text, no per-character delay, and a neutral sound.
D4 db PaletteWordId : dw NewValue Sets any of the $100 palette colors to NewValue. The main use for this is to set words $11 and $12, which are in BG3 palette 4.
D5 db Delay Sets a frame delay of 1 frame plus the argument before the printing of each character. To return to drawing all text simultaneously, clear the screen and create a new box.
D6 db DictEntryIndex Include text from the dictionary with entries indexed at $81eba8.
D7 db DictEntryIndex Include text from the dictionary with entries indexed at $81f54d.
D8 db Char1,Char2,...,$00 Escape byte. Parses and prints subsequent bytes as characters even if they're in the opcode range. This is used for printing the hieroglyph texts, which occupy characters $C0 - $CB. The escaped sequence is terminated by a $00 byte.

Byte values below $C0 print a double-height "character". The byte indexes a BG3 VRAM tile, which becomes the upper half of the printed character. The lower half is the tile $10 positions after the upper tile. Because half of the bytes index the lower half of a letter, and some of the VRAM space is blank or used for other purposes, fewer than half of the bytes encode a printable character. They are:

Byte Character 
0D ?
0E '
20 0
21 1
22 2
23 3
24 4
25 5
26 6
27 7
28 8
29 9
2A .
2B ,
2C
2D
2E
2F :
40 A
41 B
42 C
43 D
44 E
45 F
46 G
47 H
48 I
49 J
4A K
4B L
4C M
4D N
4E O
4F !
60 P
61 Q
62 R
63 S
64 T
65 U
66 V
67 W
68 X
69 Y
6A Z
6B /
6C *
6D -
6E (
6F )
80 a
81 b
82 c
83 d
84 e
85 f
86 g
87 h
88 i
89 j
8A k
8B l
8C m
8D n
8E o
A0 p
A1 q
A2 r
A3 s
A4 t
A5 u
A6 v
A7 w
A8 x
A9 y
AA z
AC Space; prints silently

The dictionary entries indexed by opcodes $D6 and $D7 all end with a blank space, so you don't need to add a space after including one. The dictionaries were apparently developed for the beta translation and not fully updated for the final translation: note the use of "Morris", "Rob", and "Eric", and the spelling of "Itorie"; "Kara" and "Karen" both are present; "Will" is present, but not "Tim".

These are the dictionary entries for opcode $D6:

Byte Text 
00 Attack
01 Angel
02 After
03 Aura
04 Ankor
05 Black
06 Bill:
07 Crystal
08 City
09 Come
0A Condor's
0B Change
0C Dark
0D Don't
0E Diamond
0F Drifting,
10 Eric:
11 Edward
12 Even
13 Elder:
14 Earth
15 Freedan's
16 Great
17 Grandma
18 Good.
19 Gold
1A Grandpa
1B Hieroglyph
1C Hey,
1D It's
1E Inca
1F I'll
20 I've
21 Itorie
22 Jewels
23 Jewels!
24 Just
25 Kara:
26 Kara
27 King
28 Knight
29 Karen's
2A Lilly:
2B Let's
2C Lilly
2D Lola's
2E Lola:
2F Mystery
30 Maybe
31 Moon
32 Man:
33 Morris's
34 Melody
35 Morris:
36 Neil:
37 Neil's
38 Only
39 Once
3A Oink
3B Please
3C Psycho
3D People
3E Prayer
3F Pyramid
40 Purification
41 Plate
42 Quit
43 Rob:
44 Rolek
45 Soldier:
46 Strange
47 South
48 Statue
49 Somehow
4A Sometimes
4B Something
4C Shadow's
4D Someone
4E This
4F Will:
50 There's
51 Will's
52 There
53 That's
54 Tower
55 They
56 Then
57 Trip
58 Thank
59 Take
5A Will.
5B That
5C Will,
5D They're
5E These
5F Vampire
60 Village.
61 When
62 What
63 Well,
64 What's
65 Where
66 Woman:
67 You've
68 Your
69 You're
6A Yes,
6B about
6C anything
6D around
6E another
6F ancient
70 been
71 become
72 body
73 back
74 before
75 brought
76 beautiful
77 being
78 can't
79 come
7A could
7B comet
7C company
7D children
7E constellation
7F changed
80 came
81 coming
82 don't
83 didn't
84 doesn't
85 distant
86 different
87 demons
88 destroy
89 everyone
8A explorer
8B everyone's
8C enemies
8D exposed
8E from
8F found
90 find
91 feel
92 father's
93 going
94 good
95 great
96 ground
97 give
98 have
99 heard
9A human
9B hear
9C huge
9D happened
9E hieroglyph
9F it's
A0 inventory
A1 into
A2 inside
A3 just
A4 know
A5 like
A6 long
A7 little
A8 light
A9 look
AA looks
AB looking
AC leave
AD labor
AE left
AF live
B0 life
B1 living
B2 must
B3 made
B4 melody
B5 move
B6 many
B7 more
B8 matter
B9 nothing
BA need
BB never
BC next
BD other
BE over
BF outside
C0 original
C1 people
C2 power
C3 present.
C4 playing
C5 raised
C6 right-hand
C7 strange
C8 something
C9 statue
CA should
CB started
CC seems
CD same
CE such
CF someone
D0 some
D1 save
D2 statues
D3 still
D4 said
D5 stands
D6 this
D7 that
D8 thought
D9 there
DA think
DB there's
DC through
DD tried
DE terrible
DF time
E0 things
E1 their
E2 town
E3 thing
E4 these
E5 temple
E6 them
E7 take
E8 turned,
E9 understand
EA under
EB underwater
EC village
ED very
EE voice
EF will
F0 with
F1 want
F2 were
F3 would
F4 where
F5 world
F6 when
F7 what
F8 without
F9 wonder
FA won't
FB wouldn't
FC wanted
FD waiting
FE your
FF you're

These are the dictionary entries for opcode $D7:

Byte Text 
00 1994
01 Actually,
02 Button
03 Buttons
04 Back
05 Child:
06 Defeating
07 Didn't
08 Desert
09 Erasquez:
0A Elder
0B Earth's
0C Eric's
0D Everybody
0E Flute:
0F First
10 Firebird,
11 From
12 Gaia,
13 Gorgon
14 Have
15 Hey!
16 Istar's
17 Ishtar's
18 Lilly's
19 Larai
1A Looking
1B Main
1C Man's
1D Memory
1E Mountain
1F Morris
20 Many
21 Native
22 Native's
23 Neil
24 Naska
25 Power
26 Prison
27 Play
28 Panther's
29 Rob's
2A Recently
2B Restores
2C Right
2D Russian
2E Rofsky
2F Rearrange
30 Special
31 Spin
32 Seaside
33 She's
34 Shall
35 Sorry
36 Select
37 Show
38 Soon
39 Will!
3A Will...
3B Tell
3C Uses
3D Wind
3E We'll
3F We're
40 With
41 We've
42 Wait
43 Watermia.
44 always
45 approaching
46 animals
47 almost
48 also
49 anything.
4A abolition
4B breath
4C birthday
4D best
4E bring
4F bouquet
50 better
51 behind
52 change
53 castle
54 called
55 comet's
56 condition.
57 care
58 door
59 destroyed
5A disappeared
5B discovered
5C dark
5D ever
5E elevator
5F explorer.
60 eyes
61 first
62 fish
63 followed
64 fountain
65 floor
66 finally
67 father
68 flower
69 forced
6A forget
6B fate
6C girl
6D gets
6E guess
6F girl's
70 house
71 hope
72 home
73 here
74 here.
75 home!
76 happen....
77 houses
78 inventing
79 last
7A lost
7B language.
7C list
7D life.
7E laborer
7F letter
80 looked
81 lose
82 left-hand
83 mushrooms
84 make
85 mother
86 merchants
87 meet
88 most
89 only
8A ocean
8B opened
8C outskirts
8D president
8E please
8F probably
90 place
91 prison
92 pretty
93 palace
94 right
95 really
96 returned
97 running
98 ruins
99 right.
9A ruins,
9B road
9C rest
9D stone
9E seem
9F scattered
A0 seemed
A1 softly
A2 soldiers
A3 shape
A4 since
A5 surprised
A6 sure
A7 seen
A8 shouldn't
A9 somewhere
AA times
AB they
AC talk
AD tell
AE townspeople
AF taken
B0 they're
B1 travelling
B2 trying
B3 turned
B4 temporary
B5 than
B6 then
B7 too,
B8 thousands
B9 turn
BA treasure,
BB used
BC until
BD village.
BE vampire
BF while
C0 water
C1 went
C2 wondered
C3 written
C4 woman
C5 wind
C6 years
C7 you.

General BG3

A general BG3 script is called by cop #$BD. In addition to the common features, it supports relocating the cursor, clearing specific sections of the screen, changing the tilemap character mask, printing non-BCD values in base 10, and various other conveniences. These scripts don't support the controls available in the dialogue engine, like page-advancing. The trade-off is that this engine is unrestricted and can draw any BG3 tile at any screen position.

All byte values from $00 to $11 are opcodes. They function as follows:

Opcode Arguments Notes
00 None Ends the script, returning control to the caller.
01 dw TargetTile Sets the cursor position on the screen. The tile at screen position X,Y is represented by TargetTile = 2*X + $40*Y.
02 dl IncludeAddr Long-include. Includes the script at the long address IncludeAddr.
03 db NewHighMask Masks the high byte of subsequent tiles, i.e., the "vhopppcc" bits. The palette bits are cleared, then the byte is OR'd with NewHighMask. This is commonly used to change the text palette, by giving an argument of a palette ID left-shifted twice.
04 dl AddressOfPointerList : dw AddressContainingIndex Indexed include; given a list of pointers to text scripts, the value at AddressContainingIndex (in the bank of the pointer list) indexes the script to include. In other words, this includes the script at the address whose value is at AddressOfPointerList + twice the value at AddressContainingIndex.
05 db NumDigits : dw AddressContainingNumber Prints the hexadecimal numeric value of a memory address. The least significant NumDigits digits are drawn; leading zeroes are drawn as blank tiles. To print the value in base 10, store it as BCD or use code $0E instead.
06 db SizeX, SizeY : dw StartTile Draws a rectangle of blank tiles of width SizeX and height SizeY, with upper-left corner at StartTile = 2*StartX + $40*StartY.
07 dw NumTiles Clears NumTiles consecutive tiles, starting from the cursor position.
08 db Character : dw AddressContainingCount Writes consecutive copies of a given character. The number of instances to write is pulled from AddressContainingCount.
09 db AltChar1,AltChar2,...,$FF Probably not useful in non-Japanese localizations. Writes the given "characters", which are two tiles wide and high (i.e. 16px by 16px) and have a unique encoding: values between 0 and 7 index the tiles in BG3 VRAM from word-addresses $01F0/$01F1 to $01FE/$01FF; values between 8 and $17 similarly index VRAM tiles starting at $02E0. The latter set of tiles contains radar graphics. In the Japanese release, the former set was a buffer region, used for constructing room names; in the English localization that region is unused. The terminal byte can be any negative value.
0A None Draws the player's HP bar.
0B None Draws the last-damaged enemy's HP bar.
0C db Char1,Char2,...,$FF Escape byte. Parses and prints subsequent bytes as characters even if they're in the code range. This is not generally useful.
0D None Double-spaced newline: places the cursor two tiles below its initial position, as two carriage-returns + line-feeds, and sets the new initial position there. This is the normal newline character for inventory text.
0E dw AddressContainingNumber Prints the decimal numeric value at a memory address, modulo 100 (i.e. only the tens and ones places). A leading zero is drawn as a blank tile.
0F db SizeX, SizeY Draws a rectangle of blank tiles of width SizeX and height SizeY, from the cursor position.
10 db DictEntryIndex Includes text from the dictionary with entries indexed at $81eb0f.
11 None Single-spaced newline: places the cursor one tile below its initial position, as a carriage-return + line-feed, and sets the new initial position there. If writing text, the result is quite compact.

Byte values $12 and above draw a character. Since the two 'c' bits of the character high byte are editable (via opcode $03), in principle all BG3 VRAM characters are accessible. However, only the first page is likely to be useful. The second page contains double-height dialogue characters, the fourth page contains a tilemap instead of VRAM characters, and the third page contains radar screen graphics, which are (inexplicably) drawn by manual RAM writes, not by a BG3 script.

Even so, the first BG3 VRAM page also includes many characters that are not generally useful, such as HUD graphics and dakuten/handakuten. The following bytes correspond to a printable character that is probably useful:

Byte Character
1D /
20 .
21 !
28 [
29 ]
2A *
2C ,
2D +
2E -
30 0
31 1
32 2
33 3
34 4
35 5
36 6
37 7
38 8
39 9
3A :
3C (
3D ·
3E )
3F ?
40 Space (black tile)
41 A
42 B
43 C
44 D
45 E
46 F
47 G
48 H
49 I
4A J
4B K
4C L
4D M
4E N
4F O
50 P
51 Q
52 R
53 S
54 T
55 U
56 V
57 W
58 X
59 Y
5A Z
5B
5C '
5D <
5E >
5F #
60 Transparent tile
61 a
62 b
63 c
64 d
65 e
66 f
67 g
68 h
69 i
6A j
6B k
6C l
6D m
6E n
6F o
70 p
71 q
72 r
73 s
74 t
75 u
76 v
77 w
78 x
79 y
7A z
7C _
7D {
7E }
7F |
80 =
81 ;
82 "

The dictionary entries indexed by opcode $10 all end with a space (character $40), so there is no need to add a space after the word when including it. The entries are:

Byte Text 
00 Crystal
01 Diamond
02 Dark
03 Hieroglyph
04 Inca
05 Mystery
06 Prison
07 Plate
08 Psycho
09 Restores
0a Rearrange
0b Select
0c Statue
0d Uses
0e from