-- Standalone Lua 5.1+; mocked APIs only, never starts Luanti. local target=assert(arg[1],"botany.lua path required") local factory=dofile(target) local passed=0 local B,H,A,world,fields,defs,time,season,allowed,room,leftover,added,auth,writes local policies,seasonal_ids,clock_reads,climate_calls local p={x=2,y=1,z=3} local function key(q) return q.x..":"..q.y..":"..q.z end local function reset() world,fields,defs={},{},{} policies,seasonal_ids,clock_reads,climate_calls={},{},0,0 time,season,allowed,room,leftover,added,auth,writes=100000,"Summer",true,true,0,0,0,0 A={registered_nodes=defs,now=function() clock_reads=clock_reads+1; return time end} function A.get_node_or_nil(q) return world[key(q)] end function A.get_meta(q) fields[key(q)]=fields[key(q)] or {}; local f=fields[key(q)] return {get_string=function(_,k) return f[k] or "" end, set_string=function(_,k,v) writes=writes+1; f[k]=v end} end -- Missing construction at factory time is deliberate: content wires it later. H={climate_at=function(epoch) assert(type(epoch)=="number" and epoch==epoch and epoch==math.floor(time)) climate_calls=climate_calls+1; return {season=season} end, seasons={ add_variants=function(id,definitions,active) assert(not policies[id],id); policies[id]={definitions=definitions,active=active} for s in pairs(definitions) do seasonal_ids["hearth:"..id.."_"..s:lower()]=id end end, kind=function(name) return seasonal_ids[name] or name:match("^hearth:(.+)$") end, }} B=factory(H,A) H.construction={allowed=function(q,player,action) auth=auth+1; assert(q==p and player:is_player() and action=="use"); return allowed end} end local inv={room_for_item=function(_,list,item) assert(list=="main" and (item=="hearth:berries 2" or item=="hearth:berries 3")); return room end, add_item=function(_,list,item) assert(list=="main" and (item=="hearth:berries 2" or item=="hearth:berries 3")); added=added+tonumber(item:match("(%d+)$"))-leftover return {get_count=function() return leftover end} end} local player={is_player=function() return true end,get_inventory=function() return inv end} local function test(label,fn) reset(); local ok,err=pcall(fn); assert(ok,label..": "..tostring(err)) passed=passed+1; print("PASS botany "..passed.." - "..label) end local function habitat(id) local r=assert(B.get(id)); local n=r.niche local ground=next(n.substrates) if n.aquatic=="surface" or n.aquatic=="submerged" then ground="alluvium" end return {loaded=true,ground=ground,substrate=id=="hornwort" and "water" or nil, light=(n.light[1]+n.light[2])/2,moisture=(n.moisture[1]+n.moisture[2])/2, ph=(n.ph[1]+n.ph[2])/2,water=n.water[1],aquatic=n.aquatic, freshwater=true,nutrients=2,host=n.hosts and next(n.hosts) or nil} end local function register() B.register_nodes(function(id,def) assert(not defs["hearth:"..id],id); defs["hearth:"..id]=def end) end local function bush(id) world[key(p)]={name="hearth:"..(id or "bilberry")} end test("exactly one hundred disjoint real taxa in eight uneven groups",function() assert(#B.groups==8 and #B.catalogue==100 and #B.list()==100) assert(table.concat(B.groups,",")=="trees,shrubs,grasses,herbs,ferns_mosses,fungi,aquatic,lichens") local ids,latin={},{},{} for _,g in ipairs(B.groups) do assert(#B.list(g)==({trees=13,shrubs=14,grasses=10,herbs=22,ferns_mosses=11,fungi=10,aquatic=10,lichens=10})[g],g) for _,r in ipairs(B.list(g)) do assert(not ids[r.id] and not latin[r.latin]); ids[r.id]=true; latin[r.latin]=true assert(r.common~="" and r.latin:match("^[A-Z][a-z]+ [a-z%-]+$"),r.latin) assert(r.group==g and B.group_of(r.node)==g and B.kind(r.node)==r.id) end end end) test("twenty expansion identities match the lifecycle contract with nitrogen and flower seasons",function() local L=dofile(target:gsub("[^/\\]+$","lifecycle.lua")) local ids={"dandelion","daisy","red_clover","birdsfoot_trefoil","ribwort_plantain","selfheal","knapweed", "oxeye_daisy","field_scabious","teasel","cow_parsley","garlic_mustard","honeysuckle","ivy","gorse","broom", "crab_apple","wild_cherry","field_maple","holly_fern"} assert(#ids==20) for _,id in ipairs(ids) do local r=B.get(id); local p=L.plant(id) assert(r and p and r.latin==p.latin and r.group==p.group and not r.food,id) end for _,id in ipairs({"red_clover","birdsfoot_trefoil","gorse","broom"}) do assert(B.nitrogen(id,0)==1) end assert(B.pollinator("ivy","Autumn") and not B.pollinator("ivy","Spring")) assert(B.pollinator("gorse","Winter") and not B.pollinator("ribwort_plantain","Summer")) assert(B.seeding("garlic_mustard","Summer") and not B.seeding("garlic_mustard","Spring")) end) test("legacy IDs map exactly without counting generic bushes or patches",function() local expected={oak="Quercus robur",birch="Betula pendula",pine="Pinus sylvestris",willow="Salix alba", alder="Alnus glutinosa",clover="Trifolium repens",nettle="Urtica dioica",heather="Calluna vulgaris", bluebell="Hyacinthoides non-scripta",sedge="Carex paniculata",bulrush="Typha latifolia", moss="Hypnum cupressiforme",lichen="Rhizocarpon geographicum"} for id,latin in pairs(expected) do local r=B.get(id); assert(r.existing and r.latin==latin) end for _,id in ipairs({"berry_bush","fern","meadow_grass","wildflowers","tree_seedling"}) do assert(not B.get(id)) end assert(not B.get("foreign:oak") and not B.kind(false)) end) test("tree stages and resources resolve through seasons without registering them",function() H.seasons={kind=function(value) return value:gsub("_winter$",""):gsub("^hearth:","") end} for _,r in ipairs(B.list("trees")) do for _,part in ipairs({"seedling","young","mature","leaves","log","planks"}) do assert(B.kind("hearth:"..r.id.."_"..part.."_winter")==r.id) end end register() for _,r in ipairs(B.list("trees")) do assert(not defs[r.node]) end end) test("catalogue snapshots and getters cannot alter internal policy",function() B.catalogue[1].latin="broken"; B.get("oak").niche.light[1]=100 local list=B.list(); list[1].tags.pollinator=true assert(B.get("oak").latin=="Quercus robur" and not B.has_tag("oak","pollinator")) assert(B.get("oak").niche.light[1]==4) end) test("all one hundred species have a passing season niche vector",function() for _,r in ipairs(B.list()) do assert(B.suitable(r.id,habitat(r.id),next(r.seasons)),r.id) end end) test("each niche dimension rejects out-of-window values for all species",function() for _,r in ipairs(B.list()) do local s=next(r.seasons) for _,field in ipairs({"light","moisture","ph"}) do local h=habitat(r.id); h[field]=r.niche[field][1]-.01 assert(not B.suitable(r.id,h,s),r.id..field) h[field]=r.niche[field][2]+.01; assert(not B.suitable(r.id,h,s)) end local h=habitat(r.id); h.ground="lava"; h.substrate="lava"; assert(not B.suitable(r.id,h,s)) end end) test("unloaded, malformed and nonfinite habitats fail closed",function() for _,field in ipairs({"light","moisture","ph","water"}) do for _,bad in ipairs({0/0,math.huge,-math.huge,"5"}) do local h=habitat("clover"); h[field]=bad; assert(not B.suitable("clover",h,"Summer")) end end local h=habitat("clover"); h.loaded=false; assert(not B.suitable("clover",h,"Summer")) h.loaded=true; h.ph=nil; assert(not B.suitable("clover",h,"Summer")) assert(not B.suitable("clover",nil,"Summer") and not B.suitable("unknown",{},"Summer")) end) test("spring ephemerals and fungal fruit seasons are not year-round",function() for _,id in ipairs({"bluebell","wild_garlic","cowslip","wood_anemone"}) do assert(B.suitable(id,habitat(id),"Spring")); assert(not B.suitable(id,habitat(id),"Summer")) end assert(B.active("wood_blewit","Winter") and not B.active("fly_agaric","Winter")) assert(B.active("moss","Winter") and not B.active("oak","Winter")) end) test("host-specific root fungi reject unrelated or absent tree roots",function() local h=habitat("birch_bolete"); assert(B.suitable("birch_bolete",h,"Autumn")) h.host="oak"; assert(not B.suitable("birch_bolete",h,"Autumn")) h.host=nil; assert(not B.suitable("birch_bolete",h,"Autumn")) assert(B.root_fungi("birch_bolete","birch_seedling",2)==1) assert(B.root_fungi("birch_bolete","oak",2)==0) end) test("deadwood decomposers require deadwood rather than live bark",function() local h=habitat("birch_polypore"); assert(B.suitable("birch_polypore",h,"Autumn")) h.ground="bark"; assert(not B.suitable("birch_polypore",h,"Autumn")) h.ground="deadwood"; h.host="pine"; assert(not B.suitable("birch_polypore",h,"Autumn")) assert(B.has_tag("honey_fungus","parasite") and not B.interactions("honey_fungus").parasite.enabled) end) test("lichens distinguish acid stone, bark and humid old-woodland niches",function() local h=habitat("lichen"); assert(B.suitable("lichen",h,"Winter")) h.ground="limestone"; assert(not B.suitable("lichen",h,"Winter")) h=habitat("oakmoss"); h.ground="soil"; assert(not B.suitable("oakmoss",h,"Winter")) h=habitat("lungwort_lichen"); h.moisture=.2; assert(not B.suitable("lungwort_lichen",h,"Winter")) end) test("surface plants need freshwater, depth and appropriate bottom",function() for _,id in ipairs({"duckweed","yellow_waterlily","white_waterlily","water_crowfoot","pondweed"}) do local h=habitat(id); assert(B.suitable(id,h,"Summer")) h.freshwater=false; assert(not B.suitable(id,h,"Summer")); h.freshwater=true h.water=0; assert(not B.suitable(id,h,"Summer")); h.water=1 h.aquatic="submerged"; assert(not B.suitable(id,h,"Summer")) end end) test("submerged rootless hornwort and rooted starwort have separate supports",function() local h=habitat("hornwort"); assert(B.suitable("hornwort",h,"Summer")) h.substrate=nil; assert(not B.suitable("hornwort",h,"Summer")) h=habitat("water_starwort"); assert(B.suitable("water_starwort",h,"Summer")) h.ground="stone"; assert(not B.suitable("water_starwort",h,"Summer")) end) test("wet banks are dry destination cells, not overwritten water sources",function() for _,id in ipairs({"sedge","bulrush","water_mint","brooklime","water_forgetmenot"}) do local h=habitat(id); assert(h.water==0 and B.suitable(id,h,"Summer")) h.aquatic="land"; assert(not B.suitable(id,h,"Summer")); h.aquatic="bank" h.water=1; assert(not B.suitable(id,h,"Summer")) end end) test("deterministic choose returns only a suitable candidate and never mutates",function() local h=habitat("clover"); local a,r=B.choose(p,h,"Summer") assert(a and a==B.choose(p,h,"Summer") and B.suitable(a,h,"Summer")) assert(r.node=="hearth:"..a and B.group_of(a)~="trees" and writes==0 and next(world)==nil) h.loaded=false; assert(not B.choose(p,h,"Summer")) assert(not B.choose({x=0/0,y=1,z=2},h,"Summer") and not B.choose(nil,h,"Summer")) end) test("choose respects surface and submerged niches without map operations",function() for _,id in ipairs({"duckweed","hornwort"}) do local h=habitat(id); local k=B.choose(p,h,"Summer") assert(k and B.group_of(k)=="aquatic" and B.suitable(k,h,"Summer")) end assert(writes==0) end) test("pollinator support follows flowers, not wind-pollinated grasses or fungi",function() assert(B.pollinator("bluebell","Spring") and not B.pollinator("bluebell","Summer")) assert(B.pollinator("heather","Autumn") and not B.pollinator("heather","Spring")) for _,id in ipairs({"sedge","oak","birch","nettle","fly_agaric","bracken","moss"}) do assert(not B.pollinator(id,"Summer")) end end) test("only three fictional game berry outputs are food, never toxic taxa or fungi",function() local count=0 for _,r in ipairs(B.list()) do if r.food then count=count+1; assert(({bilberry=true,blackberry=true,raspberry=true})[r.id] and not r.toxic) end if r.toxic or r.group=="fungi" then assert(not r.food and not B.edible(r.id,"Summer")) end end assert(count==3 and B.edible("blackberry","Autumn") and not B.edible("raspberry","Winter")) assert(not B.edible("clover","Summer") and B.browse("clover","Summer")) end) test("interaction descriptors cover every actual tag without mutation",function() local seen={} for _,r in ipairs(B.list()) do local d=B.interactions(r.id) for tag in pairs(r.tags) do assert(d[tag] and #d[tag].description>20); seen[tag]=true end end for _,tag in ipairs({"nitrogenfixer","browse","pollinator","ectomycorrhiza","saprotroph","parasite","engineer","nursery","allelopathy","resin"}) do assert(seen[tag],tag) end local d=B.interactions("bracken"); d.allelopathy.enabled=true assert(B.interactions("bracken").allelopathy.enabled==false and writes==0) end) test("nitrogen is capped and fungi transfer only an existing positive budget",function() assert(B.nitrogen("alder",0)==1 and B.nitrogen("clover",1.5)==.5 and B.nitrogen("clover",2)==0) assert(B.nitrogen("oak",0)==0 and B.nitrogen("alder",0/0)==0) assert(B.root_fungi("fly_agaric","pine",.25)==.25 and B.root_fungi("fly_agaric","pine",0)==0) assert(B.root_fungi("turkey_tail","oak",2)==0 and B.root_fungi("fly_agaric","pine",math.huge)==0) assert(B.resin("spruce") and B.resin("pine_log") and not B.resin("oak")) assert(writes==0) end) test("register exactly seventy-nine missing understory nodes, never legacy or trees",function() register(); local count=0 for name,def in pairs(defs) do count=count+1; local r=B.get(name) assert(r and not r.existing and r.group~="trees") assert(def.tiles[1]=="hearth_botany_"..r.id..".png" and not def.mesh) assert(def.description==r.common.." ("..r.latin..")") assert(not def.walkable and not def.buildable_to and def.groups.hearth_ecosystem==1) end assert(count==79); register(); local again=0; for _ in pairs(defs) do again=again+1 end; assert(again==count) end) test("existing registered definitions are preserved and construction stays late-bound",function() local marker={description="external owner"}; defs["hearth:bilberry"]=marker register(); assert(defs["hearth:bilberry"]==marker) H.construction=nil; bush(); assert(not B.harvest(p,player) and added==0) end) test("normal plants dig as nodes; shrubs drop sticks preventing cooldown-reset replant",function() register() assert(defs["hearth:yarrow"].drop=="hearth:yarrow") for _,r in ipairs(B.list("shrubs")) do assert(defs[r.node].drop=="hearth:sticks") end defs["hearth:bilberry"].after_place_node(p) assert(fields[key(p)].player_placed=="1") end) test("aquatic sprites and flat pads remain inside one block and are never liquids",function() register() for _,r in ipairs(B.list("aquatic")) do local d=defs[r.node]; assert(not d.groups.water and not d.liquidtype) if r.niche.aquatic=="surface" then assert(d.drawtype=="nodebox") for _,v in ipairs(d.node_box.fixed) do assert(v>=-.5 and v<=.5) end else assert(d.drawtype=="plantlike") end if r.niche.aquatic=="surface" or r.niche.aquatic=="submerged" then local stack={}; assert(d.on_place(stack,player,{})==stack and writes==0) end end end) test("harvest checks permission before metadata or inventory and returns at most two",function() bush(); allowed=false; local ok,reason=B.harvest(p,player) assert(not ok and reason=="denied" and auth==1 and writes==0 and added==0) allowed=true; ok,reason=B.harvest(p,player) assert(ok and reason==2 and added==2 and tonumber(fields[key(p)].botany_ready_at)==time+365.2425*86400) end) test("cooldown has no catchup and survives a new module instance",function() bush(); assert(B.harvest(p,player)); assert(not B.harvest(p,player) and added==2) policies={} -- a fresh factory may reinstall the same seasonal policy B=factory(H,A); assert(not B.harvest(p,player)); time=time+86400*30 assert(not B.harvest(p,player) and added==2) time=time+86400*400 assert(B.harvest(p,player) and added==4); assert(not B.harvest(p,player) and added==4) end) test("full inventory does not consume stock or start cooldown",function() bush(); room=false; local ok,reason=B.harvest(p,player) assert(not ok and reason=="inventory" and fields[key(p)].botany_ready_at=="" and added==0) room=true; leftover=2; assert(not B.harvest(p,player) and fields[key(p)].botany_ready_at=="") leftover=1; ok,reason=B.harvest(p,player); assert(ok and reason==1 and added==1) assert(not B.harvest(p,player)) end) test("harvest rejects wrong node, season, unloaded and nonplayer callers",function() for _,id in ipairs({"elder","holly","foxglove","fly_agaric","berry_bush","bilberry_fake"}) do bush(id); assert(not B.harvest(p,player) and added==0) end bush(); season="Winter"; assert(not B.harvest(p,player)) season="Summer"; world[key(p)]=nil; assert(not B.harvest(p,player)) assert(not B.harvest(p,nil) and not B.harvest(p,{is_player=function() return false end})) assert(writes==0) end) test("malformed clocks and cooldown metadata fail closed",function() bush() for _,t in ipairs({0/0,math.huge,-math.huge,-1,"clock",9007199254740991}) do time=t; local ok,reason=B.harvest(p,player); assert(not ok and reason=="clock") end time=nil; local ok,reason=B.harvest(p,player); assert(not ok and reason=="clock") assert(climate_calls==0 and writes==0 and next(fields)==nil) time=100000; A.get_meta(p):set_string("botany_ready_at","broken"); assert(not B.harvest(p,player)) A.get_meta(p):set_string("botany_ready_at","-2"); assert(not B.harvest(p,player)) assert(added==0) end) test("rightclick returns item stack and reentrant inventory cannot duplicate",function() register(); bush(); local original=inv.add_item inv.add_item=function(self,list,item) assert(not B.harvest(p,player)); return original(self,list,item) end local stack={}; assert(defs["hearth:bilberry"].on_rightclick(p,{},player,stack)==stack) inv.add_item=original; assert(added==2) end) test("the actual position selector can return each of all hundred exact node IDs",function() for index,r in ipairs(B.list()) do local h=habitat(r.id); h.include_trees=true local x=0 while (x*17+7)%100~=index-1 do x=x+1; assert(x<100) end local k,selected=B.choose({x=x,y=1,z=0},h,next(r.seasons)) assert(k==r.node:sub(8) and selected.id==r.id,r.id) assert(B.suitable(k,h,next(r.seasons))) end assert(writes==0) end) test("submerged candidates work as separate air-cell stands, never as liquid node definitions",function() register() for _,id in ipairs({"water_starwort","hornwort"}) do local r=B.get(id); local h=habitat(id) assert(B.suitable(id,h,"Summer") and h.aquatic=="submerged" and h.water>=1) local def=defs[r.node] assert(def.groups.hearth_submerged==1 and not def.groups.water and not def.liquidtype) assert(not def.buildable_to and not def.walkable and def.drawtype=="plantlike") end end) test("all seventy-nine new understory policies match actual activity and dormant art",function() local count=0 for _,r in ipairs(B.list()) do local policy=policies[r.id] if r.existing or r.group=="trees" then assert(not policy,r.id) else count=count+1; assert(policy,r.id) for _,s in ipairs({"Spring","Summer","Autumn","Winter"}) do assert((policy.active[s]==true)==B.active(r.id,s)) local v=policy.definitions[s] assert((v~=nil)==not B.active(r.id,s),r.id..s) if v then assert(v.texture=="hearth_botany_"..r.id.."_dormant.png") assert(B.kind(r.node.."_"..s:lower())==r.id) end end end end assert(count==79) policies.holly.active.Winter=false; assert(B.active("holly","Winter")) assert(next(policies.juniper.definitions)==nil and next(policies.holly.definitions)==nil) end) test("optional seasons mocks without add_variants still support base harvesting",function() for _,mock in ipairs({{}, {kind=function(name) return name:match("^hearth:(.+)$") end}}) do H.seasons=mock; B=factory(H,A); bush(); fields={} assert(B.harvest(p,player)) end H.seasons=nil; B=factory(H,A); fields={}; assert(B.harvest(p,player)) end) test("harvest samples one validated epoch before climate and uses the same cooldown clock",function() time=1782043200.75; bush() H.climate_at=function(epoch) climate_calls=climate_calls+1 assert(clock_reads==1 and epoch==1782043200) return {season="Summer"} end assert(B.harvest(p,player) and clock_reads==1 and climate_calls==1) assert(tonumber(fields[key(p)].botany_ready_at)==1782043200+365.2425*86400) end) test("only exact owned seasonal aliases harvest in an active fruit season",function() bush("bilberry_winter"); season="Winter" local ok,reason=B.harvest(p,player); assert(not ok and reason=="season" and writes==0) -- A loaded appearance can lag the climate; harvesting never resets its stock. season="Summer"; assert(B.harvest(p,player)) bush(); assert(not B.harvest(p,player) and added==2) H.seasons.kind=function() return "bilberry" end for _,id in ipairs({"bilberry_fake","bilberry_summer","bilberry_winter_extra"}) do bush(id); ok,reason=B.harvest(p,player); assert(not ok and reason=="species") end world[key(p)]={name="foreign:bilberry"}; ok,reason=B.harvest(p,player) assert(not ok and reason=="species" and added==2) end) test("southern berry seasons use the solar model at the validated epoch",function() local model=dofile(target:gsub("[^/\\]+$","model.lua")) H.climate_at=function(epoch) assert(epoch==time); return model.solar(epoch,-45,0) end bush(); time=1782043200 local ok,reason=B.harvest(p,player); assert(not ok and reason=="season" and writes==0) time=1797854400; assert(B.harvest(p,player) and added==2) assert(tonumber(fields[key(p)].botany_ready_at)==time+365.2425*86400) end) test("shared annual fruit readiness rejects an animal debit and has no daily refill",function() bush(); local m=A.get_meta(p) m:set_string("botany_ready_at",tostring(time+365.2425*86400)) assert(not B.harvest(p,player) and added==0) time=time+86400; assert(not B.harvest(p,player)) time=time+(365.2425-1)*86400; assert(B.harvest(p,player) and added==2) end) test("one unexpired pollination credit adds only one berry and is spent once",function() bush(); local m=A.get_meta(p) m:set_string("succ_pollinated_until",tostring(time+86400)) local ok,n=B.harvest(p,player); assert(ok and n==3 and added==3) assert(m:get_string("succ_pollinated_until")=="0") assert(not B.harvest(p,player) and added==3) time=time+365.2425*86400; ok,n=B.harvest(p,player) assert(ok and n==2 and added==5) end) test("expired pollination is not a bonus and invalid credits fail closed",function() bush(); local m=A.get_meta(p) for _,v in ipairs({"broken","-1",tostring(time+86401),"inf"}) do m:set_string("succ_pollinated_until",v) local before=writes; assert(not B.harvest(p,player) and added==0 and writes==before) end m:set_string("succ_pollinated_until",tostring(time)) local ok,n=B.harvest(p,player); assert(ok and n==2 and added==2) end) test("empty fruit cannot be manufactured by fresh pollination",function() bush(); local m=A.get_meta(p) m:set_string("botany_ready_at",tostring(time+86400)) m:set_string("succ_pollinated_until",tostring(time+86400)) local before=writes; assert(not B.harvest(p,player) and writes==before and added==0) assert(tonumber(m:get_string("succ_pollinated_until"))==time+86400) end) test("inventory rejection restores fruit and credit; partial delivery spends both",function() bush(); local m=A.get_meta(p); local pollen=tostring(time+86400) m:set_string("succ_pollinated_until",pollen) room=false; assert(not B.harvest(p,player)) assert(m:get_string("botany_ready_at")=="" and m:get_string("succ_pollinated_until")==pollen) room=true; leftover=3; assert(not B.harvest(p,player)) assert(m:get_string("botany_ready_at")=="" and m:get_string("succ_pollinated_until")==pollen) leftover=2; local ok,n=B.harvest(p,player); assert(ok and n==1 and added==1) assert(m:get_string("succ_pollinated_until")=="0" and not B.harvest(p,player)) end) test("both inventory callbacks observe reserved fruit and pollination credit",function() bush(); local m=A.get_meta(p); m:set_string("succ_pollinated_until",tostring(time+86400)) local old_room,old_add=inv.room_for_item,inv.add_item local function check() assert(not B.harvest(p,player)) assert(m:get_string("succ_pollinated_until")=="0") end inv.room_for_item=function(...) check(); return old_room(...) end inv.add_item=function(...) check(); return old_add(...) end local ok,n=B.harvest(p,player) inv.room_for_item,inv.add_item=old_room,old_add assert(ok and n==3 and added==3) end) assert(passed==44,"Unexpected focused botany integration count") print("BOTANY_SPEC_PASS "..passed)