-- Isolated journal / recipe / HUD tests; no engine or real player required. local target = assert(arg[1], "interface.lua path required") local sibling = target:match("^(.*[/\\])") or "" local items = { ["hearth:knife"] = {description="Flint knife"}, ["hearth:loose_stone"] = {description="Loose flint"}, ["hearth:firestarter"] = {description="Firestarter"}, ["hearth:torch"] = {description="Reed torch"}, ["hearth:bedroll"] = {description="Bedroll"}, ["hearth:reeds"] = {description="Reeds"}, ["hearth:planks"] = {description="Planks"}, ["hearth:fire"] = {description="Stone hearth"}, ["hearth:quartz"] = {description="Quartz crystal"}, ["hearth:window"] = {description="Reed-lattice window"}, ["hearth:copper_ore"] = {description="Copper ore"}, ["hearth:charcoal"] = {description="Prepared charcoal"}, ["hearth:copper_ingot"] = {description="Copper ingot"}, ["hearth:copper_charge"] = {description="Copper charge"}, ["hearth:copper_pick"] = {description="Copper pick"}, ["hearth:sticks"] = {description="Dry sticks"}, ["hearth:log"] = {description="Timber log"}, ["hearth:stone"] = {description="Weathered stone"}, ["hearth:alluvium"] = {description="Alluvium"}, } local species = {"oak", "birch", "pine", "willow", "alder", "beech", "aspen", "rowan", "hazel", "spruce", "crab_apple", "wild_cherry", "field_maple"} local rocks = {"granite", "basalt", "limestone", "slate", "sandstone"} for _, s in ipairs(species) do items["hearth:"..s.."_log"] = {description=s.." log"} items["hearth:"..s.."_planks"] = {description=s.." planks"} end for _, rock in ipairs(rocks) do items["hearth:"..rock] = {description=rock.." bedrock"} items["hearth:"..rock.."_bricks"] = {description=rock.." masonry"} end local registered, screens, commands = {}, {}, {} core = {registered_items=items, is_singleplayer=function() return true end} function core.get_modpath() return sibling end function core.formspec_escape(s) return (tostring(s):gsub("\\","\\\\"):gsub("%[","\\["):gsub("%]","\\]"):gsub(";","\\;"):gsub(",","\\,")) end function core.colorize(color, s) return "\27(c@" .. color .. ")" .. s .. "\27(c@#ffffff)" end function core.register_on_player_receive_fields(fn) registered.fields=fn end function core.register_chatcommand(name, def) commands[name]=def end function core.get_all_craft_recipes(output) local recipes = { ["hearth:knife"] = {{method="normal",width=3,items={"hearth:loose_stone","hearth:loose_stone",nil,nil,"hearth:loose_stone"},output="hearth:knife"}}, ["hearth:firestarter"] = {{method="normal",width=0,items={"hearth:loose_stone","hearth:sticks"},output="hearth:firestarter"}}, ["hearth:torch"] = {{method="normal",width=0,items={"hearth:sticks","hearth:reeds"},output="hearth:torch 2"}}, ["hearth:bedroll"] = {{method="normal",width=3,items={"hearth:planks","hearth:planks","hearth:planks","hearth:reeds","hearth:reeds","hearth:reeds"},output="hearth:bedroll"}}, ["hearth:fire"] = {{method="normal",width=3,items={"hearth:loose_stone"},output="hearth:fire"}}, ["hearth:copper_charge"] = {{method="normal",width=0,items={"hearth:copper_ore","hearth:charcoal"},output="hearth:copper_charge"}}, ["hearth:copper_pick"] = {{method="normal",width=3,items={"hearth:copper_ingot","hearth:copper_ingot","hearth:copper_ingot","","hearth:sticks","","","hearth:sticks",""},output="hearth:copper_pick"}}, ["hearth:window"] = { {method="normal",width=0,items={"hearth:quartz","hearth:quartz","hearth:quartz"},output="hearth:window"}, {method="normal",width=3,items={"hearth:planks","hearth:reeds","hearth:planks","hearth:planks","hearth:reeds","hearth:planks"},output="hearth:window"}}, ["hearth:log"] = {}, ["hearth:planks"] = {}, ["hearth:stone"] = {}, } for _, s in ipairs(species) do recipes["hearth:"..s.."_planks"] = {{method="normal",width=0,items={"hearth:"..s.."_log"},output="hearth:"..s.."_planks 4"}} table.insert(recipes["hearth:log"], {method="normal",width=0,items={"hearth:"..s.."_log","hearth:"..s.."_log"},output="hearth:log 2"}) table.insert(recipes["hearth:planks"], {method="normal",width=0,items={"hearth:"..s.."_planks"},output="hearth:planks"}) end for _, rock in ipairs(rocks) do recipes["hearth:"..rock.."_bricks"] = {{method="normal",width=0,items={"hearth:"..rock},output="hearth:"..rock.."_bricks 4"}} table.insert(recipes["hearth:stone"], {method="normal",width=0,items={"hearth:"..rock,"hearth:"..rock},output="hearth:stone 2"}) end return recipes[output] end function core.show_formspec(name, form, spec) screens[#screens+1]={name=name,form=form,spec=spec} end hearth = {sessions={alice={}}, model={location=function() end}} dofile(target) local player={get_player_name=function() return "alice" end} local player_metadata={} function player:get_meta() return {get_string=function(_,key) return player_metadata[key] or "" end, set_string=function(_,key,value) player_metadata[key]=value end} end function player:get_pos() return {x=10,y=4,z=-2} end function player:set_inventory_formspec(spec) self.inventory_spec=spec end local hud = {} function player:hud_add(def) hud[#hud+1]=def; return #hud end function player:hud_change(id, field, value) assert(hud[id],"unknown HUD id"); hud[id][field]=value end function player:hud_set_flags(flags) self.flags=flags end function player:hud_set_hotbar_itemcount(count) self.hotbar_count=count end function player:hud_set_hotbar_image(texture) self.hotbar_texture=texture end function player:hud_set_hotbar_selected_image(texture) self.hotbar_selected=texture end function core.get_player_by_name(name) return name=="alice" and player or nil end local total=0 local function test(name, fn) local ok, err=pcall(fn); assert(ok, name..": "..tostring(err)); total=total+1 print("PASS " .. name) end local function latest() return screens[#screens] end test("search lists craft recipes and elapsed real-time process instructions", function() hearth.recipes(player,"",1) local spec=latest().spec assert(latest().form=="hearth:recipes" and spec:find("Page 1 /",1,true)) hearth.recipes(player,"knife",1) assert(latest().spec:find("Flint knife",1,true)) hearth.recipes(player,"Cooked forage",1) assert(latest().spec:find("6 real seconds",1,true), latest().spec) hearth.recipes(player,"Boiled water",1) assert(latest().spec:find("4 real seconds",1,true), latest().spec) hearth.recipes(player,"Pottery firing",1) assert(latest().spec:find("120 real seconds",1,true), latest().spec) hearth.recipes(player,"River fish trap",1) assert(latest().spec:find("600 real seconds",1,true), latest().spec) hearth.recipes(player,"Spring fish run",1) assert(latest().spec:find("scheduled deadline",1,true), latest().spec) hearth.recipes(player,"River fish trap",1) assert(latest().spec:find("600 real seconds",1,true), latest().spec) hearth.recipes(player,"Spring fish run",1) assert(latest().spec:find("scheduled deadline",1,true), latest().spec) hearth.recipes(player,"firestarter",1) assert(latest().spec:find("Firestarter",1,true)) hearth.recipes(player,"bedroll",1) assert(latest().spec:find("Bedroll",1,true)) end) test("plain text searches match ingredients and result names", function() hearth.recipes(player,"Seasonal forage",1) assert(latest().spec:find("Spring greens",1,true) and latest().spec:find("Autumn mushrooms",1,true)) hearth.recipes(player,"loose_stone",1) assert(latest().spec:find("Flint knife",1,true)) hearth.recipes(player,"torch",1) assert(latest().spec:find("Reed torch",1,true)) end) test("search is bounded and formspec escaped, unknown term reports no matches", function() local query=string.rep("x",80).."];button[0,0;" hearth.recipes(player,query,1) local spec=latest().spec assert(spec:find("x",1,true) and not spec:find(query,1,true)) hearth.recipes(player,"no-such-item",1) assert(latest().spec:find("No recipe or process matches",1,true)) end) test("guide navigation and chat command enter the real recipe index", function() assert(commands.recipes and commands.recipes.func("alice","reeds")) assert(latest().form=="hearth:recipes" and latest().spec:find("Reed torch",1,true)) registered.fields(player,"hearth:guide",{recipe_index=true}) assert(latest().form=="hearth:recipes") registered.fields(player,"hearth:recipes",{close=true}) assert(latest().form=="hearth:guide") end) test("all journal surfaces share Mocha controls, lists and botanical dividers", function() local function themed(spec) for _,color in ipairs({"#1e1e2e","#181825","#292940","#3c3d56","#cdd6f4","#cba6f7","#94e2d5"}) do assert(spec:find(color,1,true), "missing palette anchor "..color) end assert(spec:find("style_type[button:hovered,button_exit:hovered",1,true)) assert(spec:find("bgimg="..core.formspec_escape("[fill:1x1:#ffffff"),1,true),"borderless buttons need visible backing") assert(spec:find("style_type[field,textarea;",1,true)) assert(spec:find("listcolors[#292940;#3c3d56;#3c3d56;#181825;#cdd6f4]",1,true)) assert(spec:find("0.16,0.07;#94e2d5]",1,true),"pressed-sprig divider missing") end hearth.guide(player,1); themed(latest().spec) hearth.recipes(player,"",1); themed(latest().spec) hearth.inventory(player); themed(player.inventory_spec) hearth.location_form(player); themed(latest().spec) end) test("guide keeps eight pages and every sidebar button inside 14 by 10", function() for page=1,8 do hearth.guide(player,page) local spec,count=latest().spec,0 assert(spec:find("size[14,10]",1,true)) assert(spec:find(string.format("%02d / 08",page),1,true)) for x,y,w,h in spec:gmatch("button%[([%d.]+),([%d.]+);([%d.]+),([%d.]+);page_%d+;") do assert(tonumber(x)+tonumber(w)<=3.3 and tonumber(y)+tonumber(h)<8.5) count=count+1 end assert(count==8 and not spec:find("page_9",1,true)) end hearth.guide(player,6) assert(latest().spec:find("Living ecosystems",1,true)) assert(latest().spec:find("Dynamic Life",1,true)) assert(not latest().spec:find("Riverward field season",1,true)) assert(latest().spec:find("no offline catch-up",1,true)) hearth.guide(player,0/0); assert(hearth.sessions.alice.page==1) hearth.guide(player,math.huge); assert(hearth.sessions.alice.page==8) end) test("resource searches show quartz windows copper charges picks and process contract", function() for _, query in ipairs({"quartz", "hearth:quartz"}) do hearth.recipes(player,query,1) assert(latest().spec:find("Reed-lattice window <- Quartz crystal + Quartz crystal + Quartz crystal",1,true)) end hearth.recipes(player,"window",1) assert(latest().spec:find("Planks + Planks + Planks + Planks + Reeds + Reeds",1,true)) hearth.recipes(player,"copper_ore",1) assert(latest().spec:find("Copper charge <- Copper ore + Prepared charcoal",1,true)) hearth.recipes(player,"copper_pick",1) assert(latest().spec:find("Copper pick <- Copper ingot + Copper ingot + Copper ingot + Dry sticks + Dry sticks",1,true)) hearth.recipes(player,"Copper smelting",1) for _, text in ipairs({"45 real seconds", "30 fuel seconds", "75 fuel seconds", "one hearth:copper_ingot", "full inventory"}) do assert(latest().spec:find(text,1,true),text) end hearth.recipes(player,"Fire fuel",1) for _, text in ipairs({"180", "90", "240", "600", "nonrenewable", "not consumed"}) do assert(latest().spec:find(text,1,true),text) end end) for _, s in ipairs(species) do test(s.." search discovers matching planks and canonical timber conversions", function() hearth.recipes(player,"hearth:"..s.."_log",1) assert(latest().spec:find("4 x "..s.." planks <- "..s.." log",1,true)) assert(latest().spec:find("2 x Timber log <- "..s.." log + "..s.." log",1,true)) assert(not latest().spec:find("\nTimber log <- "..s.." log",1,true)) hearth.recipes(player,"hearth:"..s.."_planks",1) assert(latest().spec:find("Planks <- "..s.." planks",1,true)) end) end for _, rock in ipairs(rocks) do test(rock.." search discovers four bricks and canonical stone", function() hearth.recipes(player,"hearth:"..rock,1) assert(latest().spec:find("4 x "..rock.." masonry <- "..rock.." bedrock",1,true)) assert(latest().spec:find("2 x Weathered stone <- "..rock.." bedrock + "..rock.." bedrock",1,true)) assert(not latest().spec:find("\nWeathered stone <- "..rock.." bedrock",1,true)) end) end test("regional guide covers ten biomes conversions and operator-controlled old-world seams", function() hearth.guide(player,8) for _, text in ipairs({"Temperate meadow", "oak woodland", "birch forest", "pine taiga", "Wet forest", "floodplain", "Dry scrub", "alpine meadow", "rocky peaks", "riverbank", "canonical log", "45 real seconds", "240 seconds", "fresh worlds only", "unexplored old-world boundaries", "Operators must explicitly choose", "climate HUD remains shared"}) do assert(latest().spec:find(text,1,true),text) end for _, text in ipairs({"two matching species logs to two canonical logs", "two matching rocks convert to two canonical stones", "separate crafting slots", "no single-log or single-rock canonical conversion"}) do assert(latest().spec:find(text,1,true),text) end registered.fields(player,"hearth:guide",{page_8=true}) assert(hearth.sessions.alice.page==8) hearth.guide(player,6) for _,text in ipairs({"Birch pioneers", "Evergreen pine and spruce", "alluvium/peat", "two nutrient units", "Nitrogen-fixer policy", "not a separate fertilizer process", "not fully simulated hydrology", "no offline catch-up"}) do assert(latest().spec:find(text,1,true),text) end hearth.recipes(player,"Species timber",1) assert(latest().spec:find("convert species logs in matching pairs first",1,true)) assert(latest().spec:find("two canonical hearth:log",1,true)) hearth.recipes(player,"Regional geology",1) assert(latest().spec:find("two matching rocks to two canonical stones",1,true)) hearth.recipes(player,"Finite nitrogen",1) assert(latest().spec:find("at most two nutrient units",1,true)) end) test("inventory retains crafting, preview, ten by four storage and transfer rings", function() hearth.inventory(player) assert(player.inventory_spec:find("style_type[list;size=1,1;spacing=0.12,0.12]",1,true)) assert(0.6+10+9*0.12<12.4 and 5.7+4+3*0.12<11.3,"inventory grid must fit the paper") for _,element in ipairs({"list[current_player;craft;0.6,1.8;3,3;]", "list[current_player;craftpreview;4.8,2.9;1,1;]", "list[current_player;main;0.6,5.7;10,4;]", "listring[current_player;main]listring[current_player;craft]", "field_guide;Open field notes]"}) do assert(player.inventory_spec:find(element,1,true),element) end registered.fields(player,"",{field_guide=true}); assert(latest().form=="hearth:guide") end) test("recipe query escaping, literal matching and pagination remain bounded", function() hearth.recipes(player,"];button[0,0;1,1;attack;Injected]",0/0) assert(latest().spec:find(core.formspec_escape("];button[0,0;1,1;attack;Injected]"),1,true)) assert(not latest().spec:find(";attack;Injected]",1,true)) hearth.recipes(player,"%[.*",1) assert(latest().spec:find("No recipe or process matches",1,true)) hearth.recipes(player,"\n\tfood",1); assert(hearth.sessions.alice.recipe_query==" food") hearth.recipes(player,"",1) registered.fields(player,"hearth:recipes",{next=true}); assert(hearth.sessions.alice.recipe_page==2) registered.fields(player,"hearth:recipes",{previous=true}); assert(hearth.sessions.alice.recipe_page==1) registered.fields(player,"hearth:recipes",{key_enter=true,query="torch"}) assert(latest().spec:find("Reed torch",1,true)) end) test("ecosystem command and guide button only read one habitat and status snapshot", function() local reads, snapshots=0,0 local function forbidden() error("observations must never mutate or load the map") end for _,name in ipairs({"set_node","swap_node","emerge_area","forceload_block","add_entity","get_objects_inside_radius","get_meta"}) do core[name]=forbidden end hearth.succession={habitat=function(pos) assert(pos.x==10 and pos.y==4 and pos.z==-2); reads=reads+1 return {grass=3,flowers=2,cover=4,water=1,canopy=1.5,moisture=0.65,light=10,loaded=true, ground="alluvium",pollination=7,nutrients=2} end,step=forbidden,forage=forbidden,deposit=forbidden} hearth.wildlife={status=function() snapshots=snapshots+1 return {hare=3,deer=2,fox=1,total=6,global_cap=32,local_cap=6,player_cap=6, recruitment={attempts=8,successes=2,last_reason="admitted"}} end} assert(commands.ecosystem.func("alice")) assert(reads==1 and snapshots==1 and latest().form=="hearth:ecosystem") for _,text in ipairs({"Natural grass cover: 3", "Flower-support cover: 2", "Shrub / fern / aquatic / tree cover: 4", "Moisture: 65%", "Hares: 3", "Deer: 2", "Foxes: 1", "Active total: 6 / 32 globally", "not a local census", "Loaded sample", "No terrain is loaded", "Ground: Alluvium", "Pollinator-support plants: 7", "Nutrient reserve: 2 / 8", "pine and spruce stay evergreen", "deciduous canopy thins", "Admissions: 2 / 8 attempts", "Latest outcome: admitted"}) do assert(latest().spec:find(text,1,true),text) end registered.fields(player,"hearth:guide",{ecosystem_survey=true}) assert(reads==2 and snapshots==2 and latest().form=="hearth:ecosystem") registered.fields(player,"hearth:ecosystem",{refresh=true}) assert(reads==3 and snapshots==3) registered.fields(player,"hearth:ecosystem",{field_notes=true}) assert(hearth.sessions.alice.page==6 and reads==3 and snapshots==3) assert(not commands.ecosystem.func("offline")) end) test("survey discloses unloaded and unavailable data without changing sessions or world", function() hearth.succession.habitat=function() return {loaded=false,grass=1} end hearth.ecosystem(player) assert(latest().spec:find(core.formspec_escape("Incomplete sample / unloaded cells are unknown, not empty"),1,true)) assert(latest().spec:find("Natural grass cover: 1",1,true)) assert(latest().spec:find("Ground: unknown",1,true)) assert(latest().spec:find("Pollinator-support plants: unavailable",1,true)) assert(latest().spec:find("Nutrient reserve: unavailable",1,true)) hearth.succession.habitat=function() return {loaded=true,ground="];button[0,0;1,1;attack;Injected]",pollination=0/0,nutrients=math.huge} end hearth.ecosystem(player) assert(not latest().spec:find(";attack;Injected]",1,true)) assert(latest().spec:find(core.formspec_escape("hearth:];button[0,0;1,1;attack;Injected]"),1,true)) assert(latest().spec:find("Pollinator-support plants: 0",1,true)) assert(latest().spec:find("Nutrient reserve: 0 / 8",1,true)) hearth.succession,hearth.wildlife=nil,nil hearth.ecosystem(player) assert(latest().spec:find("Succession observations are not available",1,true)) assert(latest().spec:find("Wildlife observations are not available",1,true)) local session,count=hearth.sessions.alice,#screens hearth.sessions.alice=nil assert(not commands.ecosystem.func("alice")) registered.fields(player,"hearth:guide",{ecosystem_survey=true}) registered.fields(player,"hearth:ecosystem",{refresh=true}) assert(#screens==count) hearth.sessions.alice=session end) test("botanical guide tolerates absent empty and partial optional catalogue APIs", function() for _, mock in ipairs({false, {}, {list=function() return {} end}}) do hearth.botany=mock or nil hearth.botanical_guide(player,"",math.huge) assert(latest().form=="hearth:botany" and latest().spec:find("catalogue unavailable",1,true)) assert(hearth.sessions.alice.botany_page==1) hearth.guide(player,6) assert(latest().spec:find("catalogue unavailable",1,true)) end end) -- Actual catalogue factory/methods, not an invented list of test species. local botany_path = target:match("^(.*[/\\])") or "" local actual_botany = dofile(botany_path .. "botany.lua")(hearth,{}) test("actual botanical catalogue displays one hundred species in ten ten-row pages and eight uneven groups", function() hearth.botany=actual_botany local records, groups = actual_botany.list(), {} assert(#records==100) for page=1,10 do hearth.botanical_guide(player,"",page) local spec,rows=latest().spec,0 assert(spec:find("Page "..page.." / 10 / 100 matches",1,true)) for _ in spec:gmatch("Habitat:") do rows=rows+1 end assert(rows==10,"each page must contain ten species rows") for i=(page-1)*10+1,page*10 do local r=records[i]; groups[r.group]=(groups[r.group] or 0)+1 assert(spec:find(core.formspec_escape(r.common.." ("..r.latin..")"),1,true),r.id) assert(spec:find("Active seasons:",1,true) and spec:find("Roles / conditional policy or descriptors:",1,true)) end end local sizes={trees=13,shrubs=14,grasses=10,herbs=22,ferns_mosses=11,fungi=10,aquatic=10,lichens=10} local count=0; for g,n in pairs(groups) do assert(n==sizes[g]); count=count+1 end; assert(count==8) hearth.guide(player,6) for _,r in ipairs(records) do assert(latest().spec:find(core.formspec_escape(r.common.." ("..r.latin..")"),1,true),r.id) end end) test("botanical searches use real scientific names niches seasons and honest role labels", function() for _,query in ipairs({"Fagus sylvatica", "beech"}) do hearth.botanical_guide(player,query,1) assert(latest().spec:find("European beech (Fagus sylvatica)",1,true)) end hearth.botanical_guide(player,"fly_agaric",1) for _,text in ipairs({"Amanita muscaria", "toxic flag: YES", "Summer", "Autumn", "hosts:", "no catalogue food harvest"}) do assert(latest().spec:find(text,1,true),text) end hearth.botanical_guide(player,"hornwort",1) for _,text in ipairs({"Ceratophyllum demersum", "submerged", "water depth 1-4", "NOT food permission", "habitat engineering (descriptive tag)"}) do assert(latest().spec:find(core.formspec_escape(text),1,true),text) end hearth.botanical_guide(player,"bracken",1) assert(latest().spec:find("descriptor only",1,true) and latest().spec:find("no killing",1,true)) hearth.botanical_guide(player,"honey_fungus",1) assert(latest().spec:find("no plant damage",1,true)) hearth.botanical_guide(player,"bilberry",1) assert(latest().spec:find("seasonal game berry harvest",1,true)) assert(latest().spec:find("NOT a real-world forage guide",1,true)) end) test("botanical query escaping literal search and finite pagination bounds", function() local query="];button[0,0;1,1;attack;Injected]" hearth.botanical_guide(player,query,0/0) assert(latest().spec:find(core.formspec_escape(query),1,true)) assert(not latest().spec:find(";attack;Injected]",1,true)) hearth.botanical_guide(player,"%[.*",1) assert(latest().spec:find("No species matches that literal search",1,true)) hearth.botanical_guide(player,string.rep("x",70),999) assert(#hearth.sessions.alice.botany_query==48 and hearth.sessions.alice.botany_page==1) hearth.botanical_guide(player,"\n\tlichen",-math.huge) assert(hearth.sessions.alice.botany_query==" lichen" and hearth.sessions.alice.botany_page==1) for _,p in ipairs({0/0,-8,"bad",0}) do hearth.botanical_guide(player,"",p); assert(hearth.sessions.alice.botany_page==1) end hearth.botanical_guide(player,"",math.huge); assert(hearth.sessions.alice.botany_page==10) local record=actual_botany.list()[1]; record.common=query hearth.botany={list=function() return {record} end} hearth.botanical_guide(player,"",1) assert(latest().spec:find(core.formspec_escape(query),1,true) and not latest().spec:find(";attack;Injected]",1,true)) hearth.botany=actual_botany end) test("botanical commands buttons pagination and exits route without world access", function() assert(commands.botany.func("alice","Quercus robur")) assert(latest().form=="hearth:botany" and latest().spec:find("Pedunculate oak",1,true)) assert(not commands.botany.func("offline","")) registered.fields(player,"hearth:guide",{botany_index=true}) assert(latest().form=="hearth:botany") registered.fields(player,"hearth:botany",{next=true,query="ignored"}); assert(hearth.sessions.alice.botany_page==2) registered.fields(player,"hearth:botany",{previous=true}); assert(hearth.sessions.alice.botany_page==1) registered.fields(player,"hearth:botany",{key_enter=true,query="Picea abies"}) assert(latest().spec:find("Norway spruce",1,true)) registered.fields(player,"hearth:botany",{search=true,query="Betula pendula"}) assert(latest().spec:find("Silver birch",1,true) and hearth.sessions.alice.botany_page==1) registered.fields(player,"hearth:botany",{field_notes=true}); assert(latest().form=="hearth:guide" and hearth.sessions.alice.page==6) registered.fields(player,"hearth:ecosystem",{botany_index=true}); assert(latest().form=="hearth:botany") registered.fields(player,"hearth:botany",{ecosystem_survey=true}); assert(latest().form=="hearth:ecosystem") local session,count=hearth.sessions.alice,#screens; hearth.sessions.alice=nil assert(not commands.botany.func("alice","")) hearth.botanical_guide(player,"",1) registered.fields(player,"hearth:botany",{next=true}) registered.fields(player,"hearth:ecosystem",{botany_index=true}) registered.fields(player,"hearth:guide",{botany_index=true}) assert(#screens==count); hearth.sessions.alice=session end) test("botanical navigation fits existing panels and does not add a ninth journal page", function() hearth.guide(player,6) assert(latest().spec:find("botany_index;Botanical guide",1,true) and not latest().spec:find("page_9",1,true)) local guide=latest().spec hearth.ecosystem(player); local survey=latest().spec hearth.botanical_guide(player,"",4); local browser=latest().spec for _,spec in ipairs({guide,survey,browser}) do local right=0 for x,y,w,h in spec:gmatch("button[%w_]*%[([%d.]+),([%d.]+);([%d.]+),([%d.]+);") do x,y,w,h=tonumber(x),tonumber(y),tonumber(w),tonumber(h) assert(x+w<=14 and y+h<=10) if y==8.85 then assert(x>=right,"footer buttons overlap"); right=x+w end end end end) test("survey exposes actual optional botanical habitat fields without inventing counts", function() hearth.succession={habitat=function() return {loaded=true,ground="peat",grass=2,flowers=1,cover=4, near_water=8,water=2,aquatic="submerged",ph=4.5,nursery=3} end} hearth.ecosystem(player) for _,text in ipairs({"Nearby water cells: 8", "Water depth: 2", "Game substrate pH: 4.5", "Nursery-support plants: 3", "Aquatic niche: submerged"}) do assert(latest().spec:find(text,1,true),text) end hearth.succession.habitat=function() return {loaded=false} end hearth.ecosystem(player) for _,text in ipairs({"Game substrate pH: unavailable", "Nursery-support plants: unavailable", "Water depth: unavailable", "Aquatic niche: unavailable"}) do assert(latest().spec:find(text,1,true),text) end end) test("fallen log guidance matches existing axe-only sideways cube behavior", function() hearth.recipes(player,"Fallen logs",1) for _,text in ipairs({"sideways", "full-cube", "axe", "one canonical hearth:log", "hands do not yield sticks"}) do assert(latest().spec:find(text,1,true),text) end end) test("location stays local-only, bounded and escaped", function() local writes, checks=0,0 hearth.set_location=function(lat,lon) assert(lat==45 and lon==0); writes=writes+1 end hearth.model.location=function(lat,lon) checks=checks+1; return lat=="45" and lon=="0" end core.is_singleplayer=function() return false end registered.fields(player,"hearth:location",{save_location=true,latitude="45",longitude="0"}) assert(writes==0 and checks==0 and not commands.location.func("alice")) core.is_singleplayer=function() return true end for _,value in ipairs({string.rep("1",17), "];button[0,0;", 42}) do registered.fields(player,"hearth:location",{save_location=true,latitude=value,longitude="0"}) assert(writes==0 and latest().form=="hearth:location") end hearth.location_form(player,"];button[0,0;1,1;attack;Injected]") assert(latest().spec:find(core.formspec_escape("];button[0,0;1,1;attack;Injected]"),1,true)) assert(not latest().spec:find(";attack;Injected]",1,true)) registered.fields(player,"hearth:location",{save_location=true,latitude="45",longitude="0"}) assert(writes==1 and latest().form=="hearth:guide") end) test("HUD preserves runtime keys, tiles ten hotbar slots and uses readable graphical reserves", function() hearth.make_hud(player) local ids=hearth.sessions.alice.hud for _,key in ipairs({"title","clock","needs","context","help"}) do assert(hud[ids[key]],key) end assert(player.hotbar_count==10 and player.flags.minimap==false) local expected="[combine:320x32" for i=0,9 do expected=expected..":"..(i*32)..",0=hearth_hotbar.png" end assert(player.hotbar_texture==expected and player.hotbar_selected=="hearth_selected.png") hearth.clock={hour=13.5,season="Autumn",date="2026-11-02"} hearth.players={alice={warmth=10,water=35,food=75,stamina=100,wet=30}} hearth.update_hud(player,{temperature=8,shelter=1,rain=true,heat=1,smoke=true}) assert(hud[ids.clock].text:find("Solar 13:30",1,true)) assert(hud[ids.warmth_label].text=="WARMTH 10%" and hud[ids.warmth_label].number==0xf38ba8) assert(hud[ids.water_label].number==0xf9e2af and hud[ids.food_label].number==0x94e2d5) assert(hud[ids.food_fill].text=="[fill:165x6:#94e2d5") assert(hud[ids.context].text:find("SMOKE / open a wall or roof vent",1,true)) assert(hud[ids.context].number==0xf38ba8) hearth.players.alice.food=0; hearth.players.alice.stamina=200 hearth.update_hud(player,{temperature=8,shelter=0,roof=true,heat=0}) assert(hud[ids.food_fill].text=="[fill:1x6:#18182500") assert(hud[ids.stamina_label].text=="STAMINA 100%") end) test("HUD accepts legacy mocks without optional hotbar image setters", function() local image,selected=player.hud_set_hotbar_image,player.hud_set_hotbar_selected_image player.hud_set_hotbar_image,player.hud_set_hotbar_selected_image=nil,nil hearth.make_hud(player) assert(hearth.sessions.alice.hud.needs) player.hud_set_hotbar_image,player.hud_set_hotbar_selected_image=image,selected end) assert(total==41,"The original checks plus three new tree recipe searches must remain intact") test("inspect command uses exact species and rejects absent session unknown and URL input",function() hearth.botany=actual_botany hearth.wiki_articles=dofile(sibling.."wiki_articles.lua") hearth.species_effects=dofile(sibling.."species_effects.lua")(hearth,{}) assert(commands.inspect and not commands.inspect.privs) assert(commands.inspect.func("alice","Quercus robur")) assert(latest().form=="hearth:species" and latest().spec:find("Wikipedia excerpt (offline)",1,true)) assert(not commands.inspect.func("alice","https://en.wikipedia.org/wiki/Oak")) assert(not commands.inspect.func("alice","Quercus")) assert(not commands.inspect.func("offline","oak")) end) test("botanical article selection uses trusted current-page numeric rows only",function() hearth.botanical_guide(player,"",2) assert(latest().spec:find("tooltip[species_choice;Species article;#181825;#cdd6f4]",1,true)) registered.fields(player,"hearth:botany",{species_choice="2",species_article=true,id="oak"}) assert(latest().form=="hearth:species" and hearth.sessions.alice.species_reader.id==actual_botany.list()[12].id) for _,v in ipairs({"oak","0","11","2.5","1e0","999",{},string.rep("1",1000)}) do hearth.botanical_guide(player,"",2); local n=#screens registered.fields(player,"hearth:botany",{species_choice=v,species_article=true}) assert(#screens==n,"forged choice opened article") end hearth.botanical_guide(player,"no-such-species",1); local n=#screens registered.fields(player,"hearth:botany",{species_article=true,id="oak"}); assert(#screens==n) end) test("article navigation retains server species and returns to selected botanical page",function() hearth.botanical_guide(player,"",3) registered.fields(player,"hearth:botany",{species_choice="2",species_article=true}) local id=hearth.sessions.alice.species_reader.id registered.fields(player,"hearth:species",{reader_tab="2",id="pine",url="bad",text="edited"}) assert(hearth.sessions.alice.species_reader.id==id and latest().spec:find("SIMULATED",1,true)) registered.fields(player,"hearth:species",{reader_botany=true}) assert(latest().form=="hearth:botany" and hearth.sessions.alice.botany_page==3) registered.fields(player,"hearth:botany",{species_choice="1",species_article=true}) registered.fields(player,"hearth:species",{quit=true}); local n=#screens registered.fields(player,"hearth:species",{reader_tab="1"}); assert(#screens==n) end) test("fieldnotes inspect target uses loaded eye ray without changing other actions",function() core.get_node_or_nil=function() return {name="hearth:oak_leaves"} end function player:get_properties() return {eye_height=1.6} end function player:get_look_dir() return {x=0,y=0,z=1} end core.raycast=function(start,finish,objects,liquids) assert(start.y==5.6 and finish.z==4 and not objects and not liquids) local used=false return function() if used then return nil end; used=true return {type="node",under={x=10,y=6,z=0},intersection_point={x=10,y=5.6,z=0}} end end hearth.guide(player,6); assert(latest().spec:find("inspect_target;Inspect target",1,true)) registered.fields(player,"hearth:guide",{inspect_target=true}) assert(latest().form=="hearth:species" and hearth.sessions.alice.species_reader.id=="oak") assert(commands.inspect.func("alice","")) core.get_node_or_nil=function() return nil end assert(not commands.inspect.func("alice","")) end) test("dropdown labels escape cached IDs and absent sessions reject article fields",function() local r=actual_botany.list()[1]; r.id="];button[0,0;1,1;attack;Injected]" hearth.botany={list=function() return {r} end} hearth.botanical_guide(player,"",1) assert(latest().spec:find(core.formspec_escape(r.id),1,true) and not latest().spec:find(";attack;Injected]",1,true)) hearth.botany=actual_botany local s,n=hearth.sessions.alice,#screens; hearth.sessions.alice=nil registered.fields(player,"hearth:botany",{species_choice="1",species_article=true}) registered.fields(player,"hearth:species",{reader_tab="2"}) assert(not commands.inspect.func("alice","oak") and #screens==n) hearth.sessions.alice=s end) test("inventory appearance entry opens only the requesting player's creator", function() local called = 0 hearth.character = {show = function(p) assert(p == player); called = called + 1 end} hearth.inventory(player) assert(player.inventory_spec:find("character_creator;Character appearance]",1,true)) assert(player.inventory_spec:find("Cosmetic only",1,true)) registered.fields(player,"",{character_creator=true}); assert(called == 1) registered.fields(player,"hearth:recipes",{character_creator=true}); assert(called == 1) hearth.character = nil registered.fields(player,"",{character_creator=true}); assert(called == 1) end) assert(total==47,"Unexpected recipe/journal/character integration count") test("inventory and journal tooltips contain escaped text, never an invalid third argument",function() local specs={} hearth.inventory(player); specs[#specs+1]=player.inventory_spec local text="Preview woodland clothing and features. Cosmetic only; Save / Apply to keep changes." assert(player.inventory_spec:find("tooltip[character_creator;"..core.formspec_escape(text)..";#181825;#cdd6f4]",1,true)) hearth.guide(player,6); specs[#specs+1]=latest().spec hearth.recipes(player,"",1); specs[#specs+1]=latest().spec hearth.location_form(player); specs[#specs+1]=latest().spec hearth.botanical_guide(player,"",1); specs[#specs+1]=latest().spec hearth.ecosystem(player); specs[#specs+1]=latest().spec local n=0 for _,spec in ipairs(specs) do for body in spec:gmatch("tooltip%[(.-)%]") do local clean=body:gsub("\\.",""); local _,separators=clean:gsub(";","") assert(separators==1 or separators==3,"invalid tooltip arity: "..body); n=n+1 end end assert(n>=10) end) test("survey loops actual counts for all 23 animals without changing the world",function() hearth.lifecycle=dofile(sibling.."lifecycle.lua") hearth.fauna=dofile(sibling.."fauna.lua")(hearth,{}) local counts={}; for i,r in ipairs(hearth.fauna.list()) do counts[r.id]=i end local calls=0 hearth.wildlife={status=function() calls=calls+1; return {counts=counts,total=64,global_cap=64,local_cap=12,player_cap=16} end} hearth.ecosystem(player); assert(calls==1) for id,n in pairs(counts) do local title=({hare="Hares",deer="Deer",fox="Foxes"})[id] or id:gsub("_"," ") assert(latest().spec:find(title..": "..n,1,true),id) end assert(latest().spec:find("Calendar age includes unloaded time",1,true)) end) test("actual fauna guide shows all identities sources and conservative lifecycle status read-only",function() assert(commands.fauna.func("alice","")) local records=hearth.fauna.list() assert(#records==23 and latest().form=="hearth:fauna") for page=1,5 do hearth.fauna_guide(player,"",page) assert(latest().spec:find("Page "..page.." / 5 / 23 matches",1,true)) for i=(page-1)*5+1,math.min(#records,page*5) do local r=records[i] assert(latest().spec:find(core.formspec_escape(r.source_url),1,true),r.id) assert(latest().spec:find(core.formspec_escape(r.latin or "UNKNOWN species identity (legacy deer)"),1,true)) end assert(latest().spec:find("automatic breeding disabled",1,true)) assert(latest().spec:find("no lifespan-endpoint death",1,true)) end hearth.fauna_guide(player,"rabbit",1) assert(latest().spec:find("Maturity: UNKNOWN",1,true)) hearth.fauna_guide(player,"bank_vole",1) assert(latest().spec:find(core.formspec_escape("42-56 real days [source_reported]"),1,true)) assert(not commands.fauna.func("offline","")) end) test("fauna search pagination routing and escaping stay bounded and session-owned",function() local query="];button[0,0;1,1;attack;Injected]" hearth.fauna_guide(player,query,0/0) assert(latest().spec:find(core.formspec_escape(query),1,true) and not latest().spec:find(";attack;Injected]",1,true)) assert(latest().spec:find("No animal matches",1,true)) hearth.fauna_guide(player,string.rep("x",80),1); assert(#hearth.sessions.alice.fauna_query==48) hearth.fauna_guide(player,"",math.huge); assert(hearth.sessions.alice.fauna_page==5) registered.fields(player,"hearth:guide",{fauna_index=true}); assert(hearth.sessions.alice.fauna_page==1) registered.fields(player,"hearth:fauna",{next=true}); assert(hearth.sessions.alice.fauna_page==2) registered.fields(player,"hearth:fauna",{previous=true}); assert(hearth.sessions.alice.fauna_page==1) registered.fields(player,"hearth:fauna",{search=true,query="Lutra lutra"}); assert(latest().spec:find("otter",1,true)) registered.fields(player,"hearth:fauna",{botany_index=true}); assert(latest().form=="hearth:botany") registered.fields(player,"hearth:botany",{fauna_index=true}); assert(latest().form=="hearth:fauna") registered.fields(player,"hearth:fauna",{field_notes=true}); assert(latest().form=="hearth:guide") registered.fields(player,"hearth:ecosystem",{fauna_index=true}); assert(latest().form=="hearth:fauna") local original=hearth.fauna local r=original.list()[1]; r.common=query; r.source_url=query hearth.fauna={list=function() return {r} end}; hearth.fauna_guide(player,"",1) assert(latest().spec:find(core.formspec_escape(query),1,true) and not latest().spec:find(";attack;Injected]",1,true)) hearth.fauna=nil; hearth.fauna_guide(player,"",1); assert(latest().spec:find("catalogue unavailable",1,true)) hearth.fauna=original local session,n=hearth.sessions.alice,#screens; hearth.sessions.alice=nil assert(not commands.fauna.func("alice","")); hearth.fauna_guide(player,"",1) registered.fields(player,"hearth:fauna",{next=true}); assert(#screens==n) hearth.sessions.alice=session end) test("fauna species selection and live inspection show bounded read-only species details",function() local records=hearth.fauna.list(); local before=hearth.wildlife.status() hearth.fauna_guide(player,"",1) assert(latest().spec:find("species_choice;hare,deer,fox,rabbit,red_squirrel",1,true)) registered.fields(player,"hearth:fauna",{species_choice="3",species_details=true}) assert(latest().form=="hearth:fauna_detail" and latest().spec:find("Vulpes vulpes",1,true)) assert(latest().spec:find("Modeled prey:",1,true) and latest().spec:find("Winter rest policy:",1,true)) registered.fields(player,"hearth:fauna_detail",{fauna_index=true}) assert(latest().form=="hearth:fauna" and hearth.sessions.alice.fauna_page==1) local now=os.time() assert(hearth.inspect_fauna(player,"hare",{born_at=now-500*86400,origin="born",foodreserve=.42, resident=true,observation="rest"})) local spec=latest().spec for _,text in ipairs({"Lepus europaeus","Lifecycle: mature","model years","real calendar days", "Coarse modeled food reserve (policy, not biology): 42%","Current animation observation: resting", "persistent registered resident","Modeled diet: browse","Activity / sleep policy: night / solar_night", "Winter rest policy:","Modeled behavior:","Modeled prey:","Reproduction: enabled only within the coarse model", "Active loaded animals of this species:"}) do assert(spec:find(core.formspec_escape(text),1,true),text) end local after=hearth.wildlife.status() assert(after.total==before.total and after.counts.hare==before.counts.hare,"panel reads status without game changes") local original_get=hearth.fauna.get; local injection="];button[0,0;1,1;attack;Injected]" hearth.fauna.get=function(id) local r=original_get(id) if id=="hare" then r.common=injection; r.latin=injection; r.source_url=injection end return r end hearth.inspect_fauna(player,"hare",nil) assert(latest().spec:find(core.formspec_escape(injection),1,true) and not latest().spec:find(";attack;Injected]",1,true)) hearth.fauna.get=original_get local session,count=hearth.sessions.alice,#screens; hearth.sessions.alice=nil assert(not hearth.inspect_fauna(player,"hare",nil) and #screens==count) hearth.sessions.alice=session local impostor={get_player_name=function() return "alice" end} assert(not hearth.inspect_fauna(impostor,"hare",nil) and #screens==count) assert(not hearth.inspect_fauna(player,"hare",setmetatable({},{})) and #screens==count) assert(not hearth.inspect_fauna(player,"unknown",nil) and #screens==count) assert(#records==23) end) test("wildlife encounters add canonical per-player field-journal entries; catalogue browsing does not",function() local key="hearth:fauna_observations_v1" player_metadata[key]="" hearth.fauna_guide(player,"",1) assert(latest().spec:find("0 / 23 species recorded",1,true)) hearth.inspect_fauna(player,"hare",{born_at=os.time()-86400,origin="established",foodreserve=.5, resident=true,observation="awake"}) assert(player_metadata[key]=="hare") assert(latest().spec:find("Field journal: first recorded encounter",1,true)) hearth.inspect_fauna(player,"fox",{born_at=os.time()-86400,origin="established",foodreserve=.5, resident=true,observation="awake"}) assert(player_metadata[key]=="fox,hare","journal storage is canonical and sorted") hearth.inspect_fauna(player,"hare",{born_at=os.time()-86400,origin="established",foodreserve=.5, resident=true,observation="awake"}) assert(latest().spec:find("already recorded",1,true)) hearth.inspect_fauna(player,"deer",nil) assert(player_metadata[key]=="fox,hare","catalogue profile selection is not a sighting") hearth.fauna_guide(player,"",1) assert(latest().spec:find("2 / 23 species recorded",1,true)) assert(latest().spec:find("recorded encounter",1,true)) player_metadata[key]="hare,unknown" assert(hearth.inspect_fauna(player,"deer",{born_at=os.time()-86400,origin="established",foodreserve=.5, resident=true,observation="awake"})) assert(player_metadata[key]=="hare,unknown","malformed journal data fails closed without overwriting") player_metadata[key]=nil end) test("calendar guidance never promises accelerated saplings daily berries or complete biology",function() hearth.recipes(player,"Renewal",1) for _,s in ipairs({"365.2425 days","no 24-hour refill","at most one berry","one real year","UNKNOWN"}) do assert(latest().spec:find(s,1,true),s) end hearth.guide(player,6) for _,s in ipairs({"100 named species","23 animal identities","eight uneven","real years to decades","fungi do not accelerate growth","disable breeding"}) do assert(latest().spec:find(s,1,true),s) end assert(not latest().spec:find("eight categories of ten",1,true)) end) assert(total==54,"Unexpected focused integration count") print("PASS "..total.." recipe discovery tests")