A few weeks ago, the Break the Targets community had the idea for a target coordinate modifier, and after doing some searching in this forum, I contacted Punkline for advice, on the basis that he seemed to have experience with stage modifications. Since then, Punkline wrote several versions of this code, culminating in a fully-featured Break the Targets custom stage handler, and it escalated into me harnessing it (at a high level) to build a Break the Targets randomizer with randomized target positions (with rules to prevent impossible targets), spawn positions, and allowing an arbitrary number of targets.
Generate randomizer Gecko codes here: bttrandomizer.com
Introduction video:
Custom stage handler code:
Stage header specification:
Generate randomizer Gecko codes here: bttrandomizer.com
Introduction video:
Custom stage handler code:
Rich (BB code):
-==-
BTT Custom Stage Handler 0.2.1
Installs custom Break the Targets stages
[Punkline]
NTSC 1.02 ----- 801c4228 --- 93c10018 -> branch
# BTT spawner is about to begin, loop isn't started yet
# at this moment, r3...r12 and r30 are free to use
# - r31 = rStage -- base of global stage data
# - 0x8(sp) and 0xC(sp) should be free to use as padding left over in the stack frame
#
# --- Stack Symbols:
# These are stored in the runtime stack frame padding, where the next injection can make use of them
sp.xHead = 0x8 # if a matching stage is found, then the sub-table header is copied here
sp.xArray = 0xC # the current address within an interating array
# --- Custom Data Symbols:
# This is the header struct of each sub-table in private in-line data, included with code
data.xQR = 0x0 # HWORD - data is put directly into QR7 load params, for compression type
data.xKey = 0x2 # BYTE - identifies a stage, or terminates the array of sub-tables
data.xCount = 0x3 # BYTE - counts the number of targets (allows for TSeak to be included)
data.xArrayXY = 0x4 # array of 'Count' elements of size 'QR.Type' (float, hword, or byte alignment)
data.mScale = 0x3F000000 # 6-bit mask for QR scale
data.mCustomSpawn = 0x00100000 # bool for including a custom spawn XY
data.mType = 0x00070000 # 3-bit mask for QR type
data.mCount = 0x0000FF00 # byte for counting targets
data.mKey = 0x000000FF # byte for selecting a stage
QR.float = 0 # data type IDs
QR.byte = 6 # signed fixed point byte
QR.hword = 7 # signed fixed point hword
# --- Stage IDs:
# DBG STAGES
stage.TEST = 0x01 # TEST stage
# STANDARD
stage.Izumi = 0x02 # Fountain of Dreams (Izumi)
stage.Pstadium = 0x03 # Pokémon Stadium (Pstadium)
stage.Castle = 0x04 # Princess Peach's Castle (Castle)
stage.Kongo = 0x05 # Kongo Jungle (Kongo)
stage.Zebes = 0x06 # Brinstar (Zebes)
stage.Corneria = 0x07 # Corneria
stage.Story = 0x08 # Yoshi's Story (Story)
stage.Onett = 0x09 # Onett
stage.MuteCity = 0x0A # Mute City
stage.RCruise = 0x0B # Rainbow Cruise (RCruise)
stage.Garden = 0x0C # Jungle Japes (Garden)
stage.GreatBay = 0x0D # Great Bay
stage.Shrine = 0x0E # Hyrule Temple (Shrine)
stage.Kraid = 0x0F # Brinstar Depths (Kraid)
stage.Yoster = 0x10 # Yoshi's Island (Yoster)
stage.Greens = 0x11 # Green Greens (Greens)
stage.Fourside = 0x12 # Fourside
stage.Inishie1 = 0x13 # Mushroom Kingdom I (Inishie1)
stage.Inishie2 = 0x14 # Mushroom Kingdom II (Inishie2)
stage.Akaneia = 0x15 # Akaneia (Deleted Stage)
stage.Venom = 0x16 # Venom
stage.Pura = 0x17 # Poké Floats (Pura)
stage.BigBlue = 0x18 # Big Blue
stage.Icemt = 0x19 # Icicle Mountain (Icemt)
stage.Icetop = 0x1A # Icetop
stage.FlatZone = 0x1B # Flat Zone
stage.ppp = 0x1C # Dream Land N64 (old ppp)
stage.yosh = 0x1D # Yoshi's Island N64 (old yosh)
stage.kong = 0x1E # Kongo Jungle N64 (old kong)
stage.battle = 0x1F # Battlefield (battle)
stage.last = 0x20 # Final Destination (last)
# TARGET TEST
stage.TMario = 0x21 # Mario (TMario)
stage.TCaptain = 0x22 # C. Falcon (TCaptain)
stage.TClink = 0x23 # Young Link (TClink)
stage.TDonkey = 0x24 # Donkey Kong (TDonkey)
stage.TDrmario = 0x25 # Dr. Mario (TDrmario)
stage.TFalco = 0x26 # Falco (TFalco)
stage.TFox = 0x27 # Fox (TFox)
stage.TIceclim = 0x28 # Ice Climbers (TIceclim)
stage.TKirby = 0x29 # Kirby (TKirby)
stage.TKoopa = 0x2A # Bowser (TKoopa)
stage.TLink = 0x2B # Link (TLink)
stage.TLuigi = 0x2C # Luigi (TLuigi)
stage.TMars = 0x2D # Marth (TMars)
stage.TMewtwo = 0x2E # Mewtwo (TMewtwo)
stage.TNess = 0x2F # Ness (TNess)
stage.TPeach = 0x30 # Peach (TPeach)
stage.TPichu = 0x31 # Pichu (TPichu)
stage.TPikachu = 0x32 # Pikachu (TPikachu)
stage.TPurin = 0x33 # Jigglypuff (TPurin)
stage.TSamus = 0x34 # Samus (TSamus)
stage.TSeak = 0x35 # Sheik (TSeak)
stage.TYoshi = 0x36 # Yoshi (TYoshi)
stage.TZelda = 0x37 # Zelda (TZelda)
stage.TGamewat = 0x38 # Mr. Game & Watch (TGamewat)
stage.TEmblem = 0x39 # Roy (TEmblem)
stage.TGanon = 0x3A # Ganondorf (TGanon)
# ADVENTURE MODE
stage.Kinoko = 0x3B # 1 Kinoko (Mushroom Kingdom Adventure)
stage.Meiktu = 0x3F # 3 Meiktu (Zelda Adventure[Underground Maze])
stage.Dassyut = 0x42 # 4 Dassyut (Escape from Brinstar Adventure)
stage.BRoute = 0x49 # 8 B Route (F //Zero Adventure[F //Zero Grand Prix])
# BONUS STAGES
stage.Takisusume = 0x52 # Takisusume (Race to the Finish Classic)
stage.figureget = 0x53 # Grab the Trophies (figureget)
stage.homerun = 0x54 # Homerun Contest (homerun)
stage.Heart = 0x55 # Heal (All-Star's Stage Inbetween Matches)
# --- Melee Data Symbols:
global.xStageID = -0x6CB8 # current stage ID -- see stage.* symbols for IDs
stw r30, 0x0018(sp) # original instruction
bl _get_data
# --- branch over inline data:
b _end_of_data; _get_data: blrl
# --- Link:
.byte 0, 0, 0, stage.TLink
# scale = 0 (no scaling)
# type = 0 (floating points)
# spawn = Default
# count = 0 (becomes 10, to handle default)
# key = 0x2B (TLink)
.float -20,30, -10,30, 0,30, 10,30, 20,30
.float -20,20, -10,20, 0,20, 10,20, 20,20
# 10 uncompressed floating point pairs
# --- Fox:
.byte 4, 7, 4, stage.TFox
# scale = 4 (0x000.0)
# type = 7 (signed hwords)
# spawn = Default
# count = 4 (only 4 targets are set up)
# key = 0x27 (TFox)
.hword -0x800,-0x800, 0x800,-0x800
.hword -0x800, 0x800, 0x800, 0x800
# 4 compressed hword pairs, using a scale of >>4
# --- Mewtwo:
.byte 0, 6, 15, stage.TMewtwo
# type = 6 (signed bytes)
.byte -20,-10, -10,-10, 0,-10, 10,-10, 20,-10
.byte -20, 0, -10, 0, 0, 0, 10, 0, 20, 0
.byte -20, 10, -10, 10, 0, 10, 10, 10, 20, 10
.align 2 # don't forget to align your small integer pairs
# --- Game and Watch:
.byte 0, 6, 45, stage.TGamewat
# count = 50 (overflows icon display until 16 or fewer targets remain)
y = -30
.rept 5
y = y + 10
x = -40
.rept 9
x = x + 10
.byte x, y
.endr
.endr
.align 2 # assembler loop creates gird of 5 by 9 targets in GAW stage
# --- null terminator
.long 0
_end_of_data:
# now we start the injection code
rStage = 31; rData = 30; rHead = 29; rOffset = 28
rKey = 4; rArray = 5; rBytes = 6; rTotal = 7; rQuery = 8; rHead = 9; rOffset = 10
# r31 is already written
# r30 and r5 will be overwritten before the next injection; but they are free now
mflr rData
li rOffset, 0
lwz rKey, global.xStageID(r13)
# Current stage ID is now in rKey
# rOffset is pointing to the next (first) header from 'rData'
_for_each_subtable:
lwzux rHead, rData, rOffset # -u = update rData; -x = use another register for index
andi. rQuery, rHead, data.mKey # the '.' compares the result to 0 in cr0
beq- _write_to_stack
# terminate loop if a null stage key is found
# --- get table pair size:
li rBytes, 8
rlwinm. r0, rHead, 16, data.mType>>16 # r0 = extracted 3-bit type
cmpwi cr1, r0, 6
# check if type is type 0, 6, or 7
beq- 0f
li rBytes, 2
beq- cr1, 0f
li rBytes, 4
# select a data size accordingly
0: rlwimi rHead, rBytes, 0, data.mKey
# overwrite header key with size of table pair, for next injection to use
# --- check if matching:
cmpw cr1, rQuery, rKey
rlwinm. rTotal, rHead, 24, data.mCount>>8
bne+ 0f # cr0 eq means count is empty
li rTotal, 10
# then assign default of 10 targets
0:
rlwimi rHead, rTotal, 8, data.mCount
# update header word, in case it is saved for match
beq- cr1, _write_to_stack
# continue loop if no match is found by using rBytes to calculate offset of next header
# else, make some modifications to header before saving it for next injection to use...
_iterate:
andis. r0, rHead, data.mCustomSpawn@h
beq+ 0f
addi rTotal, rTotal, 1
# add 1 to count if custom spawn flag is true -- for finding next header
0:
mullw rOffset, rBytes, rTotal
addi rOffset, rOffset, 4 + 3
rlwinm rOffset, rOffset, 0, ~3
# round offset up to nearest word, to catch any mis-aligned bytes
b _for_each_subtable
# get next subtable
_write_to_stack:
addi rArray, rData, 4
sub rArray, rArray, rBytes
stw rHead, sp.xHead(sp)
stw rArray, sp.xArray(sp)
# most of the work is now done before loop begins in the spawner function
_return_from_injection:
.long 0
NTSC 1.02 ----- 801c4244 --- 28050000 -> branch
# Injects into the middle of BTT target setup routine
# - the routine sets up targets by parsing a list of global pointers left by the stage file data
# - this list of pointers includes a bit over 20 spaces, where blanks are skipped over
# - BTT stages provide 10 JObjs that get listed here to produce a spawning point for target items
# The injection interrupts the check for whether or not these pointers are null
# - this gives it power over whether or not a target item is instantiated, and what joint is used
# The only volatile register in use right now is r5:
# - r3, r4, r6...r12 are free to read/write from/to
# - r5 is safe to read from, as the current JObj being loaded to create a reference for a new target
# - overwriting r5 will affect if/what joint is used to load the next new target item
# saved registers are all in use:
# - r31 = rStage -- base of stage
# - r30 = rTPoint -- current index of array of target RefJObj pointers
# - r29 = rCount -- the counter, for storing number of targets spawned -- we can edit this
# - r28 = rID -- unknown ID that gets counted up with rCount, and is loop termination value
# - 0x8(sp) and 0xC(sp) should be free to use as padding left over in the stack frame
#
# --- Stack Symbols:
# These are stored in the runtime stack frame padding, where the next injection can make use of them
sp.xHead = 0x8 # if a matchink stage is found, then the sub-table header is copied here
sp.xArray = 0xC # the current address within an interating array
# --- Custom Data Symbols:
# This is the header struct of each sub-table in private in-line data, included with code
data.xQR = 0x0 # HWORD - data is put directly into QR7 load params, for compression type
data.xKey = 0x2 # BYTE - identifies a stage, or terminates the array of sub-tables
data.xCount = 0x3 # BYTE - counts the number of targets (allows for TSeak to be included)
data.xArrayXY = 0x4 # array of 'Count' elements of size 'QR.Type' (float, hword, or byte alignment)
data.mScale = 0x3F000000 # 6-bit mask for QR scale
data.mCustomSpawn = 0x00100000 # bool for including a custom spawn XY
data.mType = 0x00070000 # 3-bit mask for QR type
data.mCount = 0x0000FF00 # byte for counting targets
data.mOffset = 0x000000FF # byte for memorizing array element size
QR.float = 0 # data type IDs
QR.byte = 6 # signed fixed point byte
QR.hword = 7 # signed fixed point hword
# --- Melee Data Symbols:
global.xStageID = -0x6CB8 # current stage ID -- see stage.* symbols for IDs
JObj.xFlags = 0x14 # bools and small ints for JObj settings
JObj.xPosXY = 0x38 # XY loaded/stored by a paired singles instruction
JObj.xPosX_MTX = 0x50 # absolute X used by camera MTX
JObj.xPosY_MTX = 0x60 # absolute Y used by camera MTX
JObj.xDesc = 0x84 # point to JObjDesc for this JObj -- for instantiating new joints
JObj.mUSER_MTX = 0x00800000 # this mask can be ORed into a JObj bools to stop animated MTX updates
stage.xP1SpawnJObj = 0x280 # offset of pointer to live JObj used for player 1 spawner
func.HSD_JObjLoadJoint = 0x80370e44 # function for loading JObjs
# --- Register names:
rStage = 31; rPoint = 30; rCount = 29; rID = 28
rOffset = 4; rJObj = 5; rHead = 6; rArray = 7; rSpawn = 8; rTotal = 9; rBackup = 10
# rJObj is the JObj we were modifying the JObj.xPosXY_MTX values of in previous concepts
# it has just been confirmed to be a valid pointer, so we KNOW this is a JObj belonging to target
# - we can just turn off MTX updates for this JObj to stop all animations from affecting it
lwz rHead, sp.xHead(sp)
andi. r0, rHead, data.mOffset
# if a matching key was found, it was replaced with an offset value in the stack frame
# - we can check it for 0 to see if this injection should mutate the target positions with rArray
beq- _return_from_injection
# if this stage is not keyed in custom data table, then ignore the code's mutation
# - else, mutate this iteration of the natural Target Spawner loop:
# --- implement JObj limiter, for making fewer targets:
rlwinm rTotal, rHead, 24, 0xFF
cmpw cr1, rCount, rTotal
# condition register 1 holds memory of comparison between rCount and rTotal
blt cr1, _start_mutation
# if rTotal >= rCount...
li rJObj, 0
b _return_from_injection
# then nullify the JObj to prevent more from being spawned
# else, begin the mutation of this joint...
_start_mutation:
lwz rArray, sp.xArray(sp)
mr. rBackup, rJObj
lwz rSpawn, stage.xP1SpawnJObj(rStage)
# rSpawn keeps JObj of P1 spawn ready for edit and/or JObj cloning, for making dummy joints
# --- implement custom spawn location:
cmpwi rCount, 0
bne+ _begin_check # if this is the first target...
andis. r0, rHead, data.mCustomSpawn@h
beq+ _begin_check # and mCustom Spawn is used...
# then set up JObj spawner in r5:
mr rJObj, rSpawn
# this will create 2 passes in the first JObj write
# - the first will be the spawn JObj write
# - then, rBackup will be moved back to rJObj for a second pass -- to act on the first target
# --- implement JObj spawner for making extra targets:
_begin_check:
cmpwi rJObj, 0
blt+ _JObj_write # if this stage target JObj pointer was blank...
bgt+ cr1, _return_from_injection # and target counter is > specified counter...
# then return without creating more dummy joints
subi rID, rID, 1 # ID is used as terminator check, so we stall it
subi rPoint, rPoint, 4 # also stall pointer index, to prevent overflowing array
# and generate a new JObj from the Spawner JObjDesc pointer:
lwz r3, JObj.xDesc(rSpawn)
lis r0, func.HSD_JObjLoadJoint@h
ori r0, r0, func.HSD_JObjLoadJoint@l
mtlr r0
blrl # this function will instantiate a new JObj, if given a JObjDesc
mr rJObj, r3
# rJObj now has a new dummy joint to make use of
# --- implement XY translation and disabled MTX updates:
_JObj_write:
lwz rHead, sp.xHead(sp)
lwz rArray, sp.xArray(sp)
rlwinm rOffset, rHead, 0, data.mOffset
andis. r0, rHead, data.mScale | data.mType@h
# r0 = masked graphical quantization register data
# - only the load portion (high end) of the register is used
mtspr 919, r0
# load masked data into QR7
# spr 919 is the ID for GQR7
# - 0...5 are used by the OS to create static scales/types for quick casting
# - 6 and 7 can be used like volatile registers
psq_lux f0, rArray, rOffset, 0, 7
# paired singles quantized load -update -indexed
# the ending '0, 7' means 'load pair, and use QR7'
# - loads a pair using the type/scale defined in QR7
# - uses rOffset to use size variable calculated in previous injection
# - updates rArray += rOffset on load
psq_st f0, JObj.xPosXY(rJObj), 0, 0
# QR0 stores as uncompressed floating point singles
# - JObj XY has been updated, for initialization of references made by other joints
ps_merge10 f1, f0, f0
# f0 pair = XY
# f1 pair = YX -- swapped
# - this allows us to store X and Y individually, for the MTX edit
stfs f0, JObj.xPosX_MTX(rJObj)
stfs f1, JObj.xPosY_MTX(rJObj)
# manually overwrite un-paired X and Y from JObj MTX
# - JObj will now always reference this position, since MTX updates are turned off with USER_MTX
lwz r0, JObj.xFlags(rJObj)
oris r0, r0, JObj.mUSER_MTX@h
stw r0, JObj.xFlags(rJObj)
# this flag will stop the MTX from updating from normal XYZ translation/scale/rotation properties
# - joint will still update with direct edits to the MTX values
# - this prevents animations from influencing targets, making them static
stw rArray, sp.xArray(sp)
# update array pointer for next iteration
cmpw rSpawn, rJObj
bne+ _return_from_injection
mr rJObj, rBackup
b _begin_check
# rJObj will be == rSpawn if the spawner joint was just modified as part of a custom spawn
# - in this case, a second pass is executed with the JObj held in rBackup
# - else, in most cases it just returns from injection:
_return_from_injection:
cmpwi rJObj, 0 # original instruction
.long 0
Rich (BB code):
#+ 0xFF000000 = compression_scale value (signed)
#+ 0x00100000 = custom_spawn bool
#+ 0x00070000 = compression_type value (unsigned)
#+ 0x0000FF00 = target_count value (unsigned)
#+ 0x000000FF = stage_key ID (unsigned)
# - by leaving everything but the stage key as 0; default settings will be used
# if compression_type is 0, then each coordinate value in XY pair is 4 bytes
# if compression_type is 6, then each coordinate value in XY pair is 1 byte
# if compression_type is 7, then each coordinate value in XY pair is 2 bytes
# if custom_spawn is TRUE, then an extra XY coord pair is added after header to record location
# target_count determines the number of other XY coord pairs that follow
# -- if a stage_key ID is null, then it terminates the super-table (list of sub-tables)
# -- else, another sub-table follows this one
Last edited: