-- Original terrain content. Registration only: never rewrites generated land, -- changes world settings, clears registries, or owns ecological growth timers. return function(hearth, api, register_node, construction) assert(type(hearth) == "table" and type(api) == "table") assert(type(register_node) == "function" and type(construction) == "table") local M = {} local function node(name) return "hearth:" .. name end local function tile(name) return "hearth_" .. name .. ".png" end local function names(list) local out = {} for i, name in ipairs(list) do out[i] = node(name) end return out end M.species_order = {"oak", "birch", "pine", "willow", "alder", "beech", "aspen", "rowan", "hazel", "spruce"} M.rocks = {"granite", "basalt", "limestone", "slate", "sandstone"} -- Moisture/fertility/drainage are normalized habitat preferences, NOT player -- climate values. The succession owner decides how to consume these hints. M.surfaces = { grass = {moisture=0.45, fertility=0.7, drainage=0.7, vegetated=true}, forest_floor = {moisture=0.6, fertility=0.9, drainage=0.6, vegetated=true}, podzol = {moisture=0.4, fertility=0.35, drainage=0.8, vegetated=true}, alluvium = {moisture=0.85, fertility=1, drainage=0.3, vegetated=true}, peat = {moisture=1, fertility=0.3, drainage=0.1, vegetated=true}, alpine_turf = {moisture=0.35, fertility=0.3, drainage=0.9, vegetated=true}, soil = {moisture=0.45, fertility=0.5, drainage=0.6, vegetated=true}, sand = {moisture=0.2, fertility=0.1, drainage=1}, gravel = {moisture=0.2, fertility=0.1, drainage=1}, scree = {moisture=0.1, fertility=0.05, drainage=1}, snow = {moisture=0.1, fertility=0, drainage=0.5}, } for _, rock in ipairs(M.rocks) do M.surfaces[rock] = {moisture=0.1, fertility=0, drainage=1} end M.surfaces.stone = {moisture=0.1, fertility=0, drainage=1} function M.surface(value) local name = type(value) == "table" and value.name or value if type(name) ~= "string" then return nil end return M.surfaces[(name:gsub("^hearth:", ""))] end M.profiles = { oak = {height=7, width=7, crown="broad", heat={35,80}, humidity={30,75}, y_min=3, y_max=100, soils={"grass", "forest_floor"}, biomes={"hearth_oak_woodland", "hearth_temperate"}, shade=0.8, litter=0.9}, birch = {height=8, width=5, crown="slender", heat={20,65}, humidity={35,80}, y_min=6, y_max=145, soils={"forest_floor", "grass", "podzol"}, biomes={"hearth_birch_forest"}, shade=0.45, litter=0.7}, pine = {height=9, width=7, crown="conical", heat={0,45}, humidity={15,70}, y_min=8, y_max=175, soils={"podzol", "alpine_turf"}, biomes={"hearth_pine_taiga"}, shade=0.65, litter=0.45}, willow = {height=7, width=9, crown="drooping", heat={35,85}, humidity={70,100}, y_min=2, y_max=8, soils={"alluvium", "peat"}, biomes={"hearth_floodplain"}, shade=0.85, litter=0.8, riparian=true}, alder = {height=6, width=5, crown="riparian", heat={20,65}, humidity={75,100}, y_min=2, y_max=45, soils={"alluvium", "peat", "forest_floor"}, biomes={"hearth_wet_forest", "hearth_floodplain"}, shade=0.6, litter=1, riparian=true, nitrogen_fixer=true}, beech = {height=8, width=9, crown="domed", heat={35,70}, humidity={35,70}, y_min=4, y_max=115, soils={"forest_floor", "grass"}, biomes={"hearth_oak_woodland", "hearth_temperate"}, shade=0.95, litter=1}, aspen = {height=9, width=5, crown="columnar", heat={20,65}, humidity={35,75}, y_min=5, y_max=140, soils={"grass", "forest_floor", "alluvium"}, biomes={"hearth_birch_forest", "hearth_temperate"}, shade=0.45, litter=0.7}, rowan = {height=6, width=7, crown="open", heat={10,55}, humidity={30,80}, y_min=10, y_max=180, soils={"podzol", "alpine_turf", "forest_floor", "grass"}, biomes={"hearth_birch_forest", "hearth_pine_taiga", "hearth_alpine_meadow"}, shade=0.4, litter=0.6}, hazel = {height=5, width=7, crown="coppice", heat={35,75}, humidity={35,75}, y_min=3, y_max=95, soils={"forest_floor", "grass", "soil"}, biomes={"hearth_oak_woodland", "hearth_temperate"}, shade=0.7, litter=0.8}, spruce = {height=10, width=5, crown="spire", heat={0,40}, humidity={40,85}, y_min=8, y_max=185, soils={"podzol", "forest_floor", "alpine_turf"}, biomes={"hearth_pine_taiga", "hearth_birch_forest"}, shade=0.85, litter=0.5}, } function M.supports_species(species, surface) local p = M.profiles[species] local name = type(surface) == "table" and surface.name or surface if not p or type(name) ~= "string" then return false end name = name:gsub("^hearth:", "") for _, soil in ipairs(p.soils) do if soil == name then return true end end return false end -- x fastest, then y, then z. Empty cells have probability zero, so these -- bounded full-block schematics cannot carve slopes or erase other trees. function M.tree_schematic(species, canonical, stage) local p = assert(M.profiles[species], "unknown tree species") assert(stage == nil or stage == "mature" or stage == "young", "unknown tree stage") local w, h = p.width, p.height if stage == "young" then w, h = 3, (species == "pine" or species == "spruce") and 5 or 4 end local c, data = math.floor(w / 2), {} for z = 0, w - 1 do for y = 0, h - 1 do for x = 0, w - 1 do local dx, dz = math.abs(x-c), math.abs(z-c) local r = math.max(dx, dz) local trunk = dx == 0 and dz == 0 and y < h-1 local leaf = false if stage == "young" then leaf = y >= h-3 and dx+dz <= (y == h-1 and 0 or 1) elseif species == "oak" then leaf = y >= 3 and r <= (y == h-1 and 1 or (y == 3 and 2 or 3)) and dx+dz <= (y == h-1 and 2 or 4) trunk = trunk or (y == 3 and dx+dz <= 2 and (dx == 0 or dz == 0)) elseif species == "birch" then leaf = y >= 4 and dx+dz <= (y == h-1 and 1 or 2) elseif species == "pine" then local radius = ({[2]=3,[3]=2,[4]=2,[5]=1,[6]=1,[7]=1,[8]=0})[y] leaf = radius ~= nil and r <= radius and dx+dz <= radius+1 elseif species == "willow" then leaf = (y >= 4 and r <= (y == h-1 and 2 or 3) and dx+dz <= 5) or (y >= 1 and y <= 4 and r == 3 and (dx+dz)%2 == 0) trunk = trunk or (y == 4 and dx+dz <= 3 and (dx == 0 or dz == 0)) elseif species == "alder" then leaf = y >= 2 and dx+dz <= (y == h-1 and 1 or 3) elseif species == "beech" then leaf = y >= 3 and r <= (y == h-1 and 2 or 4) and dx+dz <= (y == h-1 and 3 or 6) trunk = trunk or (y == 3 and dx+dz <= 3 and (dx == 0 or dz == 0)) elseif species == "aspen" then leaf = y >= 3 and r <= (y == h-1 and 1 or 2) and dx+dz <= 2 elseif species == "rowan" then leaf = y >= 2 and dx+dz <= (y == h-1 and 1 or 3) and not (dx == dz and r == 2) trunk = trunk or (y == 2 and dx+dz == 1) elseif species == "hazel" then leaf = y >= 1 and dx+dz <= (y == h-1 and 2 or 4) trunk = trunk or (y <= 2 and dx+dz == 1) elseif species == "spruce" then local radius = y >= 1 and math.min(2,math.floor((h-1-y)/3)) or -1 leaf = r <= radius and dx+dz <= radius+1 end local stem = canonical and "log" or species .. "_log" local leaves = canonical and "leaves" or species .. "_leaves" data[#data+1] = {name=trunk and node(stem) or (leaf and node(leaves) or "air"), prob=(trunk or leaf) and 255 or 0, param2=0} end end end return {size={x=w, y=h, z=w}, data=data} end -- Heat/humidity are Voronoi centers, not hard climate bounds. Altitude bands -- are hard bounds; all climate points converge on one permanent snow biome. M.biome_profiles = { {name="hearth_temperate", top="grass", filler="soil", rock="limestone", heat=52, humidity=45, min=2, max=110}, {name="hearth_oak_woodland", top="forest_floor", filler="soil", rock="limestone", heat=65, humidity=62, min=3, max=110}, {name="hearth_birch_forest", top="forest_floor", filler="soil", rock="granite", heat=36, humidity=58, min=6, max=150}, {name="hearth_pine_taiga", top="podzol", filler="soil", rock="granite", heat=18, humidity=38, min=2, max=180}, {name="hearth_wet_forest", top="peat", filler="alluvium", rock="slate", heat=42, humidity=90, min=2, max=65}, {name="hearth_floodplain", top="alluvium", filler="alluvium", rock="sandstone", heat=68, humidity=90, min=2, max=8}, {name="hearth_dry_scrub", top="grass", filler="soil", rock="sandstone", heat=83, humidity=18, min=2, max=135}, {name="hearth_alpine_meadow", top="alpine_turf", filler="scree", rock="slate", heat=30, humidity=48, min=111, max=210}, {name="hearth_rocky_peaks", top="snow", filler="scree", rock="granite", heat=25, humidity=45, min=211, max=31000}, {name="hearth_riverbank", top="sand", filler="gravel", rock="sandstone", heat=50, humidity=50, min=-31000, max=1}, } local registered = false function M.register() if registered then return M end -- Fail closed on an integration collision instead of silently overriding -- another owner's node definitions or publishing partly compatible art. local function add(name, def) assert(not (api.registered_nodes or {})[node(name)], "duplicate terrain node: " .. name) def.tiles = def.tiles or {tile(name)} register_node(name, def) end for _, rock in ipairs(M.rocks) do add(rock, {description=rock:gsub("^%l", string.upper) .. " bedrock", groups={cracky=3, stone=1, hearth_geology=1}}) add(rock .. "_bricks", {description=rock:gsub("^%l", string.upper) .. " masonry", -- Brick uses a derived world texture, not an inventory silhouette. groups={cracky=3, stone=1, hearth_building=1}, on_place=construction.place_building, after_place_node=construction.owner}) api.register_craft({type="shapeless", output=node(rock .. "_bricks") .. " 4", recipe={node(rock)}}) -- A pair preserves mass without shadowing the single-rock brick recipe. api.register_craft({type="shapeless", output=node("stone") .. " 2", recipe={node(rock), node(rock)}}) end for _, soil in ipairs({"forest_floor", "podzol", "alluvium", "peat", "alpine_turf"}) do add(soil, {description=soil:gsub("_", " "):gsub("^%l", string.upper), tiles={tile(soil), tile("soil"), tile("soil")}, groups={crumbly=3, soil=1, hearth_habitat=1}, drop=node("soil")}) end for _, loose in ipairs({"gravel", "scree", "snow"}) do add(loose, {description=loose:gsub("^%l", string.upper), groups={crumbly=3, falling_node=loose == "snow" and nil or 1}, drop=loose == "snow" and node("snow") or node("loose_stone")}) end for _, species in ipairs(M.species_order) do local title = species:gsub("^%l", string.upper) add(species .. "_log", {description=title .. " log (requires an axe)", paramtype2="facedir", tiles={tile(species .. "_log_top"), tile(species .. "_log_top"), tile(species .. "_log_side")}, groups={tree=1, choppy=2, flammable=2, hearth_canopy=1}}) add(species .. "_leaves", {description=title .. " leaves", drawtype="allfaces_optional", paramtype="light", sunlight_propagates=true, use_texture_alpha="clip", waving=1, groups={snappy=3, leaves=1, flammable=2, hearth_canopy=1}, -- Species recruitment/seed nodes belong to the ecosystem module. drop=node("sticks")}) add(species .. "_planks", {description=title .. " planks", groups={choppy=3, wood=1, flammable=2}}) api.register_craft({type="shapeless", output=node(species .. "_planks") .. " 4", recipe={node(species .. "_log")}}) -- Separate slots, not a counted stack: one log must still split to planks. api.register_craft({type="shapeless", output=node("log") .. " 2", recipe={node(species .. "_log"), node(species .. "_log")}}) api.register_craft({type="shapeless", output=node("planks"), recipe={node(species .. "_planks")}}) end for _, resource in ipairs({{"quartz", "quartz_vein", "Quartz crystal"}, {"copper_ore", "copper_ore_vein", "Copper ore"}, {"coal", "coal_ore_vein", "Bituminous coal"}}) do assert(not (api.registered_items or {})[node(resource[1])], "duplicate resource item") api.register_craftitem(node(resource[1]), {description=resource[3], inventory_image="hearth_item_" .. resource[1] .. ".png"}) add(resource[2], {description=resource[3] .. " seam (requires a pick)", groups={cracky=3, stone=1}, drop=node(resource[1]) .. " 2"}) end -- Non-forcing source aliases only. Existing game/mapgen bindings win. for _, alias in ipairs({{"mapgen_stone", "stone"}, {"mapgen_water_source", "water_source"}, {"mapgen_river_water_source", "water_source"}}) do if not (api.registered_aliases or {})[alias[1]] and not (api.registered_items or {})[alias[1]] then api.register_alias(alias[1], node(alias[2])) end end for _, p in ipairs(M.biome_profiles) do api.register_biome({name=p.name, node_top=node(p.top), depth_top=1, node_filler=node(p.filler), depth_filler=p.name == "hearth_rocky_peaks" and 1 or 3, node_stone=node(p.rock), node_water=node("water_source"), node_river_water=node("water_source"), node_riverbed=node("gravel"), depth_riverbed=2, y_min=p.min, y_max=p.max, heat_point=p.heat, humidity_point=p.humidity, vertical_blend=0}) end -- Ordered geological replacement precedes ores: coherent deep intrusions, -- then nested ore clusters restricted to their actual host lithologies. -- v7 reselects biomes at lower altitude limits. Below the riverbank band -- the initial host is sandstone, NOT the surface woodland's node_stone. -- Explicit beds keep all deep resources reachable in that real engine case. for _, bed in ipairs({{"granite", -31000, -80}, {"slate", -79, -24}, {"limestone", -23, -8}}) do api.register_ore({name="hearth:" .. bed[1] .. "_bed", ore_type="stratum", ore=node(bed[1]), -- A zero cluster size is valid for strata and avoids Ore::placeOre -- skipping a one-node slice where a bed meets a mapchunk boundary. wherein=names({"sandstone", "stone"}), y_min=bed[2], y_max=bed[3], clust_size=0, clust_scarcity=1}) end api.register_ore({name="hearth:basalt_intrusions", ore_type="blob", ore=node("basalt"), wherein=names({"granite", "stone", "slate", "limestone", "sandstone"}), clust_scarcity=48*48*48, clust_size=24, y_min=-31000, y_max=-24, noise_threshold=0.15, noise_params={offset=0, scale=1, spread={x=96,y=64,z=96}, seed=7131, octaves=3, persistence=0.6}}) local function ore(name, hosts, distance, count, size, ymin, ymax) api.register_ore({name="hearth:" .. name .. "_" .. tostring(ymax), ore_type="scatter", ore=node(name), wherein=names(hosts), clust_scarcity=distance^3, clust_num_ores=count, clust_size=size, y_min=ymin, y_max=ymax}) end ore("quartz_vein", {"granite", "slate"}, 11, 6, 3, -31000, 160) ore("copper_ore_vein", {"basalt", "limestone", "slate"}, 13, 5, 3, -31000, 32) ore("coal_ore_vein", {"sandstone", "slate", "limestone"}, 10, 9, 3, -31000, 70) ore("iron_ore_vein", {"basalt", "granite", "limestone", "stone"}, 11, 6, 3, -31000, -8) ore("iron_ore_vein", {"basalt", "granite", "limestone", "stone"}, 19, 3, 2, -7, 85) local surfaces = {} for name in pairs(M.surfaces) do surfaces[#surfaces+1] = node(name) end table.sort(surfaces) local function deco(id, plant, ground, ratio, ymin, ymax, biomes, extra) local def = {name="hearth:" .. id, deco_type="simple", place_on=ground, sidelen=16, fill_ratio=ratio, y_min=ymin, y_max=ymax, biomes=biomes, decoration=node(plant)} for k, v in pairs(extra or {}) do def[k] = v end api.register_decoration(def) end local wet = {"hearth_wet_forest", "hearth_floodplain", "hearth_riverbank"} local woods = {"hearth_oak_woodland", "hearth_birch_forest", "hearth_pine_taiga", "hearth_wet_forest"} local meadow = {"hearth_temperate", "hearth_dry_scrub", "hearth_alpine_meadow"} local near_water = {spawn_by=names({"water_source", "water_flowing"}), num_spawn_by=1, check_offset=-1} -- Establish real canopies before ground flora can occupy the trunk cell. for _, species in ipairs(M.species_order) do local p = M.profiles[species] local def = {name="hearth:" .. species .. "_trees", deco_type="schematic", place_on=names(p.soils), sidelen=16, fill_ratio=species == "willow" and 0.004 or 0.012, y_min=p.y_min, y_max=p.y_max, biomes=p.biomes, schematic=M.tree_schematic(species), rotation="random", flags="place_center_x,place_center_z", place_offset_y=1} if p.riparian then def.spawn_by, def.num_spawn_by, def.check_offset = near_water.spawn_by, 1, -1 end api.register_decoration(def) end -- Canonical timber remains discoverable by legacy starting-resource probes. api.register_decoration({name="hearth:temperate_trees", deco_type="schematic", place_on=names({"grass"}), sidelen=16, fill_ratio=0.012, y_min=3, y_max=110, biomes={"hearth_temperate"}, schematic=M.tree_schematic("oak", true), rotation="random", flags="place_center_x,place_center_z", place_offset_y=1}) -- Sparse guaranteed-compatible gathering substrate, not guaranteed spawns. -- No soil type can strand progression merely because grass is absent. deco("loose_stone", "loose_stone", surfaces, 0.035, 1, 31000) deco("sticks", "sticks", surfaces, 0.03, 1, 210) deco("berry_bush", "berry_bush", names({"grass", "forest_floor", "podzol", "alluvium"}), 0.018, 2, 130) deco("grain_patch", "grain_patch", names({"grass", "alluvium"}), 0.009, 2, 100, {"hearth_temperate", "hearth_floodplain"}) -- Springs precede water-dependent plants: decorations see earlier writes, -- so upland rushes can establish at the newly generated seep edges. deco("seep_springs", "water_source", names({"grass", "forest_floor", "podzol"}), 0.0015, 3, 150, {"hearth_temperate", "hearth_oak_woodland", "hearth_birch_forest", "hearth_pine_taiga"}, {place_offset_y=-1, flags="force_placement"}) deco("reeds", "reeds", names({"sand", "gravel", "alluvium", "peat", "grass", "forest_floor", "podzol"}), 0.09, 1, 150, nil, near_water) deco("clay_bank", "clay_bank", names({"sand", "gravel", "alluvium"}), 0.04, -2, 8, wet, near_water) deco("meadow_grasses", "meadow_grass", names({"grass", "alpine_turf"}), 0.15, 2, 210, meadow) deco("meadow_flowers", "wildflowers", names({"grass", "alpine_turf"}), 0.025, 2, 210, meadow) deco("woodland_ferns", "fern", names({"forest_floor", "peat", "alluvium"}), 0.06, 2, 145, woods) deco("woodland_litter", "litter", names({"forest_floor", "podzol", "peat"}), 0.08, 2, 180, woods) deco("woodland_fungi", "fungi", names({"forest_floor", "peat"}), 0.012, 2, 130, woods) -- Three ordinary log cubes on their sides; top grain faces +X (12/4=3). -- Random schematic rotation also permits Z-aligned logs. Never force-place. api.register_decoration({name="hearth:woodland_deadwood", deco_type="schematic", place_on=names({"forest_floor", "podzol", "peat"}), sidelen=16, fill_ratio=0.007, y_min=2, y_max=160, biomes=woods, rotation="random", place_offset_y=1, flags="place_center_x,place_center_z", schematic={size={x=3,y=1,z=1},data={ {name=node("deadwood"),prob=255,param2=12}, {name=node("deadwood"),prob=255,param2=12}, {name=node("deadwood"),prob=255,param2=12}}}}) deco("spring_foraging", "spring_greens_patch", names({"grass", "alluvium"}), 0.009, 2, 100, {"hearth_temperate", "hearth_floodplain"}) deco("autumn_foraging", "autumn_mushrooms_patch", names({"forest_floor", "podzol"}), 0.012, 2, 145, woods) -- Retain the old landmark contract without force-writing banks/water. local weir = {size={x=7,y=3,z=5}, data={}} for z=0,4 do for y=0,2 do for x=0,6 do local edge = z == 0 or z == 4 or x == 0 or x == 6 local wall = edge and (y == 0 or ((z == 0 or z == 4) and y == 1) or ((x == 0 or x == 6) and z >= 1 and z <= 3)) weir.data[#weir.data+1] = {name=wall and node("weir_stone") or "air", prob=wall and 255 or 0} end end end api.register_decoration({name="hearth:old_river_weir", deco_type="schematic", place_on=names({"sand", "gravel", "alluvium"}), sidelen=16, fill_ratio=0.001, y_min=0, y_max=8, biomes=wet, schematic=weir, rotation="random", flags="place_center_x,place_center_z", place_offset_y=1, spawn_by=near_water.spawn_by, num_spawn_by=1, check_offset=-1}) registered = true return M end return M end