-- Standalone Lua 5.1+: controlled names, actual-epoch climate and lossless swaps. local target = assert(arg[1], "seasons.lua path required") local real_time, now, passed = os.time, 1782043200, 0 os.time = function() return now end local season, world, data, missing, protected, swaps, meta_writes, lbm local p = {x=0,y=1,z=0} local hearth, api, seasons local function key(q) return q.x..":"..q.y..":"..q.z end local function reset() season, world, data, missing, protected, swaps, meta_writes = "Summer", {}, {}, false, false, 0, 0 api = {registered_nodes={}} api.get_node_or_nil = function(q) return not missing and world[key(q)] or nil end api.swap_node = function(q,n) swaps=swaps+1; world[key(q)]=n end api.get_meta = function() error("appearance must not access metadata") end api.register_node = function(name, def) assert(not api.registered_nodes[name]); api.registered_nodes[name]=def end api.register_lbm = function(def) lbm=def end hearth = {climate_at=function(epoch) assert(epoch==now); return {season=season} end, external_protected=function() return protected end, claim_owner_at=function() return "builder" end} seasons = dofile(target)(hearth,api) hearth.seasons = seasons for _, name in ipairs(seasons.names()) do local kind=seasons.kind(name) local base="hearth:"..kind api.registered_nodes[base] = api.registered_nodes[base] or { drawtype=(kind=="young_tree" or kind=="mature_tree" or kind=="tree_seedling") and "mesh" or "plantlike", groups={flora=1}, drop="hearth:sticks", tiles={"base.png"}, can_dig=function() return false end, on_rightclick=function() end, after_place_node=function() end, collision_box={type="fixed"}, } end seasons.register() end local function put(name) world[key(p)]={name="hearth:"..name,param1=7,param2=3} end local function name() return world[key(p)].name end local function test(label,fn) reset() local ok,err=pcall(fn); assert(ok,label..": "..tostring(err)) passed=passed+1; print("ok "..passed.." - "..label) end test("all seasons use explicit variants and summer restores the canonical tree", function() put("mature_tree") for _, s in ipairs({"Spring","Autumn","Winter","Summer"}) do season=s; assert(seasons.reconcile(p)) assert(name()=="hearth:mature_tree"..(s=="Summer" and "" or "_"..s:lower())) assert(world[key(p)].param1==7 and world[key(p)].param2==3) assert(not seasons.reconcile(p), "same-season appearance is idempotent") end assert(swaps==4) end) test("winter bare mesh returns to the base mesh in spring", function() for _, kind in ipairs({"young_tree","mature_tree"}) do local winter=api.registered_nodes["hearth:"..kind.."_winter"] local spring=api.registered_nodes["hearth:"..kind.."_spring"] assert(winter.mesh=="hearth_"..kind.."_winter.obj") assert(spring.mesh=="hearth_"..kind..".obj") assert(winter.tiles[1]=="hearth_tree_atlas_winter.png") end end) test("legacy mapgen leaves recolor without changing timber or destroying canopy", function() put("leaves"); season="Autumn"; lbm.action(p) assert(name()=="hearth:leaves_autumn") season="Winter"; lbm.action(p); assert(name()=="hearth:leaves_winter") put("log"); assert(not seasons.reconcile(p) and name()=="hearth:log") assert(not seasons.kind("hearth:foreign_tree_winter"):match("^mature_tree$")) end) test("perennials and flowers rest in the correct seasons without adding needless variants", function() for _, kind in ipairs({"meadow_grass","fern","wildflowers"}) do put(kind); season="Winter"; assert(seasons.reconcile(p)) assert(api.registered_nodes[name()].tiles[1]=="hearth_"..kind.."_dormant.png") season="Spring"; assert(seasons.reconcile(p) and name()=="hearth:"..kind) end put("wildflowers"); season="Autumn"; assert(seasons.reconcile(p)) assert(name()=="hearth:wildflowers_autumn" and not seasons.available("wildflowers")) assert(not api.registered_nodes["hearth:fern_spring"]) assert(seasons.available("litter") and seasons.available("fungi")) end) test("food patches only bloom in their season and stay occupied when dormant", function() for _, s in ipairs({"Spring","Summer","Autumn","Winter"}) do season=s assert(seasons.available("spring_greens_patch")== (s=="Spring")) assert(seasons.available("autumn_mushrooms_patch")== (s=="Autumn")) assert(seasons.name_for("spring_greens_patch")=="hearth:spring_greens_patch"..(s=="Spring" and "_spring" or "")) end end) test("callbacks, authorization, drops and geometry survive every variant", function() for _, name in ipairs(seasons.names()) do local kind=seasons.kind(name) local base,def=api.registered_nodes["hearth:"..kind],api.registered_nodes[name] assert(def.can_dig==base.can_dig and def.on_rightclick==base.on_rightclick) assert(def.after_place_node==base.after_place_node and def.collision_box==base.collision_box) assert(def.drop==base.drop and def.groups.flora==1) end end) test("claimed handplaced flora can recolor with no metadata access or harvest refill", function() data={succ_age="8412",succ_natural="1",succ_birth_at="1700000000",succ_lifecycle_version="1", succ_seed_epoch=tostring(now+86400),succ_harvest_ready_at=tostring(now+86400),player_placed="1",owner="builder"} local reference=data put("spring_greens_patch"); season="Spring"; assert(seasons.reconcile(p)) season="Winter"; assert(seasons.reconcile(p)) assert(data==reference and data.succ_age=="8412" and data.player_placed=="1") assert(data.succ_birth_at=="1700000000" and data.succ_lifecycle_version=="1" and data.succ_seed_epoch==tostring(now+86400)) assert(data.succ_harvest_ready_at==tostring(now+86400) and meta_writes==0) end) test("unloaded, ignore, protected and uncontrolled names cause no writes", function() season="Winter"; put("leaves"); missing=true assert(not seasons.reconcile(p)); missing=false; protected=true assert(not seasons.reconcile(p)); protected=false world[key(p)]={name="ignore"}; assert(not seasons.reconcile(p)) put("foreign_tree"); assert(not seasons.reconcile(p)) assert(swaps==0) end) test("hemisphere seasons come from the existing solar model, not the host month", function() local model=dofile(target:gsub("[^/\\]+$","model.lua")) local south=model.solar(now,-45,0) local north=model.solar(now,45,0) assert(north.season=="Summer" and south.season=="Winter") hearth.climate_at=function(epoch) assert(epoch==now); return model.solar(epoch,-45,0) end assert(seasons.season()=="Winter") put("young_tree"); assert(seasons.reconcile(p)) assert(name()=="hearth:young_tree_winter") now=1797854400 -- December, southern summer. assert(seasons.reconcile(p) and name()=="hearth:young_tree") end) test("extension is available before registration and missing optional bases are safe", function() seasons.add_variants("optional_plant",{Winter={texture="optional_dormant.png"}}) seasons.register() assert(not api.registered_nodes["hearth:optional_plant_winter"]) api.registered_nodes["hearth:optional_plant"]={groups={flora=1},drop="",tiles={"optional.png"}} seasons.register() assert(api.registered_nodes["hearth:optional_plant_winter"].tiles[1]=="optional_dormant.png") assert(seasons.kind("hearth:optional_plant_winter")=="optional_plant") end) test("full block stage variants retain bark faces and never acquire sculpted meshes", function() local stage={drawtype="normal",tiles={"end.png","end.png","bark.png"},paramtype2="facedir",groups={tree=1},drop="hearth:log"} for _,kind in ipairs({"young_tree","mature_tree","oak_young","oak_mature"}) do api.registered_nodes["hearth:"..kind]=stage seasons.add_variants(kind,{Winter={texture="canopy_atlas.png",mesh="obsolete_tree.obj"}}) api.registered_nodes["hearth:"..kind.."_winter"]=nil end seasons.register() for _,kind in ipairs({"young_tree","mature_tree","oak_young","oak_mature"}) do local d=api.registered_nodes["hearth:"..kind.."_winter"] assert(d.drawtype=="normal" and d.mesh==nil and d.tiles==stage.tiles) assert(d.tiles[3]=="bark.png" and d.paramtype2=="facedir" and d.drop=="hearth:log") end end) test("all twelve original calendar months retain hemisphere-aware seasons", function() local model=dofile(target:gsub("[^/\\]+$","model.lua")) local north={"Winter","Winter","Spring","Spring","Spring","Summer","Summer","Summer","Autumn","Autumn","Autumn","Winter"} local opposite={Winter="Summer",Spring="Autumn",Summer="Winter",Autumn="Spring"} for month=1,12 do now=real_time({year=2026,month=month,day=15,hour=12,min=0,sec=0}) for _,latitude in ipairs({45,-45}) do hearth.climate_at=function(epoch) assert(epoch==now); return model.solar(epoch,latitude,0) end local expected=latitude>0 and north[month] or opposite[north[month]] assert(seasons.season()==expected) assert(seasons.available("wildflowers")== (expected=="Spring" or expected=="Summer")) end end end) local function install_botany() local botany=dofile(target:gsub("[^/\\]+$","botany.lua"))(hearth,api) botany.register_nodes(function(id,def) api.register_node("hearth:"..id,def) end) seasons.register() return botany end test("actual botany factory installs all new policies before seasonal registration", function() local b=install_botany(); local count=0 for _,r in ipairs(b.list()) do if not r.existing and r.group~="trees" then count=count+1 for _,s in ipairs({"Spring","Summer","Autumn","Winter"}) do season=s; local active=b.active(r.id,s) local expected=r.node..(active and "" or "_"..s:lower()) assert(seasons.name_for(r.id,now)==expected,r.id..s) assert(seasons.available(r.id,now)==active,r.id..s) local def=assert(api.registered_nodes[expected]) assert(def.description==r.common.." ("..r.latin..")") assert(def.tiles[1]=="hearth_botany_"..r.id..(active and "" or "_dormant")..".png") assert(seasons.kind(expected)==r.id and b.kind(expected)==r.id) end end end assert(count==79) end) test("botany leaves diversity-owned taxa and block tree policies untouched", function() local diversity=dofile(target:gsub("[^/\\]+$","diversity.lua"))(hearth,api) -- Production shares exactly this seasonal service with both factories. assert(diversity.plants and #diversity.plants==8) local before={} for _,id in ipairs(diversity.plants) do before[id]={} for _,s in ipairs({"Spring","Summer","Autumn","Winter"}) do season=s; before[id][s]={seasons.name_for(id,now),seasons.available(id,now)} end end install_botany() for id,states in pairs(before) do for s,state in pairs(states) do season=s; assert(seasons.name_for(id,now)==state[1] and seasons.available(id,now)==state[2]) end end for _,id in ipairs({"holly","juniper","harts_tongue","polypody","turkey_tail","oakmoss"}) do season="Winter"; assert(seasons.available(id,now) and seasons.name_for(id,now)=="hearth:"..id) assert(not api.registered_nodes["hearth:"..id.."_winter"]) end end) test("dormant aquatic variants retain geometry callbacks and separate real water", function() local b=install_botany() for _,r in ipairs(b.list("aquatic")) do local base=api.registered_nodes[r.node] world[key(p)]={name=r.node,param1=7,param2=3} world["0:0:0"]={name="hearth:water",param2=0} local water=world["0:0:0"] season="Winter"; assert(seasons.reconcile(p)) local def=api.registered_nodes[name()] assert(def.tiles[1]=="hearth_botany_"..r.id.."_dormant.png") assert(def.inventory_image==def.tiles[1] and def.wield_image==def.tiles[1]) assert(def.drawtype==base.drawtype and def.node_box==base.node_box and def.selection_box==base.selection_box) assert(def.on_place==base.on_place and def.after_place_node==base.after_place_node) assert(def.drop==base.drop and not def.walkable and not def.buildable_to) assert(not def.liquidtype and not def.groups.water) assert(def.groups.hearth_submerged==base.groups.hearth_submerged) if def.on_place then local stack={}; assert(def.on_place(stack,nil,{})==stack) end assert(world["0:0:0"]==water and water.name=="hearth:water") assert(world[key(p)].param1==7 and world[key(p)].param2==3) season="Summer"; assert(seasons.reconcile(p) and name()==r.node) end end) test("berry seasonal swaps preserve cooldown provenance and harvest authorization", function() install_botany() local base=api.registered_nodes["hearth:bilberry"] data={botany_ready_at=tostring(now+86400),player_placed="1",owner="builder"} local reference=data put("bilberry"); season="Winter"; assert(seasons.reconcile(p)) local def=api.registered_nodes[name()] assert(def.drop=="hearth:sticks" and def.on_rightclick==base.on_rightclick) assert(def.after_place_node==base.after_place_node and def.groups.not_in_creative_inventory==1) assert(not seasons.available(name(),now)) season="Summer"; assert(seasons.reconcile(p) and name()=="hearth:bilberry") assert(data==reference and data.botany_ready_at==tostring(now+86400) and data.owner=="builder") assert(data.player_placed=="1" and meta_writes==0) end) os.time=real_time print("LAST_HEARTH_SEASONS_PASS "..passed)