diff --git a/autogen/convert_constants.py b/autogen/convert_constants.py
index 409cd8427..896e32e2e 100644
--- a/autogen/convert_constants.py
+++ b/autogen/convert_constants.py
@@ -55,6 +55,7 @@ in_files = [
"src/engine/lighting_engine.h",
"include/PR/gbi.h",
"include/PR/gbi_extension.h",
+ "src/engine/surface_load.h",
]
exclude_constants = {
@@ -68,6 +69,7 @@ exclude_constants = {
"src/pc/lua/smlua_hooks.h": [ "MAX_HOOKED_MOD_MENU_ELEMENTS", "^HOOK_RETURN_.*", "^ACTION_HOOK_.*", "^MOD_MENU_ELEMENT_.*" ],
"src/pc/djui/djui_panel_menu.h": [ "RAINBOW_TEXT_LEN" ],
"src/pc/mods/mod_fs.h": [ "INT_TYPE_MAX", "FLOAT_TYPE_MAX", "FILE_SEEK_MAX" ],
+ "src/engine/surface_load.h": [ "NUM_CELLS" ],
}
include_constants = {
diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py
index 9a9f7d53a..2279aac23 100644
--- a/autogen/convert_functions.py
+++ b/autogen/convert_functions.py
@@ -105,7 +105,7 @@ override_allowed_functions = {
override_disallowed_functions = {
"src/audio/external.h": [ " func_" ],
- "src/engine/surface_load.h": [ "load_area_terrain", "alloc_surface_pools", "clear_dynamic_surfaces", "get_area_terrain_size" ],
+ "src/engine/surface_load.h": [ "load_area_terrain", "alloc_surface_pools", "clear_dynamic_surfaces", "get_area_terrain_size", "alloc_surface", "add_surface", "remove_surface_from_partition", "delete_surface", "swap_and_pop_surface_pool", "add_surface_without_hook" ],
"src/engine/surface_collision.h": [ " debug_", "f32_find_wall_collision" ],
"src/game/mario_actions_airborne.c": [ "^[us]32 act_.*" ],
"src/game/mario_actions_automatic.c": [ "^[us]32 act_.*" ],
diff --git a/autogen/convert_structs.py b/autogen/convert_structs.py
index 3a88390ec..0c83ef763 100644
--- a/autogen/convert_structs.py
+++ b/autogen/convert_structs.py
@@ -105,6 +105,7 @@ override_field_invisible = {
override_field_deprecated = {
"NetworkPlayer": [ "paletteIndex", "overridePaletteIndex", "overridePaletteIndexLp" ],
+ "StaticObjectCollision": [ "index" ],
}
override_field_immutable = {
@@ -115,6 +116,7 @@ override_field_immutable = {
"NetworkPlayer": [ "*" ],
"TextureInfo": [ "*" ],
"Object": ["oSyncID", "coopFlags", "oChainChompSegments", "oWigglerSegments", "oHauntedChairUnk100", "oTTCTreadmillBigSurface", "oTTCTreadmillSmallSurface", "bhvStackIndex", "respawnInfoType", "numSurfaces", "bhvStack" ],
+ "Surface": [ "poolType", "socId" ],
"GlobalObjectAnimations": [ "*"],
"SpawnParticlesInfo": [ "model" ],
"WaterDropletParams": [ "model" ],
diff --git a/autogen/lua_definitions/constants.lua b/autogen/lua_definitions/constants.lua
index 8e160c5d4..15af3aea2 100644
--- a/autogen/lua_definitions/constants.lua
+++ b/autogen/lua_definitions/constants.lua
@@ -10590,6 +10590,15 @@ SOUND_OBJ2_BOSS_DIALOG_GRUNT = SOUND_ARG_LOAD(SOUND_BANK_OBJ2, 0x69, 0x40, SOUND
--- @type integer
SOUND_OBJ2_MRI_SPINNING = SOUND_ARG_LOAD(SOUND_BANK_OBJ2, 0x6B, 0x00, SOUND_DISCRETE)
+--- @type integer
+SURFACE_POOL_STATIC = 0
+
+--- @type integer
+SURFACE_POOL_DYNAMIC = 1
+
+--- @type integer
+SURFACE_POOL_SOC = 2
+
--- @type integer
SURFACE_DEFAULT = 0x0000
diff --git a/autogen/lua_definitions/functions.lua b/autogen/lua_definitions/functions.lua
index 746a8f4c4..89b6abf11 100644
--- a/autogen/lua_definitions/functions.lua
+++ b/autogen/lua_definitions/functions.lua
@@ -10700,6 +10700,32 @@ function smlua_collision_util_find_surface_types(data)
-- ...
end
+--- @param dynamic boolean
+--- @param surfaceType integer
+--- @param vertex1 Vec3s
+--- @param vertex2 Vec3s
+--- @param vertex3 Vec3s
+--- @return Surface
+--- Allocates a new collision surface with the given vertices, computes the surface normal and other fields, and inserts it into the spatial partition. Returns the new surface, or `nil` if the triangle is degenerate (zero area). Set `dynamic` to `true` for surfaces that are cleared each frame, or `false` for persistent static surfaces
+function smlua_collision_add_surface(dynamic, surfaceType, vertex1, vertex2, vertex3)
+ -- ...
+end
+
+--- @param surface Surface
+--- @param vertex1 Vec3s
+--- @param vertex2 Vec3s
+--- @param vertex3 Vec3s
+--- Moves an existing collision surface to new vertex positions. Recalculates the surface normal, origin offset, and Y bounds, removes the surface from its old spatial partition cells, and re-adds it to the correct cells. The previous vertices are preserved for interpolation
+function smlua_collision_move_surface(surface, vertex1, vertex2, vertex3)
+ -- ...
+end
+
+--- @param surface Surface
+--- Fully deletes a collision surface: removes it from the spatial partitions and frees its pool slot.
+function smlua_collision_delete_surface(surface)
+ -- ...
+end
+
--- @param surf Surface
--- @return boolean
--- Checks if the surface is quicksand
@@ -12530,6 +12556,12 @@ function get_static_object_surface(col, index)
-- ...
end
+--- @param col StaticObjectCollision
+--- Removes all surfaces belonging to a static object collision and reclaims the SOC metadata
+function remove_static_object_collision(col)
+ -- ...
+end
+
--- @param o Object
--- @param index integer
--- @return Surface
diff --git a/autogen/lua_definitions/structs.lua b/autogen/lua_definitions/structs.lua
index 5441e7424..48a4714c0 100644
--- a/autogen/lua_definitions/structs.lua
+++ b/autogen/lua_definitions/structs.lua
@@ -2235,6 +2235,7 @@
--- @field public type integer
--- @field public flags integer
--- @field public room integer
+--- @field public poolType integer
--- @field public force integer
--- @field public lowerY integer
--- @field public upperY integer
@@ -2247,6 +2248,7 @@
--- @field public normal Vec3f
--- @field public originOffset number
--- @field public modifiedTimestamp integer
+--- @field public socId integer
--- @field public object Object
--- @class TextureInfo
diff --git a/docs/lua/constants.md b/docs/lua/constants.md
index 0c92edac9..945c1fc88 100644
--- a/docs/lua/constants.md
+++ b/docs/lua/constants.md
@@ -90,6 +90,7 @@
- [smlua_model_utils.h](#smlua_model_utilsh)
- [enum ModelExtendedId](#enum-ModelExtendedId)
- [sounds.h](#soundsh)
+- [surface_load.h](#surface_loadh)
- [surface_terrains.h](#surface_terrainsh)
- [types.h](#typesh)
- [enum SpTaskState](#enum-SpTaskState)
@@ -4483,6 +4484,15 @@
+## [surface_load.h](#surface_load.h)
+- SURFACE_POOL_STATIC
+- SURFACE_POOL_DYNAMIC
+- SURFACE_POOL_SOC
+
+[:arrow_up_small:](#)
+
+
+
## [surface_terrains.h](#surface_terrains.h)
- SURFACE_DEFAULT
- SURFACE_BURNING
diff --git a/docs/lua/examples/runtime-surface-examples/lib/dbg.lua b/docs/lua/examples/runtime-surface-examples/lib/dbg.lua
new file mode 100644
index 000000000..f52704767
--- /dev/null
+++ b/docs/lua/examples/runtime-surface-examples/lib/dbg.lua
@@ -0,0 +1,421 @@
+-----------------
+-- Dbg Library --
+-- v1.1 --
+-----------------
+
+---------------------------------------------------------------------
+--
+-- A tiny, immediate-mode debug-drawing library for Lua that
+-- lets you quickly visualize things in your game world or on
+-- the HUD.
+--
+-- Use the following functions at any time:
+-- Dbg.point(pos, color, size) to draw 3D points
+-- Dbg.line(a, b, color, thickness) to draw 3D lines
+-- Dbg.text(str, pos, color, scale) for world-space labels
+--
+-- The rendered points/lines/text automatically disappear on the
+-- next frame unless you set Dbg.clear_automatically to false
+--
+-- Credits for the original line rendering code go to Isaac
+--
+---------------------------------------------------------------------
+
+---@class Dbg
+local Dbg = {
+ clear_automatically = true,
+ alter_position_function = nil
+}
+
+Dbg.colors = {
+ red = { 1, 0, 0 },
+ green = { 0, 1, 0 },
+ blue = { 0, 0, 1 },
+
+ cyan = { 0, 1, 1 },
+ magenta = { 1, 0, 1 },
+ yellow = { 1, 1, 0 },
+
+ white = { 1, 1, 1 },
+ grey = { 0.2, 0.2, 0.2 },
+ black = { 0, 0, 0 },
+}
+
+Dbg.htext = nil
+
+---------------------------------------------------------------------
+-- FRAME-LOCAL BUFFERS (auto-cleared each world_step)
+---------------------------------------------------------------------
+local _debug_points = { points = {}, count = 0 }
+local _debug_lines = { lines = {}, count = 0 }
+local _debug_texts = { texts = {}, count = 0 }
+local _debug_game_tick = 0
+
+local function point_new()
+ return {
+ position = { 0, 0, 0 },
+ color = { 0, 0, 0 },
+ scale = 1
+ }
+end
+
+local function line_new()
+ return {
+ p1 = { 0, 0, 0 },
+ p2 = { 0, 0, 0 },
+ color = { 0, 0, 0 },
+ thickness = 1
+ }
+end
+
+local function text_new()
+ return {
+ string = '',
+ position = { 0, 0, 0 },
+ color = { 0, 0, 0 },
+ scale = 1
+ }
+end
+
+local function clear_check()
+ if not Dbg.clear_automatically then return end
+
+ local tick = get_global_timer()
+ if _debug_game_tick ~= tick then
+ _debug_game_tick = tick
+ Dbg.clear()
+ end
+
+end
+
+---------------------------------------------------------------------
+-- LINES - CLIPPING
+---------------------------------------------------------------------
+
+local tmp_out1 = { x = 0, y = 0, z = 0}
+local tmp_out2 = { x = 0, y = 0, z = 0}
+
+-- scratch buffers (create once, reuse every call)
+local low_pt = { 0, 0, 0 }
+local high_pt = { 0, 0, 0 }
+local mid_pt = { 0, 0, 0 }
+local screen = { x=0, y=0, z=0 }
+local tmp_world = { x=0, y=0, z=0 }
+
+local MAX_ITERS = 8 -- tweak for precision vs cost
+
+--- try to clip the line so that it fits in the camera
+local function clip_segment_against_camera(out_a, out_b, p1, p2)
+ -- test both endpoints
+ tmp_world.x, tmp_world.y, tmp_world.z = p1[1], p1[2], p1[3]
+ djui_hud_world_pos_to_screen_pos(tmp_world, screen)
+ local ok1 = screen.z < -260
+
+ tmp_world.x, tmp_world.y, tmp_world.z = p2[1], p2[2], p2[3]
+ djui_hud_world_pos_to_screen_pos(tmp_world, screen)
+ local ok2 = screen.z < -260
+
+ -- trivial accept / reject
+ if ok1 and ok2 then
+ out_a[1], out_a[2], out_a[3] = p1[1], p1[2], p1[3]
+ out_b[1], out_b[2], out_b[3] = p2[1], p2[2], p2[3]
+ return true
+ end
+ if not ok1 and not ok2 then
+ return false
+ end
+
+ -- decide which end is inside, which is outside
+ local inside, outside
+ local p1Inside = ok1
+ if p1Inside then
+ inside, outside = p1, p2
+ else
+ inside, outside = p2, p1
+ end
+
+ -- seed the binary-search bracket
+ low_pt[1], low_pt[2], low_pt[3] = inside[1], inside[2], inside[3]
+ high_pt[1], high_pt[2], high_pt[3] = outside[1], outside[2], outside[3]
+
+ -- binary-search edge of the frustum in MAX_ITERS steps
+ for i = 1, MAX_ITERS do
+ mid_pt[1], mid_pt[2], mid_pt[3] =
+ low_pt[1] + (high_pt[1] - low_pt[1]) * 0.5,
+ low_pt[2] + (high_pt[2] - low_pt[2]) * 0.5,
+ low_pt[3] + (high_pt[3] - low_pt[3]) * 0.5
+
+ -- test mid_pt
+ tmp_world.x, tmp_world.y, tmp_world.z = mid_pt[1], mid_pt[2], mid_pt[3]
+ djui_hud_world_pos_to_screen_pos(tmp_world, screen)
+ if screen.z < -260 then
+ -- mid is still in front -> push low_pt up
+ low_pt[1], low_pt[2], low_pt[3] = mid_pt[1], mid_pt[2], mid_pt[3]
+ else
+ -- mid went behind -> pull high_pt down
+ high_pt[1], high_pt[2], high_pt[3] = mid_pt[1], mid_pt[2], mid_pt[3]
+ end
+ end
+
+ -- assign outputs in the original p1->p2 order
+ if p1Inside then
+ -- segment is p1 (inside) -> boundary near p2
+ out_a[1], out_a[2], out_a[3] = p1[1], p1[2], p1[3]
+ out_b[1], out_b[2], out_b[3] = low_pt[1], low_pt[2], low_pt[3]
+ else
+ -- segment is boundary near p1 -> p2 (inside)
+ out_a[1], out_a[2], out_a[3] = low_pt[1], low_pt[2], low_pt[3]
+ out_b[1], out_b[2], out_b[3] = p2[1], p2[2], p2[3]
+ end
+
+ return true
+end
+
+---------------------------------------------------------------------
+-- RENDER: LINES
+---------------------------------------------------------------------
+
+local function render_debug_lines()
+ local cam = (gMarioStates[0].area or {}).camera
+ if not cam then return end
+ djui_hud_set_resolution(RESOLUTION_N64) -- use default pixel grid
+
+ for i = 1, _debug_lines.count do
+ local l = _debug_lines.lines[i]
+ local c = l.color
+ djui_hud_set_color(c[1]*255, c[2]*255, c[3]*255, c[4] and c[4]*255 or 255)
+
+ if not clip_segment_against_camera(tmp_out1, tmp_out2, l.p1, l.p2) then
+ goto continue
+ end
+
+ djui_hud_world_pos_to_screen_pos({ x = tmp_out1[1], y = tmp_out1[2], z = tmp_out1[3] }, screen)
+ local s1x, s1y = screen.x, screen.y
+
+ djui_hud_world_pos_to_screen_pos({ x = tmp_out2[1], y = tmp_out2[2], z = tmp_out2[3] }, screen)
+ local s2x, s2y = screen.x, screen.y
+
+ if s1x and s2x then
+ local dx, dy = s2x - s1x, s2y - s1y
+ local angle = atan2s(dy, dx) - 0x4000 -- rotate by -90 degree for Djui rects
+ local len = math.sqrt(dx * dx + dy * dy)
+ djui_hud_set_rotation(angle, 0, 0.5)
+ djui_hud_render_rect(s1x, s1y, len, l.thickness * 0.5)
+ end
+ ::continue::
+ end
+end
+
+---------------------------------------------------------------------
+-- RENDER: POINT ICONS
+---------------------------------------------------------------------
+
+local tmp_pos_in = { x = 0, y = 0, z = 0 }
+
+local function render_debug_points()
+ djui_hud_set_resolution(RESOLUTION_N64) -- use default pixel grid
+
+ tmp_out1.x, tmp_out1.y, tmp_out1.z = 0, 0, 0
+
+ for i = 1, _debug_points.count do
+ local p = _debug_points.points[i]
+ tmp_pos_in.x, tmp_pos_in.y, tmp_pos_in.z = p.position[1], p.position[2], p.position[3]
+ djui_hud_world_pos_to_screen_pos(tmp_pos_in, tmp_out1)
+ if tmp_out1.z <= -260 then -- clip very near points
+ local c = p.color
+
+ local scale = p.scale * 2
+ local x, y = tmp_out1.x - scale * 0.5, tmp_out1.y - scale * 0.5
+ local padding = 0.3
+
+ djui_hud_set_color(0, 0, 0, c[4] and c[4]*200 or 200)
+ djui_hud_render_rect(x - padding, y - padding, scale + padding * 2, scale + padding * 2)
+
+ djui_hud_set_color(c[1] * 255, c[2] * 255, c[3] * 255, c[4] and c[4]*255 or 255)
+ djui_hud_render_rect(x, y, scale, scale)
+ end
+ end
+end
+
+---------------------------------------------------------------------
+-- RENDER: TEXTS
+---------------------------------------------------------------------
+
+local function render_debug_texts()
+ djui_hud_set_resolution(RESOLUTION_N64) -- use default pixel grid
+
+ tmp_out1.x, tmp_out1.y, tmp_out1.z = 0, 0, 0
+
+ for i = 1, _debug_texts.count do
+ local t = _debug_texts.texts[i]
+ tmp_pos_in.x, tmp_pos_in.y, tmp_pos_in.z = t.position[1], t.position[2], t.position[3]
+ djui_hud_world_pos_to_screen_pos(tmp_pos_in, tmp_out1)
+ if tmp_out1.z <= -260 then -- clip very near points
+ local c = t.color
+ djui_hud_set_font(FONT_ALIASED)
+ djui_hud_set_color(0, 0, 0, c[4] and c[4] * 200 or 200)
+ djui_hud_print_text(t.string, tmp_out1.x + 0.4, tmp_out1.y + 0.4, t.scale * 0.3)
+ djui_hud_set_color(c[1] * 255, c[2] * 255, c[3] * 255, c[4] and c[4] * 255 or 255)
+ djui_hud_print_text(t.string, tmp_out1.x, tmp_out1.y, t.scale * 0.3)
+ end
+ end
+end
+
+
+---------------------------------------------------------------------
+-- RENDER: DEBUG TEXT
+---------------------------------------------------------------------
+
+local function render_debug_text()
+ -- set text and scale
+ local text = Dbg.htext
+ local scale = 1
+
+ -- render to native screen space, with the MENU font
+ djui_hud_set_resolution(RESOLUTION_DJUI)
+ djui_hud_set_font(FONT_MENU)
+
+ -- get width of screen and text
+ local screen_width = djui_hud_get_screen_width()
+ local width = djui_hud_measure_text(text) * scale
+
+ -- get height of screen and text
+ local screen_height = djui_hud_get_screen_height()
+ local height = 64 * scale
+
+ -- set location
+ local x = screen_width - width - 100
+ local y = screen_height - height - 50
+
+ -- set color and render
+ djui_hud_set_color(255, 0, 255, 255)
+ djui_hud_print_text(text, x, y, scale)
+end
+
+---------------------------------------------------------------------
+-- ENTRY HOOK
+---------------------------------------------------------------------
+
+local function on_debug_hud_render()
+ clear_check()
+ render_debug_points()
+ render_debug_lines()
+ render_debug_texts()
+ if Dbg.htext ~= nil then render_debug_text() end
+end
+
+hook_event(HOOK_ON_HUD_RENDER, on_debug_hud_render)
+
+---------------------------------------------------------------------
+-- UTILS
+---------------------------------------------------------------------
+
+local num_str = "number"
+
+local function parse_vec3(dst, src)
+ local t = type(src)
+ if t ~= "table" and t ~= "userdata" then return nil end
+
+ -- parse { x = ..., y = ..., z = ... }
+ if src.x and src.y and src.z then
+ if type(src.x) == num_str and type(src.y) == num_str and type(src.z) == num_str then
+ dst[1], dst[2], dst[3] = src.x, src.y, src.z
+ else
+ return nil
+ end
+
+ -- parse { ..., ..., ... }
+ elseif #src == 3 and type(src[1]) == num_str and type(src[2]) == num_str and type(src[3]) == num_str then
+ dst[1], dst[2], dst[3] = src[1], src[2], src[3]
+ else
+ return nil
+ end
+
+ -- alter if we want to
+ if Dbg.alter_position_function then
+ Dbg.alter_position_function(dst)
+ end
+
+ return dst
+end
+
+---------------------------------------------------------------------
+-- PUBLIC API
+---------------------------------------------------------------------
+
+function Dbg.point(pos, color, scale)
+ clear_check()
+ color = color or Dbg.colors.white
+ scale = scale or 1
+
+ -- increment / allocate
+ _debug_points.count = _debug_points.count + 1
+ if not _debug_points.points[_debug_points.count] then
+ _debug_points.points[_debug_points.count] = point_new()
+ end
+ local p = _debug_points.points[_debug_points.count]
+
+ -- parse vec
+ if not parse_vec3(p.position, pos) then
+ _debug_points.count = _debug_points.count - 1
+ return
+ end
+
+ p.color[1], p.color[2], p.color[3], p.color[4] = color[1], color[2], color[3], color[4] or 1
+ p.scale = scale
+end
+
+function Dbg.line(p1, p2, color, thickness)
+ clear_check()
+ color = color or Dbg.colors.white
+ thickness = thickness or 1
+
+ if not p1 or not p2 then return end
+
+ -- increment / allocate
+ _debug_lines.count = _debug_lines.count + 1
+ if not _debug_lines.lines[_debug_lines.count] then
+ _debug_lines.lines[_debug_lines.count] = line_new()
+ end
+ local l = _debug_lines.lines[_debug_lines.count]
+
+ -- parse vecs
+ if not parse_vec3(l.p1, p1) or not parse_vec3(l.p2, p2) then
+ _debug_lines.count = _debug_lines.count - 1
+ return
+ end
+
+ l.color[1], l.color[2], l.color[3], l.color[4] = color[1], color[2], color[3], color[4] or 1
+ l.thickness = thickness
+end
+
+function Dbg.text(string, pos, color, scale)
+ clear_check()
+ color = color or Dbg.colors.white
+ scale = scale or 1
+
+ -- increment / allocate
+ _debug_texts.count = _debug_texts.count + 1
+ if not _debug_texts.texts[_debug_texts.count] then
+ _debug_texts.texts[_debug_texts.count] = text_new()
+ end
+ local t = _debug_texts.texts[_debug_texts.count]
+
+ -- parse vec
+ if not parse_vec3(t.position, pos) then
+ _debug_texts.count = _debug_texts.count - 1
+ return
+ end
+
+ t.string = tostring(string)
+ t.color[1], t.color[2], t.color[3], t.color[4] = color[1], color[2], color[3], color[4] or 1
+ t.scale = scale
+end
+
+function Dbg.clear()
+ _debug_points.count = 0
+ _debug_lines.count = 0
+ _debug_texts.count = 0
+end
+
+return Dbg
\ No newline at end of file
diff --git a/docs/lua/examples/runtime-surface-examples/main.lua b/docs/lua/examples/runtime-surface-examples/main.lua
new file mode 100644
index 000000000..6d5f9351e
--- /dev/null
+++ b/docs/lua/examples/runtime-surface-examples/main.lua
@@ -0,0 +1,115 @@
+-- name: Runtime Surface Examples
+-- description: Shows how to create, move, and delete surfaces at runtime. Also shows how to load static object collision and draw debug visuals for surfaces.
+
+local Dbg = require('/lib/dbg')
+
+local moving_surface = nil
+
+local function draw_surface(surface, color, thickness)
+ thickness = thickness or 0.5
+ Dbg.line(surface.vertex1, surface.vertex2, color, thickness)
+ Dbg.line(surface.vertex2, surface.vertex3, color, thickness)
+ Dbg.line(surface.vertex3, surface.vertex1, color, thickness)
+end
+
+local sSoc = nil
+local sObject = nil
+local id_bhvSOC = hook_behavior(nil, OBJ_LIST_SURFACE, false,
+ function (obj)
+ obj.collisionData = gGlobalObjectCollisionData.bbh_seg7_collision_coffin
+ sSoc = load_static_object_collision()
+ end,
+ function (obj)
+ if not sSoc then return end
+ for i = 1, sSoc.length do
+ local surf = get_static_object_surface(sSoc, i)
+ if surf then
+ draw_surface(surf, Dbg.colors.yellow)
+ end
+ end
+ end
+)
+
+hook_event(HOOK_UPDATE, function ()
+ local m = gMarioStates[0]
+ local pressed = m.controller.buttonPressed
+
+ -- D_JPAD: create a surface at Mario's position
+ if (pressed & D_JPAD) ~= 0 then
+ local v1 = { x = m.pos.x - 500, y = m.pos.y, z = m.pos.z - 500 }
+ local v2 = { x = m.pos.x + 500, y = m.pos.y, z = m.pos.z - 500 }
+ local v3 = { x = m.pos.x, y = m.pos.y, z = m.pos.z + 500 }
+ moving_surface = smlua_collision_add_surface(false, SURFACE_DEFAULT, v3, v2, v1)
+ end
+
+ -- U_JPAD: move the last created surface to Mario's position
+ if (pressed & U_JPAD) ~= 0 and moving_surface then
+ local v1 = { x = m.pos.x - 500, y = m.pos.y - 100, z = m.pos.z - 500 }
+ local v2 = { x = m.pos.x + 500, y = m.pos.y - 100, z = m.pos.z - 500 }
+ local v3 = { x = m.pos.x, y = m.pos.y - 100, z = m.pos.z + 500 }
+ smlua_collision_move_surface(moving_surface, v3, v2, v1)
+ end
+
+ -- L_JPAD: delete the floor
+ if (pressed & L_JPAD) ~= 0 and m.floor then
+ smlua_collision_delete_surface(m.floor)
+ end
+
+ -- R_JPAD: delete the wall
+ if (pressed & R_JPAD) ~= 0 and m.wall then
+ smlua_collision_delete_surface(m.wall)
+ end
+
+ -- X_BUTTON: load static object collision
+ if (pressed & X_BUTTON) ~= 0 then
+ if sSoc and not ~sSoc then
+ remove_static_object_collision(sSoc)
+ sSoc = nil
+ end
+ if sObject then
+ sObject.activeFlags = ACTIVE_FLAG_DEACTIVATED
+ sObject = nil
+ end
+ sObject = spawn_non_sync_object(id_bhvSOC, E_MODEL_BBH_WOODEN_TOMB, m.pos.x, m.pos.y, m.pos.z, nil)
+ end
+
+ -- draw debug visuals
+ if m.floor then
+ draw_surface(m.floor, Dbg.colors.blue, 1)
+ end
+ if m.wall then
+ draw_surface(m.wall, Dbg.colors.green, 1)
+ end
+ if moving_surface and not ~moving_surface then
+ draw_surface(moving_surface, Dbg.colors.red)
+ end
+end)
+
+hook_event(HOOK_ON_HUD_RENDER, function ()
+ djui_hud_set_resolution(RESOLUTION_N64)
+ djui_hud_set_font(FONT_NORMAL)
+ local scale = 0.25
+ local lineH = 32 * scale
+ local x = 16
+ local y = 32
+
+ local function row(label, desc)
+ djui_hud_set_color(0, 0, 0, 255)
+ djui_hud_print_text(label, x + 0.4, y + 0.4, scale)
+ djui_hud_set_color(255, 220, 60, 255)
+ djui_hud_print_text(label, x, y, scale)
+
+ local desc_x = x + djui_hud_measure_text(label) * scale + 8
+ djui_hud_set_color(0, 0, 0, 255)
+ djui_hud_print_text(desc, desc_x + 0.4, y + 0.4, scale)
+ djui_hud_set_color(255, 255, 255, 255)
+ djui_hud_print_text(desc, desc_x, y, scale)
+ y = y + lineH
+ end
+
+ row("D-pad Down: ", "create surface")
+ row("D-pad Up: ", moving_surface and not ~moving_surface and "move surface" or "move surface (none)")
+ row("D-pad Left: ", "delete floor")
+ row("D-pad Right:", "delete wall")
+ row("X: ", "reload static collision")
+end)
\ No newline at end of file
diff --git a/docs/lua/functions-6.md b/docs/lua/functions-6.md
index 41b220737..559d08ef6 100644
--- a/docs/lua/functions-6.md
+++ b/docs/lua/functions-6.md
@@ -7378,6 +7378,82 @@ Gets a table of the surface types from `data`
+## [smlua_collision_add_surface](#smlua_collision_add_surface)
+
+### Description
+Allocates a new collision surface with the given vertices, computes the surface normal and other fields, and inserts it into the spatial partition. Returns the new surface, or `nil` if the triangle is degenerate (zero area). Set `dynamic` to `true` for surfaces that are cleared each frame, or `false` for persistent static surfaces
+
+### Lua Example
+`local surfaceValue = smlua_collision_add_surface(dynamic, surfaceType, vertex1, vertex2, vertex3)`
+
+### Parameters
+| Field | Type |
+| ----- | ---- |
+| dynamic | `boolean` |
+| surfaceType | `integer` |
+| vertex1 | [Vec3s](structs.md#Vec3s) |
+| vertex2 | [Vec3s](structs.md#Vec3s) |
+| vertex3 | [Vec3s](structs.md#Vec3s) |
+
+### Returns
+- [Surface](structs.md#Surface)
+
+### C Prototype
+`struct Surface* smlua_collision_add_surface(bool dynamic, s16 surfaceType, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3);`
+
+[:arrow_up_small:](#)
+
+
+
+## [smlua_collision_move_surface](#smlua_collision_move_surface)
+
+### Description
+Moves an existing collision surface to new vertex positions. Recalculates the surface normal, origin offset, and Y bounds, removes the surface from its old spatial partition cells, and re-adds it to the correct cells. The previous vertices are preserved for interpolation
+
+### Lua Example
+`smlua_collision_move_surface(surface, vertex1, vertex2, vertex3)`
+
+### Parameters
+| Field | Type |
+| ----- | ---- |
+| surface | [Surface](structs.md#Surface) |
+| vertex1 | [Vec3s](structs.md#Vec3s) |
+| vertex2 | [Vec3s](structs.md#Vec3s) |
+| vertex3 | [Vec3s](structs.md#Vec3s) |
+
+### Returns
+- None
+
+### C Prototype
+`void smlua_collision_move_surface(struct Surface *surface, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3);`
+
+[:arrow_up_small:](#)
+
+
+
+## [smlua_collision_delete_surface](#smlua_collision_delete_surface)
+
+### Description
+Fully deletes a collision surface: removes it from the spatial partitions and frees its pool slot.
+
+### Lua Example
+`smlua_collision_delete_surface(surface)`
+
+### Parameters
+| Field | Type |
+| ----- | ---- |
+| surface | [Surface](structs.md#Surface) |
+
+### Returns
+- None
+
+### C Prototype
+`void smlua_collision_delete_surface(struct Surface *surface);`
+
+[:arrow_up_small:](#)
+
+
+
## [surface_is_quicksand](#surface_is_quicksand)
### Description
diff --git a/docs/lua/functions-7.md b/docs/lua/functions-7.md
index 207edbb6f..ff0d55353 100644
--- a/docs/lua/functions-7.md
+++ b/docs/lua/functions-7.md
@@ -5038,6 +5038,29 @@ Gets a surface corresponding to `index` from the static object collision
+## [remove_static_object_collision](#remove_static_object_collision)
+
+### Description
+Removes all surfaces belonging to a static object collision and reclaims the SOC metadata
+
+### Lua Example
+`remove_static_object_collision(col)`
+
+### Parameters
+| Field | Type |
+| ----- | ---- |
+| col | [StaticObjectCollision](structs.md#StaticObjectCollision) |
+
+### Returns
+- None
+
+### C Prototype
+`void remove_static_object_collision(struct StaticObjectCollision *col);`
+
+[:arrow_up_small:](#)
+
+
+
## [obj_get_surface_from_index](#obj_get_surface_from_index)
### Description
diff --git a/docs/lua/functions.md b/docs/lua/functions.md
index 4076375df..170c4dff7 100644
--- a/docs/lua/functions.md
+++ b/docs/lua/functions.md
@@ -1914,6 +1914,9 @@
- [smlua_collision_util_get_current_terrain_collision](functions-6.md#smlua_collision_util_get_current_terrain_collision)
- [smlua_collision_util_get_level_collision](functions-6.md#smlua_collision_util_get_level_collision)
- [smlua_collision_util_find_surface_types](functions-6.md#smlua_collision_util_find_surface_types)
+ - [smlua_collision_add_surface](functions-6.md#smlua_collision_add_surface)
+ - [smlua_collision_move_surface](functions-6.md#smlua_collision_move_surface)
+ - [smlua_collision_delete_surface](functions-6.md#smlua_collision_delete_surface)
- [surface_is_quicksand](functions-6.md#surface_is_quicksand)
- [surface_is_not_hard](functions-6.md#surface_is_not_hard)
- [surface_is_painting_warp](functions-6.md#surface_is_painting_warp)
@@ -2223,6 +2226,7 @@
- [load_static_object_collision](functions-7.md#load_static_object_collision)
- [toggle_static_object_collision](functions-7.md#toggle_static_object_collision)
- [get_static_object_surface](functions-7.md#get_static_object_surface)
+ - [remove_static_object_collision](functions-7.md#remove_static_object_collision)
- [obj_get_surface_from_index](functions-7.md#obj_get_surface_from_index)
- [surface_has_force](functions-7.md#surface_has_force)
diff --git a/docs/lua/structs.md b/docs/lua/structs.md
index 79c6d19ff..7d548be98 100644
--- a/docs/lua/structs.md
+++ b/docs/lua/structs.md
@@ -2931,7 +2931,6 @@
| Field | Type | Access |
| ----- | ---- | ------ |
-| index | `integer` | read-only |
| length | `integer` | read-only |
[:arrow_up_small:](#)
@@ -2945,6 +2944,7 @@
| type | `integer` | |
| flags | `integer` | |
| room | `integer` | |
+| poolType | `integer` | read-only |
| force | `integer` | |
| lowerY | `integer` | |
| upperY | `integer` | |
@@ -2957,6 +2957,7 @@
| normal | [Vec3f](structs.md#Vec3f) | read-only |
| originOffset | `number` | |
| modifiedTimestamp | `integer` | |
+| socId | `integer` | read-only |
| object | [Object](structs.md#Object) | |
[:arrow_up_small:](#)
diff --git a/include/types.h b/include/types.h
index ea64ff19b..006531203 100644
--- a/include/types.h
+++ b/include/types.h
@@ -282,10 +282,13 @@ struct Object
enum AreaTimerType areaTimerType;
Mat4 transform;
-
+
+ // Obsolete: pool index is no longer stable
u32 firstSurface;
+
+ // Tracks the number of dynamic surfaces currently owned by this object.
u32 numSurfaces;
-
+
u32 heldByPlayerIndex;
u8 setHome;
@@ -344,14 +347,15 @@ struct Waypoint
struct Surface
{
// For optimization reasons, See MarioState
-
+
s16 type;
s8 flags;
s8 room;
+ s8 poolType;
s16 force;
s16 lowerY;
s16 upperY;
-
+
Vec3s vertex1;
Vec3s vertex2;
Vec3s vertex3;
@@ -363,10 +367,11 @@ struct Surface
f32 y;
f32 z;
} normal;
-
+
f32 originOffset;
u32 modifiedTimestamp;
-
+ u32 socId;
+
struct Object *object;
};
diff --git a/src/engine/surface_load.c b/src/engine/surface_load.c
index 345a73202..c611750b0 100644
--- a/src/engine/surface_load.c
+++ b/src/engine/surface_load.c
@@ -62,8 +62,12 @@ s32 gNumSOCSurfaces;
/**
* Pools of data to contain either surface nodes or surfaces.
*/
-static struct GrowingArray *sSurfaceNodePool = NULL;
-static struct GrowingArray *sSurfacePool = NULL;
+static struct GrowingArray *sSurfaceStaticNodePool = NULL;
+static struct GrowingArray *sSurfaceStaticPool = NULL;
+static struct GrowingArray *sSurfaceSOCNodePool = NULL;
+static struct GrowingArray *sSurfaceSOCPool = NULL;
+static struct GrowingArray *sSurfaceDynamicNodePool = NULL;
+static struct GrowingArray *sSurfaceDynamicPool = NULL;
/**
* Pool of data for static object collisions.
@@ -71,20 +75,53 @@ static struct GrowingArray *sSurfacePool = NULL;
static struct GrowingArray *sSOCPool = NULL;
/**
- * Allocate the part of the surface node pool to contain a surface node.
+ * Counter for assigning unique SOC IDs.
*/
-static struct SurfaceNode *alloc_surface_node(void) {
- sSurfaceNodePool->count = gSurfaceNodesAllocated++;
- return growing_array_alloc(sSurfaceNodePool, sizeof(struct SurfaceNode));
+static u32 sSOCIdCounter = 0;
+
+/**
+ * When true, add_surface skips firing HOOK_ON_ADD_SURFACE.
+ */
+static bool sSkipAddSurfaceHook = false;
+
+/**
+ * Custom free function for StaticObjectCollision entries.
+ * Invalidates the Lua CObject, then frees the struct.
+ */
+static void free_static_object_collision(void *ptr) {
+ smlua_free_soc(ptr);
}
/**
- * Allocate the part of the surface pool to contain a surface and
- * initialize the surface.
+ * Allocate a surface node from the appropriate pool.
*/
-static struct Surface *alloc_surface(void) {
- sSurfacePool->count = gSurfacesAllocated++;
- return growing_array_alloc(sSurfacePool, sizeof(struct Surface));
+static struct SurfaceNode *alloc_surface_node(s32 poolType) {
+ gSurfaceNodesAllocated++;
+ struct GrowingArray *pool;
+ switch (poolType) {
+ case SURFACE_POOL_DYNAMIC: pool = sSurfaceDynamicNodePool; break;
+ case SURFACE_POOL_SOC: pool = sSurfaceSOCNodePool; break;
+ default: pool = sSurfaceStaticNodePool; break;
+ }
+ return growing_array_alloc(pool, sizeof(struct SurfaceNode));
+}
+
+/**
+ * Allocate a surface from the appropriate pool.
+ */
+struct Surface *alloc_surface(s32 poolType) {
+ gSurfacesAllocated++;
+ struct GrowingArray *pool;
+ switch (poolType) {
+ case SURFACE_POOL_DYNAMIC: pool = sSurfaceDynamicPool; break;
+ case SURFACE_POOL_SOC: pool = sSurfaceSOCPool; break;
+ default: pool = sSurfaceStaticPool; break;
+ }
+ struct Surface *surface = growing_array_alloc(pool, sizeof(struct Surface));
+ if (surface != NULL) {
+ surface->poolType = poolType;
+ }
+ return surface;
}
static struct StaticObjectCollision *alloc_static_object_collision(void) {
@@ -111,18 +148,39 @@ static void clear_spatial_partition(SpatialPartitionCell *cells) {
*/
static void clear_static_surfaces(void) {
clear_spatial_partition(&gStaticSurfacePartition[0][0]);
- sSOCPool = growing_array_init(sSOCPool, 0x100, malloc, smlua_free_soc);
+
+ // Invalidate Lua CObjects for surfaces that are about to be recycled
+ if (sSurfaceStaticPool) {
+ for (u32 i = 0; i < sSurfaceStaticPool->count; i++) {
+ if (sSurfaceStaticPool->buffer[i]) {
+ smlua_invalidate_surface(sSurfaceStaticPool->buffer[i]);
+ }
+ }
+ sSurfaceStaticPool->count = 0;
+ }
+ if (sSurfaceSOCPool) {
+ for (u32 i = 0; i < sSurfaceSOCPool->count; i++) {
+ if (sSurfaceSOCPool->buffer[i]) {
+ smlua_invalidate_surface(sSurfaceSOCPool->buffer[i]);
+ }
+ }
+ sSurfaceSOCPool->count = 0;
+ }
+ if (sSurfaceStaticNodePool) { sSurfaceStaticNodePool->count = 0; }
+ if (sSurfaceSOCNodePool) { sSurfaceSOCNodePool->count = 0; }
+
+ sSOCPool = growing_array_init(sSOCPool, 0x100, malloc, free_static_object_collision);
+ sSOCIdCounter = 0;
}
/**
* Add a surface to the correct cell list of surfaces.
- * @param dynamic Determines whether the surface is static or dynamic
* @param cellX The X position of the cell in which the surface resides
* @param cellZ The Z position of the cell in which the surface resides
* @param surface The surface to add
*/
-static void add_surface_to_cell(s16 dynamic, s16 cellX, s16 cellZ, struct Surface *surface) {
- struct SurfaceNode *newNode = alloc_surface_node();
+static void add_surface_to_cell(s16 cellX, s16 cellZ, struct Surface *surface) {
+ struct SurfaceNode *newNode = alloc_surface_node(surface->poolType);
if (newNode == NULL) { return; }
struct SurfaceNode *list;
s16 surfacePriority;
@@ -159,7 +217,7 @@ static void add_surface_to_cell(s16 dynamic, s16 cellX, s16 cellZ, struct Surfac
newNode->surface = surface;
- if (dynamic) {
+ if (surface->poolType == SURFACE_POOL_DYNAMIC) {
list = &gDynamicSurfacePartition[cellZ][cellX][listIndex];
} else {
list = &gStaticSurfacePartition[cellZ][cellX][listIndex];
@@ -260,10 +318,9 @@ static s16 upper_cell_index(s32 coord) {
* Every level is split into 16x16 cells, this takes a surface, finds
* the appropriate cells (with a buffer), and adds the surface to those
* cells.
- * @param surface The surface to check
- * @param dynamic Boolean determining whether the surface is static or dynamic
+ * @param surface The surface to add
*/
-static void add_surface(struct Surface *surface, s32 dynamic) {
+void add_surface(struct Surface *surface) {
// minY/maxY maybe? s32 instead of s16, though.
s16 minX, minZ, maxX, maxZ;
@@ -271,7 +328,9 @@ static void add_surface(struct Surface *surface, s32 dynamic) {
s16 cellZ, cellX;
- smlua_call_event_hooks(HOOK_ON_ADD_SURFACE, surface, dynamic);
+ if (!sSkipAddSurfaceHook) {
+ smlua_call_event_hooks(HOOK_ON_ADD_SURFACE, surface, surface->poolType == SURFACE_POOL_DYNAMIC);
+ }
minX = min_3(surface->vertex1[0], surface->vertex2[0], surface->vertex3[0]);
minZ = min_3(surface->vertex1[2], surface->vertex2[2], surface->vertex3[2]);
@@ -285,18 +344,150 @@ static void add_surface(struct Surface *surface, s32 dynamic) {
for (cellZ = minCellZ; cellZ <= maxCellZ; cellZ++) {
for (cellX = minCellX; cellX <= maxCellX; cellX++) {
- add_surface_to_cell(dynamic, cellX, cellZ, surface);
+ add_surface_to_cell(cellX, cellZ, surface);
}
}
}
+/**
+ * Add a surface, but don't call HOOK_ON_ADD_SURFACE.
+ */
+void add_surface_without_hook(struct Surface *surface) {
+ sSkipAddSurfaceHook = true;
+ add_surface(surface);
+ sSkipAddSurfaceHook = false;
+}
+
+/**
+ * Swap-and-pop a surface out of its owning pool, selected by poolType.
+ * Does not touch counters or partitions; the caller is responsible.
+ */
+bool swap_and_pop_surface_pool(s32 poolType, struct Surface *surface) {
+ switch (poolType) {
+ case SURFACE_POOL_DYNAMIC: return growing_array_swap_and_pop(sSurfaceDynamicPool, surface);
+ case SURFACE_POOL_SOC: return growing_array_swap_and_pop(sSurfaceSOCPool, surface);
+ default: return growing_array_swap_and_pop(sSurfaceStaticPool, surface);
+ }
+}
+
+/**
+ * Removes a surface from the spatial partition, reclaiming any nodes that were allocated for it.
+ */
+void remove_surface_from_partition(struct Surface *surface) {
+ if (surface == NULL) { return; }
+
+ s32 poolType = surface->poolType;
+ bool isDynamic = (poolType == SURFACE_POOL_DYNAMIC);
+
+ SpatialPartitionCell (*partition)[NUM_CELLS] = isDynamic
+ ? gDynamicSurfacePartition
+ : gStaticSurfacePartition;
+
+ struct GrowingArray *nodePool;
+
+ switch (poolType) {
+ case SURFACE_POOL_DYNAMIC: nodePool = sSurfaceDynamicNodePool; break;
+ case SURFACE_POOL_SOC: nodePool = sSurfaceSOCNodePool; break;
+ default: nodePool = sSurfaceStaticNodePool; break;
+ }
+
+ for (s32 cellZ = 0; cellZ < NUM_CELLS; cellZ++) {
+ for (s32 cellX = 0; cellX < NUM_CELLS; cellX++) {
+ for (s32 listIndex = 0; listIndex < 3; listIndex++) {
+ struct SurfaceNode *prev = &partition[cellZ][cellX][listIndex];
+ struct SurfaceNode *node = prev->next;
+
+ while (node != NULL) {
+ if (node->surface != surface) {
+ prev = node;
+ node = node->next;
+ continue;
+ }
+
+ if (growing_array_swap_and_pop(nodePool, node)) {
+ prev->next = node->next;
+ gSurfaceNodesAllocated--;
+ node = prev->next;
+ continue;
+ }
+
+ prev = node;
+ node = node->next;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Fully delete a surface: invalidate any Lua CObject reference,
+ * remove it from spatial partitions, swap-and-pop it out of its
+ * owning pool, and update counters.
+ */
+void delete_surface(struct Surface *surface) {
+ if (surface == NULL) { return; }
+
+ s32 poolType = surface->poolType;
+
+ // Invalidate Lua CObject reference
+ smlua_invalidate_surface(surface);
+
+ // Snapshot the relevant node pool count to track how many nodes are reclaimed
+ struct GrowingArray *nodePool;
+ switch (poolType) {
+ case SURFACE_POOL_DYNAMIC: nodePool = sSurfaceDynamicNodePool; break;
+ case SURFACE_POOL_SOC: nodePool = sSurfaceSOCNodePool; break;
+ default: nodePool = sSurfaceStaticNodePool; break;
+ }
+ u32 nodesBefore = nodePool ? nodePool->count : 0;
+
+ // Remove from spatial partitions (also reclaims node pool slots)
+ remove_surface_from_partition(surface);
+
+ u32 nodesRemoved = nodesBefore - (nodePool ? nodePool->count : 0);
+
+ // Decrement the owning SOC's tracked length
+ if (poolType == SURFACE_POOL_SOC && sSOCPool) {
+ for (u32 ci = 0; ci < sSOCPool->count; ci++) {
+ struct StaticObjectCollision *col = sSOCPool->buffer[ci];
+ if (col && col->index == surface->socId) {
+ if (col->length > 0) { col->length--; }
+ break;
+ }
+ }
+ }
+
+ // Swap-and-pop from the owning pool and update counters
+ switch (poolType) {
+ case SURFACE_POOL_SOC:
+ growing_array_swap_and_pop(sSurfaceSOCPool, surface);
+ gNumSOCSurfaces--;
+ gNumSOCSurfaceNodes -= nodesRemoved;
+ break;
+ case SURFACE_POOL_STATIC:
+ growing_array_swap_and_pop(sSurfaceStaticPool, surface);
+ gNumStaticSurfaces--;
+ gNumStaticSurfaceNodes -= nodesRemoved;
+ break;
+ case SURFACE_POOL_DYNAMIC:
+ if (surface->object && surface->object->numSurfaces > 0) {
+ surface->object->numSurfaces--;
+ }
+ growing_array_swap_and_pop(sSurfaceDynamicPool, surface);
+ break;
+ }
+ gSurfacesAllocated--;
+}
+
/**
* Initializes a Surface struct using the given vertex data
* @param vertexData The raw data containing vertex positions
* @param vertexIndices Helper which tells positions in vertexData to start reading vertices
+ * @param poolType The pool type to allocate the surface from
+ * @return A pointer to the newly allocated surface, or NULL on failure
*/
-static struct Surface *read_surface_data(s16 *vertexData, s16 **vertexIndices) {
+static struct Surface *read_surface_data(s16 *vertexData, s16 **vertexIndices, s32 poolType) {
if (vertexData == NULL || vertexIndices == NULL || *vertexIndices == NULL) { return NULL; }
struct Surface *surface;
@@ -356,7 +547,7 @@ static struct Surface *read_surface_data(s16 *vertexData, s16 **vertexIndices) {
ny *= mag;
nz *= mag;
- surface = alloc_surface();
+ surface = alloc_surface(poolType);
if (surface == NULL) { return NULL; }
vec3s_copy(surface->prevVertex1, surface->vertex1);
@@ -435,7 +626,7 @@ static void load_static_surfaces(s16 **data, s16 *vertexData, s16 surfaceType, s
*surfaceRooms += 1;
}
- surface = read_surface_data(vertexData, data);
+ surface = read_surface_data(vertexData, data, SURFACE_POOL_STATIC);
if (surface != NULL) {
surface->room = room;
surface->type = surfaceType;
@@ -447,7 +638,7 @@ static void load_static_surfaces(s16 **data, s16 *vertexData, s16 surfaceType, s
surface->force = 0;
}
- add_surface(surface, FALSE);
+ add_surface(surface);
}
*data += 3;
@@ -513,8 +704,14 @@ void alloc_surface_pools(void) {
clear_static_surfaces();
clear_dynamic_surfaces();
- sSurfaceNodePool = growing_array_init(sSurfaceNodePool, 0x1000, malloc, free);
- sSurfacePool = growing_array_init(sSurfacePool, 0x400, malloc, smlua_free_surface);
+ sSurfaceStaticNodePool = growing_array_init(sSurfaceStaticNodePool, 0x1000, malloc, free);
+ sSurfaceStaticPool = growing_array_init(sSurfaceStaticPool, 0x400, malloc, smlua_free_surface);
+
+ sSurfaceSOCNodePool = growing_array_init(sSurfaceSOCNodePool, 0x800, malloc, free);
+ sSurfaceSOCPool = growing_array_init(sSurfaceSOCPool, 0x200, malloc, smlua_free_surface);
+
+ sSurfaceDynamicNodePool = growing_array_init(sSurfaceDynamicNodePool, 0x1000, malloc, free);
+ sSurfaceDynamicPool = growing_array_init(sSurfaceDynamicPool, 0x400, malloc, smlua_free_surface);
gEnvironmentRegions = NULL;
gSurfaceNodesAllocated = 0;
@@ -638,17 +835,32 @@ void load_area_terrain(s16 index, s16 *data, s8 *surfaceRooms, s16 *macroObjects
* If not in time stop, clear the surface partitions.
*/
void clear_dynamic_surfaces(void) {
- if (!(gTimeStopState & TIME_STOP_ACTIVE)) {
- gSurfacesAllocated = gNumStaticSurfaces + gNumSOCSurfaces;
- gSurfaceNodesAllocated = gNumStaticSurfaceNodes + gNumSOCSurfaceNodes;
+ if (gTimeStopState & TIME_STOP_ACTIVE) { return; }
- clear_spatial_partition(&gDynamicSurfacePartition[0][0]);
-
- for (u16 i = 0; i < OBJECT_POOL_CAPACITY; i++) {
- struct Object *obj = &gObjectPool[i];
- obj->firstSurface = 0;
- obj->numSurfaces = 0;
+ if (sSurfaceDynamicPool) {
+ // Invalidate Lua CObjects for dynamic surfaces being recycled
+ for (u32 i = 0; i < sSurfaceDynamicPool->count; i++) {
+ if (sSurfaceDynamicPool->buffer[i]) {
+ smlua_invalidate_surface(sSurfaceDynamicPool->buffer[i]);
+ }
}
+ sSurfaceDynamicPool->count = 0;
+ }
+
+ if (sSurfaceDynamicNodePool) {
+ sSurfaceDynamicNodePool->count = 0;
+ }
+
+ gSurfacesAllocated = (sSurfaceStaticPool ? sSurfaceStaticPool->count : 0)
+ + (sSurfaceSOCPool ? sSurfaceSOCPool->count : 0);
+
+ gSurfaceNodesAllocated = (sSurfaceStaticNodePool ? sSurfaceStaticNodePool->count : 0)
+ + (sSurfaceSOCNodePool ? sSurfaceSOCNodePool->count : 0);
+
+ clear_spatial_partition(&gDynamicSurfacePartition[0][0]);
+
+ for (u16 i = 0; i < OBJECT_POOL_CAPACITY; i++) {
+ gObjectPool[i].numSurfaces = 0;
}
}
@@ -714,7 +926,9 @@ void load_object_surfaces(s16** data, s16* vertexData, bool isSOC) {
hasForce = surface_has_force(surfaceType);
flags = surf_has_no_cam_collision(surfaceType) ? SURFACE_FLAG_NO_CAM_COLLISION : 0;
- flags |= SURFACE_FLAG_DYNAMIC;
+ if (!isSOC) {
+ flags |= SURFACE_FLAG_DYNAMIC;
+ }
// The DDD warp is initially loaded at the origin and moved to the proper
// position in paintings.c and doesn't update its room, so set it here.
@@ -725,19 +939,10 @@ void load_object_surfaces(s16** data, s16* vertexData, bool isSOC) {
}
for (i = 0; i < numSurfaces; i++) {
- struct Surface* surface = read_surface_data(vertexData, data);
+ s32 poolType = isSOC ? SURFACE_POOL_SOC : SURFACE_POOL_DYNAMIC;
+ struct Surface* surface = read_surface_data(vertexData, data, poolType);
if (surface != NULL) {
- if (!isSOC) {
- // Set index of first surface
- if (gCurrentObject->firstSurface == 0) {
- gCurrentObject->firstSurface = gSurfacesAllocated - 1;
- }
-
- // Increase surface count
- gCurrentObject->numSurfaces++;
- }
-
surface->object = gCurrentObject;
surface->type = surfaceType;
@@ -749,7 +954,11 @@ void load_object_surfaces(s16** data, s16* vertexData, bool isSOC) {
surface->flags |= flags;
surface->room = (s8)room;
- add_surface(surface, !isSOC);
+ add_surface(surface);
+
+ if (!isSOC) {
+ gCurrentObject->numSurfaces++;
+ }
}
if (hasForce) {
@@ -852,36 +1061,38 @@ void load_object_collision_model(void) {
}
struct StaticObjectCollision *load_static_object_collision() {
- struct StaticObjectCollision *col;
- u32 lastSurfaceIndex = gSurfacesAllocated;
- u32 lastSurfaceNodeIndex = gSurfaceNodesAllocated;
- u32 lastSOCSurfaceIndex = gNumStaticSurfaces + gNumSOCSurfaces;
- u32 lastSOCSurfaceNodeIndex = gNumStaticSurfaceNodes + gNumSOCSurfaceNodes;
+ u32 startCount = sSurfaceSOCPool->count;
+ u32 startNodeCount = sSurfaceSOCNodePool->count;
load_object_collision_model_internal(true);
- // Reorder surfaces and nodes and update SOC variables
- u32 addedSurfaces = gSurfacesAllocated - lastSurfaceIndex;
- u32 addedSurfaceNodes = gSurfaceNodesAllocated - lastSurfaceNodeIndex;
- if (addedSurfaces > 0) {
- growing_array_move(sSurfacePool, lastSurfaceIndex, lastSOCSurfaceIndex, addedSurfaces);
- gNumSOCSurfaces += addedSurfaces;
- }
- if (addedSurfaceNodes > 0) {
- growing_array_move(sSurfaceNodePool, lastSurfaceNodeIndex, lastSOCSurfaceNodeIndex, addedSurfaceNodes);
- gNumSOCSurfaceNodes += addedSurfaceNodes;
- }
+ u32 addedSurfaces = sSurfaceSOCPool->count - startCount;
+ u32 addedNodes = sSurfaceSOCNodePool->count - startNodeCount;
- col = alloc_static_object_collision();
- col->index = lastSOCSurfaceIndex;
+ gNumSOCSurfaces += addedSurfaces;
+ gNumSOCSurfaceNodes += addedNodes;
+
+ struct StaticObjectCollision *col = alloc_static_object_collision();
+ col->index = ++sSOCIdCounter;
col->length = addedSurfaces;
+ // Tag each new surface with this SOC's unique ID
+ for (u32 i = 0; i < addedSurfaces; i++) {
+ struct Surface *surf = sSurfaceSOCPool->buffer[startCount + i];
+ if (surf) { surf->socId = col->index; }
+ }
+
return col;
}
void toggle_static_object_collision(struct StaticObjectCollision *col, bool tangible) {
- for (s32 i = 0; i < col->length; i++) {
- struct Surface *surf = sSurfacePool->buffer[col->index + i];
+ if (!col || !sSurfaceSOCPool) { return; }
+
+ for (u32 i = 0; i < sSurfaceSOCPool->count; i++) {
+ struct Surface *surf = sSurfaceSOCPool->buffer[i];
+
+ if (!surf || surf->socId != col->index) { continue; }
+
if (tangible) {
surf->flags &= ~SURFACE_FLAG_INTANGIBLE;
} else {
@@ -891,15 +1102,56 @@ void toggle_static_object_collision(struct StaticObjectCollision *col, bool tang
}
struct Surface *get_static_object_surface(struct StaticObjectCollision *col, u32 index) {
- if (!col) { return NULL; }
- if (index >= col->length) { return NULL; }
- struct Surface *surf = sSurfacePool->buffer[col->index + index];
- return surf;
+ if (!col || !sSurfaceSOCPool) { return NULL; }
+
+ u32 count = 0;
+
+ for (u32 i = 0; i < sSurfaceSOCPool->count; i++) {
+ struct Surface *surf = sSurfaceSOCPool->buffer[i];
+ if (!surf || surf->socId != col->index) { continue; }
+ if (count == index) { return surf; }
+ count++;
+ }
+
+ return NULL;
+}
+
+void remove_static_object_collision(struct StaticObjectCollision *col) {
+ if (!col || !sSurfaceSOCPool) { return; }
+
+ // delete_surface uses swap-and-pop, so after deleting buffer[i] the slot
+ // is filled by the former last element (and must be checked again)
+ u32 i = 0;
+ while (i < sSurfaceSOCPool->count) {
+ struct Surface *surf = sSurfaceSOCPool->buffer[i];
+ if (surf && surf->socId == col->index) {
+ delete_surface(surf);
+ } else {
+ i++;
+ }
+ }
+
+ col->length = 0;
+ col->index = 0;
+
+ // reclaim the SOC metadata from the pool
+ smlua_cobject_invalidate(col, LOT_STATICOBJECTCOLLISION);
+ growing_array_swap_and_pop(sSOCPool, col);
}
struct Surface *obj_get_surface_from_index(struct Object *o, u32 index) {
- if (!o || o->firstSurface == 0) { return NULL; }
- if (index >= o->numSurfaces) { return NULL; }
- struct Surface *surf = sSurfacePool->buffer[o->firstSurface + index];
- return surf;
+ if (!o || !sSurfaceDynamicPool) { return NULL; }
+
+ u32 count = 0;
+ for (u32 i = 0; i < sSurfaceDynamicPool->count; i++) {
+ struct Surface *surf = sSurfaceDynamicPool->buffer[i];
+ if (surf && surf->object == o) {
+ if (count == index) {
+ return surf;
+ }
+ count++;
+ }
+ }
+
+ return NULL;
}
diff --git a/src/engine/surface_load.h b/src/engine/surface_load.h
index d0f2db1ad..7ceb84a30 100644
--- a/src/engine/surface_load.h
+++ b/src/engine/surface_load.h
@@ -34,7 +34,17 @@ extern s32 gNumStaticSurfaces;
extern s32 gNumSOCSurfaceNodes;
extern s32 gNumSOCSurfaces;
+#define SURFACE_POOL_STATIC 0
+#define SURFACE_POOL_DYNAMIC 1
+#define SURFACE_POOL_SOC 2
+
void alloc_surface_pools(void);
+struct Surface *alloc_surface(s32 poolType);
+void add_surface(struct Surface *surface);
+void add_surface_without_hook(struct Surface *surface);
+void remove_surface_from_partition(struct Surface *surface);
+void delete_surface(struct Surface *surface);
+bool swap_and_pop_surface_pool(s32 poolType, struct Surface *surface);
u32 get_area_terrain_size(s16 *data);
@@ -54,6 +64,8 @@ struct StaticObjectCollision *load_static_object_collision();
void toggle_static_object_collision(struct StaticObjectCollision *col, bool tangible);
/* |description|Gets a surface corresponding to `index` from the static object collision|descriptionEnd| */
struct Surface *get_static_object_surface(struct StaticObjectCollision *col, u32 index);
+/* |description|Removes all surfaces belonging to a static object collision and reclaims the SOC metadata|descriptionEnd| */
+void remove_static_object_collision(struct StaticObjectCollision *col);
/* |description|Gets a surface corresponding to `index` from the surface pool buffer|descriptionEnd| */
struct Surface *obj_get_surface_from_index(struct Object *o, u32 index);
/* |description|Checks if a surface has force|descriptionEnd| */
diff --git a/src/game/memory.c b/src/game/memory.c
index feca24fd1..515587fe4 100644
--- a/src/game/memory.c
+++ b/src/game/memory.c
@@ -246,6 +246,34 @@ void growing_array_move(struct GrowingArray *array, u32 from, u32 to, u32 count)
}
}
+/**
+ * Swap-and-pop the entry at position `index` out of the array.
+ * The slot is filled by the last live entry, and the count is decremented.
+ * Returns true if the index was valid.
+ */
+bool growing_array_swap_and_pop_index(struct GrowingArray *array, u32 index) {
+ if (!array || index >= array->count) { return false; }
+ array->count--;
+ void *tmp = array->buffer[index];
+ array->buffer[index] = array->buffer[array->count];
+ array->buffer[array->count] = tmp;
+ return true;
+}
+
+/**
+ * Swap-and-pop the first slot whose pointer equals `ptr`.
+ * Returns true if the entry was found and removed.
+ */
+bool growing_array_swap_and_pop(struct GrowingArray *array, void *ptr) {
+ if (!array) { return false; }
+ for (u32 i = 0; i < array->count; i++) {
+ if (array->buffer[i] == ptr) {
+ return growing_array_swap_and_pop_index(array, i);
+ }
+ }
+ return false;
+}
+
void growing_array_free(struct GrowingArray **array) {
if (*array) {
for (u32 i = 0; i != (*array)->capacity; ++i) {
diff --git a/src/game/memory.h b/src/game/memory.h
index d8f469814..ba7b9186d 100644
--- a/src/game/memory.h
+++ b/src/game/memory.h
@@ -78,6 +78,8 @@ void growing_pool_free_pool(struct GrowingPool *pool);
struct GrowingArray *growing_array_init(struct GrowingArray *array, u32 capacity, GrowingArrayAllocFunc alloc, GrowingArrayFreeFunc free);
void *growing_array_alloc(struct GrowingArray *array, u32 size);
void growing_array_move(struct GrowingArray *array, u32 from, u32 to, u32 count);
+bool growing_array_swap_and_pop_index(struct GrowingArray *array, u32 index);
+bool growing_array_swap_and_pop(struct GrowingArray *array, void *ptr);
void growing_array_free(struct GrowingArray **array);
void growing_array_debug_print(struct GrowingArray *array, const char *name, s32 x, s32 y);
diff --git a/src/pc/lua/smlua_cobject_autogen.c b/src/pc/lua/smlua_cobject_autogen.c
index 35a3ce52d..9faf92601 100644
--- a/src/pc/lua/smlua_cobject_autogen.c
+++ b/src/pc/lua/smlua_cobject_autogen.c
@@ -2576,7 +2576,7 @@ static struct LuaObjectField sStaticObjectCollisionFields[LUA_STATIC_OBJECT_COLL
{ "length", LVT_U16, offsetof(struct StaticObjectCollision, length), true, LOT_NONE, 1, sizeof(u16) },
};
-#define LUA_SURFACE_FIELD_COUNT 16
+#define LUA_SURFACE_FIELD_COUNT 18
static struct LuaObjectField sSurfaceFields[LUA_SURFACE_FIELD_COUNT] = {
{ "flags", LVT_S8, offsetof(struct Surface, flags), false, LOT_NONE, 1, sizeof(s8) },
{ "force", LVT_S16, offsetof(struct Surface, force), false, LOT_NONE, 1, sizeof(s16) },
@@ -2585,10 +2585,12 @@ static struct LuaObjectField sSurfaceFields[LUA_SURFACE_FIELD_COUNT] = {
{ "normal", LVT_COBJECT, offsetof(struct Surface, normal), true, LOT_VEC3F, 1, sizeof(Vec3f) },
{ "object", LVT_COBJECT_P, offsetof(struct Surface, object), false, LOT_OBJECT, 1, sizeof(struct Object*) },
{ "originOffset", LVT_F32, offsetof(struct Surface, originOffset), false, LOT_NONE, 1, sizeof(f32) },
+ { "poolType", LVT_S8, offsetof(struct Surface, poolType), true, LOT_NONE, 1, sizeof(s8) },
{ "prevVertex1", LVT_COBJECT, offsetof(struct Surface, prevVertex1), true, LOT_VEC3S, 1, sizeof(Vec3s) },
{ "prevVertex2", LVT_COBJECT, offsetof(struct Surface, prevVertex2), true, LOT_VEC3S, 1, sizeof(Vec3s) },
{ "prevVertex3", LVT_COBJECT, offsetof(struct Surface, prevVertex3), true, LOT_VEC3S, 1, sizeof(Vec3s) },
{ "room", LVT_S8, offsetof(struct Surface, room), false, LOT_NONE, 1, sizeof(s8) },
+ { "socId", LVT_U32, offsetof(struct Surface, socId), true, LOT_NONE, 1, sizeof(u32) },
{ "type", LVT_S16, offsetof(struct Surface, type), false, LOT_NONE, 1, sizeof(s16) },
{ "upperY", LVT_S16, offsetof(struct Surface, upperY), false, LOT_NONE, 1, sizeof(s16) },
{ "vertex1", LVT_COBJECT, offsetof(struct Surface, vertex1), true, LOT_VEC3S, 1, sizeof(Vec3s) },
diff --git a/src/pc/lua/smlua_constants_autogen.c b/src/pc/lua/smlua_constants_autogen.c
index d7ca5a882..21b734011 100644
--- a/src/pc/lua/smlua_constants_autogen.c
+++ b/src/pc/lua/smlua_constants_autogen.c
@@ -4453,6 +4453,9 @@ char gSmluaConstants[] = ""
"SOUND_OBJ2_MONTY_MOLE_APPEAR=SOUND_ARG_LOAD(SOUND_BANK_OBJ2, 0x67, 0x80, SOUND_DISCRETE)\n"
"SOUND_OBJ2_BOSS_DIALOG_GRUNT=SOUND_ARG_LOAD(SOUND_BANK_OBJ2, 0x69, 0x40, SOUND_DISCRETE)\n"
"SOUND_OBJ2_MRI_SPINNING=SOUND_ARG_LOAD(SOUND_BANK_OBJ2, 0x6B, 0x00, SOUND_DISCRETE)\n"
+"SURFACE_POOL_STATIC=0\n"
+"SURFACE_POOL_DYNAMIC=1\n"
+"SURFACE_POOL_SOC=2\n"
"SURFACE_DEFAULT=0x0000\n"
"SURFACE_BURNING=0x0001\n"
"SURFACE_RAYCAST=0x0003\n"
diff --git a/src/pc/lua/smlua_functions_autogen.c b/src/pc/lua/smlua_functions_autogen.c
index e227a54aa..9bce4eb98 100644
--- a/src/pc/lua/smlua_functions_autogen.c
+++ b/src/pc/lua/smlua_functions_autogen.c
@@ -31732,6 +31732,83 @@ int smlua_func_smlua_collision_util_find_surface_types(lua_State* L) {
return 1;
}
+int smlua_func_smlua_collision_add_surface(lua_State* L) {
+ if (L == NULL) { return 0; }
+
+ int top = lua_gettop(L);
+ if (top != 5) {
+ LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "smlua_collision_add_surface", 5, top);
+ return 0;
+ }
+
+ bool dynamic = smlua_to_boolean(L, 1);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "smlua_collision_add_surface"); return 0; }
+ s16 surfaceType = smlua_to_integer(L, 2);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 2, "smlua_collision_add_surface"); return 0; }
+
+ Vec3s vertex1;
+ smlua_get_vec3s(vertex1, 3);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 3, "smlua_collision_add_surface"); return 0; }
+
+ Vec3s vertex2;
+ smlua_get_vec3s(vertex2, 4);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 4, "smlua_collision_add_surface"); return 0; }
+
+ Vec3s vertex3;
+ smlua_get_vec3s(vertex3, 5);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 5, "smlua_collision_add_surface"); return 0; }
+
+ smlua_push_object(L, LOT_SURFACE, smlua_collision_add_surface(dynamic, surfaceType, vertex1, vertex2, vertex3), NULL);
+
+ return 1;
+}
+
+int smlua_func_smlua_collision_move_surface(lua_State* L) {
+ if (L == NULL) { return 0; }
+
+ int top = lua_gettop(L);
+ if (top != 4) {
+ LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "smlua_collision_move_surface", 4, top);
+ return 0;
+ }
+
+ struct Surface* surface = (struct Surface*)smlua_to_cobject(L, 1, LOT_SURFACE);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "smlua_collision_move_surface"); return 0; }
+
+ Vec3s vertex1;
+ smlua_get_vec3s(vertex1, 2);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 2, "smlua_collision_move_surface"); return 0; }
+
+ Vec3s vertex2;
+ smlua_get_vec3s(vertex2, 3);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 3, "smlua_collision_move_surface"); return 0; }
+
+ Vec3s vertex3;
+ smlua_get_vec3s(vertex3, 4);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 4, "smlua_collision_move_surface"); return 0; }
+
+ smlua_collision_move_surface(surface, vertex1, vertex2, vertex3);
+
+ return 1;
+}
+
+int smlua_func_smlua_collision_delete_surface(lua_State* L) {
+ if (L == NULL) { return 0; }
+
+ int top = lua_gettop(L);
+ if (top != 1) {
+ LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "smlua_collision_delete_surface", 1, top);
+ return 0;
+ }
+
+ struct Surface* surface = (struct Surface*)smlua_to_cobject(L, 1, LOT_SURFACE);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "smlua_collision_delete_surface"); return 0; }
+
+ smlua_collision_delete_surface(surface);
+
+ return 1;
+}
+
int smlua_func_surface_is_quicksand(lua_State* L) {
if (L == NULL) { return 0; }
@@ -36641,6 +36718,23 @@ int smlua_func_get_static_object_surface(lua_State* L) {
return 1;
}
+int smlua_func_remove_static_object_collision(lua_State* L) {
+ if (L == NULL) { return 0; }
+
+ int top = lua_gettop(L);
+ if (top != 1) {
+ LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "remove_static_object_collision", 1, top);
+ return 0;
+ }
+
+ struct StaticObjectCollision* col = (struct StaticObjectCollision*)smlua_to_cobject(L, 1, LOT_STATICOBJECTCOLLISION);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "remove_static_object_collision"); return 0; }
+
+ remove_static_object_collision(col);
+
+ return 1;
+}
+
int smlua_func_obj_get_surface_from_index(lua_State* L) {
if (L == NULL) { return 0; }
@@ -38530,6 +38624,9 @@ void smlua_bind_functions_autogen(void) {
smlua_bind_function(L, "smlua_collision_util_get_current_terrain_collision", smlua_func_smlua_collision_util_get_current_terrain_collision);
smlua_bind_function(L, "smlua_collision_util_get_level_collision", smlua_func_smlua_collision_util_get_level_collision);
smlua_bind_function(L, "smlua_collision_util_find_surface_types", smlua_func_smlua_collision_util_find_surface_types);
+ smlua_bind_function(L, "smlua_collision_add_surface", smlua_func_smlua_collision_add_surface);
+ smlua_bind_function(L, "smlua_collision_move_surface", smlua_func_smlua_collision_move_surface);
+ smlua_bind_function(L, "smlua_collision_delete_surface", smlua_func_smlua_collision_delete_surface);
smlua_bind_function(L, "surface_is_quicksand", smlua_func_surface_is_quicksand);
smlua_bind_function(L, "surface_is_not_hard", smlua_func_surface_is_not_hard);
smlua_bind_function(L, "surface_is_painting_warp", smlua_func_surface_is_painting_warp);
@@ -38825,6 +38922,7 @@ void smlua_bind_functions_autogen(void) {
smlua_bind_function(L, "load_static_object_collision", smlua_func_load_static_object_collision);
smlua_bind_function(L, "toggle_static_object_collision", smlua_func_toggle_static_object_collision);
smlua_bind_function(L, "get_static_object_surface", smlua_func_get_static_object_surface);
+ smlua_bind_function(L, "remove_static_object_collision", smlua_func_remove_static_object_collision);
smlua_bind_function(L, "obj_get_surface_from_index", smlua_func_obj_get_surface_from_index);
smlua_bind_function(L, "surface_has_force", smlua_func_surface_has_force);
diff --git a/src/pc/lua/smlua_utils.c b/src/pc/lua/smlua_utils.c
index a680fe2b0..3696b0df4 100644
--- a/src/pc/lua/smlua_utils.c
+++ b/src/pc/lua/smlua_utils.c
@@ -869,7 +869,7 @@ void smlua_logline(void) {
}
}
-void smlua_free(void *ptr, u16 lot) {
+static void smlua_cobject_invalidate_internal(void *ptr, u16 lot) {
if (ptr && gLuaState) {
lua_State *L = gLuaState;
LUA_STACK_CHECK_BEGIN(L);
@@ -890,5 +890,13 @@ void smlua_free(void *ptr, u16 lot) {
lua_pop(L, 1);
LUA_STACK_CHECK_END(L);
}
+}
+
+void smlua_free(void *ptr, u16 lot) {
+ smlua_cobject_invalidate_internal(ptr, lot);
free(ptr);
}
+
+void smlua_cobject_invalidate(void *ptr, u16 lot) {
+ smlua_cobject_invalidate_internal(ptr, lot);
+}
diff --git a/src/pc/lua/smlua_utils.h b/src/pc/lua/smlua_utils.h
index 6324fc57f..bb8227121 100644
--- a/src/pc/lua/smlua_utils.h
+++ b/src/pc/lua/smlua_utils.h
@@ -67,10 +67,16 @@ void smlua_dump_stack(void);
void smlua_dump_globals(void);
void smlua_dump_table(int index);
void smlua_free(void *ptr, u16 lot);
+void smlua_cobject_invalidate(void *ptr, u16 lot);
#define smlua_free_lot(name, lot) \
static inline void smlua_free_##name(void *ptr) { smlua_free(ptr, lot); }
+#define smlua_invalidate_lot(name, lot) \
+static inline void smlua_invalidate_##name(void *ptr) { smlua_cobject_invalidate(ptr, lot); }
+
+smlua_invalidate_lot(surface, LOT_SURFACE);
+
smlua_free_lot(surface, LOT_SURFACE);
smlua_free_lot(soc, LOT_STATICOBJECTCOLLISION);
diff --git a/src/pc/lua/utils/smlua_collision_utils.c b/src/pc/lua/utils/smlua_collision_utils.c
index 0a83bc172..17ce534a7 100644
--- a/src/pc/lua/utils/smlua_collision_utils.c
+++ b/src/pc/lua/utils/smlua_collision_utils.c
@@ -1,10 +1,12 @@
#include "types.h"
+#include "engine/math_util.h"
#include "engine/surface_collision.h"
#include "include/surface_terrains.h"
#include "game/mario_step.h"
#include "game/area.h"
#include "engine/surface_load.h"
+#include "game/game_init.h"
#include "pc/lua/smlua.h"
#include "smlua_collision_utils.h"
@@ -238,6 +240,150 @@ void smlua_collision_util_find_surface_types(Collision* data) {
lua_pushnil(L);
}
+/**
+ * Compute the normal, plane equation, and Y bounds for a triangle.
+ * Only writes to the surface on success. Returns false if the triangle
+ * is degenerate, leaving the surface completely untouched.
+ */
+static bool calc_surface_fields(struct Surface *surface, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3) {
+ s32 x1 = vertex1[0], y1 = vertex1[1], z1 = vertex1[2];
+ s32 x2 = vertex2[0], y2 = vertex2[1], z2 = vertex2[2];
+ s32 x3 = vertex3[0], y3 = vertex3[1], z3 = vertex3[2];
+
+ // Compute normal via cross product: (v2 - v1) x (v3 - v2)
+ f32 nx = (y2 - y1) * (z3 - z2) - (z2 - z1) * (y3 - y2);
+ f32 ny = (z2 - z1) * (x3 - x2) - (x2 - x1) * (z3 - z2);
+ f32 nz = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
+ f32 mag = sqrtf(nx * nx + ny * ny + nz * nz);
+
+ // Reject degenerate triangles without touching the surface
+ if (mag < 0.0001f) { return false; }
+
+ mag = 1.0f / mag;
+
+ // All checks passed; commit fields to the surface
+ surface->normal.x = nx * mag;
+ surface->normal.y = ny * mag;
+ surface->normal.z = nz * mag;
+ surface->originOffset = -(surface->normal.x * x1 + surface->normal.y * y1 + surface->normal.z * z1);
+
+ surface->lowerY = MIN(MIN(y1, y2), y3) - 5;
+ surface->upperY = MAX(MAX(y1, y2), y3) + 5;
+
+ return true;
+}
+
+struct Surface* smlua_collision_add_surface(bool dynamic, s16 surfaceType, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3) {
+ s32 poolType = dynamic ? SURFACE_POOL_DYNAMIC : SURFACE_POOL_STATIC;
+
+ // Allocate surface from the appropriate pool
+ struct Surface *surface = alloc_surface(poolType);
+ if (surface == NULL) { return NULL; }
+
+ // Compute normal, plane equation, and Y bounds
+ if (!calc_surface_fields(surface, vertex1, vertex2, vertex3)) {
+ // Degenerate triangle: reclaim the pool slot that alloc_surface reserved.
+ // Cannot use delete_surface() here because the surface was never added
+ // to a partition or counter, so delete_surface's counter decrements
+ // would cause drift.
+ smlua_invalidate_surface(surface);
+ swap_and_pop_surface_pool(poolType, surface);
+ gSurfacesAllocated--;
+ return NULL;
+ }
+
+ // Set vertices (prevVertex = vertex for first frame)
+ vec3s_copy(surface->vertex1, vertex1);
+ vec3s_copy(surface->vertex2, vertex2);
+ vec3s_copy(surface->vertex3, vertex3);
+ vec3s_copy(surface->prevVertex1, vertex1);
+ vec3s_copy(surface->prevVertex2, vertex2);
+ vec3s_copy(surface->prevVertex3, vertex3);
+ surface->modifiedTimestamp = gGlobalTimer;
+
+ // Set surface properties
+ surface->type = surfaceType;
+ surface->force = 0;
+ surface->flags = dynamic ? SURFACE_FLAG_DYNAMIC : 0;
+ surface->room = 0;
+ surface->object = NULL;
+
+ // Snapshot node count before add_surface allocates nodes
+ s32 nodesBefore = gSurfaceNodesAllocated;
+
+ // Add to spatial partition
+ add_surface(surface);
+
+ s32 nodesAdded = gSurfaceNodesAllocated - nodesBefore;
+
+ // Update surface/node counters to stay in sync with load_area_terrain
+ if (poolType == SURFACE_POOL_STATIC) {
+ gNumStaticSurfaces++;
+ gNumStaticSurfaceNodes += nodesAdded;
+ }
+
+ return surface;
+}
+
+void smlua_collision_move_surface(struct Surface *surface, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3) {
+ if (surface == NULL) { return; }
+
+ s32 poolType = surface->poolType;
+
+ // Snapshot node count before removal
+ s32 nodesBefore = gSurfaceNodesAllocated;
+
+ // Remove from old spatial partition cells
+ remove_surface_from_partition(surface);
+
+ // Compute normal, plane equation, and Y bounds (surface is untouched on failure)
+ if (!calc_surface_fields(surface, vertex1, vertex2, vertex3)) {
+ // Degenerate triangle: re-add at old position without firing the hook
+ add_surface_without_hook(surface);
+
+ // Fix node counter drift from the remove + re-add round-trip
+ s32 nodeDelta = gSurfaceNodesAllocated - nodesBefore;
+ if (poolType == SURFACE_POOL_STATIC) {
+ gNumStaticSurfaceNodes += nodeDelta;
+ } else if (poolType == SURFACE_POOL_SOC) {
+ gNumSOCSurfaceNodes += nodeDelta;
+ }
+ return;
+ }
+
+ // Update previous vertices for interpolation
+ vec3s_copy(surface->prevVertex1, surface->vertex1);
+ vec3s_copy(surface->prevVertex2, surface->vertex2);
+ vec3s_copy(surface->prevVertex3, surface->vertex3);
+
+ // Set new vertices
+ vec3s_copy(surface->vertex1, vertex1);
+ vec3s_copy(surface->vertex2, vertex2);
+ vec3s_copy(surface->vertex3, vertex3);
+ surface->modifiedTimestamp = gGlobalTimer;
+
+ // Clear X_PROJECTION flag so add_surface can re-evaluate it
+ surface->flags &= ~SURFACE_FLAG_X_PROJECTION;
+
+ // Re-add to spatial partition without firing the hook (this is a move, not a new insertion)
+ add_surface_without_hook(surface);
+
+ // Update gNum*SurfaceNodes to account for the net change;
+ // cell range may differ after a move, so node count can change.
+ s32 nodeDelta = gSurfaceNodesAllocated - nodesBefore;
+ if (poolType == SURFACE_POOL_STATIC) {
+ gNumStaticSurfaceNodes += nodeDelta;
+ } else if (poolType == SURFACE_POOL_SOC) {
+ gNumSOCSurfaceNodes += nodeDelta;
+ }
+ // Dynamic nodes are reset each frame, no tracking needed
+}
+
+void smlua_collision_delete_surface(struct Surface *surface) {
+ if (surface == NULL) { return; }
+ delete_surface(surface);
+}
+
bool surface_is_quicksand(struct Surface* surf) {
if (surf == NULL) { return FALSE; }
return SURFACE_IS_QUICKSAND(surf->type);
diff --git a/src/pc/lua/utils/smlua_collision_utils.h b/src/pc/lua/utils/smlua_collision_utils.h
index 8836a7162..c8dd7ec33 100644
--- a/src/pc/lua/utils/smlua_collision_utils.h
+++ b/src/pc/lua/utils/smlua_collision_utils.h
@@ -142,6 +142,23 @@ Collision *smlua_collision_util_get_level_collision(u32 level, u16 area);
/* |description|Gets a table of the surface types from `data`|descriptionEnd| */
void smlua_collision_util_find_surface_types(Collision* data);
+/* |description|
+Allocates a new collision surface with the given vertices, computes the surface normal and other fields, and inserts it into the spatial partition.
+Returns the new surface, or `nil` if the triangle is degenerate (zero area).
+Set `dynamic` to `true` for surfaces that are cleared each frame, or `false` for persistent static surfaces
+|descriptionEnd| */
+struct Surface* smlua_collision_add_surface(bool dynamic, s16 surfaceType, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3);
+
+/* |description|
+Moves an existing collision surface to new vertex positions.
+Recalculates the surface normal, origin offset, and Y bounds, removes the surface from its old spatial partition cells, and re-adds it to the correct cells.
+The previous vertices are preserved for interpolation
+|descriptionEnd| */
+void smlua_collision_move_surface(struct Surface *surface, Vec3s vertex1, Vec3s vertex2, Vec3s vertex3);
+
+/* |description|Fully deletes a collision surface: removes it from the spatial partitions and frees its pool slot.|descriptionEnd| */
+void smlua_collision_delete_surface(struct Surface *surface);
+
/* |description|Checks if the surface is quicksand|descriptionEnd| */
bool surface_is_quicksand(struct Surface* surf);
/* |description|Checks if the surface is not a hard surface|descriptionEnd| */