Skip to content

Instantly share code, notes, and snippets.

@charasyn
Created June 17, 2021 19:26
Show Gist options
  • Select an option

  • Save charasyn/af92641ad2a1e06395f0c114c1e7e6c4 to your computer and use it in GitHub Desktop.

Select an option

Save charasyn/af92641ad2a1e06395f0c114c1e7e6c4 to your computer and use it in GitHub Desktop.
EarthBound movement script decompiler
// NPCs and Sprites used:
// TPT 989: Body ; Spr=410 ; Mov=711
// TPT 990: Main rotor ; Spr=420 ; Mov=8,713
// TPT 991: Tail rotor ; Spr=424 ; Mov=8,714
// TPT 992: Shadow ; Spr=230 ; Mov=704,715
// Spr 106: Coordinator ; Spr=106 ; Mov=712
/* Pokey text, triggered by movement 711 once players enter region
text_pokey:
unset(flag 522)
"[1F 15 6A 00 C8 02 01]" // Create sprite 106 with movement 712
"[1F 61]" // Wait for sprite 106 to be done initializing
"[1F E7 6A 00]" // Stop sprite 106 from moving
"[1F 07 03]" // Sound effect
pause(120)
music(172)
"[1F F1 DE 03 C9 02]" // Give TPT 990 movement 713
"[1F F1 DF 03 CA 02]" // Give TPT 991 movement 714
"[1F E9 DD 03]" // Allow TPT 989 to move
"[1F 61]" // Wait for main rotor to finish spinning up
"[1F EA 6A 00]" // Allow sprite 106 to move
"[1F F1 E0 03 CB 02]" // Give TPT 992 movement 715
"[1F 61]" // Wait for helicopter to finish flying around
"[1F E7 6A 00]" // Stop sprite 106 from moving
"[1F E6 DD 03]" // Stop TPT 989 from moving
"[1F E6 DE 03]" // Stop TPT 990 from moving
"[1F E6 DF 03]" // Stop TPT 991 from moving
"[1F 15 A9 01 CC 02 01]" // Create sprite 425 with movement 716
pause(60)
window_open(1)
"@[1C 02 01], you pin-headed idiot,{pause(15)} you're just a half-step too slow!" next
"@I'm getting outta here!" next
"@Since Monotoli has become a plain,{pause(5)} old man again,"
"{pause(15)} I have no more use for him." next
"@This helicopter will really come in handy." next
"@Looks like you're the world-class loser again!"
wait
window_closeall
pause(20)
"[1F 1F A9 01 06]" // Delete sprite 425 - bye-bye to Pokey's face
"[1F EA 6A 00]" // Allow sprite 106 to move
"[1F E9 DD 03]" // Allow TPT 989 to move
"[1F E9 DE 03]" // Allow TPT 990 to move
"[1F E9 DF 03]" // Allow TPT 991 to move
"[1F 07 03]" // Sound effect
"[1F 61]" // Wait for helicopter to go offscreen
"[1F 1F 6A 00 06]" // Delete sprites
"[1F 1E DD 03 06]" // Delete TPT
"[1F 1E DE 03 06]" // Delete TPT
"[1F 1E DF 03 06]" // Delete TPT
pause(12)
set(flag 672) // 0x2a0 - prevents heli from showing next time
unset(flag 11)
eob
*/
define flag_turn_around = 0x020a
// Shadow script 1 - used when static
script_704: /* C386A9 */
movasm_disable_collisions
priority(0x03)
mov_jmp(script_8)
script_8: /* C3A2AA */
onmove(0x9FF0)
setanim(0x00)
zerovel
movasm_unk_surface_flags
movasm_update_sprite_graphics_1
script_8_on_screen: /* C3A2B8 */
mov_wait(0x08)
asmcall(0xC0C6B6) // Check if on screen (I think)
jne(script_8_on_screen)
movasm_destroy_thisobj
mov_end
// Shadow script 2 - used when flickering while heli is taking off
script_715: /* C389BD */
movasm_disable_collisions
priority(0x03)
onmove(0x9FF0)
setanim(0x00)
zerovel
movasm_update_sprite_graphics_1
mov_wait(0x20)
startloop(0x08)
setanim(0xFF)
mov_wait(0x01)
setanim(0x00)
mov_wait(0x01)
endloop
mov_jmp(unknown_C3A204)
unknown_C3A204: /* C3A204 */
movasm_destroy_thisobj
mov_end
// Invisible sprite script
script_712: /* C387B6 */
movasm_copy_sprite_position(0x019A)
onposition(0xA039)
onmove(0xA37A)
setanim(0xFF)
mov_wait(0x01)
unlock_text_script
// The text script, once being unlocked here, will immediately
// lock this movement script from executing.
mov_wait(0x01)
task(task_face_players_towards_object)
// Move the helicopter to a given position
movasm_set_speed(0x0080)
mov_var(0x05,0x0001)
mov_var(0x06,0x0D88)
mov_var(0x07,0x0ED8)
mov_jsr(sub_go_to_position)
mov_wait(0x3C)
movasm_set_speed(0x00C0)
mov_var(0x06,0x0DE8)
mov_var(0x07,0x0E78)
mov_jsr(sub_go_to_position)
mov_wait(0x1E)
mov_var(0x06,0x0DC8)
mov_jsr(sub_go_to_position)
movasm_set_speed(0x0300)
mov_var(0x05,0x0003)
mov_var(0x06,0x0D90)
mov_var(0x07,0x0EB8)
mov_jsr(sub_go_to_position)
movasm_set_speed(0x00C0)
mov_var(0x05,0x0001)
mov_var(0x06,0x0D40)
mov_var(0x07,0x0EB0)
mov_jsr(sub_go_to_position)
movasm_set_speed(0x0080)
mov_var(0x06,0x0D30)
mov_var(0x07,0x0EA8)
mov_jsr(sub_go_to_position)
movasm_setevent(flag_turn_around)
movasm_set_speed(0x0040)
mov_var(0x06,0x0D20)
mov_jsr(sub_go_to_position)
unlock_text_script
// Again, this object is locked.
// Now Pokey pokes his head out of the heli and
// talks. Once that's done, this movement code
// is re-enabled so it can fly off-screen.
mov_wait(0x01)
setxvel(0x0160)
setyvel(0xFFC0)
mov_wait(0x20)
setyvel(0xFF80)
mov_wait(0x20)
setyvel(0xFF00)
mov_wait(0x40)
zerovel
unlock_text_script // Indicate we're gone off-screen
halt
sub_go_to_position: /* C3AB59 */
mov_jsr(sub_init_movement_to_position)
sub_go_to_position_loop: /* C3AB5C */
mov_wait(0x01)
movasm_walk_toward_position
jeq(sub_go_to_position_loop)
zerovel
mov_rts
sub_init_movement_to_position: /* C3AB44 */
movasm_find_angle_toward_position
movasm_set_velocity_in_direction
movasm_update_facing_from_angle
movasm_update_facing
movasm_update_sprite_graphics_1
mov_rts
task_face_players_towards_object: /* C3AFA3 */
asmcall(0xC48B3B)
mov_wait(0x03)
mov_jmp(task_face_players_towards_object)
// Main heli body script
script_711: /* C3886C */
priority(0x00)
movasm_set_surface_flags(0x00)
onmove(0x9FC8)
setanim(0x00)
zerovel
movasm_set_surface_flags(0x00)
movasm_update_sprite_graphics_1
mov_var(0x00,0x0D80) // Region center X
mov_var(0x01,0x0ED8) // Region center Y
mov_var(0x02,0x0018) // Region width on both sides
mov_var(0x03,0x0008) // Region height on both sides
mov_jsr(sub_wait_for_player_to_enter_region)
movasm_execute_text_block(text_pokey)
mov_wait(0x01)
script_711_heli_facing_left: /* C3889F */
movasm_copy_sprite_position(106)
mov_wait(0x01)
movasm_getevent(flag_turn_around)
jeq(script_711_heli_facing_left)
priority(0x03)
movasm_set_facing_anim(0x02,0x00)
script_711_heli_facing_right: /* C388B8 */
movasm_copy_sprite_position(106)
mov_wait(0x01)
mov_jmp(script_711_heli_facing_right)
sub_wait_for_player_to_enter_region: /* C3AB8A */
mov_wait(0x01)
asmcall(0xC46E74)
jeq(sub_wait_for_player_to_enter_region)
mov_rts
// Main rotor script
script_713: /* C388C3 */
priority(0x00)
movasm_set_surface_flags(0x00)
onmove(0x9FC8)
setanim(0x00)
zerovel
movasm_set_surface_flags(0x00)
task(task_toggle_anim_after_2_frames)
task(task_set_rotor_facing_based_on_var_4)
// Animate rotor spinning up
mov_var(0x04,0x0001)
mov_wait(0x3C)
mov_var(0x04,0x0000)
mov_wait(0x06)
mov_var(0x04,0x0001)
mov_wait(0x14)
mov_var(0x04,0x0000)
mov_wait(0x10)
mov_var(0x04,0x0001)
mov_wait(0x0A)
mov_var(0x04,0x0000)
mov_wait(0x18)
mov_var(0x04,0x0001)
mov_wait(0x06)
mov_var(0x04,0x0000)
mov_wait(0x78)
// Now that rotor is ready, allow cutscene to proceed
unlock_text_script
script_713_heli_facing_left: /* C3890F */
movasm_copy_sprite_position(106)
addypos(-24)
addxpos(-8)
mov_wait(1)
movasm_getevent(flag_turn_around)
jeq(script_713_heli_facing_left)
priority(0x03)
script_713_heli_facing_right: /* C38928 */
movasm_copy_sprite_position(106)
addypos(-24)
addxpos(8)
mov_wait(1)
mov_jmp(script_713_heli_facing_right)
// Tail rotor script
script_714: /* C38939 */
priority(0x00)
movasm_set_surface_flags(0x00)
onmove(0x9FC8)
setanim(0x00)
zerovel
task(task_toggle_anim_after_2_frames)
task(task_set_rotor_facing_based_on_var_4)
mov_var(0x04,0x0000)
script_714_heli_facing_left: /* C38950 */
movasm_copy_sprite_position(106)
addypos(-16)
addxpos(16)
mov_wait(1)
movasm_getevent(flag_turn_around)
jeq(script_714_heli_facing_left)
script_714_heli_facing_right: /* C38967 */
movasm_copy_sprite_position(106)
addypos(-16)
addxpos(-16)
mov_wait(1)
mov_jmp(script_714_heli_facing_right)
// Rotor common code
task_set_rotor_facing_based_on_var_4: /* C38978 */
mov_reg_var(0x04)
jne(unknown_C38992)
movasm_getevent(flag_turn_around)
jne(unknown_C3898C)
mov_reg_imm(0x0006)
mov_jmp(unknown_C38995)
unknown_C3898C: /* C3898C */
mov_reg_imm(0x0002)
mov_jmp(unknown_C38995)
unknown_C38992: /* C38992 */
mov_reg_imm(0x0004)
unknown_C38995: /* C38995 */
movasm_update_facing
mov_wait(0x01)
mov_jmp(task_set_rotor_facing_based_on_var_4)
task_toggle_anim_after_2_frames: /* C3899E */
setanim(0x00)
movasm_update_sprite_graphics_2
mov_wait(0x01)
movasm_update_sprite_graphics_2
mov_wait(0x01)
setanim(0x01)
movasm_update_sprite_graphics_3
mov_wait(0x01)
movasm_update_sprite_graphics_3
mov_wait(0x01)
mov_jmp(task_toggle_anim_after_2_frames)
// Script for Pokey's face
script_716: /* C389DD */
movasm_copy_sprite_position(410) // Heli body
addxpos(21)
addypos(-14)
priority(0x00)
onmove(0x9FF0)
setanim(0x00)
zerovel
movasm_set_surface_flags(0x00)
movasm_update_sprite_graphics_1
// After setting position and showing the correct graphics,
// stop executing.
halt
// Once Pokey's said his spiel, this sprite will be deleted
// by the CCScript. That way, the location of his face never
// has to be changed from the movement script.
script_704: /* C386A9 */
movasm_disable_collisions
priority(0x03)
mov_jmp(unknown_C3A2AA)
script_712: /* C387B6 */
movasm_copy_sprite_position(0x019A)
onposition(0xA039)
onmove(0xA37A)
setanim(0xFF)
mov_wait(0x01)
unlock_text_script
mov_wait(0x01)
task(unknown_C3AFA3)
movasm_set_speed(0x0080)
mov_var(0x05,0x0001)
mov_var(0x06,0x0D88)
mov_var(0x07,0x0ED8)
mov_jsr(unknown_C3AB59)
mov_wait(0x3C)
movasm_set_speed(0x00C0)
mov_var(0x06,0x0DE8)
mov_var(0x07,0x0E78)
mov_jsr(unknown_C3AB59)
mov_wait(0x1E)
mov_var(0x06,0x0DC8)
mov_jsr(unknown_C3AB59)
movasm_set_speed(0x0300)
mov_var(0x05,0x0003)
mov_var(0x06,0x0D90)
mov_var(0x07,0x0EB8)
mov_jsr(unknown_C3AB59)
movasm_set_speed(0x00C0)
mov_var(0x05,0x0001)
mov_var(0x06,0x0D40)
mov_var(0x07,0x0EB0)
mov_jsr(unknown_C3AB59)
movasm_set_speed(0x0080)
mov_var(0x06,0x0D30)
mov_var(0x07,0x0EA8)
mov_jsr(unknown_C3AB59)
movasm_setevent(0x020A)
movasm_set_speed(0x0040)
mov_var(0x06,0x0D20)
mov_jsr(unknown_C3AB59)
unlock_text_script
mov_wait(0x01)
setxvel(0x0160)
setyvel(0xFFC0)
mov_wait(0x20)
setyvel(0xFF80)
mov_wait(0x20)
setyvel(0xFF00)
mov_wait(0x40)
zerovel
unlock_text_script
halt
script_711: /* C3886C */
priority(0x00)
movasm_set_surface_flags(0x00)
onmove(0x9FC8)
setanim(0x00)
zerovel
movasm_set_surface_flags(0x00)
movasm_update_sprite_graphics_1
mov_var(0x00,0x0D80)
mov_var(0x01,0x0ED8)
mov_var(0x02,0x0018)
mov_var(0x03,0x0008)
mov_jsr(unknown_C3AB8A)
movasm_execute_text_block(0x00C7BD89)
mov_wait(0x01)
unknown_C3889F: /* C3889F */
movasm_copy_sprite_position(0x006A)
mov_wait(0x01)
movasm_getevent(0x020A)
jeq(unknown_C3889F)
priority(0x03)
movasm_set_facing_anim(0x02,0x00)
unknown_C388B8: /* C388B8 */
movasm_copy_sprite_position(0x006A)
mov_wait(0x01)
mov_jmp(unknown_C388B8)
script_713: /* C388C3 */
priority(0x00)
movasm_set_surface_flags(0x00)
onmove(0x9FC8)
setanim(0x00)
zerovel
movasm_set_surface_flags(0x00)
task(unknown_C3899E)
task(unknown_C38978)
mov_var(0x04,0x0001)
mov_wait(0x3C)
mov_var(0x04,0x0000)
mov_wait(0x06)
mov_var(0x04,0x0001)
mov_wait(0x14)
mov_var(0x04,0x0000)
mov_wait(0x10)
mov_var(0x04,0x0001)
mov_wait(0x0A)
mov_var(0x04,0x0000)
mov_wait(0x18)
mov_var(0x04,0x0001)
mov_wait(0x06)
mov_var(0x04,0x0000)
mov_wait(0x78)
unlock_text_script
unknown_C3890F: /* C3890F */
movasm_copy_sprite_position(0x006A)
addypos(0xFFE8)
addxpos(0xFFF8)
mov_wait(0x01)
movasm_getevent(0x020A)
jeq(unknown_C3890F)
priority(0x03)
unknown_C38928: /* C38928 */
movasm_copy_sprite_position(0x006A)
addypos(0xFFE8)
addxpos(0x0008)
mov_wait(0x01)
mov_jmp(unknown_C38928)
script_714: /* C38939 */
priority(0x00)
movasm_set_surface_flags(0x00)
onmove(0x9FC8)
setanim(0x00)
zerovel
task(unknown_C3899E)
task(unknown_C38978)
mov_var(0x04,0x0000)
unknown_C38950: /* C38950 */
movasm_copy_sprite_position(0x006A)
addypos(0xFFF0)
addxpos(0x0010)
mov_wait(0x01)
movasm_getevent(0x020A)
jeq(unknown_C38950)
unknown_C38967: /* C38967 */
movasm_copy_sprite_position(0x006A)
addypos(0xFFF0)
addxpos(0xFFF0)
mov_wait(0x01)
mov_jmp(unknown_C38967)
unknown_C38978: /* C38978 */
mov_reg_var(0x04)
jne(unknown_C38992)
movasm_getevent(0x020A)
jne(unknown_C3898C)
mov_reg_imm(0x0006)
mov_jmp(unknown_C38995)
unknown_C3898C: /* C3898C */
mov_reg_imm(0x0002)
mov_jmp(unknown_C38995)
unknown_C38992: /* C38992 */
mov_reg_imm(0x0004)
unknown_C38995: /* C38995 */
movasm_update_facing
mov_wait(0x01)
mov_jmp(unknown_C38978)
unknown_C3899E: /* C3899E */
setanim(0x00)
movasm_update_sprite_graphics_2
mov_wait(0x01)
movasm_update_sprite_graphics_2
mov_wait(0x01)
setanim(0x01)
movasm_update_sprite_graphics_3
mov_wait(0x01)
movasm_update_sprite_graphics_3
mov_wait(0x01)
mov_jmp(unknown_C3899E)
script_715: /* C389BD */
movasm_disable_collisions
priority(0x03)
onmove(0x9FF0)
setanim(0x00)
zerovel
movasm_update_sprite_graphics_1
mov_wait(0x20)
startloop(0x08)
setanim(0xFF)
mov_wait(0x01)
setanim(0x00)
mov_wait(0x01)
endloop
mov_jmp(unknown_C3A204)
script_716: /* C389DD */
movasm_copy_sprite_position(0x019A)
addxpos(0x0015)
addypos(0xFFF2)
priority(0x00)
onmove(0x9FF0)
setanim(0x00)
zerovel
movasm_set_surface_flags(0x00)
movasm_update_sprite_graphics_1
halt
unknown_C3A204: /* C3A204 */
movasm_destroy_thisobj
mov_end
unknown_C3A2AA: /* C3A2AA */
onmove(0x9FF0)
setanim(0x00)
zerovel
movasm_unk_surface_flags
movasm_update_sprite_graphics_1
unknown_C3A2B8: /* C3A2B8 */
mov_wait(0x08)
asmcall(0xC0C6B6)
jne(unknown_C3A2B8)
movasm_destroy_thisobj
mov_end
unknown_C3AB44: /* C3AB44 */
movasm_find_angle_toward_position
movasm_set_velocity_in_direction
movasm_update_facing_from_angle
movasm_update_facing
movasm_update_sprite_graphics_1
mov_rts
unknown_C3AB59: /* C3AB59 */
mov_jsr(unknown_C3AB44)
unknown_C3AB5C: /* C3AB5C */
mov_wait(0x01)
movasm_walk_toward_position
jeq(unknown_C3AB5C)
zerovel
mov_rts
unknown_C3AB8A: /* C3AB8A */
mov_wait(0x01)
asmcall(0xC46E74)
jeq(unknown_C3AB8A)
mov_rts
unknown_C3AFA3: /* C3AFA3 */
asmcall(0xC48B3B)
mov_wait(0x03)
mov_jmp(unknown_C3AFA3)
from collections import defaultdict
import re, mmap
from dataclasses import InitVar, dataclass, field
from typing import DefaultDict, Dict, List, Set, Tuple, Union
from sortedcontainers import SortedList
from functools import total_ordering
cmds_string = '''
command mov_end "[00]"
command startloop(c) "[01 {byte c}]" // Start a loop with 'c' iterations
command endloop "[02]" // End a loop and go back to start of loop body if still haven't gone through all iterations
command mov_jml(addr) "[03 {adr24(addr)}]" // Analogous to ASM JML, but for movement scripts
command mov_jsl(addr) "[04 {adr24(addr)}]" // Analogous to ASM JSL, but for movement scripts
command mov_rtl "[05]" // Analogous to ASM RTL, but for movement scripts
command mov_wait(frames) "[06 {byte frames}]" // Wait 'frames' frames (think "`pause(frames)")
command task(addr) "[07 {short addr}]" // Register a background task, a movement script that runs alongside this movement script. Must be in the same bank as the current script
command ontick(addr) "[08 {adr24(addr)}]" // Register a "TICK callback", an ASM function that runs every frame for the object
command halt "[09]" // Halt movement script (infinitely loop it, but don't outright end it)
command jeq(addr) "[0A {short addr}]" // Jump to 'addr' if REG is zero (think "BEQ_a(addr)")
command jne(addr) "[0B {short addr}]" // Jump to 'addr' if REG is not zero (think "BNE_a(addr)")
command endtask "[0C]" // End background task (if this movement script is not a task, it just ends the script, I believe)
command mem16_and(addr, val) "[0D {short addr} 00 {short val}]" // *(uint16_t*)mem &= val
command mem16_or(addr, val) "[0D {short addr} 01 {short val}]" // *(uint16_t*)mem |= val
command mem16_add(addr, val) "[0D {short addr} 02 {short val}]" // *(uint16_t*)mem += val
command mem16_xor(addr, val) "[0D {short addr} 03 {short val}]" // *(uint16_t*)mem ^= val
command mov_var(var, value) "[0E {byte var} {short value}]" // Set a specific object VAR to immediate value ('var' must be in range 0..7)
command ontick_nop "[0F]" // Same as "ontick(0xC0943B)", but faster and saves 3 bytes
command multijmp(amount) "[10 {byte amount}]" // Switch on REG and JMP (think control code "[09 XX (YYYYYYYY)*XX]")
command multijsr(amount) "[11 {byte amount}]" // Switch on REG and JSR (think control code "[1F C0 XX (YYYYYYYY)*XX]")
command mov_mem8(addr, val) "[12 {short addr} {byte val}]" // Move 8-bit immediate value to memory
command endlasttask "[13]" // End last registered background task (if no background tasks are running, it just ends the movement script, I believe)
command var_and(var, val) "[14 {byte var} 00 {short val}]" // VAR &= val
command var_or(var, val) "[14 {byte var} 01 {short val}]" // VAR |= val
command var_add(var, val) "[14 {byte var} 02 {short val}]" // VAR += val
command var_xor(var, val) "[14 {byte var} 03 {short val}]" // VAR ^= val
command mov_mem16(addr, val) "[15 {short addr} {short val}]" // Move 16-bit immediate value to memory
command breakeq(addr) "[16 {short addr}]" // JEQ and decrement stack pointer by 3 (clean STARTLOOP stuff)
command breakne(addr) "[17 {short addr}]" // JNE and decrement stack pointer by 3 (clean STARTLOOP stuff)
command mem8_and(addr, val) "[18 {short addr} 00 {byte val}]" // *(uint8_t*)mem &= val
command mem8_or(addr, val) "[18 {short addr} 01 {byte val}]" // *(uint8_t*)mem |= val
command mem8_xor(addr, val) "[18 {short addr} 03 {byte val}]" // *(uint8_t*)mem ^= val
command mem8_add(addr, val) "[18 {short addr} 02 {byte val}]" // *(uint8_t*)mem += val
command mov_jmp(addr) "[19 {short addr}]" // Analogous to ASM JMP, but for movement scripts
command mov_jsr(addr) "[1A {short addr}]" // Analogous to ASM JSR, but for movement scripts
command mov_rts "[1B]" // Analogous to ASM RTS, but for movement scripts
command setsprmap(addr) "[1C {adr24(addr)}]" // Set pointer to spritemap data. This is kinda complicated, I also never used this, so I don't really know much about the "spritemap" format
command mov_reg_imm(val) "[1D {short val}]" // Move 16-bit immediate value to REG
command mov_reg_mem(addr) "[1E {short addr}]" // Move absolute memory address to REG
command mov_var_reg(var) "[1F {byte var}]" // Move REG to object VAR
command mov_reg_var(var) "[20 {byte var}]" // Move object VAR to REG
command wait_var(var) "[21 {byte var}]" // Wait (object VAR) frames
command ondraw(addr) "[22 {short addr}]" // Assign "draw object" callback function. Argument is a short ASM pointer in bank C0
command onposition(addr) "[23 {short addr}]" // Assign "calculate screen position" callback function. Argument is a short ASM pointer in bank C0
command startloop_reg "[24]" // Start a loop with REG iterations
command onmove(addr) "[25 {short addr}]" // Assign "move object" callback function. Argument is a short ASM pointer in bank C0
command setanim_var(var) "[26 {byte var}]" // Set amimation frame to object VAR
command reg_and(val) "[27 00 {short val}]" // REG &= val
command reg_or(val) "[27 01 {short val}]" // REG |= val
command reg_add(val) "[27 02 {short val}]" // REG += val
command reg_xor(val) "[27 03 {short val}]" // REG ^= val
command setxpos(val) "[28 {short val}]" // Set object X position
command setypos(val) "[29 {short val}]" // Set object Y position
command setzpos(val) "[2A {short val}]" // Set object Z position (elevation)
command addxpos(val) "[2B {short val}]" // Add 'val' to object X position (can be negative)
command addypos(val) "[2C {short val}]" // Add 'val' to object Y position (can be negative)
command addzpos(val) "[2D {short val}]" // Add 'val' to object Z position (elevation) (can be negative)
command addxvel(val) "[2E {short val}]" // Add 'val' to object X velocity (can be negative)
command addyvel(val) "[2F {short val}]" // Add 'val' to object Y velocity (can be negative)
command addzvel(val) "[30 {short val}]" // Add 'val' to object Z velocity (elevation) (can be negative)
command zerovel "[39]" // Set object X, Y, Z velocities to zero
command setanim(anim) "[3B {byte anim}]" // Set object animation frame
command incanim "[3C]" // Increment object animation frame
command decanim "[3D]" // Decrement object animation frame
command addanim(val) "[3E {byte val}]" // Add 'val' to object animation frame (can be negative)
command setxvel(val) "[3F {short val}]" // Set object X velocity
command setyvel(val) "[40 {short val}]" // Set object Y velocity
command setzvel(val) "[41 {short val}]" // Set object Z velocity (elevation)
command priority(val) "[43 {byte val}]" // Set object draw priority ('val' must be in range 0..3)
command wait_reg "[44]" // Wait REG frames
// Following are asmcall routines that take parameters
command movasm_update_facing "[42 5F A6 C0]"
command movasm_unk_surface_flags "[42 DB C7 C0]"
command movasm_update_sprite_graphics_1 "[42 BF A4 C0]"
command movasm_update_sprite_graphics_2 "[42 A8 A4 C0]"
command movasm_update_sprite_graphics_3 "[42 B2 A4 C0]"
command movasm_disable_collisions "[42 2F A8 C0]"
command movasm_destroy_thisobj "[42 F1 20 C0]"
command movasm_find_angle_toward_position "[42 DB 6A C4]"
command movasm_update_facing_from_angle "[42 0A 6B C4]"
command movasm_set_velocity_in_direction "[42 44 70 C4]"
command movasm_walk_toward_position "[42 DC A8 C0]"
command unlock_text_script "[42 46 6E C4]"
command asmcall_C05E76(arg1, arg2) "[42 76 5E C0 {byte arg1} {adr24(arg2)}]"
command asmcall_C09E71(arg) "[42 71 9E C0 {short arg}]"
command asmcall_C09FAE(arg) "[42 AE 9F C0 {short arg}]"
command asmcall_C09FBB(arg) "[42 BB 9F C0 {short arg}]"
command asmcall_C0A643(arg) "[42 43 A6 C0 {short arg}]"
command asmcall_C0A651(arg) "[42 51 A6 C0 {byte arg}]"
command movasm_set_surface_flags(arg) "[42 79 A6 C0 {byte arg}]"
command movasm_set_speed(speed) "[42 85 A6 C0 {short speed}]"
command asmcall_C0A6A2(arg) "[42 A2 A6 C0 {short arg}]"
command asmcall_C0A6AD(arg) "[42 AD A6 C0 {short arg}]"
command asmcall_C0A841(arg) "[42 41 A8 C0 {short arg}]"
command movasm_getevent(event) "[42 4C A8 C0 {short event}]"
command movasm_setevent(event) "[42 57 A8 C0 {short event}]"
command asmcall_C0A864(arg) "[42 64 A8 C0 {byte arg}]"
command movasm_copy_sprite_position(spr) "[42 6F A8 C0 {short spr}]"
command asmcall_C0A87A(arg, arg2) "[42 7A A8 C0 {short arg} {short arg2}]"
command movasm_execute_text_block(ptr) "[42 8D A8 C0 {longbe16(ptr)}]"
command movasm_execute_text_block2(ptr) "[42 A0 A8 C0 {longbe16(ptr)}]"
command asmcall_C0A8B3(arg, arg2) "[42 B3 A8 C0 {short arg} {short arg2}]"
command asmcall_C0A907(arg) "[42 07 A9 C0 {byte arg}]"
command asmcall_C0A912(arg, arg2, arg3) "[42 12 A9 C0 {short arg} {short arg2} {byte arg3}]"
command asmcall_C0A92D(arg) "[42 2D A9 C0 {short arg}]"
command asmcall_C0A938(arg) "[42 38 A9 C0 {short arg}]"
command asmcall_C0A943(arg) "[42 43 A9 C0 {byte arg}]"
command asmcall_C0A94E(arg) "[42 4E A9 C0 {short arg}]"
command asmcall_C0A959(arg) "[42 59 A9 C0 {short arg}]"
command asmcall_C0A964(arg, arg2) "[42 64 A9 C0 {short arg} {short arg2}]"
command asmcall_C0A977(arg, arg2) "[42 77 A9 C0 {short arg} {short arg2}]"
command asmcall_C0A98B(arg, arg2) "[42 8B A9 C0 {short arg} {short arg2}]"
command asmcall_C0A99F(arg, arg2) "[42 9F A9 C0 {short arg} {short arg2}]"
command asmcall_C0A9B3(arg, arg2, arg3) "[42 B3 A9 C0 {short arg} {short arg2} {short arg3}]"
command asmcall_C0A9CF(arg, arg2, arg3) "[42 CF A9 C0 {short arg} {short arg2} {short arg3}]"
command asmcall_C0A9EB(arg, arg2, arg3) "[42 EB A9 C0 {short arg} {short arg2} {short arg3}]"
command asmcall_C0AA07(arg, arg2, arg3) "[42 07 AA C0 {short arg} {short arg2} {short arg3}]"
command asmcall_C0AA23(arg, arg2, arg3) "[42 23 AA C0 {short arg} {short arg2} {short arg3}]"
command asmcall_C0AA3F(arg, arg2, arg3) "[42 3F AA C0 {byte arg} {byte arg2} {byte arg3}]"
command movasm_set_facing_anim(facing, anim) "[42 6E AA C0 {byte facing} {byte anim}]"
command asmcall_C0AAB5(arg, arg2, arg3) "[42 B5 AA C0 {short arg} {byte arg2} {byte arg3}]"
command asmcall_C0AAD5(arg, arg2) "[42 D5 AA C0 {byte arg} {short arg2}]"
// The actual generic asmcall is at the end so that it has lowest precedence
command asmcall(addr) "[42 {adr24(addr)}]" // Call other ASM routine
'''
def addr2off(address):
return address - 0xc00000
@dataclass
class ParseItem:
pass
@dataclass
class ParseLiteralInt(ParseItem):
value: int
@dataclass
class ParseParamReference(ParseItem):
type: str
var: str
def num_bytes(self):
return {'byte':1,'short':2,'adr24':3,'long':4,'longbe16':4}[self.type]
def match_indices(self):
return {'byte':[0],'short':[0,1],'adr24':[0,1,2],'long':[0,1,2,3],'longbe16':[2,3,0,1]}[self.type]
def parse_code_str(s: str):
parsed = []
i = 0
re_const = re.compile(r'([0-9A-Fa-f]{2})')
def check_const(x, i):
match = re_const.match(x, pos=i)
if match:
value = ParseLiteralInt(int(match.group(1), base=16))
new_i = match.end(0)
return (value, new_i)
return (None, None)
re_param = re.compile(r'\{(byte|short|adr24|long|longbe16)(?:\s+|\()(\w+)\)?\}')
def check_param(x, i):
match = re_param.match(x, pos=i)
if match:
value = ParseParamReference(match.group(1), match.group(2))
new_i = match.end(0)
return (value, new_i)
return (None, None)
check_funcs = [
check_const,
check_param
]
while i < len(s):
while s[i].isspace():
i += 1
parse_good = False
for f in check_funcs:
value, new_i = f(s, i)
if value is None:
continue
parsed.append(value)
i = new_i
parse_good = True
break
if not parse_good:
raise ValueError(f'Unable to parse "{s}" at character {i+1}')
return parsed
def build_code_re(parsed):
re_string_parts = []
for p in parsed:
if isinstance(p, ParseLiteralInt):
re_string_parts.append(b'\\x' + (f'{p.value:02x}').encode('ascii'))
elif isinstance(p, ParseParamReference):
length = p.num_bytes()
for _ in range(length):
re_string_parts.append(b'(.)')
re_string = b''.join(re_string_parts)
try:
cmp = re.compile(re_string, re.DOTALL)
return cmp
except:
print(f'Unable to compile regular expression {re_string}')
raise
@dataclass
class CommandParam:
name: str = field(init=False)
match_indices: List[int] = field(init=False)
parsed_param: InitVar[ParseParamReference]
match_offset: InitVar[int]
def __post_init__(self, parsed_param: ParseParamReference, match_offset: int):
self.name = parsed_param.var
self.match_indices = [match_offset + index for index in parsed_param.match_indices()]
def num_bytes(self):
return len(self.match_indices)
def extract_value_from_match(self, match: re.Match) -> int:
match_bytes = b''.join(match.group(i) for i in self.match_indices)
match_int = int.from_bytes(match_bytes, 'little')
return match_int
@dataclass
class Command:
name: str
code_re: re.Pattern
parameters: List[CommandParam]
# def try_match(self, inp_sequence: bytes, offset: int) -> Tuple[Union[None,CommandInstance],int]:
# pass
@dataclass(eq=True, unsafe_hash=True, frozen=True)
class Label:
name: str = field(compare=False)
address: int
@total_ordering
@dataclass
class CommandInstance:
address: int
cmd: Command
cmd_bytes: bytes = field(init=False)
param_values: Dict[str,int] = field(init=False)
input_sequence: InitVar[bytes]
match: InitVar[re.Match]
def __post_init__(self, input_sequence: bytes, match: re.Match):
# Initialize param_values
self.param_values = {}
for param in self.cmd.parameters:
self.param_values[param.name] = param.extract_value_from_match(match)
# Initialize cmd_bytes
self.cmd_bytes = match.group(0)
def command_terminates(self):
return False
def __repr__(self) -> str:
return self.to_string()
def to_string(self, show_address=False, show_bytes=False) -> str:
param_parts = []
for param in self.cmd.parameters:
match_int = self.param_values[param.name]
match_hex = '0x' + hex(match_int)[2:].rjust(param.num_bytes()*2, '0').upper()
param_parts.append(match_hex)
if len(param_parts) > 0:
cmdstr = f'{self.cmd.name}({",".join(param_parts)})'
else:
cmdstr = f'{self.cmd.name}'
if show_address:
addrstr = hex(self.address)[2:].rjust(2, '0').upper()
cmdstr = f'/* {addrstr} */ {cmdstr}'
if show_bytes:
binstr = ' '.join(hex(b)[2:].rjust(2, '0').upper() for b in self.cmd_bytes)
cmdstr = f'{cmdstr.ljust(40)} /* {binstr} */'
return cmdstr
def __eq__(self, o: object) -> bool:
if isinstance(o, int):
return self.address == o
if isinstance(o, CommandInstance):
return self.address == o.address
return NotImplemented
def __gt__(self, o: object) -> bool:
if isinstance(o, int):
return self.address > o
if isinstance(o, CommandInstance):
return self.address > o.address
return NotImplemented
@dataclass
class ReturnCommandInstance(CommandInstance):
def command_terminates(self):
return True
def __repr__(self) -> str:
return self.to_string()
@dataclass
class JumpCommandInstance(CommandInstance):
noreturn: bool
target_param: CommandParam = field(init=False)
target_param_name: InitVar[str]
def __post_init__(self, input_sequence: bytes, match: re.Match, target_param_name: str):
super().__post_init__(input_sequence, match)
self.target_param = next((p for p in self.cmd.parameters if p.name == target_param_name), None)
if self.target_param is None:
raise ValueError(f"Invalid parameter '{target_param_name}' for command '{self.cmd.name}'")
def get_target_addresses(self) -> List[int]:
l = self.target_param.num_bytes()
value = self.param_values[self.target_param.name]
if l == 2:
return [self.address & 0xff0000 | value & 0xffff]
elif l == 3 or l == 4:
return [value & 0xffffff]
else:
raise ValueError(f"Can't process address with length {l}")
def command_terminates(self):
return self.noreturn
def __repr__(self) -> str:
return self.to_string()
def to_string(self, show_address=False, show_bytes=False, labels:Dict[int,Label]={}) -> str:
param_parts = []
for param in self.cmd.parameters:
if param == self.target_param:
target_address = self.get_target_addresses()[0]
match_str = labels[target_address].name
else:
match_int = self.param_values[param.name]
match_str = '0x' + hex(match_int)[2:].rjust(param.num_bytes()*2, '0').upper()
param_parts.append(match_str)
if len(param_parts) > 0:
cmdstr = f'{self.cmd.name}({",".join(param_parts)})'
else:
cmdstr = f'{self.cmd.name}'
if show_address:
addrstr = hex(self.address)[2:].rjust(2, '0').upper()
cmdstr = f'/* {addrstr} */ {cmdstr}'
if show_bytes:
binstr = ' '.join(hex(b)[2:].rjust(2, '0').upper() for b in self.cmd_bytes)
cmdstr = f'{cmdstr.ljust(40)} /* {binstr} */'
return cmdstr
cmd_types = defaultdict(lambda: (CommandInstance,()),{
'mov_end':(ReturnCommandInstance,()),
'halt': (ReturnCommandInstance,()),
'mov_rtl':(ReturnCommandInstance,()),
'mov_rts':(ReturnCommandInstance,()),
'mov_jml':(JumpCommandInstance,(True,'addr')),
'mov_jmp':(JumpCommandInstance,(True,'addr')),
'mov_jsl':(JumpCommandInstance,(False,'addr')),
'mov_jsr':(JumpCommandInstance,(False,'addr')),
'task': (JumpCommandInstance,(False,'addr')),
'jeq': (JumpCommandInstance,(False,'addr')),
'jne': (JumpCommandInstance,(False,'addr')),
})
def init_cmds():
cmds = []
cmd_pattern = re.compile(r'command\s+(\w+)((?:\([^)]*\))?)\s+"\[([^\]]*)\]"')
for line in cmds_string.splitlines():
line, _, _ = line.partition('//')
line = line.strip()
if len(line) <= 0:
continue
match = cmd_pattern.match(line)
if not match:
print(f'Bad command line "{line}"')
continue
name, param_str, code_str = tuple(match.groups())
# Get list of parameters
param_str = param_str.strip('()')
if len(param_str) == 0:
param_list = []
else:
param_list = [x.strip() for x in param_str.split(',')]
param_to_num = {x:i for i,x in enumerate(param_list)}
# Parse code str
code_parsed = parse_code_str(code_str)
# Assign match numbers to parameters
parameters = [None for _ in enumerate(param_list)]
match_offset = 1
for p in code_parsed:
if not isinstance(p, ParseParamReference):
continue
pobj = CommandParam(p, match_offset)
parameters[param_to_num[p.var]] = pobj
match_offset += p.num_bytes()
# Build code parsing regular expression
code_re = build_code_re(code_parsed)
cmd = Command(name, code_re, parameters)
cmds.append(cmd)
return cmds
cmds: List[Command] = init_cmds()
def try_create_command_instance(input_sequence: bytes, address: int) -> Union[None,CommandInstance]:
for cmd in cmds:
m = cmd.code_re.match(input_sequence, addr2off(address))
if m is None:
continue
# We have a match!
cmd_type, cmd_init_params = cmd_types[cmd.name]
inst = cmd_type(address,cmd,input_sequence,m,*cmd_init_params)
return inst
return None
funcs = {
# 0xC3878C:"script_710",
0xC386A9:"script_704",
0xC3886C:"script_711",
0xC387B6:"script_712",
0xC388C3:"script_713",
0xC38939:"script_714",
0xC389BD:"script_715",
0xC389DD:"script_716",
# 0xC389FB:"script_717",
# 0xC38AB1:"script_718",
# 0xC38ADC:"script_719",
# 0xC38B3A:"script_720",
}
with open('ebexpanded.smc','rb') as f:
mm = mmap.mmap(f.fileno(), 0x400000, access=mmap.ACCESS_READ)
labels = {f_addr: Label(f_name, f_addr) for f_addr, f_name in funcs.items()}
label_addresses: Set[int] = set(f_addr for f_addr, f_name in funcs.items())
disasm = SortedList()
to_process = [x for x in funcs.keys()]
for start_addr in to_process:
if start_addr in funcs:
f_name = funcs[start_addr]
else:
f_name = f'unknown_{start_addr:06X}'
address = start_addr
while True:
if address in disasm:
break
cmd_inst = try_create_command_instance(mm, address)
if cmd_inst is None:
raise ValueError(f'illegal command at address 0x{address:06X}')
disasm.add(cmd_inst)
if isinstance(cmd_inst, JumpCommandInstance):
for target in cmd_inst.get_target_addresses():
label_addresses.add(target)
labels[target] = Label(f'unknown_{target:06X}', target)
if target in disasm or target in to_process:
continue
to_process.append(target)
if cmd_inst.command_terminates():
break
address += len(cmd_inst.cmd_bytes)
for tmp_x in disasm:
disasm_line: Union[CommandInstance, JumpCommandInstance, ReturnCommandInstance] = tmp_x
if disasm_line.address in label_addresses:
label = labels[disasm_line.address]
print(f'{label.name}: /* {label.address:06X} */')
if isinstance(disasm_line, JumpCommandInstance):
dasm_text = disasm_line.to_string(labels=labels)
else:
dasm_text = disasm_line.to_string()
print(f' {dasm_text}')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment