364 lines
19 KiB
Lua
Executable File
364 lines
19 KiB
Lua
Executable File
-- Standalone Lua 5.1+; registration contract, no world or engine is launched.
|
|
local target = assert(arg[1], "biomes.lua path required")
|
|
local factory = dofile(target)
|
|
local passed = 0
|
|
local function contains(list, value)
|
|
for _, entry in ipairs(list or {}) do if entry == value then return true end end
|
|
return false
|
|
end
|
|
local function count(t) local n=0; for _ in pairs(t) do n=n+1 end; return n end
|
|
local function fixture(existing_alias)
|
|
local nodes, items, biomes, ores, decos, crafts = {}, {}, {}, {}, {}, {}
|
|
local aliases = {external_alias="external:stone"}
|
|
if existing_alias then aliases.mapgen_stone = "external:stone" end
|
|
for _, name in ipairs({"stone", "soil", "grass", "sand", "water_source", "water_flowing",
|
|
"log", "leaves", "planks", "sticks", "loose_stone", "berry_bush", "reeds", "grain_patch",
|
|
"clay_bank", "iron_ore_vein", "weir_stone", "meadow_grass", "wildflowers", "fern", "litter",
|
|
"fungi", "deadwood", "spring_greens_patch", "autumn_mushrooms_patch"}) do
|
|
nodes["hearth:"..name] = {sentinel=true}; items["hearth:"..name] = nodes["hearth:"..name]
|
|
end
|
|
for _, name in ipairs({"iron_ore", "charcoal", "charcoal_charge", "clay", "foundation"}) do
|
|
items["hearth:"..name] = {sentinel=true}
|
|
end
|
|
local external = {sentinel=true}
|
|
biomes.external = external
|
|
local placed, owned = function() end, function() end
|
|
local construction = {place_building=placed, owner=owned}
|
|
local api = {registered_nodes=nodes, registered_items=items, registered_aliases=aliases}
|
|
function api.register_alias(name, value)
|
|
assert(aliases[name] == nil, "alias overwritten"); aliases[name] = value
|
|
end
|
|
function api.register_craftitem(name, def)
|
|
assert(not items[name]); items[name] = def
|
|
end
|
|
function api.register_biome(def) assert(not biomes[def.name]); biomes[def.name] = def end
|
|
function api.register_ore(def) ores[#ores+1] = def end
|
|
function api.register_decoration(def)
|
|
assert(not decos[def.name]); decos[def.name] = def; decos[#decos+1] = def
|
|
end
|
|
function api.register_craft(def) crafts[#crafts+1] = def end
|
|
-- Any attempt to clear registries, force aliases, change settings, install
|
|
-- globalsteps or write world nodes is a test failure, not a permissive noop.
|
|
setmetatable(api, {__index=function(_, key) error("unapproved engine API: " .. key) end})
|
|
local registrations = 0
|
|
local function private_register(name, def)
|
|
assert(not name:find(":"), "private helper takes short names")
|
|
local full="hearth:"..name
|
|
assert(not nodes[full], "base node overwritten")
|
|
registrations=registrations+1; nodes[full]=def; items[full]=def
|
|
end
|
|
local hearth = {sentinel=external}
|
|
local m = factory(hearth, api, private_register, construction)
|
|
return {m=m, nodes=nodes, items=items, biomes=biomes, ores=ores, decos=decos, crafts=crafts,
|
|
aliases=aliases, api=api, hearth=hearth, construction=construction, external=external,
|
|
registrations=function() return registrations end}
|
|
end
|
|
local function test(label, fn)
|
|
local ok, err=pcall(fn)
|
|
assert(ok, label .. ": " .. tostring(err)); passed=passed+1
|
|
print("ok " .. passed .. " - " .. label)
|
|
end
|
|
local function loaded(existing) local f=fixture(existing); assert(f.m.register()==f.m); return f end
|
|
local function recipe(f, input, output)
|
|
local inputs = type(input) == "table" and input or {input}
|
|
local matches = 0
|
|
for _, craft in ipairs(f.crafts) do
|
|
if craft.output == output and craft.type == "shapeless" and #craft.recipe == #inputs then
|
|
local same = true
|
|
for i, value in ipairs(inputs) do if craft.recipe[i] ~= value then same = false end end
|
|
if same then matches = matches + 1 end
|
|
end
|
|
end
|
|
return matches == 1
|
|
end
|
|
local function cell(s, x,y,z) return s.data[1+x+s.size.x*(y+s.size.y*z)] end
|
|
|
|
test("factory is lazy and does not publish globals or mutate hearth", function()
|
|
local f=fixture()
|
|
assert(f.registrations()==0 and #f.ores==0 and #f.decos==0 and #f.crafts==0)
|
|
assert(count(f.hearth)==1 and f.hearth.sentinel==f.external)
|
|
assert(type(f.m.register)=="function" and type(f.m.surface)=="function")
|
|
end)
|
|
test("registration is idempotent and preserves external registries", function()
|
|
local f=loaded(); local n, ores, decos, crafts=f.registrations(), #f.ores, #f.decos, #f.crafts
|
|
assert(f.m.register()==f.m)
|
|
assert(n==f.registrations() and ores==#f.ores and decos==#f.decos and crafts==#f.crafts)
|
|
assert(f.biomes.external==f.external and f.nodes["hearth:grass"].sentinel)
|
|
assert(f.aliases.external_alias=="external:stone")
|
|
end)
|
|
test("mapgen aliases are non-forcing and keep canonical stone identity", function()
|
|
local f=loaded()
|
|
assert(f.aliases.mapgen_stone=="hearth:stone")
|
|
assert(f.aliases.mapgen_water_source=="hearth:water_source")
|
|
assert(f.aliases.mapgen_river_water_source=="hearth:water_source")
|
|
assert(loaded(true).aliases.mapgen_stone=="external:stone")
|
|
end)
|
|
test("alias with a registered item is not overwritten", function()
|
|
local f=fixture(); f.items.mapgen_stone={sentinel=true}; f.m.register()
|
|
assert(not f.aliases.mapgen_stone and f.items.mapgen_stone.sentinel)
|
|
end)
|
|
test("terrain node collision fails rather than overwriting", function()
|
|
local f=fixture(); local old={sentinel=true}; f.nodes["hearth:granite"]=old
|
|
assert(not pcall(f.m.register)); assert(f.nodes["hearth:granite"]==old)
|
|
end)
|
|
test("ten biomes preserve meadow and river names and resolve all materials", function()
|
|
local f=loaded(); assert(count(f.biomes)==11)
|
|
assert(f.biomes.hearth_temperate.node_top=="hearth:grass")
|
|
assert(f.biomes.hearth_riverbank.node_top=="hearth:sand")
|
|
for name, b in pairs(f.biomes) do if name~="external" then
|
|
assert(b.y_min<=b.y_max and b.heat_point>=0 and b.heat_point<=100)
|
|
assert(b.humidity_point>=0 and b.humidity_point<=100)
|
|
for key, value in pairs(b) do if key:match("^node_") then assert(f.nodes[value], value) end end
|
|
end end
|
|
end)
|
|
test("altitude coverage has no missing climate-independent fallback band", function()
|
|
local f=loaded()
|
|
for y=-512,512 do
|
|
local coverage=false
|
|
for _, b in ipairs(f.m.biome_profiles) do if y>=b.min and y<=b.max then coverage=true end end
|
|
assert(coverage, "altitude "..y)
|
|
end
|
|
local peak=f.biomes.hearth_rocky_peaks
|
|
assert(peak.y_min==211 and peak.node_top=="hearth:snow" and peak.vertical_blend==0)
|
|
assert(f.biomes.hearth_floodplain.y_max==8)
|
|
end)
|
|
test("habitat helper resolves qualified nodes and rejects unknown surfaces", function()
|
|
local m=loaded().m
|
|
assert(m.surface("hearth:peat").moisture > m.surface("podzol").moisture)
|
|
assert(m.surface({name="hearth:alluvium"}).fertility==1)
|
|
assert(not m.surface("other:grass") and not m.surface(false) and not m.surface({}))
|
|
assert(m.supports_species("willow", "hearth:alluvium"))
|
|
assert(not m.supports_species("willow", "hearth:podzol"))
|
|
assert(not m.supports_species("unknown", "grass"))
|
|
assert(not m.supports_species("oak", false))
|
|
end)
|
|
test("ten exact catalogue species have real full-block trunks, leaves, and timber", function()
|
|
local f=loaded(); assert(table.concat(f.m.species_order,",")=="oak,birch,pine,willow,alder,beech,aspen,rowan,hazel,spruce")
|
|
for _, species in ipairs(f.m.species_order) do
|
|
local log=f.nodes["hearth:"..species.."_log"]
|
|
local leaf=f.nodes["hearth:"..species.."_leaves"]
|
|
assert(log.groups.tree==1 and log.groups.choppy==2 and log.drawtype==nil)
|
|
assert(log.tiles[1]=="hearth_"..species.."_log_top.png")
|
|
assert(log.tiles[3]=="hearth_"..species.."_log_side.png")
|
|
assert(leaf.drawtype=="allfaces_optional" and leaf.groups.leaves==1)
|
|
assert(f.nodes["hearth:"..species.."_planks"].groups.wood==1)
|
|
assert(not f.nodes["hearth:"..species.."_seedling"])
|
|
end
|
|
end)
|
|
test("species timber converts losslessly into canonical progression ingredients", function()
|
|
local f=loaded()
|
|
for _, species in ipairs(f.m.species_order) do
|
|
local log="hearth:"..species.."_log"
|
|
assert(recipe(f,{log,log},"hearth:log 2"))
|
|
assert(not recipe(f,log,"hearth:log") and not recipe(f,log,"hearth:log 2"))
|
|
assert(recipe(f,"hearth:"..species.."_log","hearth:"..species.."_planks 4"))
|
|
assert(recipe(f,"hearth:"..species.."_planks","hearth:planks"))
|
|
end
|
|
-- Existing recipes accept these exact names for charcoal, walls and sticks.
|
|
assert(f.items["hearth:log"] and f.items["hearth:planks"] and f.items["hearth:clay"])
|
|
end)
|
|
test("geological stone remains useful masonry and canonical foundations", function()
|
|
local f=loaded()
|
|
for _, rock in ipairs(f.m.rocks) do
|
|
local raw, brick="hearth:"..rock, "hearth:"..rock.."_bricks"
|
|
assert(f.nodes[raw].groups.cracky==3 and f.nodes[raw].drop==nil)
|
|
assert(recipe(f,raw,brick.." 4") and recipe(f,{raw,raw},"hearth:stone 2"))
|
|
assert(not recipe(f,raw,"hearth:stone") and not recipe(f,raw,"hearth:stone 2"))
|
|
assert(f.nodes[brick].groups.hearth_building==1)
|
|
assert(f.nodes[brick].on_place==f.construction.place_building)
|
|
assert(f.nodes[brick].after_place_node==f.construction.owner)
|
|
end
|
|
end)
|
|
test("all 40 terrain recipes have unique shapeless inputs including multiplicity", function()
|
|
local f=loaded(); assert(#f.crafts==40)
|
|
local seen={}
|
|
for _, craft in ipairs(f.crafts) do
|
|
assert(craft.type=="shapeless")
|
|
local inputs={}
|
|
for i, input in ipairs(craft.recipe) do inputs[i]=input end
|
|
table.sort(inputs)
|
|
local key=table.concat(inputs,",")
|
|
assert(not seen[key], "colliding terrain recipe: "..craft.output.." and "..tostring(seen[key]))
|
|
seen[key]=craft.output
|
|
end
|
|
end)
|
|
test("all registered material drops and crafting outputs resolve", function()
|
|
local f=loaded()
|
|
local function resolved(stack)
|
|
local name=stack:match("^([^ ]+)"); assert(f.items[name], "unresolved "..stack)
|
|
end
|
|
for _, def in pairs(f.nodes) do if type(def.drop)=="string" and def.drop~="" then resolved(def.drop) end end
|
|
for _, craft in ipairs(f.crafts) do
|
|
resolved(craft.output); for _, input in ipairs(craft.recipe) do resolved(input) end
|
|
end
|
|
end)
|
|
test("resource items and ore nodes use precise original art names", function()
|
|
local f=loaded()
|
|
for item, vein in pairs({quartz="quartz_vein", copper_ore="copper_ore_vein", coal="coal_ore_vein"}) do
|
|
assert(f.items["hearth:"..item].inventory_image=="hearth_item_"..item..".png")
|
|
assert(f.nodes["hearth:"..vein].tiles[1]=="hearth_"..vein..".png")
|
|
assert(f.nodes["hearth:"..vein].drop=="hearth:"..item.." 2")
|
|
end
|
|
assert(f.nodes["hearth:iron_ore_vein"].sentinel)
|
|
end)
|
|
test("geology runs before nested restricted scatter ore registration", function()
|
|
local f=loaded(); assert(#f.ores==9)
|
|
assert(f.ores[4].ore_type=="blob" and f.ores[4].ore=="hearth:basalt")
|
|
assert(f.ores[4].noise_params and f.ores[4].y_max<0)
|
|
local generated={}
|
|
for _, ore in ipairs(f.ores) do
|
|
assert(f.nodes[ore.ore] and ore.clust_scarcity>0 and ore.y_min<=ore.y_max)
|
|
for _, host in ipairs(ore.wherein) do
|
|
assert(f.nodes[host] and f.nodes[host].groups and f.nodes[host].groups.stone==1 or host=="hearth:stone")
|
|
assert(not contains({"hearth:soil", "hearth:grass", "air"},host))
|
|
end
|
|
if ore.ore_type=="scatter" then
|
|
assert(ore.clust_num_ores<=ore.clust_size^3)
|
|
generated[ore.ore]=true
|
|
end
|
|
end
|
|
for _, name in ipairs({"quartz_vein", "copper_ore_vein", "coal_ore_vein", "iron_ore_vein"}) do assert(generated["hearth:"..name]) end
|
|
assert(contains(f.ores[5].wherein,"hearth:granite") and not contains(f.ores[5].wherein,"hearth:sandstone"))
|
|
assert(contains(f.ores[6].wherein,"hearth:basalt"))
|
|
assert(contains(f.ores[7].wherein,"hearth:sandstone") and not contains(f.ores[7].wherein,"hearth:granite"))
|
|
end)
|
|
test("v7 underground riverbank stone receives ordered resource-bearing beds", function()
|
|
local f=loaded()
|
|
for i=1,3 do assert(f.ores[i].clust_size==0 and f.ores[i].clust_scarcity==1) end
|
|
local function host_at(y)
|
|
local host=f.biomes.hearth_riverbank.node_stone
|
|
for _, bed in ipairs(f.ores) do
|
|
if bed.ore_type=="stratum" and y>=bed.y_min and y<=bed.y_max and contains(bed.wherein,host) then host=bed.ore end
|
|
end
|
|
return host
|
|
end
|
|
assert(host_at(-150)=="hearth:granite" and host_at(-80)=="hearth:granite")
|
|
assert(host_at(-79)=="hearth:slate" and host_at(-24)=="hearth:slate")
|
|
assert(host_at(-23)=="hearth:limestone" and host_at(-8)=="hearth:limestone")
|
|
assert(host_at(-7)=="hearth:sandstone")
|
|
for _, ore in ipairs(f.ores) do if ore.ore_type=="scatter" and ore.y_min==-31000 then
|
|
local found=false
|
|
for y=-150,-8 do if contains(ore.wherein,host_at(y)) and y<=ore.y_max then found=true end end
|
|
assert(found,"unreachable ore host "..ore.ore)
|
|
end end
|
|
end)
|
|
test("schematic dimensions, probabilities and trunk anchoring are valid", function()
|
|
local f=loaded()
|
|
for _, species in ipairs(f.m.species_order) do
|
|
local s=f.m.tree_schematic(species); local c=math.floor(s.size.x/2)
|
|
assert(s.size.y>=5 and s.size.y<=10 and s.size.x<=9 and #s.data==s.size.x*s.size.y*s.size.z)
|
|
for y=0,s.size.y-2 do assert(cell(s,c,y,c).name=="hearth:"..species.."_log") end
|
|
local leaves, air=0,0
|
|
for _, n in ipairs(s.data) do
|
|
assert(n.param2==0 and not n.force_place)
|
|
if n.name=="air" then assert(n.prob==0); air=air+1
|
|
else assert(f.nodes[n.name] and n.prob==255); if n.name:match("_leaves$") then leaves=leaves+1 end end
|
|
end
|
|
assert(leaves>10 and air>10)
|
|
end
|
|
assert(not pcall(f.m.tree_schematic,"unknown"))
|
|
end)
|
|
test("all ten silhouettes are geometrically distinct and young canopies stay bounded", function()
|
|
local m=loaded().m; local shapes={}
|
|
for _,species in ipairs(m.species_order) do
|
|
local s=m.tree_schematic(species); local shape={s.size.x..":"..s.size.y..":"..s.size.z}
|
|
for _,n in ipairs(s.data) do shape[#shape+1]=n.name=="air" and "." or (n.name:match("_log$") and "T" or "L") end
|
|
local signature=table.concat(shape,",")
|
|
assert(not shapes[signature],species.." duplicates "..tostring(shapes[signature])); shapes[signature]=species
|
|
local young=m.tree_schematic(species,false,"young"); local leaves=0
|
|
assert(young.size.x==3 and young.size.y<=5 and #young.data<=45)
|
|
for _,n in ipairs(young.data) do
|
|
assert(not n.force_place)
|
|
if n.name:match("_leaves$") then leaves=leaves+1 end
|
|
end
|
|
assert(leaves>=5)
|
|
end
|
|
assert(not pcall(m.tree_schematic,"oak",false,"invalid"))
|
|
end)
|
|
test("fallen logs are three full cubes with horizontal facedir and non-forcing mapgen only", function()
|
|
local f=loaded(); local d=f.decos["hearth:woodland_deadwood"]
|
|
assert(d.deco_type=="schematic" and d.rotation=="random" and d.place_offset_y==1)
|
|
assert(not d.flags:find("force_placement",1,true) and not d.force_placement)
|
|
assert(d.schematic.size.x==3 and d.schematic.size.y==1 and d.schematic.size.z==1)
|
|
assert(#d.schematic.data==3 and d.y_min==2 and d.y_max==160)
|
|
for _,n in ipairs(d.schematic.data) do
|
|
assert(n.name=="hearth:deadwood" and n.prob==255 and n.param2==12 and not n.force_place)
|
|
-- Luanti facedir / 4 = axis direction; axis 3 is +X, axis 1 is +Z.
|
|
assert(math.floor(n.param2/4)==3)
|
|
end
|
|
for _,ground in ipairs(d.place_on) do assert(ground~="hearth:water_source") end
|
|
end)
|
|
test("species silhouettes are distinct broad slender conical drooping riparian", function()
|
|
local m=loaded().m
|
|
local oak, birch, pine, willow, alder=m.tree_schematic("oak"),m.tree_schematic("birch"),m.tree_schematic("pine"),m.tree_schematic("willow"),m.tree_schematic("alder")
|
|
assert(oak.size.x>birch.size.x and birch.size.y>oak.size.y)
|
|
assert(cell(oak,6,4,3).name=="hearth:oak_leaves")
|
|
assert(cell(pine,0,2,3).name=="hearth:pine_leaves")
|
|
assert(cell(pine,0,7,3).name=="air" and cell(pine,3,8,3).name=="hearth:pine_leaves")
|
|
assert(cell(willow,7,1,7).name=="hearth:willow_leaves")
|
|
assert(cell(willow,4,1,4).name=="hearth:willow_log")
|
|
assert(alder.size.y==6 and m.profiles.alder.nitrogen_fixer)
|
|
end)
|
|
test("tree decorations are habitat-restricted and cannot erase terrain", function()
|
|
local f=loaded()
|
|
for _, species in ipairs(f.m.species_order) do
|
|
local d=f.decos["hearth:"..species.."_trees"]
|
|
assert(d.deco_type=="schematic" and #d.biomes>=1 and #d.place_on>=1)
|
|
assert(d.place_offset_y==1 and d.rotation=="random")
|
|
assert(not d.flags:find("force_placement",1,true))
|
|
if species=="willow" or species=="alder" then
|
|
assert(d.num_spawn_by==1 and contains(d.spawn_by,"hearth:water_source"))
|
|
end
|
|
end
|
|
end)
|
|
test("all canopy registrations precede ground flora", function()
|
|
local f=loaded(); local first_plant=math.huge
|
|
for i,d in ipairs(f.decos) do if d.deco_type=="simple" then first_plant=math.min(first_plant,i) end end
|
|
for i,d in ipairs(f.decos) do if d.name:match("_trees$") then assert(i<first_plant) end end
|
|
end)
|
|
test("canonical meadow timber and all-surface gathering remain available", function()
|
|
local f=loaded(); local d=f.decos["hearth:temperate_trees"]
|
|
assert(contains(d.biomes,"hearth_temperate")); local log=false
|
|
for _, n in ipairs(d.schematic.data) do if n.name=="hearth:log" then log=true end end
|
|
assert(log)
|
|
for name in pairs(f.m.surfaces) do
|
|
assert(contains(f.decos["hearth:loose_stone"].place_on,"hearth:"..name))
|
|
assert(contains(f.decos["hearth:sticks"].place_on,"hearth:"..name))
|
|
end
|
|
end)
|
|
test("riparian food and clay require water and upland springs are limited", function()
|
|
local f=loaded()
|
|
for _, id in ipairs({"reeds", "clay_bank", "old_river_weir"}) do
|
|
local d=f.decos["hearth:"..id]; assert(d.num_spawn_by==1 and d.check_offset==-1)
|
|
assert(contains(d.spawn_by,"hearth:water_source"))
|
|
end
|
|
assert(f.decos["hearth:clay_bank"].y_max==8)
|
|
assert(f.decos["hearth:seep_springs"].y_max==150)
|
|
assert(not contains(f.decos["hearth:seep_springs"].biomes,"hearth_dry_scrub"))
|
|
end)
|
|
test("flora communities and seasonal patches resolve without new growth nodes", function()
|
|
local f=loaded()
|
|
for _, d in ipairs(f.decos) do
|
|
for _, ground in ipairs(d.place_on) do assert(f.nodes[ground],ground) end
|
|
for _, biome in ipairs(d.biomes or {}) do assert(f.biomes[biome],biome) end
|
|
if d.decoration then assert(f.nodes[d.decoration],d.decoration) end
|
|
end
|
|
assert(contains(f.decos["hearth:woodland_ferns"].biomes,"hearth_wet_forest"))
|
|
assert(not contains(f.decos["hearth:woodland_ferns"].biomes,"hearth_dry_scrub"))
|
|
assert(f.decos["hearth:spring_foraging"].decoration=="hearth:spring_greens_patch")
|
|
assert(f.decos["hearth:autumn_foraging"].decoration=="hearth:autumn_mushrooms_patch")
|
|
end)
|
|
test("world texture contract is explicit for every new node", function()
|
|
local f=loaded(); local textures={}
|
|
for name, def in pairs(f.nodes) do if not def.sentinel then
|
|
assert(def.tiles and #def.tiles>=1,name)
|
|
for _, image in ipairs(def.tiles) do
|
|
assert(image:match("^hearth_[a-z_]+%.png$"),image); textures[image]=true
|
|
end
|
|
end end
|
|
-- Five extra woods add log top/side, leaves and planks (20 textures).
|
|
assert(count(textures)==62, "unexpected world tile count "..count(textures))
|
|
assert(f.registrations()==51, "unexpected private node count "..f.registrations())
|
|
end)
|
|
print("PASS " .. passed .. " biome/geology checks")
|