diff --git a/README.md b/README.md
index d42201b2..47b34798 100644
--- a/README.md
+++ b/README.md
@@ -558,6 +558,7 @@ Some commands only work on the game server and not on the proxy server. The chat
* The rest of the commands in this section are enabled on the game server. (They are always enabled on the proxy server.)
* `$readmem
` (game server only): Read 4 bytes from the given address and show you the values.
* `$writemem ` (game server only): Write data to the given address. Data is not required to be any specific size.
+ * `$nativecall [arg1 ...]` (game server only, GC only): Call a native function on your client. Only arguments passed in registers are supported; calling functions that take many arguments is not supported.
* `$quest ` (game server only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present. Debug is not required to be enabled if the specified quest has the AllowStartFromChatCommand field set in its metadata file.
* `$qcall `: Call a quest function on your client.
* `$qcheck ` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only).
diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc
index ec21e204..4c566beb 100644
--- a/src/ChatCommands.cc
+++ b/src/ChatCommands.cc
@@ -2830,6 +2830,56 @@ ChatCommandDefinition cc_writemem(
},
unavailable_on_proxy_server);
+ChatCommandDefinition cc_nativecall(
+ {"$nativecall"},
+ +[](const ServerArgs& a) -> void {
+ a.check_debug_enabled();
+
+ // TODO: $nativecall is not implemented on x86 (yet) because there are
+ // multiple calling conventions used within the executable (at least on
+ // Xbox and BB), so we would need a way to specify which calling
+ // convention to use, which would be annoying
+ if (is_x86(a.c->version())) {
+ throw precondition_failed("Command not supported\non x86 clients");
+ }
+
+ auto tokens = phosg::split(a.text, ' ');
+ if (tokens.size() < 1) {
+ throw precondition_failed("Incorrect arguments");
+ }
+
+ uint32_t addr = stoul(tokens[0], nullptr, 16);
+ if (!console_address_in_range(a.c->version(), addr)) {
+ throw precondition_failed("$C4Function address\nout of range");
+ }
+
+ unordered_map label_writes{{"call_addr", addr}};
+ for (size_t z = 0; z < tokens.size() - 1; z++) {
+ label_writes.emplace(phosg::string_printf("arg%zu", z), stoull(tokens[z + 1], nullptr, 16));
+ }
+
+ prepare_client_for_patches(a.c, [wc = weak_ptr(a.c), label_writes = std::move(label_writes)]() {
+ auto c = wc.lock();
+ if (!c) {
+ return;
+ }
+ try {
+ auto s = c->require_server_state();
+ const char* function_name = is_dc(c->version())
+ ? "CallNativeFunctionDC"
+ : is_gc(c->version())
+ ? "CallNativeFunctionGC"
+ : "CallNativeFunctionX86";
+ auto fn = s->function_code_index->name_to_function.at(function_name);
+ send_function_call(c, fn, label_writes);
+ c->function_call_response_queue.emplace_back(empty_function_call_response_handler);
+ } catch (const out_of_range&) {
+ throw precondition_failed("Invalid patch name");
+ }
+ });
+ },
+ unavailable_on_proxy_server);
+
////////////////////////////////////////////////////////////////////////////////
// Dispatch methods
diff --git a/system/client-functions/System/CallNativeFunctionDC.sh4.s b/system/client-functions/System/CallNativeFunctionDC.sh4.s
new file mode 100644
index 00000000..a4c7a6ef
--- /dev/null
+++ b/system/client-functions/System/CallNativeFunctionDC.sh4.s
@@ -0,0 +1,30 @@
+# This function implements the $nativecall chat command on DC clients.
+
+entry_ptr:
+reloc0:
+ .offsetof start
+
+start:
+ sts.l -[r15], pr
+ mov.l r0, [call_addr]
+ mov.l r4, [arg0]
+ mov.l r5, [arg1]
+ mov.l r6, [arg2]
+ mov.l r7, [arg3]
+ calls r0
+ nop
+ lds.l pr, [r15]+
+ rets
+ nop
+
+ .align 4
+call_addr:
+ .zero
+arg0:
+ .zero
+arg1:
+ .zero
+arg2:
+ .zero
+arg3:
+ .zero
diff --git a/system/client-functions/System/CallNativeFunctionGC.ppc.s b/system/client-functions/System/CallNativeFunctionGC.ppc.s
new file mode 100644
index 00000000..1ac71bb9
--- /dev/null
+++ b/system/client-functions/System/CallNativeFunctionGC.ppc.s
@@ -0,0 +1,52 @@
+# This function implements the $nativecall chat command on GameCube clients.
+
+entry_ptr:
+reloc0:
+ .offsetof start
+
+start:
+ mflr r0
+ stw [r1 + 4], r0
+ stwu [r1 - 0x20], r1
+ bl read
+call_addr:
+ .zero
+arg0:
+ .zero
+arg1:
+ .zero
+arg2:
+ .zero
+arg3:
+ .zero
+arg4:
+ .zero
+arg5:
+ .zero
+arg6:
+ .zero
+arg7:
+ .zero
+arg8:
+ .zero
+arg9:
+ .zero
+resume:
+ mflr r12
+ lwz r0, [r12] # call_addr
+ lwz r3, [r12 + 0x04] # arg0
+ lwz r4, [r12 + 0x08] # arg1
+ lwz r5, [r12 + 0x0C] # arg2
+ lwz r6, [r12 + 0x10] # arg3
+ lwz r7, [r12 + 0x14] # arg4
+ lwz r8, [r12 + 0x18] # arg5
+ lwz r9, [r12 + 0x1C] # arg6
+ lwz r10, [r12 + 0x20] # arg7
+ lwz r11, [r12 + 0x24] # arg8
+ lwz r12, [r12 + 0x28] # arg9
+ mtctr r0
+ bctrl
+ addi r1, r1, 0x20
+ lwz r0, [r1 + 4]
+ mtlr r0
+ blr