Initial commit
This commit is contained in:
Executable
+370
@@ -0,0 +1,370 @@
|
||||
-- Standalone Lua 5.1+ regression harness; no engine or MTG required.
|
||||
-- Pass the content module's path as the first command-line argument.
|
||||
local nodes, items, crafts, biomes, decorations, lbms = {}, {}, {}, {}, {}, {}
|
||||
local world, metas, timers, messages, state = {}, {}, {}, {}, {}
|
||||
local now, protected, claim, violations = 100000, false, true, 0
|
||||
local player
|
||||
local function key(p) return p.x .. ":" .. p.y .. ":" .. p.z end
|
||||
local function pos(x, y, z) return {x = x or 0, y = y or 0, z = z or 0} end
|
||||
local function put(p, name) world[key(p)] = {name = name, param2 = 2} end
|
||||
local function point(under, above) return {type = "node", under = under, above = above} end
|
||||
|
||||
local Stack = {}
|
||||
Stack.__index = Stack
|
||||
local function stack(text)
|
||||
local name, count = (text or ""):match("^(%S+)%s*(%d*)$")
|
||||
return setmetatable({name = name or "", count = name and (tonumber(count) or 1) or 0}, Stack)
|
||||
end
|
||||
function Stack:get_name() return self.name end
|
||||
function Stack:get_count() return self.count end
|
||||
function Stack:is_empty() return self.count == 0 end
|
||||
function Stack:take_item(n)
|
||||
self.count = math.max(0, self.count - (n or 1))
|
||||
if self.count == 0 then self.name = "" end
|
||||
end
|
||||
function Stack:replace(text)
|
||||
local new = stack(text)
|
||||
self.name, self.count = new.name, new.count
|
||||
end
|
||||
|
||||
core = {registered_nodes = nodes}
|
||||
function core.register_node(name, def) nodes[name], items[name] = def, def end
|
||||
function core.register_item(name, def) items[name] = def end
|
||||
function core.register_tool(name, def) items[name] = def end
|
||||
function core.register_craftitem(name, def) items[name] = def end
|
||||
function core.register_craft(def) crafts[#crafts + 1] = def end
|
||||
function core.register_biome(def) biomes[#biomes + 1] = def end
|
||||
function core.register_decoration(def) decorations[#decorations + 1] = def end
|
||||
function core.register_lbm(def) lbms[#lbms + 1] = def end
|
||||
local aliases = {}
|
||||
function core.register_alias(name, target) aliases[name] = target end
|
||||
function core.get_node(p) return world[key(p)] or {name = "air"} end
|
||||
function core.get_node_or_nil(p)
|
||||
if world[key(p)] == false then return nil end
|
||||
return core.get_node(p)
|
||||
end
|
||||
function core.swap_node(p, node) world[key(p)] = node end
|
||||
function core.is_protected() return protected end
|
||||
function core.record_protection_violation() violations = violations + 1 end
|
||||
function core.get_meta(p)
|
||||
local k = key(p)
|
||||
if not metas[k] then
|
||||
local fields = {}
|
||||
metas[k] = {
|
||||
set_string = function(_, name, value) fields[name] = value end,
|
||||
get_string = function(_, name) return fields[name] or "" end,
|
||||
}
|
||||
end
|
||||
return metas[k]
|
||||
end
|
||||
function core.get_node_timer(p)
|
||||
local k = key(p)
|
||||
if not timers[k] then
|
||||
timers[k] = {
|
||||
start = function(self, seconds) self.timeout = seconds end,
|
||||
stop = function(self) self.timeout = nil end,
|
||||
}
|
||||
end
|
||||
return timers[k]
|
||||
end
|
||||
function core.item_place(s, who, pointed)
|
||||
local under = core.get_node(pointed.under)
|
||||
local def = nodes[under.name]
|
||||
if def.on_rightclick and not who:get_player_control().sneak then
|
||||
return def.on_rightclick(pointed.under, under, who, s)
|
||||
end
|
||||
local p = def.buildable_to and pointed.under or pointed.above
|
||||
if core.is_protected(p, who:get_player_name()) then return s end
|
||||
local name = s:get_name()
|
||||
put(p, name)
|
||||
if nodes[name].on_construct then nodes[name].on_construct(p) end
|
||||
if nodes[name].after_place_node then nodes[name].after_place_node(p, who) end
|
||||
s:take_item()
|
||||
return s, p
|
||||
end
|
||||
nodes.air = {buildable_to = true, walkable = false}
|
||||
hearth = {
|
||||
model = {}, players = {survivor = state},
|
||||
texture = function(color) return "[fill:16x16:" .. color end,
|
||||
claim_allowed = function() return claim end,
|
||||
notify = function(_, message) messages[#messages + 1] = message end,
|
||||
}
|
||||
local inventory = {room = true, contents = {}}
|
||||
function inventory:room_for_item() return self.room end
|
||||
function inventory:add_item(_, name)
|
||||
self.contents[#self.contents + 1] = name
|
||||
return stack()
|
||||
end
|
||||
player = {position = pos(), sneak = false}
|
||||
function player:is_player() return true end
|
||||
function player:get_pos() return self.position end
|
||||
function player:get_player_name() return "survivor" end
|
||||
function player:get_inventory() return inventory end
|
||||
function player:get_player_control() return {sneak = self.sneak} end
|
||||
local real_time = os.time
|
||||
os.time = function() return now end
|
||||
assert(arg and arg[1], "Pass the path to mods/hearth/content.lua")
|
||||
dofile(arg[1])
|
||||
|
||||
local passed = 0
|
||||
local function eq(actual, expected)
|
||||
assert(actual == expected, "expected " .. tostring(expected) .. ", got " .. tostring(actual))
|
||||
end
|
||||
local function reset()
|
||||
world, metas, timers, messages = {}, {}, {}, {}
|
||||
now, protected, claim, violations = 100000, false, true, 0
|
||||
player.position, player.sneak = pos(), false
|
||||
inventory.room, inventory.contents = true, {}
|
||||
state.food, state.water, state.stamina = 100, 100, 100
|
||||
end
|
||||
local function test(name, fn)
|
||||
reset()
|
||||
local ok, err = pcall(fn)
|
||||
if not ok then error(name .. ": " .. tostring(err), 0) end
|
||||
passed = passed + 1
|
||||
print("ok " .. passed .. " - " .. name)
|
||||
end
|
||||
local firepos = pos()
|
||||
local function fire()
|
||||
put(firepos, "hearth:fire")
|
||||
nodes["hearth:fire"].on_construct(firepos)
|
||||
end
|
||||
local function use(text)
|
||||
local s = stack(text)
|
||||
local node = core.get_node(firepos)
|
||||
nodes[node.name].on_rightclick(firepos, node, player, s)
|
||||
return s
|
||||
end
|
||||
local function deadline() return tonumber(core.get_meta(firepos):get_string("fuel_until")) end
|
||||
|
||||
test("standalone registrations, recipes and mapgen resolve", function()
|
||||
for _, name in ipairs({"stone", "soil", "grass", "sand", "water_source", "water_flowing",
|
||||
"log", "leaves", "loose_stone", "sticks", "berry_bush", "reeds", "planks", "wall",
|
||||
"roof", "fire", "fire_lit", "berries", "raw_food", "cooked_food", "vessel",
|
||||
"water_raw", "water_clean", "knife", "axe", "pick", "spear"}) do
|
||||
assert(items["hearth:" .. name], name)
|
||||
end
|
||||
for _, recipe in ipairs(crafts) do
|
||||
assert(items[stack(recipe.output):get_name()])
|
||||
for _, row in ipairs(recipe.recipe) do
|
||||
for _, ingredient in ipairs(type(row) == "table" and row or {row}) do
|
||||
assert(ingredient == "" or items[ingredient], ingredient)
|
||||
end
|
||||
end
|
||||
end
|
||||
eq(#biomes, 2)
|
||||
eq(#decorations, 6)
|
||||
eq(aliases.mapgen_river_water_source, "hearth:water_source")
|
||||
for _, deco in ipairs(decorations) do
|
||||
if deco.schematic then
|
||||
eq(#deco.schematic.data, 150)
|
||||
for _, entry in ipairs(deco.schematic.data) do assert(nodes[entry.name]) end
|
||||
else assert(nodes[deco.decoration]) end
|
||||
end
|
||||
end)
|
||||
test("hand gathers soil and shrubs but cannot harvest trees or stone", function()
|
||||
local caps = items[":"].tool_capabilities.groupcaps
|
||||
assert(caps.crumbly and caps.snappy)
|
||||
eq(caps.choppy, nil)
|
||||
eq(caps.cracky, nil)
|
||||
assert(items["hearth:axe"].tool_capabilities.groupcaps.choppy)
|
||||
assert(items["hearth:pick"].tool_capabilities.groupcaps.cracky)
|
||||
end)
|
||||
test("fire consumes exactly one fuel and retains metadata through swaps", function()
|
||||
fire()
|
||||
core.get_meta(firepos):set_string("owner", "survivor")
|
||||
eq(use("hearth:sticks 3"):get_count(), 2)
|
||||
eq(deadline(), now + 45)
|
||||
eq(core.get_node(firepos).name, "hearth:fire_lit")
|
||||
eq(core.get_node(firepos).param2, 2)
|
||||
eq(core.get_meta(firepos):get_string("owner"), "survivor")
|
||||
eq(use("hearth:log"):get_count(), 0)
|
||||
eq(deadline(), now + 225)
|
||||
end)
|
||||
test("full hearth rejects fuel rather than granting or wasting it", function()
|
||||
fire()
|
||||
for _ = 1, 3 do use("hearth:log") end
|
||||
eq(use("hearth:log"):get_count(), 1)
|
||||
eq(deadline(), now + 540)
|
||||
eq(use("hearth:sticks"):get_count(), 0)
|
||||
eq(use("hearth:sticks"):get_count(), 1)
|
||||
eq(deadline(), now + 585)
|
||||
end)
|
||||
test("timer and reload expire offline fuel without resetting the clock", function()
|
||||
fire()
|
||||
use("hearth:sticks")
|
||||
now = now + 10
|
||||
nodes["hearth:fire_lit"].on_timer(firepos, 999)
|
||||
eq(deadline(), 100045)
|
||||
now = now + 1000
|
||||
lbms[1].action(firepos)
|
||||
eq(core.get_node(firepos).name, "hearth:fire")
|
||||
eq(deadline(), 0)
|
||||
eq(core.get_node_timer(firepos).timeout, nil)
|
||||
eq(use("hearth:raw_food"):get_name(), "hearth:raw_food")
|
||||
end)
|
||||
test("single food cooks in hand and debits fuel", function()
|
||||
fire()
|
||||
use("hearth:sticks")
|
||||
eq(use("hearth:raw_food"):get_name(), "hearth:cooked_food")
|
||||
eq(deadline(), now + 25)
|
||||
eq(use("hearth:raw_food"):get_name(), "hearth:cooked_food")
|
||||
eq(deadline(), now + 5)
|
||||
eq(use("hearth:raw_food"):get_name(), "hearth:raw_food")
|
||||
end)
|
||||
test("full inventory does not destroy cooking input or fuel", function()
|
||||
fire()
|
||||
use("hearth:sticks")
|
||||
inventory.room = false
|
||||
eq(use("hearth:raw_food 4"):get_count(), 4)
|
||||
eq(deadline(), now + 45)
|
||||
inventory.room = true
|
||||
eq(use("hearth:raw_food 4"):get_count(), 3)
|
||||
eq(inventory.contents[1], "hearth:cooked_food")
|
||||
eq(deadline(), now + 25)
|
||||
end)
|
||||
test("protected, claim-denied and distant fire interactions change nothing", function()
|
||||
fire()
|
||||
protected = true
|
||||
eq(use("hearth:log"):get_count(), 1)
|
||||
protected, claim = false, false
|
||||
eq(use("hearth:sticks"):get_count(), 1)
|
||||
claim, player.position = true, pos(7)
|
||||
eq(use("hearth:log"):get_count(), 1)
|
||||
eq(deadline(), 0)
|
||||
eq(violations, 2)
|
||||
for _, action in ipairs({"put", "take", "move"}) do
|
||||
eq(nodes["hearth:fire"]["allow_metadata_inventory_" .. action](firepos, "main", 1, stack("hearth:log"), player), 0)
|
||||
end
|
||||
end)
|
||||
test("vessel filling validates water, protection and six-block reach", function()
|
||||
local fill = items["hearth:vessel"].on_place
|
||||
local water = pos(6)
|
||||
put(water, "hearth:water_source")
|
||||
local pointed = point(water, pos(6, 1))
|
||||
eq(fill(stack("hearth:vessel"), player, pointed):get_name(), "hearth:water_raw")
|
||||
protected = true
|
||||
eq(fill(stack("hearth:vessel"), player, pointed):get_name(), "hearth:vessel")
|
||||
protected, claim = false, false
|
||||
eq(fill(stack("hearth:vessel"), player, pointed):get_name(), "hearth:vessel")
|
||||
claim, player.position = true, pos(-0.1)
|
||||
eq(fill(stack("hearth:vessel"), player, pointed):get_name(), "hearth:vessel")
|
||||
eq(core.get_node(water).name, "hearth:water_source")
|
||||
end)
|
||||
test("boiling requires fuel and drinking returns the carved cup only when thirsty", function()
|
||||
fire()
|
||||
eq(use("hearth:water_raw"):get_name(), "hearth:water_raw")
|
||||
use("hearth:sticks")
|
||||
local water = use("hearth:water_raw")
|
||||
eq(water:get_name(), "hearth:water_clean")
|
||||
eq(deadline(), now + 35)
|
||||
local drink = items["hearth:water_clean"].on_use
|
||||
drink(water, player)
|
||||
eq(water:get_name(), "hearth:water_clean")
|
||||
state.water = 80
|
||||
drink(water, player)
|
||||
eq(state.water, 100)
|
||||
eq(water:get_name(), "hearth:vessel")
|
||||
end)
|
||||
test("food consumption is conditional and clamps restored needs", function()
|
||||
local food = stack("hearth:berries 2")
|
||||
items["hearth:berries"].on_use(food, player)
|
||||
eq(food:get_count(), 2)
|
||||
state.food = 95
|
||||
items["hearth:berries"].on_use(food, player)
|
||||
eq(food:get_count(), 1)
|
||||
eq(state.food, 100)
|
||||
eq(state.water, 100)
|
||||
end)
|
||||
test("wall placement is supported, protected and records ownership", function()
|
||||
local ground, target = pos(0, -1), pos()
|
||||
put(ground, "hearth:stone")
|
||||
local s = stack("hearth:wall 2")
|
||||
nodes["hearth:wall"].on_place(s, player, point(ground, target))
|
||||
eq(s:get_count(), 1)
|
||||
eq(core.get_node(target).name, "hearth:wall")
|
||||
eq(core.get_meta(target):get_string("owner"), "survivor")
|
||||
protected = true
|
||||
eq(nodes["hearth:wall"].can_dig(target, player), false)
|
||||
protected, claim = false, false
|
||||
eq(nodes["hearth:wall"].can_dig(target, player), false)
|
||||
end)
|
||||
test("sequential roofs cannot extend a horizontal span beyond two blocks", function()
|
||||
put(pos(0, -1), "hearth:stone")
|
||||
put(pos(), "hearth:wall")
|
||||
for x = 1, 3 do
|
||||
local s = stack("hearth:roof")
|
||||
nodes["hearth:roof"].on_place(s, player, point(pos(x - 1), pos(x)))
|
||||
eq(s:get_count(), x <= 2 and 0 or 1)
|
||||
end
|
||||
end)
|
||||
test("unloaded, unsupported, protected and distant placements fail closed", function()
|
||||
local s = stack("hearth:wall 4")
|
||||
local p = point(pos(0, -1), pos())
|
||||
nodes["hearth:wall"].on_place(s, player, p)
|
||||
eq(s:get_count(), 4)
|
||||
put(pos(0, -1), "hearth:stone")
|
||||
claim = false
|
||||
nodes["hearth:wall"].on_place(s, player, p)
|
||||
claim, protected = true, true
|
||||
nodes["hearth:wall"].on_place(s, player, p)
|
||||
protected, player.position = false, pos(7)
|
||||
nodes["hearth:wall"].on_place(s, player, p)
|
||||
player.position = pos()
|
||||
world[key(pos(0, -1))] = false
|
||||
nodes["hearth:wall"].on_place(s, player, p)
|
||||
eq(s:get_count(), 4)
|
||||
end)
|
||||
test("node-item rightclick feeds fire even when the space above is occupied", function()
|
||||
fire()
|
||||
put(pos(0, 1), "hearth:stone")
|
||||
local s = stack("hearth:sticks 2")
|
||||
nodes["hearth:sticks"].on_place(s, player, point(firepos, pos(0, 1)))
|
||||
eq(s:get_count(), 1)
|
||||
eq(deadline(), now + 45)
|
||||
end)
|
||||
|
||||
test("construction height is limited to sixteen blocks above an anchor", function()
|
||||
put(pos(0, -1), "hearth:stone")
|
||||
for y = 0, 16 do
|
||||
player.position = pos(0, y)
|
||||
local s = stack("hearth:wall")
|
||||
nodes["hearth:wall"].on_place(s, player, point(pos(0, y - 1), pos(0, y)))
|
||||
eq(s:get_count(), y < 16 and 0 or 1)
|
||||
end
|
||||
end)
|
||||
test("water primary-use boiling obeys claim and distance checks", function()
|
||||
fire()
|
||||
use("hearth:sticks")
|
||||
local s = stack("hearth:water_raw")
|
||||
local pointed = point(firepos, pos(0, 1))
|
||||
claim = false
|
||||
items["hearth:water_raw"].on_use(s, player, pointed)
|
||||
eq(s:get_name(), "hearth:water_raw")
|
||||
claim, player.position = true, pos(7)
|
||||
items["hearth:water_raw"].on_use(s, player, pointed)
|
||||
eq(s:get_name(), "hearth:water_raw")
|
||||
player.position = pos()
|
||||
items["hearth:water_raw"].on_use(s, player, pointed)
|
||||
eq(s:get_name(), "hearth:water_clean")
|
||||
eq(deadline(), now + 35)
|
||||
end)
|
||||
test("all content nodes enforce claim-aware digging and placement", function()
|
||||
put(pos(0, -1), "hearth:stone")
|
||||
claim = false
|
||||
for name, def in pairs(nodes) do
|
||||
if name ~= "air" then
|
||||
eq(def.can_dig(pos(), player), false)
|
||||
local s = stack(name)
|
||||
def.on_place(s, player, point(pos(0, -1), pos()))
|
||||
eq(s:get_count(), 1)
|
||||
end
|
||||
end
|
||||
end)
|
||||
test("nested procedural fill texture arguments escape colons", function()
|
||||
assert(nodes["hearth:grass"].tiles[3]:find("[fill\\:16x16\\:", 1, true))
|
||||
assert(nodes["hearth:berry_bush"].tiles[1]:find("[fill\\:16x16\\:", 1, true))
|
||||
end)
|
||||
|
||||
os.time = real_time
|
||||
print("1.." .. passed)
|
||||
Executable
+169
@@ -0,0 +1,169 @@
|
||||
-- Original MIT-licensed integration checks. Staged as a disposable world mod;
|
||||
-- never copy this into the persistent play world. No engine globals are mocked.
|
||||
local mode = core.settings:get("hearth_launcher_mode")
|
||||
local token = core.settings:get("hearth_smoke_token")
|
||||
assert(mode == "check" or mode == "smoke", "Invalid launcher test mode")
|
||||
assert(token and token:match("^[%x%-]+$"), "Missing launcher test token")
|
||||
local modpath = core.get_modpath(core.get_current_modname())
|
||||
local finished = false
|
||||
|
||||
local function finish(ok, message)
|
||||
if finished then return end
|
||||
finished = true
|
||||
if ok then
|
||||
core.log("action", "LAST_HEARTH_" .. mode:upper() .. "_PASS " .. token)
|
||||
else
|
||||
core.log("error", "LAST_HEARTH_TEST_FAIL: " .. tostring(message))
|
||||
end
|
||||
core.request_shutdown("Last Hearth " .. mode .. " complete", false, 0)
|
||||
end
|
||||
|
||||
local function guarded(fn)
|
||||
local ok, message = pcall(fn)
|
||||
if not ok then finish(false, message) end
|
||||
end
|
||||
|
||||
local function syntax_check()
|
||||
local manifest = dofile(modpath .. "/syntax_manifest.lua")
|
||||
assert(#manifest > 0, "Empty syntax manifest")
|
||||
for _, entry in ipairs(manifest) do
|
||||
local chunk, message = loadfile(modpath .. "/syntax/" .. entry.file)
|
||||
assert(chunk, entry.label .. ": " .. tostring(message))
|
||||
end
|
||||
core.log("action", "Last Hearth: compiled " .. #manifest .. " Lua files (standalone specs not executed)")
|
||||
end
|
||||
|
||||
local function registry_check()
|
||||
assert(type(hearth) == "table", "Real hearth global was not loaded")
|
||||
assert(type(hearth.model) == "table", "hearth.model missing")
|
||||
assert(type(hearth.players) == "table", "hearth.players missing")
|
||||
assert(core.get_modpath("hearth"), "hearth mod missing")
|
||||
for _, name in ipairs({"default", "doors", "farming", "fire", "stairs", "wool", "sfinv"}) do
|
||||
assert(not core.get_modpath(name), "Unexpected MTG dependency: " .. name)
|
||||
end
|
||||
for name in pairs(core.registered_items) do
|
||||
assert(not name:match("^default:"), "MTG item leaked into standalone game: " .. name)
|
||||
end
|
||||
for _, name in ipairs({"stone", "soil", "grass", "sand", "water_source", "water_flowing",
|
||||
"log", "leaves", "loose_stone", "sticks", "berry_bush", "reeds", "planks",
|
||||
"wall", "roof", "fire", "fire_lit"}) do
|
||||
assert(core.registered_nodes["hearth:" .. name], "Missing node: " .. name)
|
||||
end
|
||||
for _, name in ipairs({"berries", "raw_food", "cooked_food", "vessel", "water_raw",
|
||||
"water_clean", "knife", "axe", "pick", "spear"}) do
|
||||
assert(core.registered_items["hearth:" .. name], "Missing item: " .. name)
|
||||
end
|
||||
for _, name in ipairs({"knife", "axe", "pick", "spear", "vessel", "fire", "wall",
|
||||
"roof", "planks", "sticks", "raw_food"}) do
|
||||
local output = "hearth:" .. name
|
||||
local recipe = core.get_craft_recipe(output)
|
||||
assert(recipe.items and next(recipe.items), "Missing craft: " .. output)
|
||||
for _, ingredient in pairs(recipe.items) do
|
||||
assert(ingredient == "" or core.registered_items[ingredient], "Unknown ingredient: " .. ingredient)
|
||||
end
|
||||
local inputs = {}
|
||||
for index = 1, (recipe.width == 0 and #recipe.items or recipe.width * 3) do
|
||||
inputs[index] = ItemStack(recipe.items[index] or "")
|
||||
end
|
||||
local width = recipe.width > 0 and recipe.width or #inputs
|
||||
local result = core.get_craft_result({method = "normal", width = width, items = inputs})
|
||||
assert(result.item:get_name() == output, "Engine craft result mismatch: " .. output)
|
||||
assert(result.item:get_count() == ItemStack(recipe.output):get_count(), "Craft quantity mismatch: " .. output)
|
||||
end
|
||||
assert(core.registered_aliases.mapgen_stone == "hearth:stone", "Missing mapgen stone alias")
|
||||
assert(core.registered_aliases.mapgen_water_source == "hearth:water_source", "Missing water alias")
|
||||
assert(core.registered_biomes.hearth_temperate, "Missing temperate biome")
|
||||
assert(core.registered_biomes.hearth_riverbank, "Missing riverbank biome")
|
||||
assert(core.settings:get_bool("enable_pvp") == false, "PvP must be disabled")
|
||||
assert(core.settings:get_bool("secure.enable_security") == true, "Mod security must be enabled")
|
||||
assert(core.settings:get("bind_address") == "127.0.0.1", "Server must bind loopback only")
|
||||
end
|
||||
|
||||
local function unit_check()
|
||||
-- Each standalone harness receives its own globals and os table. Mocked core,
|
||||
-- hearth and os.time never replace globals of the running game.
|
||||
local manifest = dofile(modpath .. "/syntax_manifest.lua")
|
||||
local lookup = {}
|
||||
for _, entry in ipairs(manifest) do lookup[entry.label] = modpath .. "/syntax/" .. entry.file end
|
||||
local count = 0
|
||||
for label, filename in pairs(lookup) do
|
||||
if label:match("_spec%.lua$") then
|
||||
local module = label:match("tests/(.+)_spec%.lua$")
|
||||
local target = assert(lookup["mods/hearth/" .. module .. ".lua"], "Missing test module")
|
||||
local env = setmetatable({arg = {target}, os = {}}, {__index = _G})
|
||||
for k, v in pairs(os) do env.os[k] = v end
|
||||
env._G = env
|
||||
env.dofile = function(path)
|
||||
assert(path == target, "Unit harness attempted unexpected file access")
|
||||
local chunk = assert(loadfile(path)); setfenv(chunk, env); return chunk()
|
||||
end
|
||||
local chunk = assert(loadfile(filename)); setfenv(chunk, env); chunk()
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
assert(count >= 2, "Expected model and content regression suites")
|
||||
core.log("action", "Last Hearth: " .. count .. " isolated unit suites executed")
|
||||
end
|
||||
|
||||
local function emerge_check()
|
||||
local callbacks = 0
|
||||
core.emerge_area({x = -64, y = -32, z = -64}, {x = 64, y = 96, z = 64},
|
||||
function(_, action, remaining)
|
||||
if finished then return end
|
||||
guarded(function()
|
||||
assert(action ~= core.EMERGE_ERRORED and action ~= core.EMERGE_CANCELLED,
|
||||
"Map emergence failed or was cancelled")
|
||||
callbacks = callbacks + 1
|
||||
if remaining ~= 0 then return end
|
||||
-- Inspect after mapgen callbacks have completed, on a server step.
|
||||
core.after(0, function()
|
||||
guarded(function()
|
||||
assert(callbacks > 0, "No emerge callbacks received")
|
||||
local material = false
|
||||
for z = -16, 16, 4 do
|
||||
for y = -32, 16, 4 do
|
||||
for x = -16, 16, 4 do
|
||||
local node = core.get_node_or_nil({x = x, y = y, z = z})
|
||||
assert(node and node.name ~= "ignore", "Emerged map contains unloaded nodes")
|
||||
assert(core.registered_nodes[node.name], "Unregistered generated node: " .. node.name)
|
||||
assert(node.name == "air" or node.name:match("^hearth:"),
|
||||
"Foreign generated node: " .. node.name)
|
||||
if node.name:match("^hearth:") then material = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
assert(material, "Mapgen produced no hearth terrain in the test volume")
|
||||
local resources = {}
|
||||
for _, name in ipairs({"grass", "loose_stone", "sticks", "berry_bush", "reeds", "log", "water_source"}) do
|
||||
local nodes = core.find_nodes_in_area({x = -64, y = -16, z = -64}, {x = 64, y = 96, z = 64}, {"hearth:" .. name})
|
||||
resources[#resources + 1] = name .. "=" .. #nodes
|
||||
assert(#nodes > 0, "Starting region missing required resource: " .. name)
|
||||
end
|
||||
core.log("action", "Last Hearth starting resources: " .. table.concat(resources, ", "))
|
||||
local p = {x = 0, y = 85, z = 0}
|
||||
core.set_node(vector.offset(p, 0, 3, 0), {name = "hearth:roof"})
|
||||
for _, d in ipairs({{1,0},{-1,0},{0,1},{0,-1}}) do
|
||||
core.set_node(vector.offset(p, d[1]*2, 1, d[2]*2), {name = "hearth:wall"})
|
||||
end
|
||||
local env = hearth.environment(p)
|
||||
assert(env.roof and env.shelter == 1, "A roof and four walls must enclose a shelter")
|
||||
core.remove_node(vector.offset(p, 0, 3, 0))
|
||||
assert(hearth.environment(p).shelter == 0, "Walls without a roof are exposed")
|
||||
finish(true)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
core.after(1, function()
|
||||
guarded(function()
|
||||
syntax_check()
|
||||
if mode == "check" then finish(true); return end
|
||||
registry_check()
|
||||
unit_check()
|
||||
emerge_check()
|
||||
end)
|
||||
end)
|
||||
|
||||
core.after(60, function() finish(false, "Engine test deadline exceeded") end)
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
local M = dofile(assert(arg[1], "model.lua path required"))
|
||||
local total = 0
|
||||
local function test(name, fn)
|
||||
local ok, err = pcall(fn)
|
||||
assert(ok, name .. ": " .. tostring(err)); total = total + 1
|
||||
end
|
||||
-- Explicit UTC fixtures; os.time(table) would depend on the machine timezone.
|
||||
local june_noon, december_noon = 1782043200, 1797854400
|
||||
test("reject malformed locations", function()
|
||||
assert(not M.location("bad", 0)); assert(not M.location(91, 0))
|
||||
assert(not M.location(0, 181)); assert(not M.location(0 / 0, 0))
|
||||
assert(not M.location(math.huge, 0)); assert(M.location(0, 0))
|
||||
end)
|
||||
test("latitude changes solstice elevation", function()
|
||||
local summer = M.solar(june_noon, 45, 0)
|
||||
local winter = M.solar(december_noon, 45, 0)
|
||||
assert(summer.elevation > winter.elevation + 40)
|
||||
assert(summer.temperature > winter.temperature)
|
||||
end)
|
||||
test("opposite hemispheres reverse seasons", function()
|
||||
assert(M.solar(june_noon, 45, 0).season == "Summer")
|
||||
assert(M.solar(june_noon, -45, 0).season == "Winter")
|
||||
end)
|
||||
test("longitude changes solar time", function()
|
||||
local a, b = M.solar(june_noon, 0, 0), M.solar(june_noon, 0, 90)
|
||||
assert(math.abs((b.hour - a.hour) % 24 - 6) < 0.01)
|
||||
end)
|
||||
test("polar night and polar day stay finite", function()
|
||||
assert(M.solar(june_noon, 90, 0).daylight)
|
||||
assert(not M.solar(december_noon, 90, 0).daylight)
|
||||
assert(M.solar(june_noon, -90, 0).timeofday >= 0)
|
||||
end)
|
||||
test("deterministic weather across restarts", function()
|
||||
assert(M.weather(june_noon, 30, -70) == M.weather(june_noon, 30, -70))
|
||||
end)
|
||||
local initial = {food = 80, water = 80, warmth = 80, wet = 20, stamina = 70}
|
||||
test("shelter dries while exposed rain wets", function()
|
||||
local dry = M.needs(initial, {temperature = 5, shelter = 1, rain = true}, 1)
|
||||
local wet = M.needs(initial, {temperature = 5, shelter = 0, rain = true}, 1)
|
||||
assert(dry.wet < initial.wet and wet.wet > initial.wet)
|
||||
assert(initial.wet == 20) -- no hidden mutation
|
||||
end)
|
||||
test("rest recovers sprint reserve", function()
|
||||
local rest = M.needs(initial, {temperature = 20}, 1)
|
||||
local run = M.needs(initial, {temperature = 20, sprinting = true}, 1)
|
||||
assert(rest.stamina > initial.stamina and run.stamina < initial.stamina)
|
||||
end)
|
||||
test("large pauses never replay offline starvation", function()
|
||||
local state = M.needs(initial, {temperature = -20}, 100000)
|
||||
assert(state.food > 79 and state.water > 79)
|
||||
end)
|
||||
test("negative elapsed does not reverse needs", function()
|
||||
local state = M.needs(initial, {temperature = -20}, -1)
|
||||
assert(state.food == initial.food)
|
||||
end)
|
||||
test("fuel expires and is bounded", function()
|
||||
assert(M.fuel_remaining(100, 101) == 0)
|
||||
assert(M.fuel_remaining(10000000, 0) == 600)
|
||||
end)
|
||||
print("PASS " .. total .. " model tests")
|
||||
Reference in New Issue
Block a user