Add surface collision Lua hooks (#1139)
Some checks are pending
Build coop / build-linux (push) Waiting to run
Build coop / build-steamos (push) Waiting to run
Build coop / build-windows-opengl (push) Waiting to run
Build coop / build-windows-directx (push) Waiting to run
Build coop / build-macos-arm (push) Waiting to run
Build coop / build-macos-intel (push) Waiting to run

* Add surface collision Lua hooks

- Add 6 new hooks:
    HOOK_ON_FIND_WALL_COLLISION,
    HOOK_ON_FIND_CEIL,
    HOOK_ON_FIND_FLOOR,
    HOOK_ON_FIND_WATER_LEVEL,
    HOOK_ON_FIND_POISON_GAS_LEVEL,
    HOOK_ON_FIND_SURFACE_ON_RAY

- Hooks expose current result values and allow overriding collision outputs from Lua

* Remove unnecessary branches

---------

Co-authored-by: MysterD <myster@d>
This commit is contained in:
djoslin0 2026-03-15 06:04:36 -07:00 committed by GitHub
parent 8fe56ab999
commit 2c367b556d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 368 additions and 6 deletions

View file

@ -8207,7 +8207,13 @@ HOOK_MARIO_OVERRIDE_FLOOR_CLASS = 56 --- @type LuaHookedEventType
HOOK_ON_ADD_SURFACE = 57 --- @type LuaHookedEventType
HOOK_ON_CLEAR_AREAS = 58 --- @type LuaHookedEventType
HOOK_ON_PACKET_BYTESTRING_RECEIVE = 59 --- @type LuaHookedEventType
HOOK_MAX = 60 --- @type LuaHookedEventType
HOOK_ON_FIND_WALL_COLLISION = 60 --- @type LuaHookedEventType
HOOK_ON_FIND_CEIL = 61 --- @type LuaHookedEventType
HOOK_ON_FIND_FLOOR = 62 --- @type LuaHookedEventType
HOOK_ON_FIND_WATER_LEVEL = 63 --- @type LuaHookedEventType
HOOK_ON_FIND_POISON_GAS_LEVEL = 64 --- @type LuaHookedEventType
HOOK_ON_FIND_SURFACE_ON_RAY = 65 --- @type LuaHookedEventType
HOOK_MAX = 66 --- @type LuaHookedEventType
--- @alias LuaHookedEventType
--- | `HOOK_UPDATE`
@ -8270,6 +8276,12 @@ HOOK_MAX = 60 --- @type LuaHookedEventType
--- | `HOOK_ON_ADD_SURFACE`
--- | `HOOK_ON_CLEAR_AREAS`
--- | `HOOK_ON_PACKET_BYTESTRING_RECEIVE`
--- | `HOOK_ON_FIND_WALL_COLLISION`
--- | `HOOK_ON_FIND_CEIL`
--- | `HOOK_ON_FIND_FLOOR`
--- | `HOOK_ON_FIND_WATER_LEVEL`
--- | `HOOK_ON_FIND_POISON_GAS_LEVEL`
--- | `HOOK_ON_FIND_SURFACE_ON_RAY`
--- | `HOOK_MAX`
--- @type integer

View file

@ -127,7 +127,7 @@ function update_chat_command_description(command, description)
end
--- @param hookEventType LuaHookedEventType When a function should run
--- @param func fun(...: any): any The function to run
--- @param func fun(...: any): any?, any? The function to run
--- Different hooks can pass in different parameters and have different return values. Be sure to read the hooks guide for more information.
function hook_event(hookEventType, func)
-- ...

View file

@ -3537,7 +3537,13 @@
| HOOK_ON_ADD_SURFACE | 57 |
| HOOK_ON_CLEAR_AREAS | 58 |
| HOOK_ON_PACKET_BYTESTRING_RECEIVE | 59 |
| HOOK_MAX | 60 |
| HOOK_ON_FIND_WALL_COLLISION | 60 |
| HOOK_ON_FIND_CEIL | 61 |
| HOOK_ON_FIND_FLOOR | 62 |
| HOOK_ON_FIND_WATER_LEVEL | 63 |
| HOOK_ON_FIND_POISON_GAS_LEVEL | 64 |
| HOOK_ON_FIND_SURFACE_ON_RAY | 65 |
| HOOK_MAX | 66 |
- MAX_HOOKED_BEHAVIORS
[:arrow_up_small:](#)

View file

@ -151,6 +151,12 @@ The lua functions sent to `hook_event()` will be automatically called by SM64 wh
| HOOK_MARIO_OVERRIDE_FLOOR_CLASS | Called when Mario's floor class logic updates, return a `SURFACE_CLASS_*` constant to override the type. | [MarioState](../structs.md#MarioState) mario, `integer` surfaceClass |
| HOOK_ON_ADD_SURFACE | Called when collision surfaces are added. | [Surface](../structs.md#Surface) surface, `boolean` dynamic |
| HOOK_ON_CLEAR_AREAS | Called when a level's areas are unloaded. | None |
| HOOK_ON_FIND_WALL_COLLISION | Called after wall collision detection completes. You can modify the `colData` fields directly. Return a number to override `numCollisions` | `number` posX, `number` posY, `number` posZ, [WallCollisionData](../structs.md#WallCollisionData) colData |
| HOOK_ON_FIND_CEIL | Called after ceiling detection completes. Return `height` to override height, or `height, surface` to override both | `number` posX, `number` posY, `number` posZ, [Surface](../structs.md#Surface) ceil, `number` height |
| HOOK_ON_FIND_FLOOR | Called after floor detection completes. Return `height` to override height, or `height, surface` to override both | `number` posX, `number` posY, `number` posZ, [Surface](../structs.md#Surface) floor, `number` height |
| HOOK_ON_FIND_WATER_LEVEL | Called after water level detection completes. Return a number to override the water level | `number` x, `number` z, `number` waterLevel |
| HOOK_ON_FIND_POISON_GAS_LEVEL | Called after poison gas level detection completes. Return a number to override the gas level | `number` x, `number` z, `number` gasLevel |
| HOOK_ON_FIND_SURFACE_ON_RAY | Called after ray-surface intersection completes. Return `surface` to override the hit surface, or `surface, hitPos` to override both | `Vec3f` orig, `Vec3f` dir, [Surface](../structs.md#Surface) hitSurface, `Vec3f` hitPos |
### Parameters

View file

@ -12,6 +12,7 @@
#include "game/hardcoded.h"
#include "pc/utils/misc.h"
#include "pc/network/network.h"
#include "pc/lua/smlua_hooks.h"
Vec3f gFindWallDirection = { 0 };
u8 gFindWallDirectionActive = false;
@ -343,6 +344,9 @@ s32 find_wall_collisions(struct WallCollisionData *colData) {
s32 numCollisions = 0;
s16 x = colData->x;
s16 z = colData->z;
f32 posX = colData->x;
f32 posY = colData->y;
f32 posZ = colData->z;
colData->numWalls = 0;
@ -371,6 +375,8 @@ s32 find_wall_collisions(struct WallCollisionData *colData) {
// Increment the debug tracker.
gNumCalls.wall += 1;
smlua_call_event_hooks(HOOK_ON_FIND_WALL_COLLISION, posX, posY, posZ, colData, &numCollisions);
return numCollisions;
}
@ -544,6 +550,8 @@ f32 find_ceil(f32 posX, f32 posY, f32 posZ, RET struct Surface **pceil) {
// Increment the debug tracker.
gNumCalls.ceil += 1;
smlua_call_event_hooks(HOOK_ON_FIND_CEIL, posX, posY, posZ, pceil, &height);
return height;
}
@ -882,6 +890,8 @@ f32 find_floor(f32 xPos, f32 yPos, f32 zPos, RET struct Surface **pfloor) {
// Increment the debug tracker.
gNumCalls.floor += 1;
smlua_call_event_hooks(HOOK_ON_FIND_FLOOR, xPos, yPos, zPos, pfloor, &height);
return height;
}
@ -922,6 +932,8 @@ f32 find_water_level(f32 x, f32 z) {
}
}
smlua_call_event_hooks(HOOK_ON_FIND_WATER_LEVEL, x, z, &waterLevel);
return waterLevel;
}
@ -963,6 +975,8 @@ f32 find_poison_gas_level(f32 x, f32 z) {
}
}
smlua_call_event_hooks(HOOK_ON_FIND_POISON_GAS_LEVEL, x, z, &gasLevel);
return gasLevel;
}
@ -1227,6 +1241,7 @@ void find_surface_on_ray(Vec3f orig, Vec3f dir, struct Surface **hit_surface, Ve
if (normalized_dir[1] >= 1.0f || normalized_dir[1] <= -1.0f)
{
find_surface_on_ray_cell(cellX, cellZ, orig, normalized_dir, dir_length, hit_surface, hit_pos, &max_length);
smlua_call_event_hooks(HOOK_ON_FIND_SURFACE_ON_RAY, orig, dir, hit_surface, hit_pos);
return;
}
@ -1249,4 +1264,6 @@ void find_surface_on_ray(Vec3f orig, Vec3f dir, struct Surface **hit_surface, Ve
cellX = (s16)fCellX;
cellZ = (s16)fCellZ;
}
smlua_call_event_hooks(HOOK_ON_FIND_SURFACE_ON_RAY, orig, dir, hit_surface, hit_pos);
}

View file

@ -3547,7 +3547,13 @@ char gSmluaConstants[] = ""
"HOOK_ON_ADD_SURFACE=57\n"
"HOOK_ON_CLEAR_AREAS=58\n"
"HOOK_ON_PACKET_BYTESTRING_RECEIVE=59\n"
"HOOK_MAX=60\n"
"HOOK_ON_FIND_WALL_COLLISION=60\n"
"HOOK_ON_FIND_CEIL=61\n"
"HOOK_ON_FIND_FLOOR=62\n"
"HOOK_ON_FIND_WATER_LEVEL=63\n"
"HOOK_ON_FIND_POISON_GAS_LEVEL=64\n"
"HOOK_ON_FIND_SURFACE_ON_RAY=65\n"
"HOOK_MAX=66\n"
"MAX_HOOKED_BEHAVIORS=1024\n"
"HUD_DISPLAY_LIVES=0\n"
"HUD_DISPLAY_COINS=1\n"

View file

@ -58,3 +58,9 @@ SMLUA_EVENT_HOOK(HOOK_MARIO_OVERRIDE_FLOOR_CLASS, HOOK_RETURN_ON_OUTPUT_SET, str
SMLUA_EVENT_HOOK(HOOK_ON_ADD_SURFACE, HOOK_RETURN_NEVER, struct Surface *surface, bool dynamic)
SMLUA_EVENT_HOOK(HOOK_ON_CLEAR_AREAS, HOOK_RETURN_NEVER)
SMLUA_EVENT_HOOK(HOOK_ON_PACKET_BYTESTRING_RECEIVE, HOOK_RETURN_NEVER, s32 modIndex, s32 valueIndex)
SMLUA_EVENT_HOOK(HOOK_ON_FIND_WALL_COLLISION, _, f32 posX, f32 posY, f32 posZ, struct WallCollisionData *colData, s32 *numCollisions) // Manually defined hook
SMLUA_EVENT_HOOK(HOOK_ON_FIND_CEIL, _, f32 posX, f32 posY, f32 posZ, struct Surface **pceil, f32 *height) // Manually defined hook
SMLUA_EVENT_HOOK(HOOK_ON_FIND_FLOOR, _, f32 posX, f32 posY, f32 posZ, struct Surface **pfloor, f32 *height) // Manually defined hook
SMLUA_EVENT_HOOK(HOOK_ON_FIND_WATER_LEVEL, _, f32 x, f32 z, f32 *waterLevel) // Manually defined hook
SMLUA_EVENT_HOOK(HOOK_ON_FIND_POISON_GAS_LEVEL, _, f32 x, f32 z, f32 *gasLevel) // Manually defined hook
SMLUA_EVENT_HOOK(HOOK_ON_FIND_SURFACE_ON_RAY, _, Vec3f orig, Vec3f dir, struct Surface **hit_surface, Vec3f hit_pos) // Manually defined hook

View file

@ -26,6 +26,9 @@
#include "game/print.h"
#include "gfx_dimensions.h"
extern void smlua_new_vec3f(Vec3f src);
extern void smlua_get_vec3f(Vec3f dest, int index);
#define MAX_HOOKED_REFERENCES 64
#define LUA_BEHAVIOR_FLAG (1 << 15)
@ -172,7 +175,6 @@ bool smlua_call_event_hooks_HOOK_ON_NAMETAGS_RENDER(s32 playerIndex, Vec3f pos,
lua_pushinteger(L, playerIndex);
// push pos
extern void smlua_new_vec3f(Vec3f src);
smlua_new_vec3f(pos);
// call the callback
@ -203,7 +205,6 @@ bool smlua_call_event_hooks_HOOK_ON_NAMETAGS_RENDER(s32 playerIndex, Vec3f pos,
// pos
lua_getfield(L, -1, "pos");
if (lua_type(L, -1) == LUA_TTABLE) {
extern void smlua_get_vec3f(Vec3f dest, int index);
smlua_get_vec3f(pos, -1);
override = true;
}
@ -220,6 +221,306 @@ bool smlua_call_event_hooks_HOOK_ON_NAMETAGS_RENDER(s32 playerIndex, Vec3f pos,
return false;
}
bool smlua_call_event_hooks_HOOK_ON_FIND_WALL_COLLISION(f32 posX, f32 posY, f32 posZ, struct WallCollisionData *colData, s32 *numCollisions) {
static bool sInHook = false;
lua_State *L = gLuaState;
if (L == NULL || sInHook) { return false; }
sInHook = true;
struct LuaHookedEvent *hook = &sHookedEvents[HOOK_ON_FIND_WALL_COLLISION];
for (int i = 0; i < hook->count; i++) {
s32 prevTop = lua_gettop(L);
// push the callback onto the stack
lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference[i]);
// push posX, posY, posZ
lua_pushnumber(L, posX);
lua_pushnumber(L, posY);
lua_pushnumber(L, posZ);
// push colData
smlua_push_object(L, LOT_WALLCOLLISIONDATA, colData, NULL);
// call the callback (4 args, 1 result)
if (0 != smlua_call_hook(L, 4, 1, 0, hook->mod[i], hook->modFile[i])) {
LOG_LUA("Failed to call the callback for hook %s", sLuaHookedEventTypeName[HOOK_ON_FIND_WALL_COLLISION]);
lua_settop(L, prevTop);
continue;
}
// return number overrides numCollisions
if (lua_type(L, -1) == LUA_TNUMBER) {
*numCollisions = smlua_to_integer(L, -1);
lua_settop(L, prevTop);
sInHook = false;
return true;
}
lua_settop(L, prevTop);
}
sInHook = false;
return false;
}
bool smlua_call_event_hooks_HOOK_ON_FIND_CEIL(f32 posX, f32 posY, f32 posZ, struct Surface **pceil, f32 *height) {
static bool sInHook = false;
lua_State *L = gLuaState;
if (L == NULL || sInHook) { return false; }
sInHook = true;
struct LuaHookedEvent *hook = &sHookedEvents[HOOK_ON_FIND_CEIL];
for (int i = 0; i < hook->count; i++) {
s32 prevTop = lua_gettop(L);
// push the callback onto the stack
lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference[i]);
// push posX, posY, posZ
lua_pushnumber(L, posX);
lua_pushnumber(L, posY);
lua_pushnumber(L, posZ);
// push current ceil surface (or nil)
if (pceil && *pceil) {
smlua_push_object(L, LOT_SURFACE, *pceil, NULL);
} else {
lua_pushnil(L);
}
// push current height
lua_pushnumber(L, *height);
// call the callback (5 args, 2 results)
if (0 != smlua_call_hook(L, 5, 2, 0, hook->mod[i], hook->modFile[i])) {
LOG_LUA("Failed to call the callback for hook %s", sLuaHookedEventTypeName[HOOK_ON_FIND_CEIL]);
lua_settop(L, prevTop);
continue;
}
bool override = false;
// first return value: height (number)
if (lua_type(L, -2) == LUA_TNUMBER) {
*height = smlua_to_number(L, -2);
override = true;
}
// second return value: surface (userdata)
if (lua_type(L, -1) == LUA_TUSERDATA) {
struct Surface *surface = (struct Surface *)smlua_to_cobject(L, -1, LOT_SURFACE);
if (surface && pceil) {
*pceil = surface;
override = true;
}
}
lua_settop(L, prevTop);
if (override) { sInHook = false; return true; }
}
sInHook = false;
return false;
}
bool smlua_call_event_hooks_HOOK_ON_FIND_FLOOR(f32 posX, f32 posY, f32 posZ, struct Surface **pfloor, f32 *height) {
static bool sInHook = false;
lua_State *L = gLuaState;
if (L == NULL || sInHook) { return false; }
sInHook = true;
struct LuaHookedEvent *hook = &sHookedEvents[HOOK_ON_FIND_FLOOR];
for (int i = 0; i < hook->count; i++) {
s32 prevTop = lua_gettop(L);
// push the callback onto the stack
lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference[i]);
// push posX, posY, posZ
lua_pushnumber(L, posX);
lua_pushnumber(L, posY);
lua_pushnumber(L, posZ);
// push current floor surface (or nil)
if (pfloor && *pfloor) {
smlua_push_object(L, LOT_SURFACE, *pfloor, NULL);
} else {
lua_pushnil(L);
}
// push current height
lua_pushnumber(L, *height);
// call the callback (5 args, 2 results)
if (0 != smlua_call_hook(L, 5, 2, 0, hook->mod[i], hook->modFile[i])) {
LOG_LUA("Failed to call the callback for hook %s", sLuaHookedEventTypeName[HOOK_ON_FIND_FLOOR]);
lua_settop(L, prevTop);
continue;
}
bool override = false;
// first return value: height (number)
if (lua_type(L, -2) == LUA_TNUMBER) {
*height = smlua_to_number(L, -2);
override = true;
}
// second return value: surface (userdata)
if (lua_type(L, -1) == LUA_TUSERDATA) {
struct Surface *surface = (struct Surface *)smlua_to_cobject(L, -1, LOT_SURFACE);
if (surface && pfloor) {
*pfloor = surface;
override = true;
}
}
lua_settop(L, prevTop);
if (override) { sInHook = false; return true; }
}
sInHook = false;
return false;
}
bool smlua_call_event_hooks_HOOK_ON_FIND_SURFACE_ON_RAY(Vec3f orig, Vec3f dir, struct Surface **hit_surface, Vec3f hit_pos) {
static bool sInHook = false;
lua_State *L = gLuaState;
if (L == NULL || sInHook) { return false; }
sInHook = true;
struct LuaHookedEvent *hook = &sHookedEvents[HOOK_ON_FIND_SURFACE_ON_RAY];
for (int i = 0; i < hook->count; i++) {
s32 prevTop = lua_gettop(L);
// push the callback onto the stack
lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference[i]);
// push orig, dir
smlua_new_vec3f(orig);
smlua_new_vec3f(dir);
// push hit_surface (or nil)
if (hit_surface && *hit_surface) {
smlua_push_object(L, LOT_SURFACE, *hit_surface, NULL);
} else {
lua_pushnil(L);
}
// push hit_pos
smlua_new_vec3f(hit_pos);
// call the callback (4 args, 2 results)
if (0 != smlua_call_hook(L, 4, 2, 0, hook->mod[i], hook->modFile[i])) {
LOG_LUA("Failed to call the callback for hook %s", sLuaHookedEventTypeName[HOOK_ON_FIND_SURFACE_ON_RAY]);
lua_settop(L, prevTop);
continue;
}
bool override = false;
// first return value: surface (userdata)
if (lua_type(L, -2) == LUA_TUSERDATA) {
struct Surface *surface = (struct Surface *)smlua_to_cobject(L, -2, LOT_SURFACE);
if (surface && hit_surface) {
*hit_surface = surface;
override = true;
}
}
// second return value: hitPos (table {x, y, z})
if (lua_type(L, -1) == LUA_TTABLE) {
smlua_get_vec3f(hit_pos, -1);
override = true;
}
lua_settop(L, prevTop);
if (override) { sInHook = false; return true; }
}
sInHook = false;
return false;
}
bool smlua_call_event_hooks_HOOK_ON_FIND_WATER_LEVEL(f32 x, f32 z, f32 *waterLevel) {
static bool sInHook = false;
lua_State *L = gLuaState;
if (L == NULL || sInHook) { return false; }
sInHook = true;
struct LuaHookedEvent *hook = &sHookedEvents[HOOK_ON_FIND_WATER_LEVEL];
for (int i = 0; i < hook->count; i++) {
s32 prevTop = lua_gettop(L);
// push the callback onto the stack
lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference[i]);
// push x, z
lua_pushnumber(L, x);
lua_pushnumber(L, z);
// push current water level
lua_pushnumber(L, *waterLevel);
// call the callback (3 args, 1 result)
if (0 != smlua_call_hook(L, 3, 1, 0, hook->mod[i], hook->modFile[i])) {
LOG_LUA("Failed to call the callback for hook %s", sLuaHookedEventTypeName[HOOK_ON_FIND_WATER_LEVEL]);
lua_settop(L, prevTop);
continue;
}
// return number overrides waterLevel
if (lua_type(L, -1) == LUA_TNUMBER) {
*waterLevel = smlua_to_number(L, -1);
lua_settop(L, prevTop);
sInHook = false;
return true;
}
lua_settop(L, prevTop);
}
sInHook = false;
return false;
}
bool smlua_call_event_hooks_HOOK_ON_FIND_POISON_GAS_LEVEL(f32 x, f32 z, f32 *gasLevel) {
static bool sInHook = false;
lua_State *L = gLuaState;
if (L == NULL || sInHook) { return false; }
sInHook = true;
struct LuaHookedEvent *hook = &sHookedEvents[HOOK_ON_FIND_POISON_GAS_LEVEL];
for (int i = 0; i < hook->count; i++) {
s32 prevTop = lua_gettop(L);
// push the callback onto the stack
lua_rawgeti(L, LUA_REGISTRYINDEX, hook->reference[i]);
// push x, z
lua_pushnumber(L, x);
lua_pushnumber(L, z);
// push current gas level
lua_pushnumber(L, *gasLevel);
// call the callback (3 args, 1 result)
if (0 != smlua_call_hook(L, 3, 1, 0, hook->mod[i], hook->modFile[i])) {
LOG_LUA("Failed to call the callback for hook %s", sLuaHookedEventTypeName[HOOK_ON_FIND_POISON_GAS_LEVEL]);
lua_settop(L, prevTop);
continue;
}
// return number overrides gasLevel
if (lua_type(L, -1) == LUA_TNUMBER) {
*gasLevel = smlua_to_number(L, -1);
lua_settop(L, prevTop);
sInHook = false;
return true;
}
lua_settop(L, prevTop);
}
sInHook = false;
return false;
}
////////////////////
// hooked actions //
////////////////////

View file

@ -11,6 +11,8 @@
// forward declare
struct Camera;
struct WarpDest;
struct WallCollisionData;
struct Surface;
// ! Hooks must be added at the end
enum LuaHookedEventType {
@ -74,6 +76,12 @@ enum LuaHookedEventType {
HOOK_ON_ADD_SURFACE,
HOOK_ON_CLEAR_AREAS,
HOOK_ON_PACKET_BYTESTRING_RECEIVE,
HOOK_ON_FIND_WALL_COLLISION,
HOOK_ON_FIND_CEIL,
HOOK_ON_FIND_FLOOR,
HOOK_ON_FIND_WATER_LEVEL,
HOOK_ON_FIND_POISON_GAS_LEVEL,
HOOK_ON_FIND_SURFACE_ON_RAY,
HOOK_MAX,
};