-- 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("thirteen 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,crab_apple,wild_cherry,field_maple") 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 49 terrain recipes have unique shapeless inputs including multiplicity", function() local f=loaded(); assert(#f.crafts==49) 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=1,name) for _, image in ipairs(def.tiles) do assert(image:match("^hearth_[a-z_]+%.png$"),image); textures[image]=true end end end -- Thirteen woods each have their own end grain, bark, foliage and planks. assert(count(textures)==74, "unexpected world tile count "..count(textures)) assert(f.registrations()==60, "unexpected private node count "..f.registrations()) end) print("PASS " .. passed .. " biome/geology checks")