-- Species/niche policy only. Succession owns the one fixed-eight scheduler. return function(hearth, api) local D = {species_order={"oak","birch","pine","willow","alder","beech","aspen","rowan","hazel","spruce","crab_apple","wild_cherry","field_maple"}, plants={"clover","nettle","heather","bluebell","sedge","bulrush","moss","lichen"}, grounds={grass=true,soil=true,forest_floor=true,podzol=true,alluvium=true,peat=true,alpine_turf=true}, rocks={stone=true,granite=true,basalt=true,limestone=true,slate=true,sandstone=true,scree=true}} local species, flora = {}, {} local function kind(value) if hearth.seasons then local k=hearth.seasons.kind(value); if k then return k end end local name=type(value)=="table" and value.name or value return type(name)=="string" and (name:gsub("^hearth:","")) or nil end for _, s in ipairs(D.species_order) do species[s]=true end for _, plant in ipairs(D.plants) do flora[plant]=true end function D.tree_info(value) local k=kind(value) local s,stage=(k or ""):match("^([a-z_]+)_([a-z]+)$") if species[s] and ({seedling=true,young=true,mature=true})[stage] then return s,stage end end function D.family(value) local k=kind(value) local s,stage=D.tree_info(k) if s then return ({seedling="tree_seedling",young="young_tree",mature="mature_tree"})[stage] end local wood,part=(k or ""):match("^([a-z_]+)_(leaves)$") if species[wood] then return part end wood,part=(k or ""):match("^([a-z_]+)_(log)$") return species[wood] and part or k end function D.is_worldgen_flora(value) return flora[kind(value)]==true or D.tree_info(value)~=nil or (hearth.botany and hearth.botany.kind(value)~=nil and D.family(value)~="log" and D.family(value)~="leaves" and not (kind(value) or ""):match("_planks$")) or false end function D.is_ground(value) return D.grounds[kind(value)]==true end function D.is_rock(value) return D.rocks[kind(value)]==true end function D.supports_ground(value, support) if kind(value)=="lichen" then return D.is_rock(support) or D.is_ground(support) end return D.is_ground(support) end local soils={oak={grass=true,soil=true,forest_floor=true}, birch={grass=true,soil=true,forest_floor=true,podzol=true}, pine={podzol=true,alpine_turf=true}, willow={alluvium=true,peat=true}, alder={alluvium=true,peat=true,forest_floor=true}, beech={grass=true,soil=true,forest_floor=true}, aspen={grass=true,forest_floor=true,alluvium=true}, rowan={grass=true,forest_floor=true,podzol=true,alpine_turf=true}, hazel={grass=true,soil=true,forest_floor=true}, spruce={podzol=true,forest_floor=true,alpine_turf=true}, crab_apple={grass=true,soil=true,forest_floor=true},wild_cherry={grass=true,soil=true,forest_floor=true}, field_maple={grass=true,soil=true,forest_floor=true}} function D.supports_species(s, support) if not species[s] then return false end local b=hearth.biomes if b and b.supports_species then return b.supports_species(s,support)==true end return soils[s][kind(support)]==true end function D.suitable(value, h, season) local k=kind(value) local light,moisture=h.light or 15,h.moisture or 0.45 local s=D.tree_info(k) if s then if not D.supports_species(s,h.ground) then return false end if s=="pine" then return moisture<0.8 and light>=5 end if s=="spruce" then return moisture>=0.4 and moisture<=0.85 and light>=4 end if s=="beech" then return moisture>=0.3 and moisture<=0.7 and light>=4 end if s=="willow" or s=="alder" then return moisture>=0.65 and light>=5 end return moisture>=0.25 and moisture<0.85 and light>=5 end if not D.supports_ground(k,h.ground) then return false end if k=="lichen" then return light>=11 and moisture<0.6 end if k=="clover" then return light>=10 and moisture>=0.3 and moisture<0.75 end if k=="nettle" then return light>=6 and moisture>=0.5 and moisture<0.9 and (h.nutrients or 0)>0 end if k=="heather" then return (h.ground=="podzol" or h.ground=="alpine_turf") and light>=9 and moisture<0.6 end if k=="bluebell" then return light>=4 and light<=11 and moisture>=0.45 and moisture<0.85 end if k=="sedge" or k=="bulrush" then return moisture>=0.75 and light>=7 end if k=="moss" then return light<=9 and moisture>=0.6 end return true end function D.edible(value, now) local k=kind(value) local available=not hearth.seasons or hearth.seasons.available(k,now) return available and ({meadow_grass=true,fern=true,clover=true,sedge=true,nettle=true})[k]==true end function D.pollinator(value, season) local k=kind(value) return (season=="Spring" or season=="Summer") and (k=="clover" or k=="wildflowers" or (k=="bluebell" and season=="Spring")) or (k=="heather" and season=="Autumn") end function D.choose_tree(p,h) local choices if h.ground=="podzol" or h.ground=="alpine_turf" then choices=(h.ground=="podzol" and (h.heat or 20)>55) and {"birch","pine"} or {"pine","birch"} elseif (h.moisture or 0)>=0.75 or h.ground=="alluvium" or h.ground=="peat" then choices=(h.ground=="peat" or (p.x+p.z)%2~=0) and {"alder","willow"} or {"willow","alder"} elseif h.ground=="forest_floor" and (h.canopy or 0)>=0.5 then choices={"oak","birch"} else choices={"birch","oak"} end -- Retain the old pioneer niches at the origin while admitting all thirteen -- species deterministically; no random reroll or extra world scan. local extra=({"beech","aspen","rowan","hazel","spruce","crab_apple","wild_cherry","field_maple"})[math.abs(p.x*7+p.z*11)%13] if extra and D.suitable(extra.."_seedling",h) then return extra.."_seedling" end for _,s in ipairs(choices) do if D.suitable(s.."_seedling",h) then return s.."_seedling" end end end -- Read-only geometry handoff. Fresh offset records, root at (0,0,0), no air -- writes. The ecology owner MUST preflight every destination/support before -- applying a plan, track its own components and refuse foreign/player nodes. -- "generic" uses canonical wood with the oak silhouette for legacy trees. function D.tree_plan(s,stage) stage=stage or "mature" if (not species[s] and s~="generic") or (stage~="young" and stage~="mature") then return nil end local b=hearth.biomes if not b or not b.tree_schematic then return nil end local schematic=b.tree_schematic(s=="generic" and "oak" or s,s=="generic",stage) local plan={size={x=schematic.size.x,y=schematic.size.y,z=schematic.size.z}} local cx,cz=math.floor(schematic.size.x/2),math.floor(schematic.size.z/2) local root=s=="generic" and (stage=="young" and "young_tree" or "mature_tree") or s.."_"..stage for z=0,schematic.size.z-1 do for y=0,schematic.size.y-1 do for x=0,schematic.size.x-1 do local cell=schematic.data[1+x+schematic.size.x*(y+schematic.size.y*z)] if cell.prob==255 and cell.name~="air" then plan[#plan+1]={x=x-cx,y=y,z=z-cz,name=(x==cx and y==0 and z==cz) and "hearth:"..root or cell.name, param2=cell.param2 or 0} end end end end return plan end -- Deterministic niches give useful diversity on first active establishment; -- not a random reroll, tree replacement, or a second mapgen/scanning pass. function D.choose_recruit(p,h,season) if not h.loaded or season=="Winter" then return nil end local hash=math.abs(p.x*3+p.z*5)%8 local candidates if D.rocks[h.ground] then candidates={"lichen"} elseif h.moisture>=0.75 then candidates={hash%2==0 and "sedge" or "bulrush","moss"} elseif h.light<=9 then candidates={season=="Spring" and "bluebell" or "moss","moss"} elseif h.ground=="podzol" or h.ground=="alpine_turf" then candidates={"heather","lichen"} else candidates={({[1]="clover",[2]="nettle",[3]="bluebell",[4]="clover"})[hash]} end for _,candidate in ipairs(candidates) do if D.suitable(candidate,h,season) and (candidate~="bluebell" or season=="Spring") then return candidate end end end local function unmarked(p) local m=api.get_meta(p) if m.to_table then for field,value in pairs((m:to_table() or {}).fields or {}) do if field:sub(1,5)~="succ_" and value~="" and tostring(value)~="0" then return false end end end for _,field in ipairs({"owner","planter","placed_by","player_placed","hearth_placed","hearth_player_placed","planted_at"}) do local v=m:get_string(field); if v~="" and v~="0" then return false end end if hearth.claim_owner_at then local owner,ambiguous=hearth.claim_owner_at(p); if owner or ambiguous then return false end end local protect=hearth.external_protected or api.is_protected return not (protect and protect(p,"")) end function D.bootstrap(p,n) if type(p)~="table" then return false end for _,axis in ipairs({"x","y","z"}) do local v=p[axis] if type(v)~="number" or v~=v or math.abs(v)>30990 then return false end end local current=api.get_node_or_nil(p) if not current or (n and current.name~=n.name) then return false end n=current local root_species=kind(n) and kind(n):match("^([a-z_]+)_log$") local block_root=(species[root_species] or kind(n)=="log") and hearth.botany~=nil if not n or not (D.is_worldgen_flora(n) or block_root or n.name=="hearth:deadwood") or not unmarked(p) then return false end local below={x=p.x,y=p.y-1,z=p.z} local support=api.get_node_or_nil(below) if not support or not D.supports_ground(n,support) or not unmarked(below) then return false end if block_root and not D.is_ground(support) then return false end local m=api.get_meta(p) if m:get_string("succ_natural")=="1" then return false end m:set_string("succ_natural","1") if block_root then m:set_string("succ_component",p.x..":"..p.y..":"..p.z) m:set_string("succ_worldgen_stage","mature") m:set_string("succ_species",root_species or "generic") end -- Succession adopts a known displayed stage on its first bounded visit; -- bootstrap does not backdate an invented historical birthday. if block_root or (hearth.botany and hearth.botany.kind(n)) then m:set_string("succ_pending_origin","worldgen_established") end return true end local seasons=hearth.seasons if seasons and seasons.add_variants then for _,s in ipairs(D.species_order) do local evergreen=s=="pine" or s=="spruce" for _,stage in ipairs({"seedling","young","mature","leaves"}) do local definitions={} if not evergreen then for _,season in ipairs({"Spring","Autumn","Winter"}) do definitions[season]={texture=stage=="leaves" and ("hearth_"..s.."_leaves_"..season:lower()..".png") or (stage=="seedling" and ("hearth_botany_"..s..".png") or ("hearth_"..s.."_log_top.png"))} end end seasons.add_variants(s.."_"..stage,definitions,evergreen and {Spring=true,Summer=true,Autumn=true,Winter=true} or {Spring=true,Summer=true,Autumn=true}) end end for _,plant in ipairs(D.plants) do local definitions,active={},nil if plant~="moss" and plant~="lichen" then definitions.Winter={texture="hearth_"..plant.."_dormant.png"} if plant=="bluebell" then definitions.Summer={texture="hearth_bluebell_dormant.png"} definitions.Autumn={texture="hearth_bluebell_dormant.png"}; active={Spring=true} end end seasons.add_variants(plant,definitions,active) end end local registered=false function D.register_nodes(register_node) if registered then return end registered=true local function placed(p) api.get_meta(p):set_string("player_placed","1") end for _,s in ipairs(D.species_order) do for _,stage in ipairs({"seedling","young","mature"}) do local woody=stage~="seedling" local def={description=s:gsub("^%l",string.upper).." "..stage.." tree", drawtype=woody and "normal" or "plantlike", tiles=woody and {"hearth_"..s.."_log_top.png","hearth_"..s.."_log_top.png","hearth_"..s.."_log_side.png"} or {"hearth_botany_"..s..".png"}, paramtype=not woody and "light" or nil,paramtype2=woody and "facedir" or nil, sunlight_propagates=not woody,use_texture_alpha=not woody and "clip" or nil,walkable=woody,buildable_to=false, groups=woody and {tree=1,choppy=2,flammable=2,hearth_ecosystem=1,not_in_creative_inventory=1} or {snappy=3,flora=1,flammable=2,hearth_ecosystem=1}, drop=woody and "hearth:"..s.."_log" or "",after_place_node=placed} register_node(s.."_"..stage,def) -- Preserve existing seasonal stage IDs in saves. Register bark cubes -- here so the foliage-only seasonal copier never flattens their three -- face textures into one atlas/tile. Seasons still owns name selection. if seasons and woody and s~="pine" and s~="spruce" then for _,season in ipairs({"spring","autumn","winter"}) do local variant={}; for k,v in pairs(def) do variant[k]=v end register_node(s.."_"..stage.."_"..season,variant) end end end end for _,plant in ipairs(D.plants) do register_node(plant,{description=plant:gsub("^%l",string.upper),drawtype="plantlike",tiles={"hearth_"..plant..".png"}, paramtype="light",sunlight_propagates=true,use_texture_alpha="clip",walkable=false,buildable_to=false, groups={snappy=3,flora=1,flammable=2,hearth_ecosystem=1},drop="",after_place_node=placed}) end local names={"hearth:deadwood"} if hearth.botany then names[#names+1]="hearth:log" for _,s in ipairs(D.species_order) do names[#names+1]="hearth:"..s.."_log" end end for k in pairs(flora) do names[#names+1]="hearth:"..k end if hearth.botany then for _,r in ipairs(hearth.botany.list()) do if r.group~="trees" and not flora[r.id] then names[#names+1]=r.node end end end for _,s in ipairs(D.species_order) do for _,stage in ipairs({"seedling","young","mature"}) do names[#names+1]="hearth:"..s.."_"..stage end end if seasons then for _,n in ipairs(seasons.names()) do if D.is_worldgen_flora(n) then names[#names+1]=n end end end api.register_lbm({name="hearth:diversity_natural_flora",label="Mark unmodified species worldgen flora", nodenames=names,run_at_every_load=true,action=function(p,n) D.bootstrap(p,n) end}) end return D end