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| */