diff --git a/build-windows-visual-studio/sm64ex.vcxproj b/build-windows-visual-studio/sm64ex.vcxproj
index 77db1e7ef..6ff0acf03 100644
--- a/build-windows-visual-studio/sm64ex.vcxproj
+++ b/build-windows-visual-studio/sm64ex.vcxproj
@@ -532,6 +532,7 @@
+
@@ -993,6 +994,7 @@
+
diff --git a/build-windows-visual-studio/sm64ex.vcxproj.filters b/build-windows-visual-studio/sm64ex.vcxproj.filters
index b8e39ffc0..b79a4bffe 100644
--- a/build-windows-visual-studio/sm64ex.vcxproj.filters
+++ b/build-windows-visual-studio/sm64ex.vcxproj.filters
@@ -4878,6 +4878,9 @@
Source Files\src\pc\djui\panel
+
+ Source Files\src\pc\network
+
@@ -6034,5 +6037,8 @@
Source Files\src\pc\djui\panel
+
+ Source Files\src\pc\network
+
\ No newline at end of file
diff --git a/docs/lua/constants.md b/docs/lua/constants.md
index 8a342e486..607a74b4c 100644
--- a/docs/lua/constants.md
+++ b/docs/lua/constants.md
@@ -1390,8 +1390,7 @@
| HOOK_ON_PVP_ATTACK | 5 |
| HOOK_ON_PLAYER_CONNECTED | 6 |
| HOOK_ON_PLAYER_DISCONNECTED | 7 |
-| HOOK_ON_CHAT_COMMAND | 8 |
-| HOOK_MAX | 9 |
+| HOOK_MAX | 8 |
diff --git a/docs/lua/hooks.md b/docs/lua/hooks.md
index 5d81bccaa..c0cb5f416 100644
--- a/docs/lua/hooks.md
+++ b/docs/lua/hooks.md
@@ -16,7 +16,6 @@ Hooks are a way for SM64 to trigger Lua code, whereas the functions listed in [f
| HOOK_ON_PVP_ATTACK | Called when one player attacks another | [MarioState](structs.md#MarioState) attacker, [MarioState](structs.md#MarioState) victim |
| HOOK_ON_PLAYER_CONNECTED | Called when a player connects | [MarioState](structs.md#MarioState) connector |
| HOOK_ON_PLAYER_DISCONNECTED | Called when a player disconnects | [MarioState](structs.md#MarioState) disconnector |
-| HOOK_ON_CHAT_COMMAND | Called when a player types a chat message starting with '/' | `string` message -> `return` `true` on valid command |
@@ -114,3 +113,33 @@ hook_mario_action(ACT_WALL_SLIDE, act_wall_slide)
```
[:arrow_up_small:](#)
+
+## [hook_chat_command](#hook_chat_command)
+`hook_chat_command()` allows Lua mods to react and respond to chat commands. Chat commands start with the `/` character. The function the mod passes to the hook should return `true` when the command was valid and `false` otherwise.
+
+### Parameters
+
+| Field | Type |
+| ----- | ---- |
+| command | string |
+| description | string |
+| func | Lua Function |
+
+### Lua Example
+
+```lua
+function on_test_command(msg)
+ if msg == 'on' then
+ djui_chat_message_create('Test: enabled')
+ return true
+ elseif msg == 'off' then
+ djui_chat_message_create('Test: disabled')
+ return true
+ end
+ return false
+end
+
+hook_chat_command('test', "[on|off] turn test on or off", on_hide_and_seek_command)
+```
+
+[:arrow_up_small:](#)
diff --git a/mods/hide-and-seek.lua b/mods/hide-and-seek.lua
index 12277d08d..cfa33feb2 100644
--- a/mods/hide-and-seek.lua
+++ b/mods/hide-and-seek.lua
@@ -9,6 +9,9 @@ for i=0,(MAX_PLAYERS-1) do
sCachedState[i].seeking = false
end
+-- globally sync enabled state
+gGlobalSyncTable.hideAndSeek = true
+
-- keep track of round numbers for popup
sCachedRoundNumber = 0
gGlobalSyncTable.roundNumber = 0
@@ -32,8 +35,10 @@ function server_update(m)
local hasSeeker = false
local hasHider = false
local activePlayers = {}
+ local connectedCount = 0
for i=0,(MAX_PLAYERS-1) do
if gNetworkPlayers[i].connected then
+ connectedCount = connectedCount + 1
table.insert(activePlayers, gPlayerSyncTable[i])
if gPlayerSyncTable[i].seeking then
hasSeeker = true
@@ -43,6 +48,12 @@ function server_update(m)
end
end
+ -- only change state if there are 2+ players
+ if connectedCount < 2 then
+ sStaleTimer = 0
+ return
+ end
+
-- increment stale timer
if not hasHider or not hasSeeker then
sStaleTimer = sStaleTimer + 1
@@ -80,6 +91,11 @@ function server_update(m)
end
function update()
+ -- check gamemode enabled state
+ if not gGlobalSyncTable.hideAndSeek then
+ return
+ end
+
-- only allow the server to figure out the seeker
if network_is_server() then
server_update(gMarioStates[0])
@@ -133,6 +149,11 @@ function mario_local_update(m)
end
function mario_update(m)
+ -- check gamemode enabled state
+ if not gGlobalSyncTable.hideAndSeek then
+ return
+ end
+
-- this code runs for all players
local s = gPlayerSyncTable[m.playerIndex]
@@ -161,6 +182,11 @@ function mario_update(m)
end
function mario_before_phys_step(m)
+ -- check gamemode enabled state
+ if not gGlobalSyncTable.hideAndSeek then
+ return
+ end
+
local s = gPlayerSyncTable[m.playerIndex]
-- only make seekers faster
@@ -190,6 +216,11 @@ function mario_before_phys_step(m)
end
function on_pvp_attack(attacker, victim)
+ -- check gamemode enabled state
+ if not gGlobalSyncTable.hideAndSeek then
+ return
+ end
+
-- this code runs when a player attacks another player
local sAttacker = gPlayerSyncTable[attacker.playerIndex]
local sVictim = gPlayerSyncTable[victim.playerIndex]
@@ -211,6 +242,19 @@ function on_player_connected(m)
s.seeking = false
end
+function on_hide_and_seek_command(msg)
+ if msg == 'on' then
+ djui_chat_message_create('Hide-and-seek mod: enabled')
+ gGlobalSyncTable.hideAndSeek = true
+ return true
+ elseif msg == 'off' then
+ djui_chat_message_create('Hide-and-seek mod: disabled')
+ gGlobalSyncTable.hideAndSeek = false
+ return true
+ end
+ return false
+end
+
-----------
-- hooks --
-----------
@@ -220,3 +264,5 @@ hook_event(HOOK_MARIO_UPDATE, mario_update)
hook_event(HOOK_BEFORE_PHYS_STEP, mario_before_phys_step)
hook_event(HOOK_ON_PVP_ATTACK, on_pvp_attack)
hook_event(HOOK_ON_PLAYER_CONNECTED, on_player_connected)
+
+hook_chat_command('hide', "[on|off] turn hide-and-seek on or off", on_hide_and_seek_command)
diff --git a/src/pc/djui/djui_chat_box.c b/src/pc/djui/djui_chat_box.c
index 81cb2b13c..9dee5b94a 100644
--- a/src/pc/djui/djui_chat_box.c
+++ b/src/pc/djui/djui_chat_box.c
@@ -33,8 +33,9 @@ static void djui_chat_box_input_enter(struct DjuiInputbox* chatInput) {
if (strlen(chatInput->buffer) != 0) {
if (chatInput->buffer[0] == '/') {
- if (!smlua_call_event_hook_on_chat_command(chatInput->buffer)) {
+ if (!smlua_call_chat_command_hook(chatInput->buffer)) {
djui_chat_message_create("Unrecognized chat command.");
+ smlua_display_chat_commands();
}
} else {
djui_chat_message_create_from(gNetworkPlayerLocal->globalIndex, chatInput->buffer);
diff --git a/src/pc/lua/smlua_constants_autogen.c b/src/pc/lua/smlua_constants_autogen.c
index 9c95a2eb8..c512a13cf 100644
--- a/src/pc/lua/smlua_constants_autogen.c
+++ b/src/pc/lua/smlua_constants_autogen.c
@@ -1439,8 +1439,7 @@ char gSmluaConstants[] = ""
"HOOK_ON_PVP_ATTACK = 5\n"
"HOOK_ON_PLAYER_CONNECTED = 6\n"
"HOOK_ON_PLAYER_DISCONNECTED = 7\n"
-"HOOK_ON_CHAT_COMMAND = 8\n"
-"HOOK_MAX = 9\n"
+"HOOK_MAX = 8\n"
"SPTASK_STATE_NOT_STARTED = 0\n"
"SPTASK_STATE_RUNNING = 1\n"
"SPTASK_STATE_INTERRUPTED = 2\n"
diff --git a/src/pc/lua/smlua_hooks.c b/src/pc/lua/smlua_hooks.c
index 3fb4eafaf..74e319e7c 100644
--- a/src/pc/lua/smlua_hooks.c
+++ b/src/pc/lua/smlua_hooks.c
@@ -1,4 +1,5 @@
#include "smlua.h"
+#include "pc/djui/djui_chat_message.h"
#define MAX_HOOKED_REFERENCES 64
@@ -125,31 +126,6 @@ void smlua_call_event_hooks_network_player_param(enum LuaHookedEventType hookTyp
}
}
-bool smlua_call_event_hook_on_chat_command(char* message) {
- lua_State* L = gLuaState;
- if (L == NULL) { return false; }
- bool ret = false;
-
- struct LuaHookedEvent* hook = &sHookedEvents[HOOK_ON_CHAT_COMMAND];
- for (int i = 0; i < hook->count; i++) {
- // push the callback onto the stack
- lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference[i]);
-
- // push message
- lua_pushstring(L, message);
-
- // call the callback
- if (0 != lua_pcall(L, 1, 1, 0)) {
- LOG_LUA("Failed to call the callback: %s", lua_tostring(L, -1));
- continue;
- }
-
- ret = ret || smlua_to_boolean(L, -1);
- }
-
- return ret;
-}
-
////////////////////
// hooked actions //
////////////////////
@@ -172,6 +148,11 @@ int smlua_hook_mario_action(lua_State* L) {
}
lua_Integer action = smlua_to_integer(L, -2);
+ if (action == 0 || gSmLuaConvertSuccess) {
+ LOG_LUA("Hook Action: tried to hook invalid action");
+ return 0;
+ }
+
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
if (ref == -1) {
@@ -221,6 +202,111 @@ bool smlua_call_action_hook(struct MarioState* m, s32* returnValue) {
return false;
}
+ /////////////////////////
+ // hooked chat command //
+/////////////////////////
+
+struct LuaHookedChatCommand {
+ char* command;
+ char* description;
+ int reference;
+};
+
+#define MAX_HOOKED_CHAT_COMMANDS 64
+
+static struct LuaHookedChatCommand sHookedChatCommands[MAX_HOOKED_CHAT_COMMANDS] = { 0 };
+static int sHookedChatCommandsCount = 0;
+
+int smlua_hook_chat_command(lua_State* L) {
+ if (L == NULL) { return 0; }
+ if (sHookedChatCommandsCount >= MAX_HOOKED_CHAT_COMMANDS) {
+ LOG_LUA("Hooked chat command exceeded maximum references!");
+ return 0;
+ }
+
+ const char* command = smlua_to_string(L, 1);
+ if (command == NULL || strlen(command) == 0 || !gSmLuaConvertSuccess) {
+ LOG_LUA("Hook chat command: tried to hook invalid command");
+ return 0;
+ }
+
+ const char* description = smlua_to_string(L, 2);
+ if (description == NULL || strlen(description) == 0 || !gSmLuaConvertSuccess) {
+ LOG_LUA("Hook chat command: tried to hook invalid description");
+ return 0;
+ }
+
+ int ref = luaL_ref(L, LUA_REGISTRYINDEX);
+ if (ref == -1) {
+ LOG_LUA("Hook chat command: tried to hook undefined function '%s'", command);
+ return 0;
+ }
+
+ struct LuaHookedChatCommand* hooked = &sHookedChatCommands[sHookedChatCommandsCount];
+ hooked->command = strdup(command);
+ hooked->description = strdup(description);
+ hooked->reference = ref;
+ if (!gSmLuaConvertSuccess) { return 0; }
+
+ sHookedChatCommandsCount++;
+ return 1;
+}
+
+bool smlua_call_chat_command_hook(char* command) {
+ lua_State* L = gLuaState;
+ if (L == NULL) { return false; }
+ for (int i = 0; i < sHookedChatCommandsCount; i++) {
+ struct LuaHookedChatCommand* hook = &sHookedChatCommands[i];
+ size_t commandLength = strlen(hook->command);
+ for (size_t j = 0; j < commandLength; j++) {
+ if (hook->command[j] != command[j + 1]) {
+ goto NEXT_HOOK;
+ }
+ }
+
+ char* params = &command[commandLength + 1];
+ if (*params != '\0' && *params != ' ') {
+ goto NEXT_HOOK;
+ }
+ if (*params == ' ') {
+ params++;
+ }
+
+ // push the callback onto the stack
+ lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference);
+
+ // push parameter
+ lua_pushstring(L, params);
+
+ // call the callback
+ if (0 != lua_pcall(L, 1, 1, 0)) {
+ LOG_LUA("Failed to call the callback: %s", lua_tostring(L, -1));
+ continue;
+ }
+
+ // output the return value
+ bool returnValue = smlua_to_boolean(L, -1);
+ lua_pop(L, 1);
+
+ if (!gSmLuaConvertSuccess) { return false; }
+
+ return returnValue;
+
+NEXT_HOOK:;
+ }
+
+ return false;
+}
+
+void smlua_display_chat_commands(void) {
+ for (int i = 0; i < sHookedChatCommandsCount; i++) {
+ struct LuaHookedChatCommand* hook = &sHookedChatCommands[i];
+ char msg[256] = { 0 };
+ snprintf(msg, 256, "/%s %s", hook->command, hook->description);
+ djui_chat_message_create(msg);
+ }
+}
+
//////////
// misc //
//////////
@@ -238,6 +324,17 @@ static void smlua_clear_hooks(void) {
sHookedMarioActions[i].reference = 0;
}
sHookedMarioActionsCount = 0;
+
+ for (int i = 0; i < sHookedChatCommandsCount; i++) {
+ if (sHookedChatCommands[i].command != NULL) { free(sHookedChatCommands[i].command); }
+ sHookedChatCommands[i].command = NULL;
+
+ if (sHookedChatCommands[i].description != NULL) { free(sHookedChatCommands[i].description); }
+ sHookedChatCommands[i].description = NULL;
+
+ sHookedChatCommands[i].reference = 0;
+ }
+ sHookedChatCommandsCount = 0;
}
void smlua_bind_hooks(void) {
@@ -246,5 +343,5 @@ void smlua_bind_hooks(void) {
smlua_bind_function(L, "hook_event", smlua_hook_event);
smlua_bind_function(L, "hook_mario_action", smlua_hook_mario_action);
-
+ smlua_bind_function(L, "hook_chat_command", smlua_hook_chat_command);
}
diff --git a/src/pc/lua/smlua_hooks.h b/src/pc/lua/smlua_hooks.h
index 855852c62..0d459f7d8 100644
--- a/src/pc/lua/smlua_hooks.h
+++ b/src/pc/lua/smlua_hooks.h
@@ -12,7 +12,6 @@ enum LuaHookedEventType {
HOOK_ON_PVP_ATTACK,
HOOK_ON_PLAYER_CONNECTED,
HOOK_ON_PLAYER_DISCONNECTED,
- HOOK_ON_CHAT_COMMAND,
HOOK_MAX,
};
@@ -25,17 +24,18 @@ static char* LuaHookedEventTypeName[] = {
"HOOK_ON_PVP_ATTACK",
"HOOK_ON_PLAYER_CONNECTED",
"HOOK_ON_PLAYER_DISCONNECTED",
- "HOOK_ON_CHAT_COMMAND",
"HOOK_MAX"
};
void smlua_call_event_hooks(enum LuaHookedEventType hookType);
void smlua_call_event_hooks_mario_param(enum LuaHookedEventType hookType, struct MarioState* m);
void smlua_call_event_hooks_mario_params(enum LuaHookedEventType hookType, struct MarioState* m1, struct MarioState* m2);
-bool smlua_call_event_hook_on_chat_command(char* message);
bool smlua_call_action_hook(struct MarioState* m, s32* returnValue);
+bool smlua_call_chat_command_hook(char* command);
+void smlua_display_chat_commands(void);
+
void smlua_bind_hooks(void);
#endif
\ No newline at end of file