add client function for debugging movement data

This commit is contained in:
Martin Michelsen
2026-01-14 22:06:06 -08:00
parent 890014b223
commit 1bd305d4e7
6 changed files with 155 additions and 79 deletions
+13 -13
View File
@@ -18,7 +18,7 @@ MOVEMENT DATA 52 (DIMENIAN)
MOVEMENT DATA 53 (LA_DIMENIAN) MOVEMENT DATA 53 (LA_DIMENIAN)
MOVEMENT DATA 54 (SO_DIMENIAN) MOVEMENT DATA 54 (SO_DIMENIAN)
fparam1 = idle move speed (when returning to initial position) fparam1 = idle move speed (when returning to initial position)
fparam2 = idle animation speed fparam2 = idle walking animation speed
fparam3 = engaged move speed (when approaching a player) fparam3 = engaged move speed (when approaching a player)
fparam4 = engaged animation speed fparam4 = engaged animation speed
fparam5 = MERILLIA, MERILTAS; TODO: 3OE1:800D5750; poison cloud damage fparam5 = MERILLIA, MERILTAS; TODO: 3OE1:800D5750; poison cloud damage
@@ -36,8 +36,8 @@ iparam2 = BOOTA, ZE_BOOTA, BA_BOOTA; TODO: 59NL:005A5580
iparam2 = GORAN, PYRO_GORAN, GORAN_DETONATOR; TODO: 59NL:005ACD0F iparam2 = GORAN, PYRO_GORAN, GORAN_DETONATOR; TODO: 59NL:005ACD0F
MOVEMENT DATA 00 (MOTHMANT) MOVEMENT DATA 00 (MOTHMANT)
fparam1 = TODO: 3OE1:800B184C; NNF: chase speed fparam1 = speed when low to the ground (chase mode)
iparam2 = TODO: 3OE1:800B07E8; frames between attacks?; NNF: attack speed iparam2 = delay before attack (applies when in chase mode and reached target, or between attacks when near target)
MOVEMENT DATA 01 (MONEST) MOVEMENT DATA 01 (MONEST)
(loaded with assets but not used) (loaded with assets but not used)
@@ -85,7 +85,7 @@ iparam2 = TODO: 3OE1:8009D508; NNF: Delay after laser targetting ends before sho
iparam3 = TODO: 3OE1:8009D59C; NNF: Delay after casting Zonde iparam3 = TODO: 3OE1:8009D59C; NNF: Delay after casting Zonde
iparam4 = TODO: 3OE1:8009D5B8; NNF: Number of times Zonde is cast before they go to the next cycle iparam4 = TODO: 3OE1:8009D5B8; NNF: Number of times Zonde is cast before they go to the next cycle
iparam5 = TODO: 3OE1:8009C5C8; NNF: stun frames after being hit iparam5 = TODO: 3OE1:8009C5C8; NNF: stun frames after being hit
iparam6 = TODO: 3OE1:8009B148; number of out-fighters (see CANADINE description in Map.cc); NNF: How many of the 8 ring Canadines will cast Zonde (numbers greater than 8 are treated as 8). The remaining number out of 8 will perform melee attacks instead. Value of 0 causes FSOD. iparam6 = CANANE; TODO: 3OE1:8009B148; number of out-fighters (see CANADINE description in Map.cc); NNF: How many of the 8 ring Canadines will cast Zonde (numbers greater than 8 are treated as 8). The remaining number out of 8 will perform melee attacks instead. Value of 0 causes FSOD.
MOVEMENT DATA 07 (GEE) MOVEMENT DATA 07 (GEE)
iparam5 = TODO: 3OE1:800C9778; probably same as CANADINE (movement data 07 iparam5) iparam5 = TODO: 3OE1:800C9778; probably same as CANADINE (movement data 07 iparam5)
@@ -226,13 +226,13 @@ iparam1 = TODO: 59NL:005B56F8, 59NL:005B61DE; looks like an angle in degrees (ra
iparam2 = TODO: 59NL:005B5824; looks like an angle in degrees (range [0, 359]) iparam2 = TODO: 59NL:005B5824; looks like an angle in degrees (range [0, 359])
MOVEMENT DATA 1A (NANO_DRAGON) MOVEMENT DATA 1A (NANO_DRAGON)
fparam1 = TODO: 3OE1:800DAC68; NNF: flight speed fparam1 = horizontal flight speed
fparam2 = TODO: 3OE1:800D9C70; NNF: Straight laser speed. fparam2 = straight laser speed
fparam3 = TODO: 3OE1:800D9C70; NNF: Homing laser speed (if set too low, it will go backwards). fparam3 = homing laser speed (if set too low, it will go backwards)
fparam4 = TODO: 3OE1:800D9C70; NNF: Homing laser projectile count (projectile number = number given). fparam4 = TODO: 3OE1:800D9C70; NNF: Homing laser projectile count (projectile number = number given).
fparam5 = TODO: 3OE1:800D9C70; NNF: Homing laser arc. fparam5 = TODO: 3OE1:800D9C70; NNF: Homing laser arc.
iparam1 = TODO: 3OE1:800D9C70; NNF: Straight laser damage. iparam1 = straight laser damage
iparam2 = TODO: 3OE1:800D9C70; NNF: Homing laser damage. iparam2 = homing laser damage
MOVEMENT DATA 1A (GI_GUE) MOVEMENT DATA 1A (GI_GUE)
fparam1 = TODO: 3OE1:802CA8F4, 3OE1:802CAA04; looks like a scape factor; NNF: Speed when flying away. fparam1 = TODO: 3OE1:802CA8F4, 3OE1:802CAA04; looks like a scape factor; NNF: Speed when flying away.
@@ -462,7 +462,7 @@ fparam6 = OLGA_FLOW_1; TODO: 3OE1:802B694C; must be >0, default 7; same as movem
MOVEMENT DATA 30 (POFUILLY_SLIME) MOVEMENT DATA 30 (POFUILLY_SLIME)
MOVEMENT DATA 34 (POUILLY_SLIME) MOVEMENT DATA 34 (POUILLY_SLIME)
fparam1 = TODO: 3OE1:800E8EB4; value is max(0, fparam1); NNF: Spit damage fparam1 = spit attack damage * 5 (so e.g. 1000 here means 200 damange)
MOVEMENT DATA 30 (DELDEPTH) MOVEMENT DATA 30 (DELDEPTH)
fparam1 = TODO: 3OE1:80312E04; NNF: Movement speed (Disk form). fparam1 = TODO: 3OE1:80312E04; NNF: Movement speed (Disk form).
@@ -571,10 +571,10 @@ iparam4 = attack tech level (Rabarta in Ultimate, Gibarta otherwise)
MOVEMENT DATA 48 (HILDEBEAR) MOVEMENT DATA 48 (HILDEBEAR)
MOVEMENT DATA 49 (HILDEBLUE) MOVEMENT DATA 49 (HILDEBLUE)
fparam1 = TODO: 3OE1:800AD9A4; NNF: attack speed fparam1 = punch attack speed
fparam2 = TODO: 3OE1:800ADBE0; NNF: tech range fparam2 = TODO: 3OE1:800ADBE0; NNF: tech range
fparam3 = TODO: 3OE1:800AE17C; NNF: movement speed fparam3 = movement speed (does not affect animation speed)
fparam4 = TODO: 3OE1:800AE0D0; NNF: walking animation speed fparam4 = walking animation speed
MOVEMENT DATA 4D (GRASS_ASSASSIN) MOVEMENT DATA 4D (GRASS_ASSASSIN)
(loaded with assets but not used) (loaded with assets but not used)
+20 -2
View File
@@ -1826,7 +1826,21 @@ ChatCommandDefinition cc_patch(
string patch_name = std::move(tokens[0]); string patch_name = std::move(tokens[0]);
unordered_map<string, uint32_t> label_writes; unordered_map<string, uint32_t> label_writes;
for (size_t z = 0; z < tokens.size() - 1; z++) { for (size_t z = 0; z < tokens.size() - 1; z++) {
label_writes.emplace(std::format("arg{}", z), stoul(tokens[z + 1], nullptr, 0)); const auto& token = tokens[z + 1];
size_t equals_pos = token.find('=');
string key, value;
if (equals_pos == std::string::npos) {
key = std::format("arg{}", z);
value = token;
} else {
key = token.substr(0, equals_pos);
value = token.substr(equals_pos + 1);
}
if (value.contains('.')) { // float
label_writes.emplace(std::move(key), std::bit_cast<uint32_t>(stof(value, nullptr)));
} else { // int
label_writes.emplace(std::move(key), stoul(value, nullptr, 0));
}
} }
co_await prepare_client_for_patches(a.c); co_await prepare_client_for_patches(a.c);
@@ -1834,7 +1848,11 @@ ChatCommandDefinition cc_patch(
auto s = a.c->require_server_state(); auto s = a.c->require_server_state();
// Note: We can't look this up before prepare_client_for_patches because specific_version may not be set // Note: We can't look this up before prepare_client_for_patches because specific_version may not be set
auto fn = s->function_code_index->get_patch(patch_name, a.c->specific_version); auto fn = s->function_code_index->get_patch(patch_name, a.c->specific_version);
co_await send_function_call(a.c, fn, label_writes); auto ret = co_await send_function_call(a.c, fn, label_writes);
if (fn->show_return_value) {
send_text_message_fmt(a.c, "$C6Return value:$C7\nInt: {}\nHex: {:08X}\nFloat: {:g}",
ret.return_value.load(), ret.return_value.load(), std::bit_cast<float>(ret.return_value.load()));
}
} catch (const out_of_range&) { } catch (const out_of_range&) {
send_text_message(a.c, "$C6Invalid patch name"); send_text_message(a.c, "$C6Invalid patch name");
} }
+2
View File
@@ -313,6 +313,8 @@ static vector<shared_ptr<CompiledFunctionCode>> compile_function_code(
compiled->description = it.second; compiled->description = it.second;
} else if (it.first == "client_flag") { } else if (it.first == "client_flag") {
compiled->client_flag = stoull(it.second, nullptr, 0); compiled->client_flag = stoull(it.second, nullptr, 0);
} else if (it.first == "show_return_value") {
compiled->show_return_value = true;
} else { } else {
throw runtime_error("unknown metadata key: " + it.first); throw runtime_error("unknown metadata key: " + it.first);
} }
+1
View File
@@ -33,6 +33,7 @@ struct CompiledFunctionCode {
uint64_t client_flag = 0; // From .meta client_flag directive uint64_t client_flag = 0; // From .meta client_flag directive
uint32_t menu_item_id = 0; uint32_t menu_item_id = 0;
bool hide_from_patches_menu = false; bool hide_from_patches_menu = false;
bool show_return_value = false;
uint32_t specific_version = 0; // 0 = not a client-selectable patch uint32_t specific_version = 0; // 0 = not a client-selectable patch
bool is_big_endian() const; bool is_big_endian() const;
@@ -0,0 +1,71 @@
.meta hide_from_patches_menu
.meta name="MovementDebug"
.meta description=""
.meta show_return_value
# Usage examples:
# Read movement data 09 fparam1:
# $patch MovementDebug e=0x09 f=1 r=1
# Write to movement data 09 iparam6:
# $patch MovementDebug e=0x09 i=6 v=8
# Write to movement data 36 fparam1 (v is interpreted as float if it contains a '.'):
# $patch MovementDebug e=0x36 f=1 v=60.0
entry_ptr:
reloc0:
.offsetof start
start:
mflr r12
b get_data_ptr
get_data_ptr_ret:
mflr r11
mtlr r12
li r3, 0
lwz r7, [r11] # table index
cmplwi r7, 0x60
bgelr
lwz r8, [r13 - 0x5B70]
mulli r7, r7, 0x30
add r7, r7, r8
lwz r0, [r11 + 12] # value
lwz r9, [r11 + 16] # read-only
lwz r4, [r11 + 4] # fparam number
subi r4, r4, 1
cmplwi r4, 6
bge not_fparam
rlwinm r4, r4, 2, 0, 31
lwzx r3, [r7 + r4]
cmplwi r9, 0
bnelr
stwx [r7 + r4], r0
blr
not_fparam:
lwz r4, [r11 + 8] # iparam number
subi r4, r4, 1
cmplwi r4, 6
bgelr
rlwinm r4, r4, 2, 0, 31
addi r4, r4, 0x18
lwzx r3, [r7 + r4]
cmplwi r9, 0
bnelr
stwx [r7 + r4], r0
blr
get_data_ptr:
bl get_data_ptr_ret
e: # Movement data index
.data 0xFFFFFFFF
f: # Float param index
.zero
i: # Int parameter index
.zero
v: # Value
.zero
r: # Read-only
.zero
@@ -1,32 +1,25 @@
# This function is required for loading DOLs. If it's not present, newserv can't # This function is required for loading DOLs. If it's not present, newserv can't serve DOL files to GameCube clients.
# serve DOL files to GameCube clients.
# This is also the file I've chosen to document how to write code for newserv's # This is also the file I've chosen to document how to write code for newserv's functions subsystem. There are three
# functions subsystem. There are three kinds of functions: includes, patches, # kinds of functions: includes, patches, and general functions.
# and general functions.
# - General functions are not version-specific (usually) but are architecture- # - General functions are not version-specific (usually) but are architecture-specific. This file, WriteMemoryGC, is a
# specific. This file, WriteMemoryGC, is a general function for all PowerPC # general function for all PowerPC versions of PSO, which means all GameCube versions. General functions are named
# versions of PSO, which means all GameCube versions. General functions are # like NAME.ARCH.s, where ARCH is sh4, ppc, or x86.
# named like NAME.ARCH.s, where ARCH is sh4, ppc, or x86.
# - Includes are snippets of code that are intended to be used as part of other # - Includes are snippets of code that are intended to be used as part of other general functions and patches. Includes
# general functions and patches. Includes are named like NAME.ARCH.inc.s, # are named like NAME.ARCH.inc.s, where ARCH has the same meaning as above. These can be used with the .include
# where ARCH has the same meaning as above. These can be used with the # directive; there is an example of this in the code below.
# .include directive; there is an example of this in the code below.
# - Patches are functions that are available to run upon client request. They # - Patches are functions that are available to run upon client request. They can be made available in the Patches menu
# can be made available in the Patches menu or via the $patch command. # or via the $patch command. Patches should be named like PATCHNAME.VERS.patch.s, where VERS denotes which specific
# Patches should be named like PATCHNAME.VERS.patch.s, where VERS denotes # game version the patch is for. These version codes are listed in README.md, and directly correspond to values
# which specific game version the patch is for. These version codes are # returned by the VersionDetect functions, also in this directory.
# listed in README.md, and directly correspond to values returned by the
# VersionDetect functions, also in this directory.
# For example, to use this function to write the bytes 38 00 00 05 to the # For example, to use this function to write the bytes 38 00 00 05 to the address 8010521C, send_function_call could be
# address 8010521C, send_function_call could be called like this: # called like this:
# auto fn = s->function_code_index->name_to_function.at("WriteMemoryGC"); # auto fn = s->function_code_index->name_to_function.at("WriteMemoryGC");
# unordered_map<string, uint32_t> label_writes( # unordered_map<string, uint32_t> label_writes({{"dest_addr", 0x8010521C}, {"size", 4}});
# {{"dest_addr", 0x8010521C}, {"size", 4}});
# string suffix("\x38\x00\x00\x05", 4); # string suffix("\x38\x00\x00\x05", 4);
# send_function_call( # send_function_call(
# c, // Client to send function call to # c, // Client to send function call to
@@ -34,37 +27,33 @@
# label_writes, // Variables to pass in to the function's code # label_writes, // Variables to pass in to the function's code
# suffix); // Data to append after the code (not all functions use this) # suffix); // Data to append after the code (not all functions use this)
# The meanings of label_writes and suffix are described in the comments below. # The meanings of label_writes and suffix are described in the comments below.
# Note that there is no way to specify label_writes or suffix for patches
# requested by the client, so those features should only be used in general
# functions.
# The .versions directive may be used in patches (but not in includes or # The .versions directive may be used in patches (but not in includes or general functions) and enables
# general functions) and enables parameterization. If .version is used, then # parameterization. If .version is used, then the patch may later use expressions like <VERS value1 value2 ...> to
# the patch may later use expressions like <VERS value1 value2 ...> to generate # generate the same patch with different values for different game versions. In each <VERS> expression, the number of
# the same patch with different values for different game versions. In each # values must match the number of versions given in the .versions directive.
# <VERS> expression, the number of values must match the number of versions
# given in the .versions directive.
# .versions VRS1 VRS2 VRS3 ... # .versions VRS1 VRS2 VRS3 ...
# These directives tell newserv what to show to the player in the Patches menu. # These directives tell newserv what to show to the player in the Patches menu. Neither of them is required; if the
# Neither of them is required; if the name is omitted, the filename is used # name is omitted, the filename is used instead.
# instead.
.meta name="Write memory" .meta name="Write memory"
.meta description="Writes data to any location in memory" .meta description="Writes data to any location in memory"
# To hide a patch from the Patches menu (so it can only be used with the $patch # To hide a patch from the Patches menu (so it can only be used with the $patch command), this directive can be used.
# command), this directive can be used. This has no effect if used in includes # This has no effect if used in includes or general functions.
# or general functions.
# .meta hide_from_patches_menu # .meta hide_from_patches_menu
# The entry_ptr label is required for all functions. It should point to a # When used for debugging purposes, it may be useful to see the value returned by the client function when run via the
# .offsetof directive that itself points to the actual entrypoint. # $patch chat command. This directive causes the server to tell you the return value in-game after running it.
# .meta show_return_value
# The entry_ptr label is required for all functions. It should generally point to a .offsetof directive that itself
# points to the actual entrypoint.
entry_ptr: entry_ptr:
# All labels starting with reloc signify that the following PPC word (big-endian # All labels starting with reloc signify that the following PPC word (big-endian 32-bit value) is to be relocated at
# 32-bit value) is to be relocated at runtime. That is, when the code runs on # runtime. That is, when the code runs on the client, the PPC word will contain the actual memory address relative to
# the client, the PPC word will contain the actual memory address relative to # the running code instead of the offset that it holds at assembly time. The entry_ptr label should almost always have
# the running code instead of the offset that it holds at assembly time. The # a reloc label next to it.
# entry_ptr label should almost always have a reloc label next to it.
reloc0: reloc0:
.offsetof start .offsetof start
@@ -88,15 +77,13 @@ copy_block__again:
# Flush the data cache and clear the instruction cache at the written region # Flush the data cache and clear the instruction cache at the written region
lwz r3, [r6] # r3 = dest ptr lwz r3, [r6] # r3 = dest ptr
lwz r4, [r6 + 4] # r4 = size lwz r4, [r6 + 4] # r4 = size
# A .include directive essentially pastes in the code from the referenced # A .include directive essentially pastes in the code from the referenced file. Here, we use the code from the file
# file. Here, we use the code from the file FlushCachedCode.inc.s. When # FlushCachedCode.inc.s. When compiling includes, newserv first looks in the same directory as the function's source,
# compiling includes, newserv first looks in the same directory as the # then looks in system/client-functions/System.
# function's source, then looks in system/client-functions/System.
.include FlushCachedCode .include FlushCachedCode
# Return the address after the last byte written. The value returned in r3 # Return the address after the last byte written. The value returned in r3 from the function is sent back to the
# from the function is sent back to the server in a B3 command. newserv uses # server in a B3 command. newserv uses the return value during DOL loading to know which section of the DOL file to
# the return value during DOL loading to know which section of the DOL file to
# send next, or to send the RunDOL function if all sections have been loaded. # send next, or to send the RunDOL function if all sections have been loaded.
lwz r3, [r6] # r3 = dest ptr lwz r3, [r6] # r3 = dest ptr
lwz r4, [r6 + 4] # r4 = size lwz r4, [r6 + 4] # r4 = size
@@ -109,25 +96,22 @@ get_block_ptr__ret:
mtlr r10 mtlr r10
blr blr
get_block_ptr: get_block_ptr:
# We use a trick here to get the address of the dest_addr label: since bl puts # We use a trick here to get the address of the dest_addr label: since bl puts the immediately-following address into
# the immediately-following address into the link register, we "call" # the link register, we "call" get_block_ptr__ret and get the dest_addr pointer out of the LR. We then put r10 back
# get_block_ptr__ret and get the dest_addr pointer out of the LR. We then put # into the LR so get_block_ptr__ret returns to the caller.
# r10 back into the LR so get_block_ptr__ret returns to the caller.
mflr r10 mflr r10
bl get_block_ptr__ret bl get_block_ptr__ret
# These fields are filled in right before the command is sent to the client. # These fields are filled in right before the command is sent to the client. Specifically, the label_writes argument to
# Specifically, the label_writes argument to send_function_call is responsible # send_function_call is responsible for this. The label_writes argument is a map of label name to value, and
# for this. The label_writes argument is a map of label name to value, and # send_function_call simply writes the given values after the given labels. This is a way to pass arbitrary arguments
# send_function_call simply writes the given values after the given labels. This # to a function at call time.
# is a way to pass arbitrary arguments to a function at call time.
dest_addr: dest_addr:
.zero .zero
size: size:
.zero .zero
# Finally, we use the suffix argument to instruct send_function_call to append # Finally, we use the suffix argument to instruct send_function_call to append the data we want to write to memory
# the data we want to write to memory immediately after the assembled code. # immediately after the assembled code. (The data_to_write label here is for documentation purposes only; the suffix
# (The data_to_write label here is for documentation purposes only; the suffix
# argument always appends data after the end of all the assembled code.) # argument always appends data after the end of all the assembled code.)
data_to_write: data_to_write: