Files
last-hearth/tests/presentation_spec.lua
T
2026-11-03 12:41:09 -05:00

184 lines
9.4 KiB
Lua
Executable File

-- Standalone Lua 5.1+; no real core/hearth mutation leaks into engine suites.
local target = assert(arg[1], "presentation.lua path required")
local total = 0
local function test(name, fn)
local old_core, old_hearth = core, hearth
local ok, message = pcall(fn)
core, hearth = old_core, old_hearth
assert(ok, name .. ": " .. tostring(message))
total = total + 1; print("PASS " .. name)
end
local function harness()
local h = {objects = {}, connected = {}, leaves = {}, deaths = {}, shutdowns = {}}
core = {
register_entity = function(name, def) h.entity_name, h.def = name, def end,
get_player_by_name = function(name) return h.connected[name] end,
register_on_leaveplayer = function(fn) h.leaves[#h.leaves + 1] = fn end,
register_on_dieplayer = function(fn) h.deaths[#h.deaths + 1] = fn end,
register_on_shutdown = function(fn) h.shutdowns[#h.shutdowns + 1] = fn end,
}
hearth = {players = {}, sessions = {}}
function h.player(name)
local p = {name = name or "fixture", item = "hearth:knife", hp = 20}
function p:get_player_name() return self.name end
function p:get_wielded_item() return {get_name = function() return self.item end} end
function p:get_pos() return {x = 1, y = 52, z = 3} end
function p:get_hp() return self.hp end
h.connected[p.name] = p
hearth.players[p.name] = {food = 85, stamina = 100}
hearth.sessions[p.name] = {}
return p
end
core.add_entity = function(pos, name, staticdata)
assert(name == h.entity_name and pos.y == 52)
if h.fail then return nil end
if h.throw then error("fixture failure") end
local obj = {props = {}, writes = 0}
local lua = {object = obj}; obj.lua = lua
function obj:get_pos() if not self.removed then return pos end end
function obj:remove()
assert(not self.removed, "duplicate removal")
self.removed = true; h.def.on_deactivate(lua)
end
function obj:set_properties(props)
self.writes = self.writes + 1
for k, v in pairs(props) do self.props[k] = v end
end
function obj:set_armor_groups(groups) self.armor = groups end
function obj:set_attach(parent, bone, position, rotation, forced)
if h.refuse_attach then return end
self.parent, self.bone, self.position, self.rotation, self.forced = parent, bone, position, rotation, forced
end
function obj:get_attach() return self.parent, self.bone, self.position, self.rotation, self.forced end
h.objects[#h.objects + 1] = obj
h.def.on_activate(lua, staticdata or "", 0)
return obj
end
function h.count()
local n = 0; for _, obj in ipairs(h.objects) do if not obj.removed then n = n + 1 end end; return n
end
function h.tick_objects()
for _, obj in ipairs(h.objects) do if not obj.removed then h.def.on_step(obj.lua, 0.1) end end
end
dofile(target)
return h
end
test("fixed whitelist creates all four generated UV meshes with correct bone transform", function()
local h = harness(); local p = h.player()
for _, tool in ipairs({"knife", "axe", "pick", "spear"}) do
p.item = "hearth:" .. tool; hearth.presentation_step(p)
local obj = h.objects[#h.objects]
assert(h.count() == 1 and obj.props.mesh == "hearth_" .. tool .. ".obj")
assert(obj.parent == p and obj.bone == "Hand_Right" and obj.forced == false)
assert(obj.position.x == 0 and obj.position.y == 0 and obj.position.z == 0)
assert(obj.rotation.x == 90 and obj.rotation.y == 0 and obj.rotation.z == 0)
assert(obj.props.is_visible == true and obj.armor.immortal == 1)
end
assert(h.def.initial_properties.textures[1] == "hearth_person.png")
assert(h.def.initial_properties.visual_size.x == 1 and h.def.initial_properties.visual_size.y == 1)
end)
test("steady owner has exactly one entity and no repeated property traffic", function()
local h = harness(); local p = h.player(); hearth.presentation_step(p)
local obj = h.objects[1]; local writes = obj.writes
for _ = 1, 200 do hearth.presentation_step(p); h.tick_objects() end
assert(#h.objects == 1 and h.count() == 1 and obj.writes == writes)
end)
test("weapon change removes old entity before creating replacement", function()
local h = harness(); local p = h.player(); hearth.presentation_step(p)
p.item = "hearth:axe"; hearth.presentation_step(p)
assert(h.objects[1].removed and h.count() == 1 and #h.objects == 2)
h.def.on_deactivate(h.objects[1].lua); hearth.presentation_step(p)
assert(h.count() == 1 and #h.objects == 2)
end)
test("empty unknown and path-like names remove visuals without accepting paths", function()
local h = harness(); local p = h.player()
for _, name in ipairs({"", "hearth:log", "../hearth_spear.obj", "hearth:knife.obj", "other:spear"}) do
p.item = "hearth:knife"; hearth.presentation_step(p)
p.item = name; hearth.presentation_step(p); assert(h.count() == 0)
end
end)
test("entity self-cleans on weapon change even without feedback tick", function()
local h = harness(); local p = h.player(); hearth.presentation_step(p)
p.item = ""; h.tick_objects(); assert(h.count() == 0)
end)
test("disconnect callback removes immediately and reconnect never inherits owner", function()
local h = harness(); local p = h.player(); hearth.presentation_step(p)
h.connected[p.name] = nil; h.leaves[1](p); assert(h.count() == 0)
local replacement = h.player(); hearth.presentation_step(replacement)
h.leaves[1](p); hearth.presentation_step(p)
assert(h.count() == 1 and #h.objects == 2 and h.objects[2].parent == replacement)
end)
test("session replacement invalidates old entity even on same PlayerRef", function()
local h = harness(); local p = h.player(); hearth.presentation_step(p)
hearth.sessions[p.name] = {}; h.tick_objects(); assert(h.count() == 0)
hearth.presentation_step(p); assert(h.count() == 1 and #h.objects == 2)
end)
test("lost player state session or connected identity self-cleans", function()
for _, missing in ipairs({"state", "session", "connection"}) do
local h = harness(); local p = h.player(); hearth.presentation_step(p)
if missing == "state" then hearth.players[p.name] = nil
elseif missing == "session" then hearth.sessions[p.name] = nil
else h.connected[p.name] = nil end
h.tick_objects(); assert(h.count() == 0); hearth.presentation_step(p); assert(h.count() == 0)
end
end)
test("waiting dead and death callback cleanup are idempotent", function()
local h = harness(); local p = h.player(); hearth.presentation_step(p)
hearth.sessions[p.name].waiting = true; h.tick_objects(); assert(h.count() == 0)
hearth.presentation_step(p); assert(h.count() == 0)
hearth.sessions[p.name].waiting = false; hearth.presentation_step(p)
p.hp = 0; h.tick_objects(); h.deaths[1](p); h.leaves[1](p); assert(h.count() == 0)
hearth.presentation_step(p); assert(h.count() == 0)
end)
test("foreign activation and static restoration are rejected with no saved payload", function()
local h = harness(); h.player()
for _, data in ipairs({"", "inventory payload", "{owner=fixture}"}) do
local obj = core.add_entity({y = 52}, h.entity_name, data)
assert(obj.removed and h.def.get_staticdata(obj.lua) == "")
end
assert(h.count() == 0)
end)
test("harmless entity cannot collide select attack or create inventory", function()
local h = harness(); local p = h.player(); hearth.presentation_step(p)
local def, obj = h.def, h.objects[1]; local props = def.initial_properties
assert(props.static_save == false and props.physical == false and props.pointable == false)
assert(props.collide_with_objects == false and props.is_visible == false)
assert(props.makes_footstep_sound == false and obj.lua.inventory == nil)
assert(def.on_punch(obj.lua, p) == true); def.on_rightclick(obj.lua, p)
assert(p.hp == 20 and h.count() == 1 and hearth.players[p.name].stamina == 100)
end)
test("detached redirected and removed objects are replaced without duplicates", function()
for _, mode in ipairs({"detach", "redirect", "remove"}) do
local h = harness(); local p = h.player(); hearth.presentation_step(p)
local obj = h.objects[1]
if mode == "remove" then obj:remove()
elseif mode == "detach" then obj.parent = nil
else obj.bone = "Head" end
hearth.presentation_step(p); assert(h.count() == 1 and #h.objects == 2)
end
end)
test("failed creation and silent attach failure leave no orphan", function()
for _, failure in ipairs({"fail", "throw", "refuse_attach"}) do
local h = harness(); local p = h.player(); h[failure] = true
hearth.presentation_step(p); assert(h.count() == 0)
h[failure] = false; hearth.presentation_step(p); assert(h.count() == 1)
end
end)
test("multiple owners are bounded independently and shutdown cleans all", function()
local h = harness(); local a, b = h.player("a"), h.player("b")
for _ = 1, 100 do hearth.presentation_step(a); hearth.presentation_step(b) end
assert(h.count() == 2 and #h.objects == 2)
h.shutdowns[1](); h.shutdowns[1](); assert(h.count() == 0)
end)
test("presentation never writes persisted state or session fields", function()
local h = harness(); local p = h.player()
local state, session = hearth.players[p.name], hearth.sessions[p.name]
setmetatable(state, {__newindex = function() error("state write") end})
setmetatable(session, {__newindex = function() error("session write") end})
for _, item in ipairs({"hearth:knife", "hearth:spear", ""}) do p.item = item; hearth.presentation_step(p) end
h.leaves[1](p); assert(next(session) == nil and state.food == 85 and state.stamina == 100)
assert(hearth.players[p.name] == state and hearth.sessions[p.name] == session)
end)
print("PASS " .. total .. " presentation tests")