From 2482740b89e03628bc01f3bda92ee30ac826fc68 Mon Sep 17 00:00:00 2001 From: chelsea Date: Tue, 2 Dec 2025 18:38:45 -0600 Subject: [PATCH] Add world generator sources and binary --- export_mca.py | 446 ++++++++ mc.py | 709 +++++++++++++ settings.py | 83 ++ worldgen-c/Makefile | 22 + worldgen-c/bin/worldgen | Bin 0 -> 102016 bytes worldgen-c/include/noise.h | 14 + worldgen-c/include/worldgen.h | 52 + worldgen-c/src/main.c | 818 +++++++++++++++ worldgen-c/src/noise.c | 180 ++++ worldgen-c/src/worldgen.c | 1839 +++++++++++++++++++++++++++++++++ 10 files changed, 4163 insertions(+) create mode 100644 export_mca.py create mode 100644 mc.py create mode 100644 settings.py create mode 100644 worldgen-c/Makefile create mode 100755 worldgen-c/bin/worldgen create mode 100644 worldgen-c/include/noise.h create mode 100644 worldgen-c/include/worldgen.h create mode 100644 worldgen-c/src/main.c create mode 100644 worldgen-c/src/noise.c create mode 100644 worldgen-c/src/worldgen.c diff --git a/export_mca.py b/export_mca.py new file mode 100644 index 0000000..1bf920c --- /dev/null +++ b/export_mca.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +import argparse +import math +import os +import struct +import time +import zlib +from concurrent.futures import ThreadPoolExecutor, as_completed +from io import BytesIO +from pathlib import Path + +cache_dir = Path('.numba_cache').resolve() +cache_dir.mkdir(exist_ok=True) +os.environ.setdefault("NUMBA_CACHE_DIR", str(cache_dir)) + +from mc import block_ids, worldGen +from settings import PLAYER_POS + + +TAG_END = 0 +TAG_BYTE = 1 +TAG_INT = 3 +TAG_LONG = 4 +TAG_STRING = 8 +TAG_LIST = 9 +TAG_COMPOUND = 10 +TAG_INT_ARRAY = 11 +TAG_LONG_ARRAY = 12 + +DATA_VERSION = 2586 # Minecraft 1.15.2 +DEFAULT_RADIUS = 256 # ~512 block diameter (~0.5 km) +CHUNK_HEIGHT = 256 +SECTION_COUNT = CHUNK_HEIGHT // 16 +BIOME_ID = 1 # Plains + + +def build_state_mapping(): + mapping = {} + + def set_state(name, block_name, props=None): + block_id = block_ids.get(name) + if block_id is None: + return + props_tuple = tuple(sorted((props or {}).items())) + mapping[block_id] = (block_name, props_tuple) + + set_state("air", "minecraft:air") + set_state("bedrock", "minecraft:bedrock") + set_state("stone", "minecraft:stone") + set_state("dirt", "minecraft:dirt") + set_state("grass", "minecraft:grass_block", {"snowy": "false"}) + set_state("water", "minecraft:water", {"level": "0"}) + set_state("oak_log", "minecraft:oak_log", {"axis": "y"}) + set_state("oak_leaves", "minecraft:oak_leaves", {"distance": "1", "persistent": "false"}) + set_state("birch_log", "minecraft:birch_log", {"axis": "y"}) + set_state("birch_leaves", "minecraft:birch_leaves", {"distance": "1", "persistent": "false"}) + return mapping + + +BLOCK_STATE_LOOKUP = build_state_mapping() +DEFAULT_AIR_STATE = BLOCK_STATE_LOOKUP[block_ids["air"]] + + +def to_signed_long(value): + value &= (1 << 64) - 1 + if value >= (1 << 63): + value -= 1 << 64 + return value + + +def pack_bits(indices, bits_per_value): + if not indices: + return [] + total_bits = len(indices) * bits_per_value + longs_needed = (total_bits + 63) // 64 + result = [0] * longs_needed + + for idx, value in enumerate(indices): + bit_index = idx * bits_per_value + long_id = bit_index // 64 + offset = bit_index % 64 + result[long_id] |= value << offset + + spill = offset + bits_per_value - 64 + if spill > 0: + result[long_id + 1] |= value >> (bits_per_value - spill) + + return [to_signed_long(v) for v in result] + + +def pack_heightmap(values): + return pack_bits(values, 9) + + +def block_state_from_id(block_id): + return BLOCK_STATE_LOOKUP.get(block_id, DEFAULT_AIR_STATE) + + +def build_section(chunk_x, chunk_z, section_y, heightmap, tree_blocks): + palette = [] + palette_lookup = {} + block_indices = [] + has_blocks = False + + for y in range(16): + global_y = section_y * 16 + y + if global_y >= CHUNK_HEIGHT: + break + for z in range(16): + global_z = chunk_z * 16 + z + for x in range(16): + global_x = chunk_x * 16 + x + block_id = tree_blocks.get((global_x, global_y, global_z)) + if block_id is None: + block_id = worldGen.generateBlock(global_x, global_y, global_z) + state = block_state_from_id(block_id) + + key = state + if key not in palette_lookup: + palette_lookup[key] = len(palette) + palette.append(key) + idx = palette_lookup[key] + block_indices.append(idx) + + name = state[0] + if name != "minecraft:air": + has_blocks = True + if global_y + 1 > heightmap[x][z]: + heightmap[x][z] = global_y + 1 + + if not has_blocks: + return None + + palette_entries = [] + for name, props in palette: + entry = {"Name": name} + if props: + entry["Properties"] = dict(props) + palette_entries.append(entry) + + palette_len = max(1, len(palette_entries)) + bits = max(4, math.ceil(math.log2(palette_len))) if palette_len > 1 else 4 + block_states = pack_bits(block_indices, bits) if block_indices else [] + if not block_states: + block_states = [0] + + return { + "Y": section_y, + "Palette": palette_entries, + "BlockStates": block_states, + "BlockLight": bytearray(2048), + "SkyLight": bytearray(2048), + } + + +def build_biome_array(): + return [BIOME_ID] * 256 + + +def build_chunk(chunk_x, chunk_z): + heightmap = [[0] * 16 for _ in range(16)] + sections = [] + tree_blocks = worldGen.generate_chunk_trees(chunk_x, chunk_z) + + for section_y in range(SECTION_COUNT): + section = build_section(chunk_x, chunk_z, section_y, heightmap, tree_blocks) + if section: + sections.append(section) + + height_values = [] + for z in range(16): + for x in range(16): + height_values.append(heightmap[x][z]) + + chunk = { + "chunk_x": chunk_x, + "chunk_z": chunk_z, + "sections": sections, + "heightmap": height_values, + "biomes": build_biome_array(), + } + return chunk + + +def write_string(buffer, value): + encoded = value.encode('utf-8') + buffer.write(struct.pack('>H', len(encoded))) + buffer.write(encoded) + + +def write_tag_header(buffer, tag_type, name): + buffer.write(bytes([tag_type])) + write_string(buffer, name) + + +def write_byte(buffer, name, value): + write_tag_header(buffer, TAG_BYTE, name) + buffer.write(struct.pack('>b', value)) + + +def write_int(buffer, name, value): + write_tag_header(buffer, TAG_INT, name) + buffer.write(struct.pack('>i', value)) + + +def write_long(buffer, name, value): + write_tag_header(buffer, TAG_LONG, name) + buffer.write(struct.pack('>q', value)) + + +def write_string_tag(buffer, name, value): + write_tag_header(buffer, TAG_STRING, name) + write_string(buffer, value) + + +def write_int_array(buffer, name, values): + write_tag_header(buffer, TAG_INT_ARRAY, name) + buffer.write(struct.pack('>i', len(values))) + for val in values: + buffer.write(struct.pack('>i', val)) + + +def write_long_array(buffer, name, values): + write_tag_header(buffer, TAG_LONG_ARRAY, name) + buffer.write(struct.pack('>i', len(values))) + for val in values: + buffer.write(struct.pack('>q', to_signed_long(val))) + + +def write_byte_array(buffer, name, data): + write_tag_header(buffer, 7, name) + buffer.write(struct.pack('>i', len(data))) + buffer.write(data) + + +def start_compound(buffer, name): + write_tag_header(buffer, TAG_COMPOUND, name) + + +def end_compound(buffer): + buffer.write(bytes([TAG_END])) + + +def write_list(buffer, name, tag_type, items, payload_writer=None): + write_tag_header(buffer, TAG_LIST, name) + if not items: + buffer.write(bytes([TAG_END])) + buffer.write(struct.pack('>i', 0)) + return + buffer.write(bytes([tag_type])) + buffer.write(struct.pack('>i', len(items))) + for item in items: + payload_writer(buffer, item) + + +def write_palette_entry(buffer, entry): + write_string_tag(buffer, "Name", entry["Name"]) + props = entry.get("Properties") + if props: + start_compound(buffer, "Properties") + for key, value in props.items(): + write_string_tag(buffer, key, value) + end_compound(buffer) + buffer.write(bytes([TAG_END])) + + +def write_section(buffer, section): + write_byte(buffer, "Y", section["Y"]) + write_long_array(buffer, "BlockStates", section["BlockStates"]) + write_list(buffer, "Palette", TAG_COMPOUND, section["Palette"], write_palette_entry) + write_byte_array(buffer, "BlockLight", section["BlockLight"]) + write_byte_array(buffer, "SkyLight", section["SkyLight"]) + buffer.write(bytes([TAG_END])) + + +def chunk_to_nbt(chunk): + buffer = BytesIO() + start_compound(buffer, "") + write_int(buffer, "DataVersion", DATA_VERSION) + + start_compound(buffer, "Level") + write_string_tag(buffer, "Status", "full") + write_long(buffer, "InhabitedTime", 0) + write_long(buffer, "LastUpdate", 0) + write_int(buffer, "xPos", chunk["chunk_x"]) + write_int(buffer, "zPos", chunk["chunk_z"]) + write_byte(buffer, "isLightOn", 1) + + start_compound(buffer, "Heightmaps") + write_long_array(buffer, "MOTION_BLOCKING", chunk["heightmap_packed"]) + end_compound(buffer) + + write_int_array(buffer, "Biomes", chunk["biomes"]) + write_list(buffer, "Sections", TAG_COMPOUND, chunk["sections"], write_section) + write_list(buffer, "Entities", TAG_COMPOUND, []) + write_list(buffer, "TileEntities", TAG_COMPOUND, []) + write_list(buffer, "TileTicks", TAG_COMPOUND, []) + end_compound(buffer) + + end_compound(buffer) + return buffer.getvalue() + + +def assemble_chunk_bytes(chunk): + chunk["heightmap_packed"] = pack_heightmap(chunk["heightmap"]) + return chunk_to_nbt(chunk) + + +def chunk_range(center, radius): + block_min = center - radius + block_max = center + radius + chunk_min = math.floor(block_min / 16) + chunk_max = math.floor(block_max / 16) + return chunk_min, chunk_max + + +def write_region_file(path, chunks): + offsets = bytearray(4096) + timestamps = bytearray(4096) + body = bytearray() + sector = 2 + + for index, chunk_bytes in chunks.items(): + compressed = zlib.compress(chunk_bytes) + payload = struct.pack('>I', len(compressed) + 1) + bytes([2]) + compressed + padding = (-len(payload)) % 4096 + if padding: + payload += b'\x00' * padding + sectors = len(payload) // 4096 + + offsets[index * 4:index * 4 + 3] = (sector.to_bytes(3, 'big')) + offsets[index * 4 + 3] = sectors + timestamps[index * 4:index * 4 + 4] = struct.pack('>I', int(time.time())) + + body.extend(payload) + sector += sectors + + data = bytearray(8192) + data[0:4096] = offsets + data[4096:8192] = timestamps + data.extend(body) + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + +def export_region_chunks(chunk_map, output_dir): + regions = {} + for (chunk_x, chunk_z), chunk_bytes in chunk_map.items(): + region_x = chunk_x >> 5 + region_z = chunk_z >> 5 + local_x = chunk_x - (region_x << 5) + local_z = chunk_z - (region_z << 5) + index = local_x + local_z * 32 + regions.setdefault((region_x, region_z), {})[index] = chunk_bytes + + for (region_x, region_z), chunks in regions.items(): + file_name = f"r.{region_x}.{region_z}.mca" + write_region_file(output_dir / file_name, chunks) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Export Minecraft chunks to MCA around spawn") + parser.add_argument("--radius", type=int, default=DEFAULT_RADIUS, + help="Export radius in blocks (default: %(default)s)") + parser.add_argument("--output", type=Path, default=Path("exports/mca"), + help="Destination directory for region files") + parser.add_argument("--center-x", type=int, default=None, + help="Override center X position (default: player spawn)") + parser.add_argument("--center-z", type=int, default=None, + help="Override center Z position (default: player spawn)") + parser.add_argument("--workers", type=int, default=os.cpu_count() or 1, + help="Number of worker threads (default: %(default)s)") + parser.add_argument("--minecraft-save", action="store_true", + help="Export to Minecraft saves folder (deletes old region files)") + return parser.parse_args() + + +def clean_minecraft_region_folder(region_path): + """Delete all .mca files in the region folder.""" + region_path = Path(region_path) + if not region_path.exists(): + region_path.mkdir(parents=True, exist_ok=True) + return + + mca_files = list(region_path.glob("*.mca")) + for mca_file in mca_files: + mca_file.unlink() + print(f"Deleted {mca_file}") + + +def process_chunk(args): + chunk_x, chunk_z = args + chunk_data = build_chunk(chunk_x, chunk_z) + chunk_bytes = assemble_chunk_bytes(chunk_data) + return chunk_x, chunk_z, chunk_bytes + + +def main(): + args = parse_args() + center_x = args.center_x if args.center_x is not None else int(PLAYER_POS.x) + center_z = args.center_z if args.center_z is not None else int(PLAYER_POS.z) + radius = args.radius + + # Determine output directory + if args.minecraft_save: + output_dir = Path.home() / ".minecraft" / "saves" / "New World (2)" / "region" + print(f"Cleaning Minecraft region folder: {output_dir}") + clean_minecraft_region_folder(output_dir) + else: + output_dir = args.output + + chunk_x_min, chunk_x_max = chunk_range(center_x, radius) + chunk_z_min, chunk_z_max = chunk_range(center_z, radius) + + chunk_coords = [ + (chunk_x, chunk_z) + for chunk_z in range(chunk_z_min, chunk_z_max + 1) + for chunk_x in range(chunk_x_min, chunk_x_max + 1) + ] + + chunk_bytes_map = {} + total = len(chunk_coords) + + if args.workers <= 1: + iterable = map(process_chunk, chunk_coords) + else: + executor = ThreadPoolExecutor(max_workers=args.workers) + futures = [executor.submit(process_chunk, coord) for coord in chunk_coords] + iterable = (future.result() for future in as_completed(futures)) + + try: + for processed, (chunk_x, chunk_z, chunk_bytes) in enumerate(iterable, start=1): + chunk_bytes_map[(chunk_x, chunk_z)] = chunk_bytes + if processed % 20 == 0 or processed == total: + print(f"Processed {processed}/{total} chunks") + finally: + if 'executor' in locals(): + executor.shutdown(wait=True) + + export_region_chunks(chunk_bytes_map, output_dir) + print(f"Export complete. Files written to {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/mc.py b/mc.py new file mode 100644 index 0000000..2055a93 --- /dev/null +++ b/mc.py @@ -0,0 +1,709 @@ +import math +import random +from opensimplex import OpenSimplex # OpenSimplex noise for smooth, terrain-like variation + +# --------------------------------------------------------------------------- +# BASIC WORLD CONSTANTS +# --------------------------------------------------------------------------- +# Approximate water surface level (everything below may become ocean/lake) +sea_level = 70 +# World seed: change this to get a completely different world layout, +# but the same seed always produces the same terrain. +world_seed = 123456 +# Create a global OpenSimplex noise generator. +# This object gives us deterministic noise values from coordinates + seed. +noise = OpenSimplex(world_seed) +# Cache column heights so repeated lookups (like water detection) stay cheap. +_height_cache = {} +# Cache derived column data (height + water surface) so generateBlock doesn't +# recompute expensive basin detection for every y in the same column. +_column_data_cache = {} +# Base biome temperatures (you already had this) +biome_base_temps = { + "desert": 110, + "plains": 80, + "plains_river": 80, + "rolling_hills": 75, +} +# Map block "names" to numeric IDs. +# These IDs are what your engine or save format actually uses internally. +# You can change these numbers to match your own block registry. +block_ids = { + "bedrock": 0, + "stone": 1, + "dirt": 2, + "grass": 3, + "water": 4, + "air": 5, + "oak_log": 6, + "oak_leaves": 7, + "birch_log": 8, + "birch_leaves": 9, + "coal": 10, +} + +# --------------------------------------------------------------------------- +# ORE GENERATION +# --------------------------------------------------------------------------- +class OreGeneration: + """Namespace for all ore generation systems""" + + class CoalSeams: + """Coal seam generation - each seam type is independent and combinable""" + + # --- Atomic Helper Functions --- + + @staticmethod + def _check_point_in_seam(y, seam_center, half_thickness): + """Atomic: Check if y coordinate is within seam vertical bounds""" + return abs(y - seam_center) <= half_thickness + + @staticmethod + def _calculate_seam_offset(x, y, z, scale=0.02, amplitude=2.0): + """Atomic: Calculate vertical offset for seam undulation using 3D noise""" + return noise.noise3(x * scale, y * scale, z * scale) * amplitude + + @staticmethod + def _calculate_thickness_variation(x, z, scale=0.03, amplitude=1.0): + """Atomic: Calculate thickness variation across horizontal plane using 2D noise""" + return noise.noise2(x * scale, z * scale) * amplitude + + @staticmethod + def _check_seam_continuity(x, y, z, threshold=-0.7): + """Atomic: Determine if seam is solid or pinched out at this point""" + continuity_noise = noise.noise3(x * 0.05, y * 0.05, z * 0.05) + return continuity_noise > threshold + + @staticmethod + def _check_seam(x, y, z, center, thickness, undulation_scale=0.02, undulation_amp=2.0, + thickness_scale=0.03, thickness_amp=1.0, continuity_threshold=-0.7): + """ + Generic seam checker - all seam types use this. + + Args: + center: Base y-level for seam center + thickness: Base thickness in blocks + undulation_scale/amp: Control vertical wave pattern + thickness_scale/amp: Control horizontal thickness variation + continuity_threshold: Controls gap frequency (higher = fewer gaps) + """ + seam_offset = OreGeneration.CoalSeams._calculate_seam_offset(x, y, z, undulation_scale, undulation_amp) + adjusted_center = center + seam_offset + + thickness_variation = OreGeneration.CoalSeams._calculate_thickness_variation(x, z, thickness_scale, thickness_amp) + adjusted_thickness = thickness + thickness_variation + half_thickness = adjusted_thickness / 2.0 + + if OreGeneration.CoalSeams._check_point_in_seam(y, adjusted_center, half_thickness): + if OreGeneration.CoalSeams._check_seam_continuity(x, y, z, continuity_threshold): + return True + return False + + @staticmethod + def _check_flat_ceiling_seam(x, y, z, column_height, ceiling_depth, thickness, + thickness_scale=0.03, thickness_amp=1.0, continuity_threshold=-0.7): + """ + Check for seam with flat ceiling and flat floor (strip mining incentive). + + Geometry: + - Ceiling (top): flat at column_height - ceiling_depth + - Floor (bottom): flat at column_height - ceiling_depth - thickness + - Ore exposure: wavy top surface via continuity check + + Args: + column_height: Terrain surface height at this x,z + ceiling_depth: Fixed depth below surface for seam ceiling + thickness: Fixed thickness in blocks (no variation) + thickness_scale/amp: Unused (kept for compatibility) + continuity_threshold: Controls gap frequency (higher = fewer gaps) + """ + seam_ceiling = column_height - ceiling_depth + seam_floor = seam_ceiling - thickness + + if seam_floor <= y <= seam_ceiling: + if OreGeneration.CoalSeams._check_seam_continuity(x, y, z, continuity_threshold): + return True + return False + + @staticmethod + def _check_terrain_relative_seam(x, y, z, column_height, depth_below_surface, thickness, + thickness_scale=0.03, thickness_amp=1.0, continuity_threshold=-0.7): + """ + Check for seam at fixed depth below terrain surface (perfect for strip mining). + + Args: + column_height: The terrain surface height at this x,z + depth_below_surface: How many blocks below surface the seam center is + thickness: Base thickness in blocks + thickness_scale/amp: Control horizontal thickness variation + continuity_threshold: Controls gap frequency (higher = fewer gaps) + """ + # Seam follows terrain - always at same depth below surface + seam_center = column_height - depth_below_surface + + thickness_variation = OreGeneration.CoalSeams._calculate_thickness_variation(x, z, thickness_scale, thickness_amp) + adjusted_thickness = thickness + thickness_variation + half_thickness = adjusted_thickness / 2.0 + + if OreGeneration.CoalSeams._check_point_in_seam(y, seam_center, half_thickness): + if OreGeneration.CoalSeams._check_seam_continuity(x, y, z, continuity_threshold): + return True + return False + + # --- Individual Seam Types (Combinable) --- + + @staticmethod + def deep_seam(x, y, z): + """Deep seam (y=10-15): 5 blocks thick, for room-and-pillar underground mining""" + return OreGeneration.CoalSeams._check_seam(x, y, z, center=12, thickness=5) + + @staticmethod + def medium_seam(x, y, z): + """Medium seam (y=30-35): 4 blocks thick, standard underground mining depth""" + return OreGeneration.CoalSeams._check_seam(x, y, z, center=32, thickness=4) + + @staticmethod + def shallow_mountain_seam(x, y, z): + """Shallow seam (y=80-90): 3 blocks thick, exposed on mountains for strip/MTR mining""" + return OreGeneration.CoalSeams._check_seam(x, y, z, center=85, thickness=3) + + @staticmethod + def shallow_plains_seam(x, y, z): + """Shallow plains seam (y=45): 2 blocks thick, less undulation""" + return OreGeneration.CoalSeams._check_seam( + x, y, z, center=45, thickness=2, + undulation_scale=0.015, undulation_amp=1.5, + thickness_scale=0.04, thickness_amp=0.5, + continuity_threshold=-0.6 # More continuous + ) + + @staticmethod + def medium_plains_seam(x, y, z): + """Medium plains seam (y=25): 3 blocks thick, less undulation""" + return OreGeneration.CoalSeams._check_seam( + x, y, z, center=25, thickness=3, + undulation_scale=0.015, undulation_amp=1.5, + thickness_scale=0.04, thickness_amp=0.5, + continuity_threshold=-0.6 # More continuous + ) + + @staticmethod + def surface_strip_seam(x, y, z, column_height): + """ + Strip mining seam with flat ceiling at column_height - 5. + West Kentucky accurate: 1 block thick, highly continuous. + Incentivizes leveling terrain to access the ore layer. + """ + return OreGeneration.CoalSeams._check_flat_ceiling_seam( + x, y, z, column_height, + ceiling_depth=5, + thickness=1, + thickness_scale=0.02, + thickness_amp=0.3, + continuity_threshold=-0.3 # Very high continuity, fewer gaps + ) + + # --- Biome Configurations (which seams are active) --- + + BIOME_SEAMS = { + "appalachian": ["deep_seam", "medium_seam", "shallow_mountain_seam", "surface_strip_seam"], + "plains": ["medium_plains_seam", "shallow_plains_seam", "surface_strip_seam"], + "desert": ["deep_seam"], # Only deep seams in desert + "mixed": ["deep_seam", "medium_seam", "medium_plains_seam", "surface_strip_seam"], + } + + @staticmethod + def generate_coal(x, y, z, column_height=None, biome="appalachian"): + """ + Main entry point for coal generation. + Checks all seam types active for the given biome. + + Args: + x, y, z: Block coordinates + column_height: Terrain surface height (needed for terrain-relative seams) + biome: Biome name (e.g., "appalachian", "plains", "desert") + + Returns: + True if coal should be placed at this location, False otherwise + """ + # Get seam types for this biome (default to appalachian) + seam_types = OreGeneration.CoalSeams.BIOME_SEAMS.get(biome, ["deep_seam", "medium_seam", "shallow_mountain_seam", "surface_strip_seam"]) + + # Check each active seam type + for seam_name in seam_types: + seam_func = getattr(OreGeneration.CoalSeams, seam_name, None) + if seam_func: + # Handle terrain-relative seams that need column_height + if seam_name == "surface_strip_seam": + if column_height is not None and seam_func(x, y, z, column_height): + return True + else: + # Standard fixed-depth seams + if seam_func(x, y, z): + return True + + return False + +# --------------------------------------------------------------------------- +# WORLD GENERATION +# --------------------------------------------------------------------------- +class worldGen: + # minimum bowl depth needed before we consider it a lake candidate + BASIN_MIN_DEPTH = 4 + # how far around a column we scan to estimate its bowl rim + BASIN_RADIUS = 4 + # Tree placement tuning + TREE_GRID_SPACING = 6 # coarse grid to enforce spacing + TREE_MARGIN = 3 # how far outside the chunk we look for trunks (so crowns don't cut off) + MIN_TREE_ALT = sea_level - 2 + LOW_FADE_TOP = sea_level + 6 + TREE_LINE = sea_level + 60 + TREE_LINE_FADE = 20 + MAX_SLOPE = 2 + + @staticmethod + def getBlockID(block_name: str) -> int: + """ + Convert a human-readable block name (e.g. "grass") into a numeric ID. + If the name isn't found, we default to "air" so we don't crash. + In a real engine, you'd probably want to raise an error instead. + """ + return block_ids.get(block_name, block_ids["air"]) + + @staticmethod + def _get_column_height(x: int, z: int) -> int: + key = (x, z) + if key in _height_cache: + return _height_cache[key] + + # DOMAIN WARPING - Keep this the same + warp1_x = x + noise.noise2(x * 0.001, z * 0.001) * 50 + warp1_z = z + noise.noise2((x+1000) * 0.001, (z+1000) * 0.001) * 50 + + warp2_x = warp1_x + noise.noise2(warp1_x * 0.01, warp1_z * 0.01) * 10 + warp2_z = warp1_z + noise.noise2((warp1_x+500) * 0.01, (warp1_z+500) * 0.01) * 10 + base_height = sea_level - 5 + + height = base_height + + # KEEP THESE - Large scale features (mountains, valleys) + height += noise.noise2(warp2_x * 0.0005, warp2_z * 0.0005) * 80 + height += noise.noise2(warp2_x * 0.002, warp2_z * 0.002) * 40 + height += noise.noise2(warp2_x * 0.005, warp2_z * 0.005) * 25 + height += noise.noise2(warp2_x * 0.01, warp2_z * 0.01) * 15 + + # REDUCE OR REMOVE THESE - Small scale bumps and roughness + height += noise.noise2(warp2_x * 0.02, warp2_z * 0.02) * 5 # Was 10 + height += noise.noise2(warp2_x * 0.05, warp2_z * 0.05) * 2 # Was 6 + height += noise.noise2(warp2_x * 0.1, warp2_z * 0.1) * 1 # Was 3 + # height += noise.noise2(warp2_x * 0.2, warp2_z * 0.2) * 1 # REMOVE - too rough + + _height_cache[key] = int(height) + return int(height) + + + @staticmethod + def _estimate_basin_water_surface(x: int, z: int, column_height: int): + """Look for local bowls and return a rim height if one exists.""" + samples = [] + for dx in range(-worldGen.BASIN_RADIUS, worldGen.BASIN_RADIUS + 1): + for dz in range(-worldGen.BASIN_RADIUS, worldGen.BASIN_RADIUS + 1): + if dx == 0 and dz == 0: + continue + samples.append(worldGen._get_column_height(x + dx, z + dz)) + if not samples: + return None + rim_height = min(samples) + if rim_height - column_height < worldGen.BASIN_MIN_DEPTH: + return None + return rim_height + + @staticmethod + def _get_column_data(x: int, z: int): + key = (x, z) + cached = _column_data_cache.get(key) + if cached: + return cached + column_height = worldGen._get_column_height(x, z) + basin_surface = worldGen._estimate_basin_water_surface(x, z, column_height) + if basin_surface is not None: + water_surface_y = basin_surface + elif column_height < sea_level: + water_surface_y = sea_level + else: + water_surface_y = None + data = (column_height, water_surface_y) + _column_data_cache[key] = data + return data + + @staticmethod + def generateBlock(x: int, y: int, z: int) -> int: + """ + Determine which block ID should exist at world coordinates (x, y, z). + This uses: + - A noise-based heightmap (via OpenSimplex) to decide terrain surface. + - Simple layering rules for stone/dirt/grass. + - sea_level to decide where water goes. + """ + # ------------------------------------------------------------------- + # 1. BEDROCK LAYER + # ------------------------------------------------------------------- + # We force a solid bedrock floor at the bottom of the world. + # You can change "0" to something else if you want a thicker bedrock band. + if y == 0: + return worldGen.getBlockID("bedrock") + # ------------------------------------------------------------------- + # 2. TERRAIN HEIGHT FOR THIS (x, z) COLUMN + # ------------------------------------------------------------------- + column_height, water_surface_y = worldGen._get_column_data(x, z) + # ------------------------------------------------------------------- + # 3. WATER LEVEL LOGIC + # ------------------------------------------------------------------- + # First, try finding a local bowl and fill it to the rim. If no bowl + # exists, fall back to the global sea_level rule for oceans. + # (handled inside _get_column_data now) + # ------------------------------------------------------------------- + # 4. BLOCK SELECTION BY VERTICAL POSITION + # ------------------------------------------------------------------- + # We now choose the block type based on how y compares to: + # - column_height (terrain surface) + # - water_surface_y (ocean/lake surface) + # + # Basic layering: + # - Bedrock at y <= 0 (we already handled this above) + # - From y=1 up to terrain surface - 4: stone + # - Next 3 layers under the surface: dirt + # - Surface: grass (if above water), or sand/etc. if you add biomes later + # - Between terrain surface and water surface: water + # - Above water surface: air + # 4a. Terrain below the ground surface => underground blocks + if y < column_height - 3: + # Check for coal seams first (pass column_height for terrain-relative seams) + if OreGeneration.generate_coal(x, y, z, column_height=column_height, biome="appalachian"): + return worldGen.getBlockID("coal") + # Deep underground: solid stone + return worldGen.getBlockID("stone") + if y < column_height: + # Just under the surface: a few layers of dirt + return worldGen.getBlockID("dirt") + # 4b. Exactly at the terrain surface + if y == column_height: + # If this position is also *below* sea level, we might prefer sand or + # some other block later depending on biome and proximity to water. + # For now, always grass as your basic "land" surface. + return worldGen.getBlockID("grass") + # 4c. Above the terrain surface but below water surface => water column + if water_surface_y is not None and y <= water_surface_y and y > column_height: + # This is underwater. A simple ocean/lake fill. + return worldGen.getBlockID("water") + # 4d. Everything else is air + return worldGen.getBlockID("air") + + @staticmethod + def _can_place_tree(x: int, y: int, z: int, height: int) -> bool: + """ + Check if there's enough space to place a tree. + - Must be on grass block + - Must have air above for tree height + crown + """ + ground_block = worldGen.generateBlock(x, y - 1, z) + if ground_block != worldGen.getBlockID("grass"): + return False + for check_y in range(y, y + height + 3): + if worldGen.generateBlock(x, check_y, z) != worldGen.getBlockID("air"): + return False + return True + + @staticmethod + def _tree_density_mask(x: int, z: int) -> float: + """Blend two low-frequency noises to drive forest clumping.""" + forest = noise.noise2((x + 3000) * 0.01, (z - 3000) * 0.01) + moisture = noise.noise2((x - 5000) * 0.02, (z + 5000) * 0.02) + return (forest * 0.6 + moisture * 0.4 + 1.0) * 0.5 # normalize to ~0..1 + + @staticmethod + def _ground_slope(x: int, z: int) -> int: + center = worldGen._get_column_height(x, z) + max_delta = 0 + for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)): + neighbor = worldGen._get_column_height(x + dx, z + dz) + max_delta = max(max_delta, abs(neighbor - center)) + return max_delta + + @staticmethod + def _generate_oak_tree(x: int, y: int, z: int, variation: int = 0, rng: random.Random = None): + """ + Generate an oak tree structure. + Returns a list of (x, y, z, block_id) tuples. + + variation: 0-2 for small/medium/large trees + """ + if rng is None: + rng = random + blocks = [] + log_id = worldGen.getBlockID("oak_log") + leaf_id = worldGen.getBlockID("oak_leaves") + trunk_height = 4 + variation + rng.randint(0, 2) + # Build trunk + for dy in range(trunk_height): + blocks.append((x, y + dy, z, log_id)) + # Crown starting height + crown_start = y + trunk_height - 2 + crown_top = y + trunk_height + 2 + # Build leaf crown (roughly spherical) + for cy in range(crown_start, crown_top): + distance_from_center = abs(cy - (crown_start + 2)) + if distance_from_center == 0: + radius = 2 + elif distance_from_center == 1: + radius = 2 + elif distance_from_center == 2: + radius = 1 + else: + radius = 1 + # Place leaves in a circle + for dx in range(-radius, radius + 1): + for dz in range(-radius, radius + 1): + if abs(dx) == radius and abs(dz) == radius: + if rng.random() > 0.3: + continue + if dx == 0 and dz == 0 and cy < y + trunk_height: + continue + blocks.append((x + dx, cy, z + dz, leaf_id)) + return blocks + + @staticmethod + def _generate_birch_tree(x: int, y: int, z: int, rng: random.Random = None): + """ + Generate a birch tree structure (taller, thinner than oak). + Returns a list of (x, y, z, block_id) tuples. + """ + if rng is None: + rng = random + blocks = [] + log_id = worldGen.getBlockID("birch_log") + leaf_id = worldGen.getBlockID("birch_leaves") + trunk_height = 5 + rng.randint(0, 3) + # Build trunk + for dy in range(trunk_height): + blocks.append((x, y + dy, z, log_id)) + # Smaller, higher crown + crown_start = y + trunk_height - 1 + crown_top = y + trunk_height + 2 + for cy in range(crown_start, crown_top): + distance_from_top = crown_top - cy + if distance_from_top <= 1: + radius = 1 + else: + radius = 2 + for dx in range(-radius, radius + 1): + for dz in range(-radius, radius + 1): + if abs(dx) + abs(dz) > radius + 1: + continue + if dx == 0 and dz == 0 and cy < y + trunk_height: + continue + blocks.append((x + dx, cy, z + dz, leaf_id)) + return blocks + + @staticmethod + def generateTree(x: int, y: int, z: int, tree_type: str = "oak", variation: int = None, rng: random.Random = None): + """ + Main tree generation function. + + Args: + x, y, z: Base coordinates for tree trunk + tree_type: "oak" or "birch" + variation: For oak trees, 0-2 for size variation + rng: optional random.Random for deterministic per-tree variation + + Returns: + List of (x, y, z, block_id) tuples, or None if can't place + """ + if rng is None: + rng = random + if tree_type == "oak": + if variation is None: + variation = rng.randint(0, 2) + if not worldGen._can_place_tree(x, y, z, 4 + variation): + return None + return worldGen._generate_oak_tree(x, y, z, variation, rng) + elif tree_type == "birch": + if not worldGen._can_place_tree(x, y, z, 6): + return None + return worldGen._generate_birch_tree(x, y, z, rng) + return None + + @staticmethod + def generate_chunk_trees(chunk_x: int, chunk_z: int): + """ + Populate trees for a chunk (including a small margin so crowns don't cut off). + Returns a dict keyed by (x, y, z) with block IDs to overlay on terrain. + """ + tree_blocks = {} + grid = worldGen.TREE_GRID_SPACING + margin = worldGen.TREE_MARGIN + chunk_min_x = chunk_x * 16 - margin + chunk_max_x = chunk_min_x + 16 + 2 * margin - 1 + chunk_min_z = chunk_z * 16 - margin + chunk_max_z = chunk_min_z + 16 + 2 * margin - 1 + + grid_min_x = math.floor(chunk_min_x / grid) + grid_max_x = math.floor(chunk_max_x / grid) + grid_min_z = math.floor(chunk_min_z / grid) + grid_max_z = math.floor(chunk_max_z / grid) + + for gx in range(grid_min_x, grid_max_x + 1): + for gz in range(grid_min_z, grid_max_z + 1): + rng = random.Random(world_seed + gx * 341873128712 + gz * 132897987541) + candidate_x = gx * grid + rng.randint(0, grid - 1) + candidate_z = gz * grid + rng.randint(0, grid - 1) + # Only consider trunks that could affect this chunk (with margin) + if not (chunk_min_x <= candidate_x <= chunk_max_x and chunk_min_z <= candidate_z <= chunk_max_z): + continue + + column_height, water_surface_y = worldGen._get_column_data(candidate_x, candidate_z) + # Avoid underwater or flooded tiles + if water_surface_y is not None and column_height < water_surface_y: + continue + # Elevation gates + if column_height < worldGen.MIN_TREE_ALT or column_height > worldGen.TREE_LINE: + continue + # Slope gate to keep cliffs and sharp ridges clear + if worldGen._ground_slope(candidate_x, candidate_z) > worldGen.MAX_SLOPE: + continue + + altitude_factor = 1.0 + # Fade near shore/marsh + if column_height < worldGen.LOW_FADE_TOP: + fade_span = max(1, worldGen.LOW_FADE_TOP - worldGen.MIN_TREE_ALT) + altitude_factor *= max(0.0, (column_height - worldGen.MIN_TREE_ALT) / fade_span) + # Fade near tree line + if column_height > worldGen.TREE_LINE - worldGen.TREE_LINE_FADE: + altitude_factor *= max(0.0, (worldGen.TREE_LINE - column_height) / worldGen.TREE_LINE_FADE) + if altitude_factor <= 0: + continue + + density = worldGen._tree_density_mask(candidate_x, candidate_z) + base_prob = 0.5 + spawn_prob = base_prob * (0.6 + 0.4 * density) * altitude_factor + if rng.random() > spawn_prob: + continue + + # Cooler, higher ground skews to birch + if column_height > sea_level + 35: + tree_type = "birch" if rng.random() < 0.7 else "oak" + elif column_height > sea_level + 15: + tree_type = "oak" if rng.random() < 0.7 else "birch" + else: + tree_type = "oak" + + tree = worldGen.generateTree( + candidate_x, + column_height + 1, + candidate_z, + tree_type=tree_type, + variation=None, + rng=rng, + ) + if not tree: + continue + + for bx, by, bz, block_id in tree: + # Only apply blocks that fall inside this chunk + if chunk_x * 16 <= bx <= chunk_x * 16 + 15 and chunk_z * 16 <= bz <= chunk_z * 16 + 15: + if 0 <= by < 256: + tree_blocks[(bx, by, bz)] = block_id + return tree_blocks + +# --------------------------------------------------------------------------- +# BIOME / TEMPERATURE FUNCTIONS (your original, with a small bug fix) +# --------------------------------------------------------------------------- +class biomeFuncs: + @staticmethod + def get_current_block_tmep(x, y, z): + # TODO: You can later use height, biome, time, etc. to get the exact + # temperature at this block. For now it's just a stub. + #remember to account for things getting cooler/hotter as we dig. + pass + + @staticmethod + def get_current_biome_temp(biome, time, season): + """ + Compute the current temperature for a given biome at a given time/season. + NOTE: I fixed a small bug: + - You were looping "for biome in biome_base_temps" which overwrote + the biome parameter and always ended with the last one. + - Now we just look up the base temp directly from the biome argument. + """ + # Get base temperature for the biome (e.g. plains: 80) + base_temp = biome_base_temps.get(biome, 70) # default 70 if biome unknown + # Calculate weather adjustments based on biome and season + standardWeatherAdjustment = 0 + weatherVariabilityFactor = 0 + if biome == "desert": + if season == "winter": + standardWeatherAdjustment = -30 + elif season == "fall": + standardWeatherAdjustment = -10 + elif season == "summer": + standardWeatherAdjustment = +10 + elif season == "spring": + standardWeatherAdjustment = 0 + weatherVariabilityFactor = random.randint(-20, 20) + elif biome == "plains": + if season == "winter": + standardWeatherAdjustment = -25 + elif season == "fall": + standardWeatherAdjustment = -5 + elif season == "summer": + standardWeatherAdjustment = +5 + elif season == "spring": + standardWeatherAdjustment = 0 + weatherVariabilityFactor = random.randint(-15, 15) + elif biome == "plains_river": + if season == "winter": + standardWeatherAdjustment = -20 + elif season == "fall": + standardWeatherAdjustment = -3 + elif season == "summer": + standardWeatherAdjustment = +3 + elif season == "spring": + standardWeatherAdjustment = 0 + weatherVariabilityFactor = random.randint(-10, 10) + elif biome == "rolling_hills": + if season == "winter": + standardWeatherAdjustment = -28 + elif season == "fall": + standardWeatherAdjustment = -7 + elif season == "summer": + standardWeatherAdjustment = +7 + elif season == "spring": + standardWeatherAdjustment = 0 + weatherVariabilityFactor = random.randint(-18, 18) + # ------------------------------------------------------------------- + # TIME-OF-DAY TEMPERATURE ADJUSTMENT (your original logic) + # ------------------------------------------------------------------- + time_adjustment = 0 + # Convert time (0–24000 ticks) into a "Minecraft hour" in [0, 24). + # 0 ticks = 6am, 6000 = 12pm, 12000 = 6pm, 18000 = 12am + minecraft_hour = (time / 1000 + 6) % 24 + if 6 <= minecraft_hour < 14: # Morning to peak heat + time_progress = (minecraft_hour - 6) / 8 # 0..1 + time_adjustment = time_progress * 15 # 0..+15 + elif 14 <= minecraft_hour < 20: # Afternoon to evening + time_progress = (minecraft_hour - 14) / 6 # 0..1 + time_adjustment = 15 - (time_progress * 12) # +15..+3 + elif 20 <= minecraft_hour < 24: # Evening to midnight + time_progress = (minecraft_hour - 20) / 4 # 0..1 + time_adjustment = 3 - (time_progress * 8) # +3..-5 + else: # Midnight to morning (0-6) + time_progress = minecraft_hour / 6 # 0..1 + time_adjustment = -5 + (time_progress * 5) # -5..0 + # Apply biome-specific time modifiers + if biome == "desert": + time_adjustment *= 1.5 # Deserts have more extreme swings + elif biome == "plains_river": + time_adjustment *= 0.7 # Rivers moderate temperature changes + # Final temperature + temp = base_temp + standardWeatherAdjustment + weatherVariabilityFactor + time_adjustment + return temp diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..2e1c525 --- /dev/null +++ b/settings.py @@ -0,0 +1,83 @@ +from numba import njit +import numpy as np +import glm +import math + +# OpenGL settings +MAJOR_VER, MINOR_VER = 3, 3 +DEPTH_SIZE = 24 +NUM_SAMPLES = 1 # antialiasing + +# resolution +WIN_RES = glm.vec2(1600, 900) + +# world generation +SEED = 16 + +# ray casting +MAX_RAY_DIST = 6 + +# chunk +CHUNK_SIZE = 48 +H_CHUNK_SIZE = CHUNK_SIZE // 2 +CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE +CHUNK_VOL = CHUNK_AREA * CHUNK_SIZE +CHUNK_SPHERE_RADIUS = H_CHUNK_SIZE * math.sqrt(3) + +# world +WORLD_W, WORLD_H = 20, 2 +WORLD_D = WORLD_W +WORLD_AREA = WORLD_W * WORLD_D +WORLD_VOL = WORLD_AREA * WORLD_H + +# world center +CENTER_XZ = WORLD_W * H_CHUNK_SIZE +CENTER_Y = WORLD_H * H_CHUNK_SIZE + +# camera +ASPECT_RATIO = WIN_RES.x / WIN_RES.y +FOV_DEG = 50 +V_FOV = glm.radians(FOV_DEG) # vertical FOV +H_FOV = 2 * math.atan(math.tan(V_FOV * 0.5) * ASPECT_RATIO) # horizontal FOV +NEAR = 0.1 +FAR = 2000.0 +PITCH_MAX = glm.radians(89) + +# player +PLAYER_SPEED = 0.005 +PLAYER_ROT_SPEED = 0.003 +# PLAYER_POS = glm.vec3(CENTER_XZ, WORLD_H * CHUNK_SIZE, CENTER_XZ) +PLAYER_POS = glm.vec3(CENTER_XZ, CHUNK_SIZE, CENTER_XZ) +MOUSE_SENSITIVITY = 0.002 + +# colors +BG_COLOR = glm.vec3(0.58, 0.83, 0.99) + +# textures +SAND = 1 +GRASS = 2 +DIRT = 3 +STONE = 4 +SNOW = 5 +LEAVES = 6 +WOOD = 7 + +# terrain levels +SNOW_LVL = 54 +STONE_LVL = 49 +DIRT_LVL = 40 +GRASS_LVL = 8 +SAND_LVL = 7 + +# tree settings +TREE_PROBABILITY = 0.02 +TREE_WIDTH, TREE_HEIGHT = 4, 8 +TREE_H_WIDTH, TREE_H_HEIGHT = TREE_WIDTH // 2, TREE_HEIGHT // 2 + +# water +WATER_LINE = 5.6 +WATER_AREA = 5 * CHUNK_SIZE * WORLD_W + +# cloud +CLOUD_SCALE = 25 +CLOUD_HEIGHT = WORLD_H * CHUNK_SIZE * 2 diff --git a/worldgen-c/Makefile b/worldgen-c/Makefile new file mode 100644 index 0000000..8e6a98c --- /dev/null +++ b/worldgen-c/Makefile @@ -0,0 +1,22 @@ +CC ?= gcc +CFLAGS ?= -O3 -march=native -std=c11 -Wall -Wextra -pedantic -Iinclude +LDFLAGS ?= -lm -pthread -lz + +SRC := src/main.c src/worldgen.c src/noise.c +OBJ := $(SRC:.c=.o) + +BIN_DIR := bin +TARGET := $(BIN_DIR)/worldgen + +all: $(TARGET) + +$(TARGET): $(OBJ) | $(BIN_DIR) + $(CC) $(CFLAGS) $(OBJ) -o $@ $(LDFLAGS) + +$(BIN_DIR): + mkdir -p $(BIN_DIR) + +clean: + rm -f $(OBJ) $(TARGET) + +.PHONY: all clean diff --git a/worldgen-c/bin/worldgen b/worldgen-c/bin/worldgen new file mode 100755 index 0000000000000000000000000000000000000000..850e7e997e03473c0f1d01272b1adfacc430cf63 GIT binary patch literal 102016 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%0A(?{kwox7H$HxZ>bSg`j+j8 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 z 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 HQgieJG~r5e literal 0 HcmV?d00001 diff --git a/worldgen-c/include/noise.h b/worldgen-c/include/noise.h new file mode 100644 index 0000000..e7dc1ad --- /dev/null +++ b/worldgen-c/include/noise.h @@ -0,0 +1,14 @@ +#ifndef WORLDGEN_NOISE_H +#define WORLDGEN_NOISE_H + +#include + +typedef struct { + int perm[512]; +} simplex_noise; + +void simplex_init(simplex_noise *noise, uint32_t seed); +double simplex_noise2(simplex_noise *noise, double x, double y); +double simplex_noise3(simplex_noise *noise, double x, double y, double z); + +#endif diff --git a/worldgen-c/include/worldgen.h b/worldgen-c/include/worldgen.h new file mode 100644 index 0000000..344d446 --- /dev/null +++ b/worldgen-c/include/worldgen.h @@ -0,0 +1,52 @@ +#ifndef WORLDGEN_H +#define WORLDGEN_H + +#include +#include +#include "noise.h" + +#define CHUNK_SIZE 16 +#define CHUNK_HEIGHT 256 + + enum BlockId { + BLOCK_BEDROCK = 0, + BLOCK_STONE = 1, + BLOCK_DIRT = 2, + BLOCK_GRASS = 3, + BLOCK_WATER = 4, + BLOCK_AIR = 5, + BLOCK_OAK_LOG = 6, + BLOCK_OAK_LEAVES = 7, + BLOCK_BIRCH_LOG = 8, + BLOCK_BIRCH_LEAVES = 9, + BLOCK_COAL = 10, + BLOCK_SAND = 11, + BLOCK_GRAVEL = 12, + BLOCK_SNOW = 13, + BLOCK_TALL_GRASS = 14 + }; + +struct trail_segment; + +typedef struct { + int chunk_x; + int chunk_z; + uint16_t heightmap[CHUNK_SIZE][CHUNK_SIZE]; + uint16_t blocks[CHUNK_HEIGHT][CHUNK_SIZE][CHUNK_SIZE]; +} chunk_data; + +typedef struct { + simplex_noise noise; + int sea_level; + int world_seed; + int enable_trails; + int snow_line; + struct trail_segment *trail_segments; + size_t trail_segment_count; + size_t trail_segment_cap; +} worldgen_ctx; + +void worldgen_init(worldgen_ctx *ctx, int world_seed, int sea_level, int snow_line); +void worldgen_generate_chunk(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *out); + +#endif diff --git a/worldgen-c/src/main.c b/worldgen-c/src/main.c new file mode 100644 index 0000000..fd92e2a --- /dev/null +++ b/worldgen-c/src/main.c @@ -0,0 +1,818 @@ +#include "worldgen.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +typedef struct { + int x; + int z; +} chunk_coord; + +typedef struct { + int chunk_x; + int chunk_z; + chunk_data data; +} completed_chunk; + +typedef struct { + completed_chunk *chunks; + size_t capacity; + _Atomic size_t count; + pthread_mutex_t mutex; +} results_buffer; + +typedef struct { + chunk_coord *items; + size_t count; + _Atomic size_t next_index; + _Atomic size_t done; +} job_queue; + +typedef struct { + job_queue *queue; + const char *out_dir; + int world_seed; + int sea_level; + int snow_line; + pthread_mutex_t *log_mu; + int enable_trails; + results_buffer *results; +} worker_args; + +typedef enum { + FORMAT_MCA, + FORMAT_BIN +} output_format; + +// --------------------------------------------------------------------------- +// Block state palette (fixed mapping for our limited block set) +// --------------------------------------------------------------------------- +typedef struct { + const char *key; + const char *value; +} kv_pair; + +typedef struct { + const char *name; + const kv_pair *props; + size_t prop_count; +} block_state; + +static const kv_pair PROPS_LOG_AXIS[] = {{"axis", "y"}}; +static const kv_pair PROPS_GRASS[] = {{"snowy", "false"}}; +static const kv_pair PROPS_WATER[] = {{"level", "0"}}; +static const kv_pair PROPS_LEAVES[] = {{"distance", "1"}, {"persistent", "false"}}; + +static const block_state BLOCK_STATE_TABLE[] = { + [BLOCK_BEDROCK] = {"minecraft:bedrock", NULL, 0}, + [BLOCK_STONE] = {"minecraft:stone", NULL, 0}, + [BLOCK_DIRT] = {"minecraft:dirt", NULL, 0}, + [BLOCK_GRASS] = {"minecraft:grass_block", PROPS_GRASS, 1}, + [BLOCK_WATER] = {"minecraft:water", PROPS_WATER, 1}, + [BLOCK_AIR] = {"minecraft:air", NULL, 0}, + [BLOCK_OAK_LOG] = {"minecraft:oak_log", PROPS_LOG_AXIS, 1}, + [BLOCK_OAK_LEAVES] = {"minecraft:oak_leaves", PROPS_LEAVES, 2}, + [BLOCK_BIRCH_LOG] = {"minecraft:birch_log", PROPS_LOG_AXIS, 1}, + [BLOCK_BIRCH_LEAVES] = {"minecraft:birch_leaves", PROPS_LEAVES, 2}, + [BLOCK_COAL] = {"minecraft:coal_ore", NULL, 0}, + [BLOCK_SAND] = {"minecraft:sand", NULL, 0}, + [BLOCK_GRAVEL] = {"minecraft:gravel", NULL, 0}, + [BLOCK_SNOW] = {"minecraft:snow", NULL, 0}, + [BLOCK_TALL_GRASS] = {"minecraft:grass", NULL, 0} +}; + +static const block_state *get_block_state(uint16_t id) { + if (id < sizeof(BLOCK_STATE_TABLE) / sizeof(BLOCK_STATE_TABLE[0]) && BLOCK_STATE_TABLE[id].name) { + return &BLOCK_STATE_TABLE[id]; + } + return &BLOCK_STATE_TABLE[BLOCK_AIR]; +} + +// --------------------------------------------------------------------------- +// Dynamic byte buffer helpers +// --------------------------------------------------------------------------- +typedef struct { + uint8_t *data; + size_t len; + size_t cap; +} buf; + +static void buf_reserve(buf *b, size_t extra) { + size_t need = b->len + extra; + if (need <= b->cap) return; + size_t new_cap = b->cap ? b->cap * 2 : 1024; + while (new_cap < need) new_cap *= 2; + uint8_t *new_data = (uint8_t *)realloc(b->data, new_cap); + if (!new_data) return; + b->data = new_data; + b->cap = new_cap; +} + +static void buf_put_u8(buf *b, uint8_t v) { + buf_reserve(b, 1); + b->data[b->len++] = v; +} + +static void buf_put_be16(buf *b, uint16_t v) { + buf_reserve(b, 2); + b->data[b->len++] = (uint8_t)(v >> 8); + b->data[b->len++] = (uint8_t)(v & 0xFF); +} + +static void buf_put_be32(buf *b, uint32_t v) { + buf_reserve(b, 4); + b->data[b->len++] = (uint8_t)(v >> 24); + b->data[b->len++] = (uint8_t)((v >> 16) & 0xFF); + b->data[b->len++] = (uint8_t)((v >> 8) & 0xFF); + b->data[b->len++] = (uint8_t)(v & 0xFF); +} + +static void buf_put_be64(buf *b, uint64_t v) { + buf_reserve(b, 8); + b->data[b->len++] = (uint8_t)(v >> 56); + b->data[b->len++] = (uint8_t)((v >> 48) & 0xFF); + b->data[b->len++] = (uint8_t)((v >> 40) & 0xFF); + b->data[b->len++] = (uint8_t)((v >> 32) & 0xFF); + b->data[b->len++] = (uint8_t)((v >> 24) & 0xFF); + b->data[b->len++] = (uint8_t)((v >> 16) & 0xFF); + b->data[b->len++] = (uint8_t)((v >> 8) & 0xFF); + b->data[b->len++] = (uint8_t)(v & 0xFF); +} + +static void buf_append(buf *b, const uint8_t *data, size_t n) { + buf_reserve(b, n); + memcpy(b->data + b->len, data, n); + b->len += n; +} + +// --------------------------------------------------------------------------- +// NBT helpers +// --------------------------------------------------------------------------- +enum { + TAG_END = 0, + TAG_BYTE = 1, + TAG_INT = 3, + TAG_LONG = 4, + TAG_STRING = 8, + TAG_LIST = 9, + TAG_COMPOUND = 10, + TAG_INT_ARRAY = 11, + TAG_LONG_ARRAY = 12 +}; + +static void nbt_write_string(buf *b, const char *s) { + size_t len = strlen(s); + if (len > 0xFFFF) len = 0xFFFF; + buf_put_be16(b, (uint16_t)len); + buf_append(b, (const uint8_t *)s, len); +} + +static void nbt_write_tag_header(buf *b, uint8_t tag, const char *name) { + buf_put_u8(b, tag); + nbt_write_string(b, name); +} + +static void nbt_write_byte(buf *b, const char *name, uint8_t value) { + nbt_write_tag_header(b, TAG_BYTE, name); + buf_put_u8(b, value); +} + +static void nbt_write_int(buf *b, const char *name, int32_t value) { + nbt_write_tag_header(b, TAG_INT, name); + buf_put_be32(b, (uint32_t)value); +} + +static void nbt_write_long(buf *b, const char *name, int64_t value) { + nbt_write_tag_header(b, TAG_LONG, name); + buf_put_be64(b, (uint64_t)value); +} + +static void nbt_write_string_tag(buf *b, const char *name, const char *value) { + nbt_write_tag_header(b, TAG_STRING, name); + nbt_write_string(b, value); +} + +static void nbt_write_int_array(buf *b, const char *name, const int32_t *vals, size_t count) { + nbt_write_tag_header(b, TAG_INT_ARRAY, name); + buf_put_be32(b, (uint32_t)count); + for (size_t i = 0; i < count; ++i) { + buf_put_be32(b, (uint32_t)vals[i]); + } +} + +static void nbt_write_long_array(buf *b, const char *name, const int64_t *vals, size_t count) { + nbt_write_tag_header(b, TAG_LONG_ARRAY, name); + buf_put_be32(b, (uint32_t)count); + for (size_t i = 0; i < count; ++i) { + buf_put_be64(b, (uint64_t)vals[i]); + } +} + +static void nbt_start_compound(buf *b, const char *name) { + nbt_write_tag_header(b, TAG_COMPOUND, name); +} + +static void nbt_end_compound(buf *b) { + buf_put_u8(b, TAG_END); +} + +static void nbt_start_list(buf *b, const char *name, uint8_t tag_type, int32_t length) { + nbt_write_tag_header(b, TAG_LIST, name); + buf_put_u8(b, tag_type); + buf_put_be32(b, (uint32_t)length); +} + +// --------------------------------------------------------------------------- +// Bit packing helpers +// --------------------------------------------------------------------------- +static int64_t to_signed64(uint64_t v) { + if (v <= INT64_MAX) return (int64_t)v; + return -(int64_t)((~v) + 1); +} + +static void pack_bits(const uint16_t *indices, size_t count, int bits_per_value, int64_t *out_longs, size_t out_count) { + memset(out_longs, 0, out_count * sizeof(int64_t)); + for (size_t idx = 0; idx < count; ++idx) { + uint64_t value = indices[idx]; + size_t bit_index = idx * (size_t)bits_per_value; + size_t long_id = bit_index / 64; + size_t offset = bit_index % 64; + uint64_t *target = (uint64_t *)&out_longs[long_id]; + *target |= value << offset; + int spill = (int)(offset + bits_per_value - 64); + if (spill > 0 && long_id + 1 < out_count) { + uint64_t *next = (uint64_t *)&out_longs[long_id + 1]; + *next |= value >> (bits_per_value - spill); + } + } +} + +// --------------------------------------------------------------------------- +// Chunk -> NBT helpers +// --------------------------------------------------------------------------- +static void pack_heightmap(const chunk_data *chunk, int64_t *out_longs, size_t out_count) { + uint16_t values[CHUNK_SIZE * CHUNK_SIZE]; + for (int z = 0; z < CHUNK_SIZE; ++z) { + for (int x = 0; x < CHUNK_SIZE; ++x) { + values[x + z * CHUNK_SIZE] = chunk->heightmap[x][z]; + } + } + pack_bits(values, CHUNK_SIZE * CHUNK_SIZE, 9, out_longs, out_count); + for (size_t i = 0; i < out_count; ++i) { + out_longs[i] = to_signed64((uint64_t)out_longs[i]); + } +} + +static int section_has_blocks(const chunk_data *chunk, int section_y) { + int y_base = section_y * 16; + for (int y = 0; y < 16; ++y) { + int gy = y_base + y; + if (gy >= CHUNK_HEIGHT) break; + for (int x = 0; x < CHUNK_SIZE; ++x) { + for (int z = 0; z < CHUNK_SIZE; ++z) { + if (chunk->blocks[gy][x][z] != BLOCK_AIR) { + return 1; + } + } + } + } + return 0; +} + +static void write_palette_entry(buf *b, const block_state *state) { + nbt_write_string_tag(b, "Name", state->name); + if (state->prop_count > 0) { + nbt_start_compound(b, "Properties"); + for (size_t i = 0; i < state->prop_count; ++i) { + nbt_write_string_tag(b, state->props[i].key, state->props[i].value); + } + nbt_end_compound(b); + } + buf_put_u8(b, TAG_END); // end of this palette entry compound +} + +static void write_section(buf *b, const chunk_data *chunk, int section_y) { + int palette_index[16]; + for (size_t i = 0; i < 16; ++i) palette_index[i] = -1; + const block_state *palette_states[16]; + uint16_t block_indices[4096]; + size_t palette_len = 0; + + int y_base = section_y * 16; + int idx = 0; + for (int y = 0; y < 16; ++y) { + int gy = y_base + y; + if (gy >= CHUNK_HEIGHT) break; + for (int z = 0; z < CHUNK_SIZE; ++z) { + for (int x = 0; x < CHUNK_SIZE; ++x) { + uint16_t bid = chunk->blocks[gy][x][z]; + if (palette_index[bid] == -1) { + palette_index[bid] = (int)palette_len; + palette_states[palette_len] = get_block_state(bid); + palette_len++; + } + block_indices[idx++] = (uint16_t)palette_index[bid]; + } + } + } + + int bits = 4; + if (palette_len > 1) { + int needed = (int)ceil(log2((double)palette_len)); + if (needed > bits) bits = needed; + } + size_t packed_count = ((size_t)idx * (size_t)bits + 63) / 64; + int64_t *packed = (int64_t *)calloc(packed_count, sizeof(int64_t)); + if (!packed) return; + pack_bits(block_indices, idx, bits, packed, packed_count); + + nbt_write_byte(b, "Y", (uint8_t)section_y); + nbt_write_long_array(b, "BlockStates", packed, packed_count); + + nbt_start_list(b, "Palette", TAG_COMPOUND, (int32_t)palette_len); + for (size_t i = 0; i < palette_len; ++i) { + write_palette_entry(b, palette_states[i]); + } + // Palette entries already include their end tags; list is fixed-length so no end marker here. + + // Lighting arrays (all zero) + uint8_t light[2048]; + memset(light, 0, sizeof(light)); + nbt_write_tag_header(b, 7, "BlockLight"); + buf_put_be32(b, (uint32_t)sizeof(light)); + buf_append(b, light, sizeof(light)); + nbt_write_tag_header(b, 7, "SkyLight"); + buf_put_be32(b, (uint32_t)sizeof(light)); + buf_append(b, light, sizeof(light)); + + nbt_end_compound(b); + free(packed); +} + +static void build_chunk_nbt(const chunk_data *chunk, buf *out) { + int32_t biomes[256]; + for (int i = 0; i < 256; ++i) biomes[i] = 1; // Plains biome + + int64_t heightmap[36]; + pack_heightmap(chunk, heightmap, 36); + + nbt_start_compound(out, ""); + nbt_write_int(out, "DataVersion", 2586); + + nbt_start_compound(out, "Level"); + nbt_write_string_tag(out, "Status", "full"); + nbt_write_long(out, "InhabitedTime", 0); + nbt_write_long(out, "LastUpdate", 0); + nbt_write_int(out, "xPos", chunk->chunk_x); + nbt_write_int(out, "zPos", chunk->chunk_z); + nbt_write_byte(out, "isLightOn", 1); + + nbt_start_compound(out, "Heightmaps"); + nbt_write_long_array(out, "MOTION_BLOCKING", heightmap, 36); + nbt_end_compound(out); + + nbt_write_int_array(out, "Biomes", biomes, 256); + + // Sections + int section_count = 0; + for (int sy = 0; sy < CHUNK_HEIGHT / 16; ++sy) { + if (section_has_blocks(chunk, sy)) section_count++; + } + nbt_start_list(out, "Sections", TAG_COMPOUND, section_count); + for (int sy = 0; sy < CHUNK_HEIGHT / 16; ++sy) { + if (!section_has_blocks(chunk, sy)) continue; + write_section(out, chunk, sy); + } + + // Empty entity lists + nbt_start_list(out, "Entities", TAG_COMPOUND, 0); + nbt_start_list(out, "TileEntities", TAG_COMPOUND, 0); + nbt_start_list(out, "TileTicks", TAG_COMPOUND, 0); + + nbt_end_compound(out); // Level + nbt_end_compound(out); // root +} + +static int compress_chunk(const buf *input, buf *output) { + uLongf bound = compressBound((uLong)input->len); + buf_reserve(output, bound); + uLongf dest_len = (uLongf)output->cap; + int res = compress2(output->data, &dest_len, input->data, (uLong)input->len, Z_BEST_SPEED); + if (res != Z_OK) return -1; + output->len = dest_len; + return 0; +} + +// --------------------------------------------------------------------------- +// Region file aggregation +// --------------------------------------------------------------------------- +typedef struct { + uint8_t *data; + size_t size; +} chunk_blob; + +typedef struct { + int region_x; + int region_z; + chunk_blob chunks[32 * 32]; + uint8_t present[32 * 32]; +} region_accum; + +static region_accum *find_or_add_region(region_accum **list, size_t *count, size_t *cap, int rx, int rz) { + for (size_t i = 0; i < *count; ++i) { + if ((*list)[i].region_x == rx && (*list)[i].region_z == rz) return &(*list)[i]; + } + if (*count >= *cap) { + size_t new_cap = *cap ? *cap * 2 : 8; + region_accum *new_list = (region_accum *)realloc(*list, new_cap * sizeof(region_accum)); + if (!new_list) return NULL; + *list = new_list; + *cap = new_cap; + } + region_accum *reg = &(*list)[(*count)++]; + memset(reg, 0, sizeof(*reg)); + reg->region_x = rx; + reg->region_z = rz; + return reg; +} + +static void free_regions(region_accum *regions, size_t count) { + for (size_t i = 0; i < count; ++i) { + for (int j = 0; j < 32 * 32; ++j) { + free(regions[i].chunks[j].data); + } + } + free(regions); +} + +static void write_region_file(const char *out_dir, const region_accum *reg) { + uint8_t offsets[4096]; + uint8_t timestamps[4096]; + memset(offsets, 0, sizeof(offsets)); + memset(timestamps, 0, sizeof(timestamps)); + + buf body = {0}; + uint32_t sector = 2; + time_t now = time(NULL); + + for (int idx = 0; idx < 32 * 32; ++idx) { + if (!reg->present[idx]) continue; + const chunk_blob *cb = ®->chunks[idx]; + if (!cb->data || cb->size == 0) continue; + + uint32_t length = (uint32_t)(cb->size + 1); + uint32_t padding = (4096 - ((length + 4) % 4096)) % 4096; + uint32_t total_len = length + 4 + padding; + uint32_t sectors = total_len / 4096; + + // offsets + offsets[idx * 4 + 0] = (uint8_t)((sector >> 16) & 0xFF); + offsets[idx * 4 + 1] = (uint8_t)((sector >> 8) & 0xFF); + offsets[idx * 4 + 2] = (uint8_t)(sector & 0xFF); + offsets[idx * 4 + 3] = (uint8_t)sectors; + + // timestamps + uint32_t ts = (uint32_t)now; + timestamps[idx * 4 + 0] = (uint8_t)((ts >> 24) & 0xFF); + timestamps[idx * 4 + 1] = (uint8_t)((ts >> 16) & 0xFF); + timestamps[idx * 4 + 2] = (uint8_t)((ts >> 8) & 0xFF); + timestamps[idx * 4 + 3] = (uint8_t)(ts & 0xFF); + + // payload: length (4 bytes), compression type (1), data, padding + buf_reserve(&body, total_len); + buf_put_be32(&body, length); + buf_put_u8(&body, 2); // compression type 2 = zlib + buf_append(&body, cb->data, cb->size); + if (padding) { + uint8_t zeros[4096] = {0}; + buf_append(&body, zeros, padding); + } + + sector += sectors; + } + + buf file = {0}; + buf_reserve(&file, 4096 * 2 + body.len); + buf_append(&file, offsets, sizeof(offsets)); + buf_append(&file, timestamps, sizeof(timestamps)); + buf_append(&file, body.data, body.len); + + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/r.%d.%d.mca", out_dir, reg->region_x, reg->region_z); + FILE *fp = fopen(path, "wb"); + if (fp) { + fwrite(file.data, 1, file.len, fp); + fclose(fp); + } else { + fprintf(stderr, "Failed to write region %s\n", path); + } + + free(file.data); + free(body.data); +} + +static void write_regions(const char *out_dir, region_accum *regions, size_t region_count) { + for (size_t i = 0; i < region_count; ++i) { + write_region_file(out_dir, ®ions[i]); + } +} + +static void usage(const char *prog) { + fprintf(stderr, + "Usage: %s [--radius R] [--center-x X --center-z Z] [--min-x MX --max-x MX --min-z MZ --max-z MZ]\n" + " [--threads N] [--seed S] [--sea-level L] [--snow-line H] [--format mca|bin] [--trails] [--out DIR]\n", + prog); +} + +static long parse_long(const char *s) { + char *end = NULL; + errno = 0; + long v = strtol(s, &end, 10); + if (errno != 0 || !end || *end != '\0') { + fprintf(stderr, "Invalid number: %s\n", s); + exit(1); + } + return v; +} + +static int ensure_dir(const char *path) { + char tmp[PATH_MAX]; + size_t len = strlen(path); + if (len == 0 || len >= sizeof(tmp)) return -1; + strcpy(tmp, path); + for (size_t i = 1; i <= len; ++i) { + if (tmp[i] == '/' || tmp[i] == '\0') { + char saved = tmp[i]; + tmp[i] = '\0'; + if (strlen(tmp) > 0) { + if (mkdir(tmp, 0777) != 0 && errno != EEXIST) { + if (errno != EEXIST) return -1; + } + } + tmp[i] = saved; + } + } + return 0; +} + +static void write_chunk_file(const char *out_dir, const chunk_data *chunk) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/chunk_%d_%d.bin", out_dir, chunk->chunk_x, chunk->chunk_z); + FILE *fp = fopen(path, "wb"); + if (!fp) { + fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); + return; + } + int32_t header[2] = {chunk->chunk_x, chunk->chunk_z}; + fwrite(header, sizeof(header[0]), 2, fp); + fwrite(chunk->heightmap, sizeof(uint16_t), CHUNK_SIZE * CHUNK_SIZE, fp); + fwrite(chunk->blocks, sizeof(uint16_t), CHUNK_HEIGHT * CHUNK_SIZE * CHUNK_SIZE, fp); + fclose(fp); +} + +static chunk_coord *build_job_list(int min_x, int max_x, int min_z, int max_z, size_t *out_count) { + if (min_x > max_x || min_z > max_z) return NULL; + size_t count = (size_t)(max_x - min_x + 1) * (size_t)(max_z - min_z + 1); + chunk_coord *jobs = (chunk_coord *)malloc(count * sizeof(chunk_coord)); + if (!jobs) return NULL; + size_t idx = 0; + for (int x = min_x; x <= max_x; ++x) { + for (int z = min_z; z <= max_z; ++z) { + jobs[idx].x = x; + jobs[idx].z = z; + ++idx; + } + } + *out_count = count; + return jobs; +} + +static void *worker_fn(void *ptr) { + worker_args *args = (worker_args *)ptr; + worldgen_ctx ctx; + worldgen_init(&ctx, args->world_seed, args->sea_level, args->snow_line); + ctx.enable_trails = args->enable_trails; + while (1) { + size_t idx = atomic_fetch_add(&args->queue->next_index, 1); + if (idx >= args->queue->count) break; + chunk_coord job = args->queue->items[idx]; + chunk_data chunk; + worldgen_generate_chunk(&ctx, job.x, job.z, &chunk); + if (args->results) { + pthread_mutex_lock(&args->results->mutex); + size_t result_idx = atomic_fetch_add(&args->results->count, 1); + if (result_idx < args->results->capacity) { + args->results->chunks[result_idx].chunk_x = chunk.chunk_x; + args->results->chunks[result_idx].chunk_z = chunk.chunk_z; + memcpy(&args->results->chunks[result_idx].data, &chunk, sizeof(chunk_data)); + } + pthread_mutex_unlock(&args->results->mutex); + } else { + write_chunk_file(args->out_dir, &chunk); + } + size_t done_now = atomic_fetch_add(&args->queue->done, 1) + 1; + if (args->log_mu) { + pthread_mutex_lock(args->log_mu); + double pct = (double)done_now * 100.0 / (double)args->queue->count; + fprintf(stderr, "\rProgress: %zu/%zu (%.1f%%) last (%d,%d)", done_now, args->queue->count, pct, job.x, job.z); + fflush(stderr); + pthread_mutex_unlock(args->log_mu); + } + } + return NULL; +} + +int main(int argc, char **argv) { + int have_rect = 0; + int min_x = 0, max_x = 0, min_z = 0, max_z = 0; + int center_x = 0, center_z = 0; + int radius = 1; + int threads = (int)sysconf(_SC_NPROCESSORS_ONLN); + if (threads < 1) threads = 4; + int world_seed = 123456; + int sea_level = 70; + int snow_line = INT_MIN; + int enable_trails = 0; + const char *out_dir = "output"; + output_format format = FORMAT_MCA; + + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--radius") == 0 && i + 1 < argc) { + radius = (int)parse_long(argv[++i]); + } else if (strcmp(argv[i], "--center-x") == 0 && i + 1 < argc) { + center_x = (int)parse_long(argv[++i]); + } else if (strcmp(argv[i], "--center-z") == 0 && i + 1 < argc) { + center_z = (int)parse_long(argv[++i]); + } else if (strcmp(argv[i], "--min-x") == 0 && i + 1 < argc) { + min_x = (int)parse_long(argv[++i]); + have_rect = 1; + } else if (strcmp(argv[i], "--max-x") == 0 && i + 1 < argc) { + max_x = (int)parse_long(argv[++i]); + have_rect = 1; + } else if (strcmp(argv[i], "--min-z") == 0 && i + 1 < argc) { + min_z = (int)parse_long(argv[++i]); + have_rect = 1; + } else if (strcmp(argv[i], "--max-z") == 0 && i + 1 < argc) { + max_z = (int)parse_long(argv[++i]); + have_rect = 1; + } else if (strcmp(argv[i], "--threads") == 0 && i + 1 < argc) { + threads = (int)parse_long(argv[++i]); + } else if (strcmp(argv[i], "--seed") == 0 && i + 1 < argc) { + world_seed = (int)parse_long(argv[++i]); + } else if (strcmp(argv[i], "--sea-level") == 0 && i + 1 < argc) { + sea_level = (int)parse_long(argv[++i]); + } else if (strcmp(argv[i], "--snow-line") == 0 && i + 1 < argc) { + snow_line = (int)parse_long(argv[++i]); + } else if (strcmp(argv[i], "--format") == 0 && i + 1 < argc) { + const char *f = argv[++i]; + if (strcmp(f, "mca") == 0) { + format = FORMAT_MCA; + } else if (strcmp(f, "bin") == 0) { + format = FORMAT_BIN; + } else { + fprintf(stderr, "Unknown format: %s (use mca or bin)\n", f); + return 1; + } + } else if (strcmp(argv[i], "--trails") == 0) { + enable_trails = 1; + } else if ((strcmp(argv[i], "--out") == 0 || strcmp(argv[i], "--output") == 0) && i + 1 < argc) { + out_dir = argv[++i]; + } else if (strcmp(argv[i], "--help") == 0) { + usage(argv[0]); + return 0; + } else { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + usage(argv[0]); + return 1; + } + } + + if (snow_line == INT_MIN) { + snow_line = sea_level + 38; + } + + if (!have_rect) { + min_x = center_x - radius; + max_x = center_x + radius; + min_z = center_z - radius; + max_z = center_z + radius; + } + + if (ensure_dir(out_dir) != 0) { + fprintf(stderr, "Failed to create output directory: %s\n", out_dir); + return 1; + } + + size_t job_count = 0; + chunk_coord *jobs = build_job_list(min_x, max_x, min_z, max_z, &job_count); + if (!jobs || job_count == 0) { + fprintf(stderr, "No chunks to generate.\n"); + free(jobs); + return 1; + } + + job_queue queue = {.items = jobs, .count = job_count, .next_index = 0}; + atomic_init(&queue.done, 0); + pthread_mutex_t log_mu = PTHREAD_MUTEX_INITIALIZER; + + results_buffer results; + results.chunks = (completed_chunk *)malloc(sizeof(completed_chunk) * job_count); + results.capacity = job_count; + atomic_init(&results.count, 0); + results.mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; + + if (threads < 1) threads = 1; + pthread_t *workers = (pthread_t *)malloc(sizeof(pthread_t) * (size_t)threads); + if (!workers) { + fprintf(stderr, "Failed to allocate threads array.\n"); + free(results.chunks); + free(jobs); + return 1; + } + + worker_args args = { + .queue = &queue, + .out_dir = out_dir, + .world_seed = world_seed, + .sea_level = sea_level, + .snow_line = snow_line, + .log_mu = &log_mu, + .enable_trails = enable_trails, + .results = &results}; + + for (int i = 0; i < threads; ++i) { + pthread_create(&workers[i], NULL, worker_fn, &args); + } + for (int i = 0; i < threads; ++i) { + pthread_join(workers[i], NULL); + } + + fprintf(stderr, "\n"); + + size_t completed = atomic_load(&results.count); + if (format == FORMAT_BIN) { + for (size_t i = 0; i < completed; ++i) { + write_chunk_file(out_dir, &results.chunks[i].data); + } + fprintf(stdout, "Generated %zu chunk(s) into %s (raw bin) using %d thread(s).\n", completed, out_dir, threads); + } else { + region_accum *regions = NULL; + size_t region_count = 0, region_cap = 0; + for (size_t i = 0; i < completed; ++i) { + const completed_chunk *cc = &results.chunks[i]; + buf nbt = {0}; + build_chunk_nbt(&cc->data, &nbt); + buf compressed = {0}; + if (compress_chunk(&nbt, &compressed) != 0) { + free(nbt.data); + free(compressed.data); + continue; + } + free(nbt.data); + + int cx = cc->chunk_x; + int cz = cc->chunk_z; + int region_x = (cx >= 0) ? cx / 32 : ((cx - 31) / 32); + int region_z = (cz >= 0) ? cz / 32 : ((cz - 31) / 32); + int local_x = cx - region_x * 32; + int local_z = cz - region_z * 32; + int index = local_x + local_z * 32; + + region_accum *reg = find_or_add_region(®ions, ®ion_count, ®ion_cap, region_x, region_z); + if (!reg) { + free(compressed.data); + continue; + } + if (reg->present[index]) { + free(reg->chunks[index].data); + } + reg->present[index] = 1; + reg->chunks[index].data = compressed.data; + reg->chunks[index].size = compressed.len; + } + + write_regions(out_dir, regions, region_count); + free_regions(regions, region_count); + fprintf(stdout, "Generated %zu chunk(s) into %s (MCA) using %d thread(s).\n", completed, out_dir, threads); + } + + free(results.chunks); + free(workers); + free(jobs); + return 0; +} diff --git a/worldgen-c/src/noise.c b/worldgen-c/src/noise.c new file mode 100644 index 0000000..45bd0d9 --- /dev/null +++ b/worldgen-c/src/noise.c @@ -0,0 +1,180 @@ +#include "noise.h" +#include +#include + +// Simplex noise implementation (Stefan Gustavson public domain style). +// Enough for terrain-like smooth noise; deterministic per seed. + +static const int grad3[12][3] = { + {1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, + {1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1}, + {0, 1, 1}, {0, -1, 1}, {0, 1, -1}, {0, -1, -1} +}; + +static double dot(const int *g, double x, double y) { + return g[0] * x + g[1] * y; +} + +static double dot3(const int *g, double x, double y, double z) { + return g[0] * x + g[1] * y + g[2] * z; +} + +static uint32_t lcg(uint32_t seed) { + return seed * 1664525u + 1013904223u; +} + +void simplex_init(simplex_noise *noise, uint32_t seed) { + for (int i = 0; i < 256; ++i) { + noise->perm[i] = i; + } + uint32_t s = seed; + for (int i = 255; i > 0; --i) { + s = lcg(s); + int j = (s + 31) % (i + 1); + int tmp = noise->perm[i]; + noise->perm[i] = noise->perm[j]; + noise->perm[j] = tmp; + } + for (int i = 0; i < 256; ++i) { + noise->perm[256 + i] = noise->perm[i]; + } +} + +double simplex_noise2(simplex_noise *noise, double xin, double yin) { + const double F2 = 0.5 * (sqrt(3.0) - 1.0); + const double G2 = (3.0 - sqrt(3.0)) / 6.0; + + double s = (xin + yin) * F2; + int i = (int)floor(xin + s); + int j = (int)floor(yin + s); + double t = (i + j) * G2; + double X0 = i - t; + double Y0 = j - t; + double x0 = xin - X0; + double y0 = yin - Y0; + + int i1, j1; + if (x0 > y0) { + i1 = 1; j1 = 0; + } else { + i1 = 0; j1 = 1; + } + + double x1 = x0 - i1 + G2; + double y1 = y0 - j1 + G2; + double x2 = x0 - 1.0 + 2.0 * G2; + double y2 = y0 - 1.0 + 2.0 * G2; + + int ii = i & 255; + int jj = j & 255; + int gi0 = noise->perm[ii + noise->perm[jj]] % 12; + int gi1 = noise->perm[ii + i1 + noise->perm[jj + j1]] % 12; + int gi2 = noise->perm[ii + 1 + noise->perm[jj + 1]] % 12; + + double n0 = 0.0, n1 = 0.0, n2 = 0.0; + + double t0 = 0.5 - x0 * x0 - y0 * y0; + if (t0 > 0) { + t0 *= t0; + n0 = t0 * t0 * dot(grad3[gi0], x0, y0); + } + double t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 > 0) { + t1 *= t1; + n1 = t1 * t1 * dot(grad3[gi1], x1, y1); + } + double t2 = 0.5 - x2 * x2 - y2 * y2; + if (t2 > 0) { + t2 *= t2; + n2 = t2 * t2 * dot(grad3[gi2], x2, y2); + } + + return 70.0 * (n0 + n1 + n2); +} + +double simplex_noise3(simplex_noise *noise, double xin, double yin, double zin) { + const double F3 = 1.0 / 3.0; + const double G3 = 1.0 / 6.0; + + double s = (xin + yin + zin) * F3; + int i = (int)floor(xin + s); + int j = (int)floor(yin + s); + int k = (int)floor(zin + s); + + double t = (i + j + k) * G3; + double X0 = i - t; + double Y0 = j - t; + double Z0 = k - t; + double x0 = xin - X0; + double y0 = yin - Y0; + double z0 = zin - Z0; + + int i1, j1, k1; + int i2, j2, k2; + if (x0 >= y0) { + if (y0 >= z0) { + i1 = 1; j1 = 0; k1 = 0; + i2 = 1; j2 = 1; k2 = 0; + } else if (x0 >= z0) { + i1 = 1; j1 = 0; k1 = 0; + i2 = 1; j2 = 0; k2 = 1; + } else { + i1 = 0; j1 = 0; k1 = 1; + i2 = 1; j2 = 0; k2 = 1; + } + } else { + if (y0 < z0) { + i1 = 0; j1 = 0; k1 = 1; + i2 = 0; j2 = 1; k2 = 1; + } else if (x0 < z0) { + i1 = 0; j1 = 1; k1 = 0; + i2 = 0; j2 = 1; k2 = 1; + } else { + i1 = 0; j1 = 1; k1 = 0; + i2 = 1; j2 = 1; k2 = 0; + } + } + + double x1 = x0 - i1 + G3; + double y1 = y0 - j1 + G3; + double z1 = z0 - k1 + G3; + double x2 = x0 - i2 + 2.0 * G3; + double y2 = y0 - j2 + 2.0 * G3; + double z2 = z0 - k2 + 2.0 * G3; + double x3 = x0 - 1.0 + 3.0 * G3; + double y3 = y0 - 1.0 + 3.0 * G3; + double z3 = z0 - 1.0 + 3.0 * G3; + + int ii = i & 255; + int jj = j & 255; + int kk = k & 255; + int gi0 = noise->perm[ii + noise->perm[jj + noise->perm[kk]]] % 12; + int gi1 = noise->perm[ii + i1 + noise->perm[jj + j1 + noise->perm[kk + k1]]] % 12; + int gi2 = noise->perm[ii + i2 + noise->perm[jj + j2 + noise->perm[kk + k2]]] % 12; + int gi3 = noise->perm[ii + 1 + noise->perm[jj + 1 + noise->perm[kk + 1]]] % 12; + + double n0 = 0.0, n1 = 0.0, n2 = 0.0, n3 = 0.0; + + double t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0; + if (t0 > 0) { + t0 *= t0; + n0 = t0 * t0 * dot3(grad3[gi0], x0, y0, z0); + } + double t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1; + if (t1 > 0) { + t1 *= t1; + n1 = t1 * t1 * dot3(grad3[gi1], x1, y1, z1); + } + double t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2; + if (t2 > 0) { + t2 *= t2; + n2 = t2 * t2 * dot3(grad3[gi2], x2, y2, z2); + } + double t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3; + if (t3 > 0) { + t3 *= t3; + n3 = t3 * t3 * dot3(grad3[gi3], x3, y3, z3); + } + + return 32.0 * (n0 + n1 + n2 + n3); +} diff --git a/worldgen-c/src/worldgen.c b/worldgen-c/src/worldgen.c new file mode 100644 index 0000000..f6bd4fa --- /dev/null +++ b/worldgen-c/src/worldgen.c @@ -0,0 +1,1839 @@ +#include "worldgen.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TRAIL_NODE_SPACING 1200.0 +#define TRAIL_CELL_SIZE 16.0 +#define TRAIL_MARGIN 96.0 +#define TRAIL_WIDTH 5 + +typedef enum { + BIOME_WEST_KY_COALFIELDS = 0, + BIOME_EAST_KY_RIDGEBREAKS = 1, + BIOME_OLD_GROWTH_PLAINS = 2 +} biome_id; + +typedef struct { + int height; + int water_surface; + int has_water; + int basin_rim; + int basin_depth; + int is_local_basin; + biome_id biome; +} column_data; + +typedef struct { + int x, y, z; + uint16_t id; +} placed_block; + +typedef struct { + placed_block *items; + size_t count; + size_t cap; +} block_list; + +typedef struct trail_segment { + int ax, az; + int bx, bz; + int count; + int *points; /* pairs of x,z world coordinates */ +} trail_segment; + + +static int column_height(worldgen_ctx *ctx, int x, int z); + +// --------------------------------------------------------------------------- +// RNG (deterministic, cheap) +// --------------------------------------------------------------------------- +typedef struct { + uint64_t state; +} rng_state; + +static void rng_seed(rng_state *r, uint64_t seed) { + r->state = seed ? seed : 1; +} + +static uint32_t rng_next_u32(rng_state *r) { + r->state = r->state * 6364136223846793005ULL + 1; + return (uint32_t)(r->state >> 32); +} + +static double rng_next_f64(rng_state *r) { + return (rng_next_u32(r) + 1.0) / 4294967296.0; +} + +static int rng_range_inclusive(rng_state *r, int min, int max) { + if (max <= min) return min; + int span = max - min + 1; + return min + (int)(rng_next_f64(r) * span); +} + +struct tree_archetype; +typedef void (*tree_builder_fn)(worldgen_ctx *, int, int, int, int, rng_state *, block_list *, const struct tree_archetype *); + +typedef enum { + TREE_SPECIES_OAK = 0, + TREE_SPECIES_BIRCH, + TREE_SPECIES_SPRUCE, + TREE_SPECIES_PINE, + TREE_SPECIES_WALNUT, + TREE_SPECIES_CYPRESS, + TREE_SPECIES_MAPLE, + TREE_SPECIES_COUNT +} tree_species; + +typedef struct tree_archetype { + const char *name; + tree_species species; + int log_block; + int leaf_block; + int min_height; + int max_height; + int space_radius; + int canopy_extra; + tree_builder_fn builder; +} tree_archetype; + +typedef struct { + tree_species species; + const char *label; + const int *variants; + size_t variant_count; +} tree_species_catalog; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +static inline double clamp01(double v) { + if (v < 0.0) return 0.0; + if (v > 1.0) return 1.0; + return v; +} + +static int ground_slope(worldgen_ctx *ctx, int x, int z); +static void apply_flat_river(worldgen_ctx *ctx, int x, int z, column_data *data); +static uint16_t select_surface_block(worldgen_ctx *ctx, const column_data *data, int world_x, int world_z); +static void generate_chunk_trails(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out); +static void generate_chunk_grass(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out); +static void trail_node_position(worldgen_ctx *ctx, int node_x, int node_z, double spacing, double *out_x, double *out_z); +static double old_growth_plains_mask(worldgen_ctx *ctx, int x, int z); +static double old_growth_grove_mask(worldgen_ctx *ctx, int x, int z); + +static uint32_t hash_coords(int x, int z, uint32_t seed) { + uint32_t h = (uint32_t)(x * 374761393 + z * 668265263) ^ seed; + h = (h ^ (h >> 13)) * 1274126177u; + return h ^ (h >> 16); +} + +static double worley_distance(int x, int z, double scale, uint32_t seed) { + double px = x * scale; + double pz = z * scale; + int cell_x = (int)floor(px); + int cell_z = (int)floor(pz); + double min_dist = 9999.0; + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + int cx = cell_x + dx; + int cz = cell_z + dz; + uint32_t h = hash_coords(cx, cz, seed); + double rand_x = ((h & 0xFFFF) / 65535.0); + double rand_z = (((h >> 16) & 0xFFFF) / 65535.0); + double feature_x = cx + rand_x; + double feature_z = cz + rand_z; + double dist = sqrt((feature_x - px) * (feature_x - px) + (feature_z - pz) * (feature_z - pz)); + if (dist < min_dist) min_dist = dist; + } + } + return min_dist; +} + +static double old_growth_plains_mask(worldgen_ctx *ctx, int x, int z) { + (void)ctx; + double coarse = worley_distance(x + 21000, z - 21000, 0.00065, 0x9E3779B9u); + double patch = clamp01(1.15 - coarse * 1.05); + double ridge = worley_distance(x - 18000, z + 18000, 0.00035, 0xC001D00Du); + double hollow = clamp01((ridge - 0.35) * 1.5); + double detail = simplex_noise2(&ctx->noise, x * 0.00045, z * 0.00045) * 0.5 + 0.5; + double mask = patch * (0.7 + detail * 0.3) * (1.0 - hollow * 0.6); + return clamp01(mask); +} + + +static double region_blend(worldgen_ctx *ctx, int x, int z) { + (void)ctx; + double primary = worley_distance(x, z, 0.0012, 0x87654321u); + double normalized = clamp01(1.2 - primary * 0.9); + double detail = worley_distance(x + 40000, z - 35000, 0.0023, 0x13579BDFu); + detail = clamp01(detail * 1.1); + double blend = normalized * 0.75 + detail * 0.25; + return clamp01(blend); +} + +static int local_relief(worldgen_ctx *ctx, int x, int z, int center_height) { + const int offsets[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + int max_delta = 0; + for (int i = 0; i < 4; ++i) { + int nx = x + offsets[i][0]; + int nz = z + offsets[i][1]; + int neighbor = column_height(ctx, nx, nz); + int delta = abs(neighbor - center_height); + if (delta > max_delta) max_delta = delta; + } + return max_delta; +} + +static biome_id classify_biome(worldgen_ctx *ctx, int x, int z, int column_height) { + double old_growth = old_growth_plains_mask(ctx, x, z); + if (old_growth > 0.6) { + return BIOME_OLD_GROWTH_PLAINS; + } + double blend = region_blend(ctx, x, z); + int slope = local_relief(ctx, x, z, column_height); + double slope_score = clamp01((double)slope / 10.0); + double relief = clamp01((double)(column_height - ctx->sea_level) / 45.0); + double jitter = simplex_noise2(&ctx->noise, (x - 17000) * 0.001, (z + 17000) * 0.001) * 0.1; + double decision = blend * 0.7 + slope_score * 0.2 + relief * 0.15 + jitter; + if (decision > 0.55) { + return BIOME_EAST_KY_RIDGEBREAKS; + } + return BIOME_WEST_KY_COALFIELDS; +} + +static void block_list_init(block_list *list) { + list->items = NULL; + list->count = 0; + list->cap = 0; +} + +static void block_list_free(block_list *list) { + free(list->items); + list->items = NULL; + list->count = list->cap = 0; +} + +static void block_list_push(block_list *list, int x, int y, int z, uint16_t id) { + if (list->count >= list->cap) { + size_t new_cap = list->cap ? list->cap * 2 : 64; + placed_block *new_items = (placed_block *)realloc(list->items, new_cap * sizeof(placed_block)); + if (!new_items) { + return; + } + list->items = new_items; + list->cap = new_cap; + } + list->items[list->count].x = x; + list->items[list->count].y = y; + list->items[list->count].z = z; + list->items[list->count].id = id; + list->count += 1; +} + +// --------------------------------------------------------------------------- +// Core worldgen functions +// --------------------------------------------------------------------------- +static int column_height(worldgen_ctx *ctx, int x, int z) { + double region = region_blend(ctx, x, z); + double old_growth = old_growth_plains_mask(ctx, x, z); + double warp1_x = x + simplex_noise2(&ctx->noise, x * 0.001, z * 0.001) * 50.0; + double warp1_z = z + simplex_noise2(&ctx->noise, (x + 1000) * 0.001, (z + 1000) * 0.001) * 50.0; + + double warp2_x = warp1_x + simplex_noise2(&ctx->noise, warp1_x * 0.01, warp1_z * 0.01) * 10.0; + double warp2_z = warp1_z + simplex_noise2(&ctx->noise, (warp1_x + 500) * 0.01, (warp1_z + 500) * 0.01) * 10.0; + double mountain_mask = simplex_noise2(&ctx->noise, warp2_x * 0.00035, warp2_z * 0.00035); + double base_mountain = clamp01((mountain_mask + 0.25) * 0.6); + double mountain_factor = clamp01(base_mountain * 0.85 + region * 0.35); + + double lowland = ctx->sea_level - 4; + lowland += simplex_noise2(&ctx->noise, warp2_x * 0.0004, warp2_z * 0.0004) * 52.0 * (0.45 + 0.25 * region); + lowland += simplex_noise2(&ctx->noise, warp2_x * 0.0022, warp2_z * 0.0022) * 24.0 * (0.38 + 0.2 * region); + lowland += simplex_noise2(&ctx->noise, warp2_x * 0.006, warp2_z * 0.006) * 10.0 * (0.34 + 0.2 * region); + lowland += simplex_noise2(&ctx->noise, warp2_x * 0.012, warp2_z * 0.012) * 4.0 * (0.3 + 0.15 * region); + double hummock = simplex_noise2(&ctx->noise, (warp2_x + 32000) * 0.02, (warp2_z - 32000) * 0.02); + lowland += hummock * hummock * 3.5; + + double highland = ctx->sea_level - 10; + highland += simplex_noise2(&ctx->noise, warp2_x * 0.0005, warp2_z * 0.0005) * 120.0 * (0.65 + 0.35 * mountain_factor); + highland += simplex_noise2(&ctx->noise, warp2_x * 0.002, warp2_z * 0.002) * 55.0 * (0.5 + 0.5 * mountain_factor); + highland += simplex_noise2(&ctx->noise, warp2_x * 0.005, warp2_z * 0.005) * 32.0 * (0.4 + 0.6 * mountain_factor); + highland += simplex_noise2(&ctx->noise, warp2_x * 0.01, warp2_z * 0.01) * 20.0 * (0.35 + 0.65 * mountain_factor); + + double mix_base = region * 0.35 + base_mountain * 0.55; + double mix = clamp01(mix_base * 0.65 + 0.15); + double height = lowland * (1.0 - mix) + highland * mix; + + double flat_noise = simplex_noise2(&ctx->noise, warp2_x * 0.0015, warp2_z * 0.0015) * 0.5 + 0.5; + double flat_bias = (mix < 0.45 ? (0.45 - mix) * 1.6 : 0.0) * flat_noise; + if (flat_bias > 0.0) { + double target = ctx->sea_level - 2 + simplex_noise2(&ctx->noise, warp2_x * 0.004, warp2_z * 0.004) * 4.0; + double blend = 0.3 * flat_bias; + height = height * (1.0 - blend) + target * blend; + } + + double detail_scale = 0.04 + 0.35 * mountain_factor; + double detail_weight = detail_scale * (0.35 + 0.65 * mountain_factor); + double region_bias = 0.7 + 0.3 * clamp01(region); + double plains_detail_quiet = clamp01(1.0 - mix * 1.2); + double adjusted_weight = detail_weight * region_bias * (1.0 - plains_detail_quiet * 0.8); + height += simplex_noise2(&ctx->noise, warp2_x * 0.02, warp2_z * 0.02) * 5.0 * adjusted_weight; + height += simplex_noise2(&ctx->noise, warp2_x * 0.05, warp2_z * 0.05) * 2.0 * adjusted_weight; + height += simplex_noise2(&ctx->noise, warp2_x * 0.1, warp2_z * 0.1) * 1.0 * adjusted_weight; + height += simplex_noise3(&ctx->noise, warp2_x * 0.015, warp2_z * 0.015, region * 7.0) * 1.5; + double flats = clamp01((old_growth - 0.25) / 0.75); + if (flats > 0.0) { + double plains_target = ctx->sea_level - 2 + simplex_noise2(&ctx->noise, (x - 22000) * 0.002, (z + 22000) * 0.002) * 5.0; + plains_target += simplex_noise2(&ctx->noise, (x + 33000) * 0.01, (z - 33000) * 0.01) * 1.5; + height = height * (1.0 - flats) + plains_target * flats; + } + + return (int)height; +} + +static column_data get_column_data(worldgen_ctx *ctx, int x, int z) { + column_data data; + data.height = column_height(ctx, x, z); + int rim = data.height; + int has_rim = 0; + for (int dx = -4; dx <= 4; ++dx) { + for (int dz = -4; dz <= 4; ++dz) { + if (dx == 0 && dz == 0) continue; + int neighbor = column_height(ctx, x + dx, z + dz); + if (!has_rim || neighbor < rim) { + rim = neighbor; + has_rim = 1; + } + } + } + if (has_rim && rim - data.height >= 4) { + data.has_water = 1; + data.water_surface = rim; + data.basin_rim = rim; + data.basin_depth = rim - data.height; + data.is_local_basin = 1; + } else if (data.height < ctx->sea_level) { + data.has_water = 1; + data.water_surface = ctx->sea_level; + data.basin_rim = ctx->sea_level; + data.basin_depth = ctx->sea_level - data.height; + data.is_local_basin = 0; + } else { + data.has_water = 0; + data.water_surface = 0; + data.basin_rim = data.height; + data.basin_depth = 0; + data.is_local_basin = 0; + } + + if (data.is_local_basin && data.basin_depth > 5) { + double depth = (double)data.basin_depth; + double norm = clamp01(depth / 18.0); + double carve = pow(norm, 1.2) * (2.5 + depth * 0.25); + int carve_int = (int)(carve); + if (carve_int > 0) { + data.height -= carve_int; + if (data.height < 1) data.height = 1; + data.basin_depth = data.basin_rim - data.height; + } + } + + apply_flat_river(ctx, x, z, &data); + data.biome = classify_biome(ctx, x, z, data.height); + return data; +} + +static void apply_flat_river(worldgen_ctx *ctx, int x, int z, column_data *data) { + if (data->is_local_basin) return; + if (data->height > ctx->sea_level + 8) return; + double warp_x = x + simplex_noise2(&ctx->noise, x * 0.004, z * 0.004) * 25.0; + double warp_z = z + simplex_noise2(&ctx->noise, (x + 12000) * 0.004, (z - 12000) * 0.004) * 25.0; + double river_noise = simplex_noise2(&ctx->noise, warp_x * 0.0005, warp_z * 0.0005); + double channel = 1.0 - fabs(river_noise); + if (channel < 0.985) return; + double strength = (channel - 0.985) / 0.015; + if (strength > 1.0) strength = 1.0; + if (strength < 0.0) strength = 0.0; + int surface = ctx->sea_level - 1; + if (surface < 2) surface = 2; + int depth = 2 + (int)(strength * 3.5); + int target_height = surface - depth; + if (target_height < 1) target_height = 1; + if (target_height >= data->height) return; + data->height = target_height; + data->has_water = 1; + data->water_surface = surface; + data->basin_rim = surface; + data->basin_depth = surface - target_height; + data->is_local_basin = 0; +} + +// --------------------------------------------------------------------------- +// Ore generation (coal seams) +// --------------------------------------------------------------------------- +typedef enum { + COAL_ZONE_WEST, + COAL_ZONE_FOOTHILL, + COAL_ZONE_EAST +} coal_zone; + +static coal_zone classify_coal_zone(worldgen_ctx *ctx, int column_height, biome_id biome) { + int relief = column_height - ctx->sea_level; + if (biome == BIOME_WEST_KY_COALFIELDS || biome == BIOME_OLD_GROWTH_PLAINS) { + if (relief > 24) return COAL_ZONE_FOOTHILL; + return COAL_ZONE_WEST; + } + if (relief > 36) return COAL_ZONE_EAST; + return COAL_ZONE_FOOTHILL; +} + +static int coal_min_y_for_zone(coal_zone zone) { + switch (zone) { + case COAL_ZONE_WEST: + return 10; // keep western strip seams above the deep stone layers + case COAL_ZONE_FOOTHILL: + return 6; + case COAL_ZONE_EAST: + default: + return 2; // mountains can dip close to bedrock + } +} + +static int check_point_in_seam(double y, double center, double half_thickness) { + return fabs(y - center) <= half_thickness; +} + +static double coal_seam_offset(worldgen_ctx *ctx, double x, double y, double z, double scale, double amplitude) { + return simplex_noise3(&ctx->noise, x * scale, y * scale, z * scale) * amplitude; +} + +static double coal_thickness_variation(worldgen_ctx *ctx, double x, double z, double scale, double amplitude) { + return simplex_noise2(&ctx->noise, x * scale, z * scale) * amplitude; +} + +static int coal_continuity(worldgen_ctx *ctx, double x, double y, double z, double threshold) { + double n = simplex_noise3(&ctx->noise, x * 0.05, y * 0.05, z * 0.05); + return n > threshold; +} + +static int seam_presence(worldgen_ctx *ctx, double x, double z, double scale, double threshold, double offset) { + double n = simplex_noise2(&ctx->noise, x * scale + offset, z * scale - offset); + return n > threshold; +} + +static double coal_mountain_bias(worldgen_ctx *ctx, int column_height) { + double rel = (double)(column_height - (ctx->sea_level + 20)); + return clamp01(rel / 80.0); +} + +static int seam_presence_biased(worldgen_ctx *ctx, double x, double z, double scale, double threshold, double offset, double bias) { + double adjusted = threshold + (1.0 - clamp01(bias)) * 0.4; + if (adjusted > 0.98) adjusted = 0.98; + return seam_presence(ctx, x, z, scale, adjusted, offset); +} + +static int check_seam_generic(worldgen_ctx *ctx, double x, double y, double z, double center, double thickness, + double undulation_scale, double undulation_amp, double thickness_scale, + double thickness_amp, double continuity_threshold) { + double offset = coal_seam_offset(ctx, x, y, z, undulation_scale, undulation_amp); + double adjusted_center = center + offset; + double thickness_var = coal_thickness_variation(ctx, x, z, thickness_scale, thickness_amp); + double adjusted_thickness = thickness + thickness_var; + double half_thickness = adjusted_thickness / 2.0; + if (check_point_in_seam(y, adjusted_center, half_thickness)) { + if (coal_continuity(ctx, x, y, z, continuity_threshold)) { + return 1; + } + } + return 0; +} + +static int check_flat_ceiling_seam(worldgen_ctx *ctx, double x, double y, double z, double column_height, + double ceiling_depth, double thickness, double continuity_threshold) { + double seam_ceiling = column_height - ceiling_depth; + double seam_floor = seam_ceiling - thickness; + if (y < seam_floor || y > seam_ceiling) { + return 0; + } + return coal_continuity(ctx, x, y, z, continuity_threshold); +} + +static int check_terrain_relative_seam(worldgen_ctx *ctx, double x, double y, double z, double column_height, + double depth_below_surface, double thickness, + double thickness_scale, double thickness_amp, double continuity_threshold) { + double seam_center = column_height - depth_below_surface; + double thickness_var = coal_thickness_variation(ctx, x, z, thickness_scale, thickness_amp); + double adjusted_thickness = thickness + thickness_var; + double half_thickness = adjusted_thickness / 2.0; + if (check_point_in_seam(y, seam_center, half_thickness)) { + if (coal_continuity(ctx, x, y, z, continuity_threshold)) { + return 1; + } + } + return 0; +} + +static int generate_coal(worldgen_ctx *ctx, int x, int y, int z, int column_height, biome_id biome) { + coal_zone zone = classify_coal_zone(ctx, column_height, biome); + if (y < coal_min_y_for_zone(zone)) { + return 0; + } + double mountain_bias = coal_mountain_bias(ctx, column_height); + double seam_bias = clamp01(0.05 + mountain_bias * 0.9); + if (zone == COAL_ZONE_EAST) { + seam_bias = clamp01(seam_bias + 0.3); + } else if (zone == COAL_ZONE_FOOTHILL) { + seam_bias = clamp01(seam_bias + 0.15); + } + + // Room-and-pillar seams for foothills/mountains (keep deep layers intact in lowlands). + if (zone != COAL_ZONE_WEST) { + if (seam_presence_biased(ctx, x, z, 0.0012, 0.0, 7.0, seam_bias) && + check_seam_generic(ctx, x, y, z, 12, 5, 0.02, 2.0, 0.03, 1.0, -0.7)) + return 1; + } + if (seam_presence_biased(ctx, x, z, 0.0009, -0.1, 11.0, seam_bias) && + check_seam_generic(ctx, x, y, z, 25, 3, 0.015, 1.5, 0.04, 0.5, -0.6)) + return 1; + if (seam_presence_biased(ctx, x, z, 0.0011, -0.05, -7.0, seam_bias) && + check_seam_generic(ctx, x, y, z, 32, 4, 0.02, 2.0, 0.03, 1.0, -0.7)) + return 1; + if (seam_presence_biased(ctx, x, z, 0.0015, 0.05, 3.0, seam_bias) && + check_seam_generic(ctx, x, y, z, 45, 2, 0.015, 1.5, 0.04, 0.5, -0.6)) + return 1; + if (seam_presence_biased(ctx, x, z, 0.002, 0.1, 17.0, seam_bias) && + check_seam_generic(ctx, x, y, z, 85, 3, 0.02, 2.0, 0.03, 1.0, -0.7)) + return 1; + + // Mountain strip seams: shallow, dense coal layers for high relief areas. + if (zone == COAL_ZONE_EAST) { + if (seam_presence_biased(ctx, x, z, 0.0014, -0.05, 42.0, seam_bias) && + check_terrain_relative_seam(ctx, x, y, z, column_height, 4.0, 4.0, 0.04, 0.9, -0.4)) + return 1; + } + + // Surface-adjacent seams get fatter in the western lowlands (strip mining). + double strip_thickness = (zone == COAL_ZONE_WEST) ? 2.5 : 1.3; + double strip_bias = clamp01(seam_bias * 0.6 + 0.1); + if (seam_presence_biased(ctx, x, z, 0.0007, 0.2, 28.0, strip_bias)) { + if (check_flat_ceiling_seam(ctx, x, y, z, column_height, 5, strip_thickness, -0.25)) return 1; + } + + double relative_thickness = (zone == COAL_ZONE_WEST) ? 2.6 : 1.8; + double relative_depth = (zone == COAL_ZONE_EAST) ? 11.0 : 8.0; + if (seam_presence_biased(ctx, x, z, 0.001, -0.05, 3.5, seam_bias) && + check_terrain_relative_seam(ctx, x, y, z, column_height, relative_depth, relative_thickness, 0.03, 0.8, -0.62)) + return 1; + + // Mountain seams can drop deeper for mountaintop-removal vibes. + if (zone == COAL_ZONE_EAST) { + if (seam_presence_biased(ctx, x, z, 0.0013, -0.15, -19.0, clamp01(seam_bias + 0.2)) && + check_terrain_relative_seam(ctx, x, y, z, column_height, 18, 3.5, 0.025, 0.6, -0.55)) + return 1; + if (seam_presence_biased(ctx, x, z, 0.0018, 0.05, 9.0, clamp01(seam_bias + 0.2)) && + check_seam_generic(ctx, x, y, z, 8, 4, 0.02, 1.8, 0.03, 1.0, -0.6)) + return 1; + } + return 0; +} + +// --------------------------------------------------------------------------- +// Terrain block generation +// --------------------------------------------------------------------------- +static uint16_t select_surface_block(worldgen_ctx *ctx, const column_data *data, int world_x, int world_z) { + if (data->has_water && data->water_surface >= data->height) { + int slope = ground_slope(ctx, world_x, world_z); + if (slope <= 1) return BLOCK_SAND; + if (slope <= 3) return BLOCK_GRAVEL; + return BLOCK_STONE; + } + int snow_line = ctx->snow_line; + int snow_fade = 6; + if (data->height >= snow_line) { + return BLOCK_SNOW; + } + if (data->height >= snow_line - snow_fade) { + double t = (double)(data->height - (snow_line - snow_fade)) / (double)snow_fade; + double noise = simplex_noise2(&ctx->noise, world_x * 0.02, world_z * 0.02) * 0.5 + 0.5; + if (noise < t) return BLOCK_SNOW; + } + return BLOCK_GRASS; +} + +static int sample_block(worldgen_ctx *ctx, int x, int y, int z) { + if (y == 0) return BLOCK_BEDROCK; + column_data data = get_column_data(ctx, x, z); + if (y < data.height - 3) { + if (generate_coal(ctx, x, y, z, data.height, data.biome)) { + return BLOCK_COAL; + } + return BLOCK_STONE; + } + if (y < data.height) return BLOCK_DIRT; + if (y == data.height) { + return select_surface_block(ctx, &data, x, z); + } + if (data.has_water && y <= data.water_surface && y > data.height) return BLOCK_WATER; + return BLOCK_AIR; +} + +static int ground_slope(worldgen_ctx *ctx, int x, int z) { + int center = column_height(ctx, x, z); + int max_delta = 0; + const int offsets[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (int i = 0; i < 4; ++i) { + int nx = x + offsets[i][0]; + int nz = z + offsets[i][1]; + int neighbor = column_height(ctx, nx, nz); + int delta = abs(neighbor - center); + if (delta > max_delta) max_delta = delta; + } + return max_delta; +} + +static double tree_density_mask(worldgen_ctx *ctx, int x, int z) { + double forest = simplex_noise2(&ctx->noise, (x + 3000) * 0.01, (z - 3000) * 0.01); + double moisture = simplex_noise2(&ctx->noise, (x - 5000) * 0.02, (z + 5000) * 0.02); + return (forest * 0.6 + moisture * 0.4 + 1.0) * 0.5; +} + +static double old_growth_grove_mask(worldgen_ctx *ctx, int x, int z) { + double cell = worley_distance(x + 8000, z - 8000, 0.0014, 0x8BADF00Du); + double cluster = clamp01(1.25 - cell * 1.1); + double noise = simplex_noise2(&ctx->noise, (x - 12000) * 0.003, (z + 12000) * 0.003) * 0.5 + 0.5; + return clamp01(cluster * 0.7 + noise * 0.3); +} + +static int can_place_tree(worldgen_ctx *ctx, int x, int y, int z, int trunk_height, int radius, int crown_extra) { + int ground_block = sample_block(ctx, x, y - 1, z); + if (ground_block != BLOCK_GRASS && ground_block != BLOCK_SNOW && ground_block != BLOCK_DIRT) return 0; + int top = y + trunk_height + crown_extra; + if (top >= CHUNK_HEIGHT) return 0; + for (int cy = y; cy <= top; ++cy) { + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + if (cy < y && (dx != 0 || dz != 0)) continue; + int block = sample_block(ctx, x + dx, cy, z + dz); + if (block != BLOCK_AIR && block != BLOCK_SNOW) { + return 0; + } + } + } + } + return 1; +} + +static void place_log_column(int x, int y, int z, int height, int log_block, block_list *out) { + for (int dy = 0; dy < height; ++dy) { + block_list_push(out, x, y + dy, z, (uint16_t)log_block); + } +} + +static void extend_trunk_to(int x, int y, int z, int current_height, int target_height, int log_block, block_list *out) { + for (int dy = current_height; dy < target_height; ++dy) { + block_list_push(out, x, y + dy, z, (uint16_t)log_block); + } +} + +static void reinforce_trunk(int x, int y, int z, int top_y, int log_block, block_list *out) { + if (top_y < y) return; + if (top_y >= CHUNK_HEIGHT) top_y = CHUNK_HEIGHT - 1; + for (int py = y; py <= top_y; ++py) { + block_list_push(out, x, py, z, (uint16_t)log_block); + } +} + +static void place_leaf_circle(int cx, int cy, int cz, int radius, int leaf_block, rng_state *rng, double hole_prob, block_list *out) { + int r2 = radius * radius; + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + if (dx * dx + dz * dz > r2) continue; + if (hole_prob > 0.0 && rng_next_f64(rng) < hole_prob) continue; + block_list_push(out, cx + dx, cy, cz + dz, (uint16_t)leaf_block); + } + } +} + +static void place_leaf_blob(int cx, int cy, int cz, int radius, int height, int leaf_block, rng_state *rng, block_list *out) { + for (int dy = -height; dy <= height; ++dy) { + int level_radius = radius - abs(dy) / 2; + if (level_radius < 1) level_radius = 1; + place_leaf_circle(cx, cy + dy, cz, level_radius, leaf_block, rng, 0.2, out); + } +} + +static void place_branch_span(int x, int y, int z, int dx, int dz, int length, int lift_step, int log_block, int leaf_block, rng_state *rng, block_list *out) { + int cx = x; + int cy = y; + int cz = z; + for (int step = 1; step <= length; ++step) { + cx += dx; + cz += dz; + if (lift_step > 0 && step % lift_step == 0) { + cy += 1; + } + block_list_push(out, cx, cy, cz, (uint16_t)log_block); + if (leaf_block >= 0) { + int radius = (step == length) ? 2 : 1; + double hole = 0.2 + 0.1 * step; + place_leaf_circle(cx, cy, cz, radius, leaf_block, rng, hole, out); + } + } +} + +static void clear_snow_block(worldgen_ctx *ctx, int x, int y, int z, block_list *out) { + int block = sample_block(ctx, x, y, z); + if (block == BLOCK_SNOW) { + block_list_push(out, x, y, z, BLOCK_AIR); + } +} + +static void build_oak_round(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height - 1; + if (trunk_height < 4) trunk_height = 4; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int crown_start = y + trunk_height - 2; + int crown_top = y + trunk_height + 3; + int leaf_top = crown_top; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = crown_start; cy <= crown_top; ++cy) { + int rel = cy - crown_start; + int radius = 2; + if (rel == 0) radius = 1; + else if (rel >= 3) radius = 2 + (rel == 3); + if (rel >= 4) radius = 1; + place_leaf_circle(x, cy, z, radius + rng_range_inclusive(rng, 0, 1), arch->leaf_block, rng, 0.15, out); + } +} + +static void build_oak_sprawl(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height - 2; + if (trunk_height < 5) trunk_height = 5; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int canopy_base = y + trunk_height - 1; + int leaf_top = canopy_base + 5; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int layer = 0; layer < 5; ++layer) { + int cy = canopy_base + layer; + int radius = 3 + (layer <= 1 ? 1 : -1); + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.25, out); + if (layer % 2 == 0) { + place_leaf_circle(x, cy, z, radius + 1, arch->leaf_block, rng, 0.4, out); + } + } + place_leaf_circle(x, canopy_base + 5, z, 1, arch->leaf_block, rng, 0.0, out); +} + +static void build_oak_columnar(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + rng_range_inclusive(rng, 0, 2); + if (trunk_height < 6) trunk_height = 6; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int canopy_start = y + trunk_height - 3; + int canopy_top = canopy_start + 4; + int leaf_top = canopy_top + 1; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = canopy_start; cy <= canopy_top; ++cy) { + int rel = cy - canopy_start; + int radius = 2; + if (rel == 0) radius = 3; + if (rel >= 3) radius = 1; + double holes = (rel == 1) ? 0.2 : 0.3; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, holes, out); + } + const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + int branch_y = canopy_start - 1; + for (int i = 0; i < 4; ++i) { + if (rng_next_f64(rng) > 0.75) continue; + int bx = x + dirs[i][0]; + int bz = z + dirs[i][1]; + block_list_push(out, bx, branch_y, bz, (uint16_t)arch->log_block); + place_leaf_circle(bx, branch_y, bz, 2, arch->leaf_block, rng, 0.35, out); + } + place_leaf_circle(x, canopy_top + 1, z, 1, arch->leaf_block, rng, 0.0, out); +} + +static void build_birch_slender(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height < 5 ? 5 : height; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int crown_start = y + trunk_height - 2; + int leaf_top = crown_start + 4; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int layer = 0; layer < 5; ++layer) { + int cy = crown_start + layer; + int radius = 1 + (layer >= 2 ? 1 : 0); + if (layer == 4) radius = 1; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.15 + layer * 0.05, out); + } + int mid_start = y + trunk_height / 2; + for (int cy = mid_start; cy < crown_start; cy += 2) { + int small_radius = (cy % 4 == 0) ? 2 : 1; + place_leaf_circle(x, cy, z, small_radius, arch->leaf_block, rng, 0.35, out); + } +} + +static void build_birch_brush(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height - 1; + if (trunk_height < 5) trunk_height = 5; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_base = y + trunk_height - 3; + int leaf_top = leaf_base + 5; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = leaf_base; cy <= leaf_top; ++cy) { + int rel = cy - leaf_base; + int radius = 2 + (rel <= 1 ? 1 : 0); + if (rel >= 4) radius = 1; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.2, out); + } + for (int cy = y + trunk_height / 2; cy < leaf_base; cy += 2) { + if (rng_next_f64(rng) < 0.5) { + place_leaf_circle(x, cy, z, 1, arch->leaf_block, rng, 0.15, out); + } + } +} + +static void build_birch_fan(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 1; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int canopy_start = y + trunk_height - 4; + int canopy_top = canopy_start + 5; + extend_trunk_to(x, y, z, trunk_height, canopy_top - y, arch->log_block, out); + for (int cy = canopy_start; cy <= canopy_top; ++cy) { + int rel = cy - canopy_start; + int radius = 3 - (rel / 2); + if (radius < 1) radius = 1; + double hole = 0.25 + rel * 0.05; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, hole, out); + } + const int diag[4][2] = {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; + for (int i = 0; i < 4; ++i) { + int bx = x + diag[i][0]; + int bz = z + diag[i][1]; + block_list_push(out, bx, canopy_start - 1, bz, (uint16_t)arch->leaf_block); + } +} + +static void build_pine_spire(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 2; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_start = y + trunk_height / 4; + int leaf_top = y + trunk_height + 3; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = leaf_start; cy <= leaf_top; ++cy) { + double t = (double)(cy - leaf_start) / (double)(leaf_top - leaf_start + 1); + int radius = (int)(3 - t * 3.5); + if (radius < 1) radius = 1; + double holes = 0.1 + t * 0.3; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, holes, out); + } + place_leaf_circle(x, leaf_top + 1, z, 1, arch->leaf_block, rng, 0.0, out); +} + +static void build_pine_crown(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 1; + if (trunk_height < 7) trunk_height = 7; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_base = y + trunk_height - 4; + int leaf_top = y + trunk_height + 2; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = leaf_base; cy <= leaf_top; ++cy) { + int rel = cy - leaf_base; + int radius = 3 - rel / 2; + if (radius < 1) radius = 1; + double hole = 0.1 + rel * 0.05; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, hole, out); + if (rel == 1 || rel == 2) { + place_leaf_circle(x, cy - 1, z, radius + 1, arch->leaf_block, rng, 0.35, out); + } + } + reinforce_trunk(x, y, z, leaf_top, arch->log_block, out); +} + +static void build_spruce_tiers(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 3; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_top = y + trunk_height + 1; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + int tier_count = 4 + height / 3; + int tier_spacing = 2; + for (int t = 0; t < tier_count; ++t) { + int cy = y + trunk_height - t * tier_spacing; + if (cy < y + 2) break; + int radius = 1 + (tier_count - t); + if (radius > 4) radius = 4; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.15, out); + place_leaf_circle(x, cy - 1, z, radius - 1, arch->leaf_block, rng, 0.2, out); + } + place_leaf_circle(x, y + trunk_height + 1, z, 1, arch->leaf_block, rng, 0.0, out); + reinforce_trunk(x, y, z, leaf_top, arch->log_block, out); +} + +static void build_spruce_cone(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 4; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_start = y + trunk_height / 3; + int leaf_top = y + trunk_height + 2; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = leaf_start; cy <= leaf_top; ++cy) { + double t = (double)(cy - leaf_start) / (double)(leaf_top - leaf_start + 1); + int radius = 4 - (int)round(t * 3.5); + if (radius < 1) radius = 1; + double hole = 0.1 + t * 0.25; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, hole, out); + if (t > 0.35 && rng_next_f64(rng) < 0.5) { + place_leaf_circle(x, cy - 1, z, radius + 1, arch->leaf_block, rng, 0.4, out); + } + } + reinforce_trunk(x, y, z, leaf_top, arch->log_block, out); + place_leaf_circle(x, leaf_top + 1, z, 1, arch->leaf_block, rng, 0.0, out); +} + +static void build_spruce_spread(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 1; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int tier_base = y + trunk_height - 2; + int leaf_top = y + trunk_height + 1; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int tier = 0; tier < 4; ++tier) { + int cy = tier_base - tier * 2; + if (cy < y + 2) break; + int radius = 3 - tier / 2; + if (radius < 1) radius = 1; + place_leaf_circle(x, cy, z, radius + 1, arch->leaf_block, rng, 0.2, out); + place_leaf_circle(x, cy - 1, z, radius, arch->leaf_block, rng, 0.3, out); + } + reinforce_trunk(x, y, z, leaf_top, arch->log_block, out); + place_leaf_circle(x, leaf_top + 1, z, 1, arch->leaf_block, rng, 0.0, out); +} + +static void build_walnut_table(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height - 1; + if (trunk_height < 4) trunk_height = 4; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int canopy_center = y + trunk_height; + int leaf_top = canopy_center + 3; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + place_leaf_blob(x, canopy_center, z, 4, 2, arch->leaf_block, rng, out); + place_leaf_circle(x, canopy_center + 3, z, 2, arch->leaf_block, rng, 0.15, out); +} + +static void build_walnut_vase(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int canopy_center = y + trunk_height - 1; + int leaf_top = canopy_center + 4; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + place_leaf_blob(x, canopy_center, z, 3, 3, arch->leaf_block, rng, out); + place_leaf_circle(x, canopy_center + 4, z, 2, arch->leaf_block, rng, 0.2, out); + for (int i = 0; i < 4; ++i) { + if (rng_next_f64(rng) < 0.5) { + int offset = (i % 2 == 0) ? 2 : -2; + block_list_push(out, x + offset, canopy_center - 1, z, (uint16_t)arch->log_block); + place_leaf_circle(x + offset, canopy_center - 1, z, 2, arch->leaf_block, rng, 0.35, out); + } + } +} + +static void build_cypress_column(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 4; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_start = y + trunk_height / 2; + int leaf_top = y + trunk_height + 2; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = leaf_start; cy <= leaf_top; ++cy) { + int radius = (cy > y + trunk_height) ? 1 : 1 + ((cy - leaf_start) % 2 == 0); + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.25, out); + } +} + +static void build_cypress_fan(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 2; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_start = y + trunk_height / 2; + int leaf_top = y + trunk_height + 3; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = leaf_start; cy <= leaf_top; ++cy) { + int rel = cy - leaf_start; + int radius = (rel % 2 == 0) ? 2 : 1; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.3, out); + } +} + +static void build_maple_tangle(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height - 1; + if (trunk_height < 5) trunk_height = 5; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int branch_levels = 3; + int leaf_top = y + trunk_height + 2; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int i = 0; i < branch_levels; ++i) { + int level_y = y + trunk_height - (i * 2); + int radius = 2 + i; + place_leaf_circle(x, level_y, z, radius, arch->leaf_block, rng, 0.25, out); + int branches = 4 + i; + for (int b = 0; b < branches; ++b) { + double angle = (double)b / branches * 6.28318 + rng_next_f64(rng) * 0.2; + int bx = x + (int)round(cos(angle) * radius); + int bz = z + (int)round(sin(angle) * radius); + block_list_push(out, (bx + x) / 2, level_y - 1, (bz + z) / 2, (uint16_t)arch->log_block); + place_leaf_circle(bx, level_y, bz, 1, arch->leaf_block, rng, 0.1, out); + } + } + place_leaf_circle(x, y + trunk_height + 2, z, 2, arch->leaf_block, rng, 0.0, out); +} + +static void build_maple_spread(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height - 1; + if (trunk_height < 5) trunk_height = 5; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int canopy_base = y + trunk_height - 1; + int leaf_top = canopy_base + 4; + extend_trunk_to(x, y, z, trunk_height, leaf_top - y, arch->log_block, out); + for (int cy = canopy_base; cy <= leaf_top; ++cy) { + int rel = cy - canopy_base; + int radius = 3 + (rel <= 1 ? 1 : -rel / 2); + if (radius < 1) radius = 1; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.25, out); + } + for (int b = 0; b < 6; ++b) { + double angle = (double)b / 6.0 * 6.28318 + rng_next_f64(rng) * 0.2; + int bx = x + (int)round(cos(angle) * 3.5); + int bz = z + (int)round(sin(angle) * 3.5); + block_list_push(out, bx, canopy_base - 1, bz, (uint16_t)arch->log_block); + place_leaf_circle(bx, canopy_base - 1, bz, 2, arch->leaf_block, rng, 0.35, out); + } +} + +static void build_oak_ancient(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 5 + rng_range_inclusive(rng, 0, 3); + if (trunk_height < 10) trunk_height = 10; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int canopy_start = y + trunk_height - 6; + int canopy_top = canopy_start + 6; + reinforce_trunk(x, y, z, canopy_top + 2, arch->log_block, out); + const int dirs[8][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}; + for (int i = 0; i < 8; ++i) { + int len = 3 + rng_range_inclusive(rng, 1, 3); + int lift = (i < 4) ? 2 : 3; + place_branch_span(x, canopy_start - 1 - (i % 2), z, dirs[i][0], dirs[i][1], len + 1, lift, arch->log_block, arch->leaf_block, rng, out); + } + place_leaf_blob(x, canopy_start, z, 5, 3, arch->leaf_block, rng, out); + place_leaf_circle(x, canopy_top + 1, z, 3, arch->leaf_block, rng, 0.15, out); + for (int i = 0; i < 4; ++i) { + int butt = rng_range_inclusive(rng, 1, 2); + block_list_push(out, x + dirs[i][0], y + butt, z + dirs[i][1], (uint16_t)arch->log_block); + } +} + +static void build_spruce_ancient(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 6 + rng_range_inclusive(rng, 0, 2); + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int leaf_top = y + trunk_height + 3; + reinforce_trunk(x, y, z, leaf_top, arch->log_block, out); + int tiers = 5 + rng_range_inclusive(rng, 0, 1); + for (int t = 0; t < tiers; ++t) { + int cy = y + trunk_height - t * 3; + if (cy < y + 3) break; + int radius = 4 - t / 2; + if (radius < 2) radius = 2; + place_leaf_circle(x, cy, z, radius, arch->leaf_block, rng, 0.18, out); + place_leaf_circle(x, cy - 1, z, radius - 1, arch->leaf_block, rng, 0.25, out); + if (rng_next_f64(rng) < 0.6) { + int dir = rng_range_inclusive(rng, 0, 3); + const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + place_branch_span(x, cy - 1, z, dirs[dir][0], dirs[dir][1], 2 + rng_range_inclusive(rng, 1, 2), 3, arch->log_block, arch->leaf_block, rng, out); + } + } + place_leaf_circle(x, leaf_top, z, 2, arch->leaf_block, rng, 0.0, out); + place_leaf_circle(x, leaf_top + 1, z, 1, arch->leaf_block, rng, 0.0, out); +} + +static void build_maple_ancient(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) { + (void)ctx; + int trunk_height = height + 4; + place_log_column(x, y, z, trunk_height, arch->log_block, out); + int branch_base = y + trunk_height - 4; + int canopy_top = branch_base + 5; + reinforce_trunk(x, y, z, canopy_top + 1, arch->log_block, out); + const int dirs[6][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}}; + for (int i = 0; i < 6; ++i) { + int len = 3 + rng_range_inclusive(rng, 1, 3); + place_branch_span(x, branch_base - (i % 2), z, dirs[i][0], dirs[i][1], len, 2, arch->log_block, arch->leaf_block, rng, out); + } + place_leaf_blob(x, branch_base, z, 5, 3, arch->leaf_block, rng, out); + place_leaf_circle(x, canopy_top + 1, z, 3, arch->leaf_block, rng, 0.1, out); +} + + typedef enum { + TREE_OAK_ROUND = 0, + TREE_OAK_SPRAWL, + TREE_OAK_COLUMNAR, + TREE_BIRCH_SLENDER, + TREE_BIRCH_BRUSH, + TREE_BIRCH_FAN, + TREE_PINE_SPIRE, + TREE_PINE_CROWN, + TREE_SPRUCE_TIERS, + TREE_SPRUCE_CONE, + TREE_SPRUCE_SPREAD, + TREE_WALNUT_TABLE, + TREE_WALNUT_VASE, + TREE_CYPRESS_COLUMN, + TREE_CYPRESS_FAN, + TREE_MAPLE_TANGLE, + TREE_MAPLE_SPREAD, + TREE_OAK_ANCIENT, + TREE_SPRUCE_ANCIENT, + TREE_MAPLE_ANCIENT, + TREE_COUNT + } tree_kind; + + static const tree_archetype TREE_TYPES[TREE_COUNT] = { + [TREE_OAK_ROUND] = {"oak_round", TREE_SPECIES_OAK, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 6, 9, 3, 4, build_oak_round}, + [TREE_OAK_SPRAWL] = {"oak_sprawl", TREE_SPECIES_OAK, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 7, 11, 4, 5, build_oak_sprawl}, + [TREE_OAK_COLUMNAR] = {"oak_columnar", TREE_SPECIES_OAK, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 7, 11, 3, 5, build_oak_columnar}, + [TREE_BIRCH_SLENDER] = {"birch_slender", TREE_SPECIES_BIRCH, BLOCK_BIRCH_LOG, BLOCK_BIRCH_LEAVES, 6, 9, 2, 4, build_birch_slender}, + [TREE_BIRCH_BRUSH] = {"birch_brush", TREE_SPECIES_BIRCH, BLOCK_BIRCH_LOG, BLOCK_BIRCH_LEAVES, 6, 9, 3, 4, build_birch_brush}, + [TREE_BIRCH_FAN] = {"birch_fan", TREE_SPECIES_BIRCH, BLOCK_BIRCH_LOG, BLOCK_BIRCH_LEAVES, 7, 10, 3, 4, build_birch_fan}, + [TREE_PINE_SPIRE] = {"pine_spire", TREE_SPECIES_PINE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 9, 13, 3, 5, build_pine_spire}, + [TREE_PINE_CROWN] = {"pine_crown", TREE_SPECIES_PINE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 9, 13, 3, 5, build_pine_crown}, + [TREE_SPRUCE_TIERS] = {"spruce_tiers", TREE_SPECIES_SPRUCE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 10, 14, 3, 6, build_spruce_tiers}, + [TREE_SPRUCE_CONE] = {"spruce_cone", TREE_SPECIES_SPRUCE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 10, 14, 3, 6, build_spruce_cone}, + [TREE_SPRUCE_SPREAD] = {"spruce_spread", TREE_SPECIES_SPRUCE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 9, 13, 3, 6, build_spruce_spread}, + [TREE_WALNUT_TABLE] = {"walnut_table", TREE_SPECIES_WALNUT, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 6, 9, 4, 4, build_walnut_table}, + [TREE_WALNUT_VASE] = {"walnut_vase", TREE_SPECIES_WALNUT, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 6, 9, 4, 4, build_walnut_vase}, + [TREE_CYPRESS_COLUMN] = {"cypress_column", TREE_SPECIES_CYPRESS, BLOCK_BIRCH_LOG, BLOCK_BIRCH_LEAVES, 8, 12, 2, 6, build_cypress_column}, + [TREE_CYPRESS_FAN] = {"cypress_fan", TREE_SPECIES_CYPRESS, BLOCK_BIRCH_LOG, BLOCK_BIRCH_LEAVES, 8, 12, 3, 6, build_cypress_fan}, + [TREE_MAPLE_TANGLE] = {"maple_tangle", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 7, 10, 4, 5, build_maple_tangle}, + [TREE_MAPLE_SPREAD] = {"maple_spread", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 7, 10, 4, 5, build_maple_spread}, + [TREE_OAK_ANCIENT] = {"oak_ancient", TREE_SPECIES_OAK, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 12, 16, 6, 7, build_oak_ancient}, + [TREE_SPRUCE_ANCIENT] = {"spruce_ancient", TREE_SPECIES_SPRUCE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 12, 16, 5, 7, build_spruce_ancient}, + [TREE_MAPLE_ANCIENT] = {"maple_ancient", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 11, 15, 6, 6, build_maple_ancient}, + }; + +static const int TREE_VARIANTS_OAK[] = {TREE_OAK_ROUND, TREE_OAK_SPRAWL, TREE_OAK_COLUMNAR, TREE_OAK_ANCIENT}; +static const int TREE_VARIANTS_BIRCH[] = {TREE_BIRCH_SLENDER, TREE_BIRCH_BRUSH, TREE_BIRCH_FAN}; +static const int TREE_VARIANTS_SPRUCE[] = {TREE_SPRUCE_TIERS, TREE_SPRUCE_CONE, TREE_SPRUCE_SPREAD, TREE_SPRUCE_ANCIENT}; +static const int TREE_VARIANTS_PINE[] = {TREE_PINE_SPIRE, TREE_PINE_CROWN}; +static const int TREE_VARIANTS_WALNUT[] = {TREE_WALNUT_TABLE, TREE_WALNUT_VASE}; +static const int TREE_VARIANTS_CYPRESS[] = {TREE_CYPRESS_COLUMN, TREE_CYPRESS_FAN}; +static const int TREE_VARIANTS_MAPLE[] = {TREE_MAPLE_TANGLE, TREE_MAPLE_SPREAD, TREE_MAPLE_ANCIENT}; + +static const tree_species_catalog TREE_VARIANT_CATALOG[] = { + {TREE_SPECIES_OAK, "oak", TREE_VARIANTS_OAK, sizeof(TREE_VARIANTS_OAK) / sizeof(TREE_VARIANTS_OAK[0])}, + {TREE_SPECIES_BIRCH, "birch", TREE_VARIANTS_BIRCH, sizeof(TREE_VARIANTS_BIRCH) / sizeof(TREE_VARIANTS_BIRCH[0])}, + {TREE_SPECIES_SPRUCE, "spruce", TREE_VARIANTS_SPRUCE, sizeof(TREE_VARIANTS_SPRUCE) / sizeof(TREE_VARIANTS_SPRUCE[0])}, + {TREE_SPECIES_PINE, "pine", TREE_VARIANTS_PINE, sizeof(TREE_VARIANTS_PINE) / sizeof(TREE_VARIANTS_PINE[0])}, + {TREE_SPECIES_WALNUT, "walnut", TREE_VARIANTS_WALNUT, sizeof(TREE_VARIANTS_WALNUT) / sizeof(TREE_VARIANTS_WALNUT[0])}, + {TREE_SPECIES_CYPRESS, "cypress", TREE_VARIANTS_CYPRESS, sizeof(TREE_VARIANTS_CYPRESS) / sizeof(TREE_VARIANTS_CYPRESS[0])}, + {TREE_SPECIES_MAPLE, "maple", TREE_VARIANTS_MAPLE, sizeof(TREE_VARIANTS_MAPLE) / sizeof(TREE_VARIANTS_MAPLE[0])} +}; + +static const tree_species_catalog *get_species_catalog(tree_species species) { + size_t count = sizeof(TREE_VARIANT_CATALOG) / sizeof(TREE_VARIANT_CATALOG[0]); + for (size_t i = 0; i < count; ++i) { + if (TREE_VARIANT_CATALOG[i].species == species) { + return &TREE_VARIANT_CATALOG[i]; + } + } + return NULL; +} + +static const tree_archetype *choose_species_variant(tree_species species, rng_state *rng) { + const tree_species_catalog *catalog = get_species_catalog(species); + if (!catalog || catalog->variant_count == 0) return NULL; + size_t idx = (size_t)rng_range_inclusive(rng, 0, (int)catalog->variant_count - 1); + return &TREE_TYPES[catalog->variants[idx]]; +} + +static const int TREE_POOL_WEST[] = { + TREE_OAK_ROUND, TREE_OAK_SPRAWL, TREE_OAK_COLUMNAR, + TREE_WALNUT_TABLE, TREE_WALNUT_VASE, + TREE_MAPLE_TANGLE, TREE_MAPLE_SPREAD, + TREE_CYPRESS_COLUMN, TREE_CYPRESS_FAN +}; + +static const int TREE_POOL_EAST_LOW[] = { + TREE_OAK_ROUND, TREE_OAK_SPRAWL, TREE_OAK_COLUMNAR, + TREE_MAPLE_TANGLE, TREE_MAPLE_SPREAD, + TREE_BIRCH_SLENDER, TREE_BIRCH_BRUSH, TREE_BIRCH_FAN, + TREE_PINE_SPIRE +}; + +static const int TREE_POOL_EAST_HIGH[] = { + TREE_BIRCH_SLENDER, TREE_BIRCH_BRUSH, TREE_BIRCH_FAN, + TREE_PINE_SPIRE, TREE_PINE_CROWN, + TREE_SPRUCE_TIERS, TREE_SPRUCE_CONE, TREE_SPRUCE_SPREAD, + TREE_CYPRESS_COLUMN +}; + +static const int TREE_POOL_OLD_GROWTH[] = { + TREE_OAK_ANCIENT, TREE_SPRUCE_ANCIENT, TREE_MAPLE_ANCIENT, + TREE_OAK_SPRAWL, TREE_MAPLE_SPREAD, TREE_WALNUT_VASE, + TREE_SPRUCE_TIERS, TREE_PINE_CROWN +}; + +static const tree_archetype *choose_tree_archetype(worldgen_ctx *ctx, const column_data *data, rng_state *rng) { + int altitude = data->height - ctx->sea_level; + const tree_archetype *fallback = &TREE_TYPES[TREE_OAK_ROUND]; + int snow_line = ctx->snow_line; + if (data->height >= snow_line - 2) { + const tree_archetype *snow_pick = choose_species_variant(TREE_SPECIES_SPRUCE, rng); + if (snow_pick && rng_next_f64(rng) < 0.9) return snow_pick; + } + if (data->biome == BIOME_OLD_GROWTH_PLAINS) { + size_t pool_size = sizeof(TREE_POOL_OLD_GROWTH) / sizeof(TREE_POOL_OLD_GROWTH[0]); + size_t idx = (size_t)rng_range_inclusive(rng, 0, (int)pool_size - 1); + return &TREE_TYPES[TREE_POOL_OLD_GROWTH[idx]]; + } + if (data->biome == BIOME_WEST_KY_COALFIELDS) { + size_t pool_size = sizeof(TREE_POOL_WEST) / sizeof(TREE_POOL_WEST[0]); + size_t idx = (size_t)rng_range_inclusive(rng, 0, (int)pool_size - 1); + return &TREE_TYPES[TREE_POOL_WEST[idx]]; + } + const int *pool = (altitude > 30) ? TREE_POOL_EAST_HIGH : TREE_POOL_EAST_LOW; + size_t pool_size = (altitude > 30) ? sizeof(TREE_POOL_EAST_HIGH) / sizeof(TREE_POOL_EAST_HIGH[0]) + : sizeof(TREE_POOL_EAST_LOW) / sizeof(TREE_POOL_EAST_LOW[0]); + size_t idx = (size_t)rng_range_inclusive(rng, 0, (int)pool_size - 1); + const tree_archetype *choice = &TREE_TYPES[pool[idx]]; + return choice ? choice : fallback; +} + +static void generate_tree(worldgen_ctx *ctx, const tree_archetype *arch, int x, int y, int z, rng_state *rng, block_list *out) { + if (!arch) return; + int height = rng_range_inclusive(rng, arch->min_height, arch->max_height); + if (!can_place_tree(ctx, x, y, z, height, arch->space_radius, arch->canopy_extra)) { + if (!can_place_tree(ctx, x, y, z, height, arch->space_radius - 1 >= 1 ? arch->space_radius - 1 : 1, arch->canopy_extra)) return; + } + clear_snow_block(ctx, x, y, z, out); + arch->builder(ctx, x, y, z, height, rng, out, arch); +} + +static void generate_chunk_grass(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out) { + for (int dx = 0; dx < CHUNK_SIZE; ++dx) { + for (int dz = 0; dz < CHUNK_SIZE; ++dz) { + column_data cd = columns[dx][dz]; + if (cd.height <= 0 || cd.height >= CHUNK_HEIGHT - 1) continue; + if (cd.height >= ctx->snow_line - 1) continue; + int world_x = chunk_x * CHUNK_SIZE + dx; + int world_z = chunk_z * CHUNK_SIZE + dz; + if (out->blocks[cd.height][dx][dz] != BLOCK_GRASS) continue; + if (out->blocks[cd.height + 1][dx][dz] != BLOCK_AIR) continue; + if (ground_slope(ctx, world_x, world_z) > 3) continue; + double density = tree_density_mask(ctx, world_x, world_z); + double meadow = simplex_noise2(&ctx->noise, (world_x + 1200) * 0.02, (world_z - 1200) * 0.02) * 0.5 + 0.5; + double humidity = simplex_noise2(&ctx->noise, (world_x - 9000) * 0.01, (world_z + 9000) * 0.01) * 0.5 + 0.5; + double chance = 0.15 + meadow * 0.25 + humidity * 0.2 + (1.0 - density) * 0.2; + if (cd.height < ctx->sea_level) chance *= 0.7; + chance = clamp01(chance); + uint32_t h = hash_coords(world_x, world_z, (uint32_t)(ctx->world_seed ^ 0x5F3759DFu)); + double roll = (double)(h & 0xFFFF) / 65535.0; + if (roll > chance) continue; + out->blocks[cd.height + 1][dx][dz] = BLOCK_TALL_GRASS; + if (roll < chance * 0.4) { + const int offsets[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (int i = 0; i < 4; ++i) { + int nx = dx + offsets[i][0]; + int nz = dz + offsets[i][1]; + if (nx < 0 || nx >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE) continue; + column_data nd = columns[nx][nz]; + if (nd.height <= 0 || nd.height >= CHUNK_HEIGHT - 1) continue; + if (out->blocks[nd.height][nx][nz] != BLOCK_GRASS) continue; + if (out->blocks[nd.height + 1][nx][nz] != BLOCK_AIR) continue; + out->blocks[nd.height + 1][nx][nz] = BLOCK_TALL_GRASS; + } + } + } + } +} + +static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, block_list *out) { + const int grid = 5; + const int margin = 4; + const int min_tree_alt = ctx->sea_level - 2; + const int low_fade_top = ctx->sea_level + 6; + const int tree_line = ctx->sea_level + 60; + const int tree_line_fade = 20; + const int max_slope = 2; + + int chunk_min_x = chunk_x * CHUNK_SIZE - margin; + int chunk_max_x = chunk_min_x + CHUNK_SIZE + 2 * margin - 1; + int chunk_min_z = chunk_z * CHUNK_SIZE - margin; + int chunk_max_z = chunk_min_z + CHUNK_SIZE + 2 * margin - 1; + + int grid_min_x = (int)floor((double)chunk_min_x / grid); + int grid_max_x = (int)floor((double)chunk_max_x / grid); + int grid_min_z = (int)floor((double)chunk_min_z / grid); + int grid_max_z = (int)floor((double)chunk_max_z / grid); + + for (int gx = grid_min_x; gx <= grid_max_x; ++gx) { + for (int gz = grid_min_z; gz <= grid_max_z; ++gz) { + uint64_t seed = (uint64_t)ctx->world_seed + (uint64_t)gx * 341873128712ULL + (uint64_t)gz * 132897987541ULL; + rng_state rng; + rng_seed(&rng, seed); + + int candidate_x = gx * grid + rng_range_inclusive(&rng, 0, grid - 1); + int candidate_z = gz * grid + rng_range_inclusive(&rng, 0, grid - 1); + + if (candidate_x < chunk_min_x || candidate_x > chunk_max_x || candidate_z < chunk_min_z || candidate_z > chunk_max_z) { + continue; + } + + column_data data = get_column_data(ctx, candidate_x, candidate_z); + if (data.has_water && data.height < data.water_surface) continue; + if (data.height < min_tree_alt || data.height > tree_line) continue; + if (ground_slope(ctx, candidate_x, candidate_z) > max_slope) continue; + int is_old_growth = (data.biome == BIOME_OLD_GROWTH_PLAINS); + double grove_mask = is_old_growth ? old_growth_grove_mask(ctx, candidate_x, candidate_z) : 0.0; + if (is_old_growth && grove_mask < 0.35) { + double skip_chance = 0.7 - grove_mask * 0.5; + if (rng_next_f64(&rng) < skip_chance) continue; + } + + double altitude_factor = 1.0; + if (data.height < low_fade_top) { + int span = low_fade_top - min_tree_alt; + if (span < 1) span = 1; + altitude_factor *= clamp01((double)(data.height - min_tree_alt) / span); + } + if (data.height > tree_line - tree_line_fade) { + altitude_factor *= clamp01((double)(tree_line - data.height) / tree_line_fade); + } + if (is_old_growth) { + altitude_factor *= 0.85 + 0.15 * grove_mask; + } + if (altitude_factor <= 0.0) continue; + + double density = tree_density_mask(ctx, candidate_x, candidate_z); + if (is_old_growth) { + density = clamp01(0.25 + grove_mask * 0.75); + } + double spawn_prob = 0.7 * (0.55 + 0.45 * density) * altitude_factor; + if (is_old_growth) { + spawn_prob = clamp01(0.25 + grove_mask * 0.7) * altitude_factor; + } + if (rng_next_f64(&rng) > spawn_prob) continue; + + block_list tmp; + block_list_init(&tmp); + const tree_archetype *arch = choose_tree_archetype(ctx, &data, &rng); + int base_y = data.height + 1; + uint16_t surface = select_surface_block(ctx, &data, candidate_x, candidate_z); + if (surface == BLOCK_SNOW) { + base_y = data.height; + } + generate_tree(ctx, arch, candidate_x, base_y, candidate_z, &rng, &tmp); + if (tmp.count == 0) { + block_list_free(&tmp); + continue; + } + + for (size_t i = 0; i < tmp.count; ++i) { + int bx = tmp.items[i].x; + int by = tmp.items[i].y; + int bz = tmp.items[i].z; + if (by < 0 || by >= CHUNK_HEIGHT) continue; + if (bx >= chunk_x * CHUNK_SIZE && bx <= chunk_x * CHUNK_SIZE + CHUNK_SIZE - 1 && + bz >= chunk_z * CHUNK_SIZE && bz <= chunk_z * CHUNK_SIZE + CHUNK_SIZE - 1) { + block_list_push(out, bx, by, bz, tmp.items[i].id); + } + } + block_list_free(&tmp); + } + } +} + +static void trail_node_position(worldgen_ctx *ctx, int node_x, int node_z, double spacing, double *out_x, double *out_z) { + double base_x = node_x * spacing; + double base_z = node_z * spacing; + double jitter = spacing * 0.4; + double jx = simplex_noise2(&ctx->noise, node_x * 0.11, node_z * 0.11) * jitter; + double jz = simplex_noise2(&ctx->noise, (node_x + 4000) * 0.11, (node_z - 4000) * 0.11) * jitter; + *out_x = base_x + jx; + *out_z = base_z + jz; +} + +typedef struct { + int dx; + int dz; +} trail_neighbor_offset; + +static const trail_neighbor_offset TRAIL_NEIGHBOR_OFFSETS[] = { + { 1, 0 }, + { 0, 1 }, + { 1, 1 }, + { 1, -1 }, + { 2, 0 }, + { 0, 2 }, + { 2, 1 }, + { 1, 2 } +}; + +static uint32_t trail_segment_hash(int ax, int az, int bx, int bz, uint32_t seed) { + if (bx < ax || (bx == ax && bz < az)) { + int tx = ax; + int tz = az; + ax = bx; + az = bz; + bx = tx; + bz = tz; + } + int mix_x = ax * 73856093 + bx * 19349663; + int mix_z = az * 83492791 + bz * 29791; + return hash_coords(mix_x, mix_z, seed); +} + +static int should_connect_trail_nodes(worldgen_ctx *ctx, int node_x0, int node_z0, int node_x1, int node_z1) { + if (node_x0 == node_x1 && node_z0 == node_z1) return 0; + double ax, az, bx, bz; + trail_node_position(ctx, node_x0, node_z0, TRAIL_NODE_SPACING, &ax, &az); + trail_node_position(ctx, node_x1, node_z1, TRAIL_NODE_SPACING, &bx, &bz); + double mid_x = 0.5 * (ax + bx); + double mid_z = 0.5 * (az + bz); + int mid_ix = (int)llround(mid_x); + int mid_iz = (int)llround(mid_z); + column_data mid_column = get_column_data(ctx, mid_ix, mid_iz); + double ridge_bias = clamp01((double)(mid_column.height - ctx->sea_level) / 40.0); + if (mid_column.has_water && mid_column.height <= mid_column.water_surface) { + ridge_bias -= 0.35; + } + double dir_x = bx - ax; + double dir_z = bz - az; + double dir_len = sqrt(dir_x * dir_x + dir_z * dir_z); + if (dir_len > 0.0001) { + dir_x /= dir_len; + dir_z /= dir_len; + } + double flow_x = simplex_noise2(&ctx->noise, mid_x * 0.0005, mid_z * 0.0005); + double flow_z = simplex_noise2(&ctx->noise, (mid_x + 2000) * 0.0005, (mid_z - 2000) * 0.0005); + double flow_len = sqrt(flow_x * flow_x + flow_z * flow_z); + double alignment = 0.0; + if (flow_len > 0.0001) { + flow_x /= flow_len; + flow_z /= flow_len; + alignment = fabs(dir_x * flow_x + dir_z * flow_z); + } + double dx = fabs((double)(node_x1 - node_x0)); + double dz = fabs((double)(node_z1 - node_z0)); + double grid_len = sqrt(dx * dx + dz * dz); + double base = 0.12; + if (grid_len <= 1.05) { + base += 0.2; + } else if (grid_len <= 2.2) { + base += 0.12; + } else { + base += 0.05; + } + if (dx == 0.0 || dz == 0.0) { + base += 0.05; + } + if (dx == dz && dx > 0.1) { + base += 0.03; + } + base += (alignment - 0.5) * 0.2; + base += (ridge_bias - 0.5) * 0.15; + double texture = simplex_noise2(&ctx->noise, mid_x * 0.0012, mid_z * 0.0012) * 0.5 + 0.5; + base += (texture - 0.5) * 0.15; + if (base < 0.02) base = 0.02; + if (base > 0.9) base = 0.9; + uint32_t h = trail_segment_hash(node_x0, node_z0, node_x1, node_z1, (uint32_t)ctx->world_seed ^ 0xB37D4A91u); + double r = (h + 1.0) / 4294967296.0; + return r < base; +} + +static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx, double bz, int **out_points, int *out_count) { + double min_x = fmin(ax, bx) - TRAIL_MARGIN; + double max_x = fmax(ax, bx) + TRAIL_MARGIN; + double min_z = fmin(az, bz) - TRAIL_MARGIN; + double max_z = fmax(az, bz) + TRAIL_MARGIN; + int grid_w = (int)((max_x - min_x) / TRAIL_CELL_SIZE) + 2; + int grid_h = (int)((max_z - min_z) / TRAIL_CELL_SIZE) + 2; + if (grid_w < 2) grid_w = 2; + if (grid_h < 2) grid_h = 2; + int total = grid_w * grid_h; + double *heights = (double *)malloc((size_t)total * sizeof(double)); + if (!heights) return 0; + for (int gz = 0; gz < grid_h; ++gz) { + double wz = min_z + gz * TRAIL_CELL_SIZE; + for (int gx = 0; gx < grid_w; ++gx) { + double wx = min_x + gx * TRAIL_CELL_SIZE; + heights[gz * grid_w + gx] = column_height(ctx, (int)llround(wx), (int)llround(wz)); + } + } + int sx = (int)llround((ax - min_x) / TRAIL_CELL_SIZE); + int sz = (int)llround((az - min_z) / TRAIL_CELL_SIZE); + int tx = (int)llround((bx - min_x) / TRAIL_CELL_SIZE); + int tz = (int)llround((bz - min_z) / TRAIL_CELL_SIZE); + if (sx < 0) sx = 0; + if (sx >= grid_w) sx = grid_w - 1; + if (sz < 0) sz = 0; + if (sz >= grid_h) sz = grid_h - 1; + if (tx < 0) tx = 0; + if (tx >= grid_w) tx = grid_w - 1; + if (tz < 0) tz = 0; + if (tz >= grid_h) tz = grid_h - 1; + int start = sz * grid_w + sx; + int goal = tz * grid_w + tx; + double *dist = (double *)malloc((size_t)total * sizeof(double)); + int *prev = (int *)malloc((size_t)total * sizeof(int)); + unsigned char *used = (unsigned char *)calloc((size_t)total, sizeof(unsigned char)); + if (!dist || !prev || !used) { + free(heights); + free(dist); + free(prev); + free(used); + return 0; + } + for (int i = 0; i < total; ++i) { + dist[i] = DBL_MAX; + prev[i] = -1; + } + dist[start] = 0.0; + int neighbors[8][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}}; + for (int step = 0; step < total; ++step) { + int current = -1; + double best = DBL_MAX; + for (int i = 0; i < total; ++i) { + if (!used[i] && dist[i] < best) { + best = dist[i]; + current = i; + } + } + if (current == -1 || current == goal) break; + used[current] = 1; + int cx = current % grid_w; + int cz = current / grid_w; + double current_height = heights[current]; + for (int n = 0; n < 8; ++n) { + int nx = cx + neighbors[n][0]; + int nz = cz + neighbors[n][1]; + if (nx < 0 || nx >= grid_w || nz < 0 || nz >= grid_h) continue; + int ni = nz * grid_w + nx; + if (used[ni]) continue; + double next_height = heights[ni]; + double slope = fabs(next_height - current_height); + if (slope > 3.5) continue; + double base_cost = (neighbors[n][0] == 0 || neighbors[n][1] == 0) ? 1.0 : 1.41421356237; + double cost = base_cost * (1.0 + slope * 4.0); + if (dist[current] + cost < dist[ni]) { + dist[ni] = dist[current] + cost; + prev[ni] = current; + } + } + } + int path_len = 0; + for (int cur = goal; cur != -1; cur = prev[cur]) { + path_len++; + if (cur == start) break; + } + if (path_len < 2) { + free(heights); + free(dist); + free(prev); + free(used); + return 0; + } + int *points = (int *)malloc((size_t)path_len * 2 * sizeof(int)); + if (!points) { + free(heights); + free(dist); + free(prev); + free(used); + return 0; + } + int cur = goal; + for (int i = path_len - 1; i >= 0; --i) { + int gx = cur % grid_w; + int gz = cur / grid_w; + points[i * 2] = (int)llround(min_x + gx * TRAIL_CELL_SIZE); + points[i * 2 + 1] = (int)llround(min_z + gz * TRAIL_CELL_SIZE); + cur = prev[cur]; + if (cur == -1 && i > 0) { + points[(i - 1) * 2] = points[i * 2]; + points[(i - 1) * 2 + 1] = points[i * 2 + 1]; + break; + } + } + *out_points = points; + *out_count = path_len; + free(heights); + free(dist); + free(prev); + free(used); + return 1; +} + +static trail_segment *get_trail_segment(worldgen_ctx *ctx, int node_x0, int node_z0, int node_x1, int node_z1) { + if (node_x0 == node_x1 && node_z0 == node_z1) return NULL; + if (node_x1 < node_x0 || (node_x1 == node_x0 && node_z1 < node_z0)) { + int tx = node_x0, tz = node_z0; + node_x0 = node_x1; + node_z0 = node_z1; + node_x1 = tx; + node_z1 = tz; + } + for (size_t i = 0; i < ctx->trail_segment_count; ++i) { + trail_segment *seg = &ctx->trail_segments[i]; + if (seg->ax == node_x0 && seg->az == node_z0 && seg->bx == node_x1 && seg->bz == node_z1) { + return seg; + } + } + if (ctx->trail_segment_count >= ctx->trail_segment_cap) { + size_t new_cap = ctx->trail_segment_cap ? ctx->trail_segment_cap * 2 : 16; + trail_segment *resized = (trail_segment *)realloc(ctx->trail_segments, new_cap * sizeof(trail_segment)); + if (!resized) return NULL; + ctx->trail_segments = resized; + ctx->trail_segment_cap = new_cap; + } + trail_segment *seg = &ctx->trail_segments[ctx->trail_segment_count]; + memset(seg, 0, sizeof(*seg)); + seg->ax = node_x0; + seg->az = node_z0; + seg->bx = node_x1; + seg->bz = node_z1; + double ax, az, bx, bz; + trail_node_position(ctx, node_x0, node_z0, TRAIL_NODE_SPACING, &ax, &az); + trail_node_position(ctx, node_x1, node_z1, TRAIL_NODE_SPACING, &bx, &bz); + int *points = NULL; + int count = 0; + if (!build_trail_path(ctx, ax, az, bx, bz, &points, &count)) { + points = (int *)malloc(4 * sizeof(int)); + if (!points) return NULL; + points[0] = (int)llround(ax); + points[1] = (int)llround(az); + points[2] = (int)llround(bx); + points[3] = (int)llround(bz); + count = 2; + } + seg->points = points; + seg->count = count; + ctx->trail_segment_count++; + return seg; +} + +static void place_trail_column(chunk_data *out, column_data columns[CHUNK_SIZE][CHUNK_SIZE], int chunk_x, int chunk_z, int world_x, int world_z, int target_height) { + int local_x = world_x - chunk_x * CHUNK_SIZE; + int local_z = world_z - chunk_z * CHUNK_SIZE; + if (local_x < 0 || local_x >= CHUNK_SIZE || local_z < 0 || local_z >= CHUNK_SIZE) return; + if (target_height < 1) target_height = 1; + if (target_height >= CHUNK_HEIGHT - 2) target_height = CHUNK_HEIGHT - 3; + int original_height = columns[local_x][local_z].height; + if (original_height < 1) original_height = target_height; + + for (int y = target_height - 1; y >= 1; --y) { + uint16_t block = out->blocks[y][local_x][local_z]; + if (block == BLOCK_AIR || block == BLOCK_WATER) { + out->blocks[y][local_x][local_z] = BLOCK_DIRT; + } else { + break; + } + } + + if (target_height > original_height) { + for (int y = original_height + 1; y <= target_height && y < CHUNK_HEIGHT; ++y) { + uint16_t fill = (y == target_height) ? BLOCK_GRAVEL : BLOCK_DIRT; + out->blocks[y][local_x][local_z] = fill; + } + } else { + out->blocks[target_height][local_x][local_z] = BLOCK_GRAVEL; + for (int y = target_height + 1; y <= original_height && y < CHUNK_HEIGHT; ++y) { + if (y <= target_height + 3) { + out->blocks[y][local_x][local_z] = BLOCK_AIR; + } else { + break; + } + } + } + columns[local_x][local_z].height = target_height; +} + +static void carve_trail_span(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *out, column_data columns[CHUNK_SIZE][CHUNK_SIZE], int x0, int z0, int x1, int z1, int width) { + int dx = abs(x1 - x0); + int sx = (x0 < x1) ? 1 : -1; + int dz = abs(z1 - z0); + int sz = (z0 < z1) ? 1 : -1; + int err = dx - dz; + int last_height = INT32_MIN; + const int max_step_up = 1; + const int max_step_down = 1; + const double carry_weight = 0.65; + while (1) { + column_data data = get_column_data(ctx, x0, z0); + int target = data.height; + if (last_height != INT32_MIN) { + double blended = carry_weight * (double)last_height + (1.0 - carry_weight) * (double)target; + target = (int)llround(blended); + if (target > last_height + max_step_up) { + target = last_height + max_step_up; + } else if (target < last_height - max_step_down) { + target = last_height - max_step_down; + } + } + last_height = target; + double tx = x1 - x0; + double tz = z1 - z0; + double len = sqrt(tx * tx + tz * tz); + double nx = 0.0, nz = 0.0; + if (len > 0.0001) { + nx = -tz / len; + nz = tx / len; + } + for (int w = -width / 2; w <= width / 2; ++w) { + int wx = x0 + (int)llround(nx * w); + int wz = z0 + (int)llround(nz * w); + place_trail_column(out, columns, chunk_x, chunk_z, wx, wz, target); + } + if (x0 == x1 && z0 == z1) break; + int err2 = 2 * err; + if (err2 > -dz) { + err -= dz; + x0 += sx; + } + if (err2 < dx) { + err += dx; + z0 += sz; + } + } +} + +static void generate_chunk_trails(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out) { + const double spacing = TRAIL_NODE_SPACING; + int chunk_min_x = chunk_x * CHUNK_SIZE; + int chunk_max_x = chunk_min_x + CHUNK_SIZE - 1; + int chunk_min_z = chunk_z * CHUNK_SIZE; + int chunk_max_z = chunk_min_z + CHUNK_SIZE - 1; + int min_node_x = (int)floor((chunk_min_x - spacing) / spacing); + int max_node_x = (int)ceil((chunk_max_x + spacing) / spacing); + int min_node_z = (int)floor((chunk_min_z - spacing) / spacing); + int max_node_z = (int)ceil((chunk_max_z + spacing) / spacing); + int width = TRAIL_WIDTH; + size_t neighbor_count = sizeof(TRAIL_NEIGHBOR_OFFSETS) / sizeof(TRAIL_NEIGHBOR_OFFSETS[0]); + for (int nx = min_node_x; nx <= max_node_x; ++nx) { + for (int nz = min_node_z; nz <= max_node_z; ++nz) { + for (size_t n = 0; n < neighbor_count; ++n) { + int nnx = nx + TRAIL_NEIGHBOR_OFFSETS[n].dx; + int nnz = nz + TRAIL_NEIGHBOR_OFFSETS[n].dz; + if (!should_connect_trail_nodes(ctx, nx, nz, nnx, nnz)) continue; + trail_segment *seg = get_trail_segment(ctx, nx, nz, nnx, nnz); + if (!seg || seg->count < 2) continue; + for (int i = 1; i < seg->count; ++i) { + int x0 = seg->points[(i - 1) * 2]; + int z0 = seg->points[(i - 1) * 2 + 1]; + int x1 = seg->points[i * 2]; + int z1 = seg->points[i * 2 + 1]; + int span_min_x = (x0 < x1) ? x0 : x1; + int span_max_x = (x0 > x1) ? x0 : x1; + int span_min_z = (z0 < z1) ? z0 : z1; + int span_max_z = (z0 > z1) ? z0 : z1; + if (span_max_x < chunk_min_x - 4 || span_min_x > chunk_max_x + 4) continue; + if (span_max_z < chunk_min_z - 4 || span_min_z > chunk_max_z + 4) continue; + carve_trail_span(ctx, chunk_x, chunk_z, out, columns, x0, z0, x1, z1, width); + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- +void worldgen_init(worldgen_ctx *ctx, int world_seed, int sea_level, int snow_line) { + ctx->world_seed = world_seed; + ctx->sea_level = sea_level; + ctx->snow_line = snow_line; + ctx->enable_trails = 0; + ctx->trail_segments = NULL; + ctx->trail_segment_count = 0; + ctx->trail_segment_cap = 0; + simplex_init(&ctx->noise, (uint32_t)world_seed); +} + +void worldgen_generate_chunk(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *out) { + memset(out, 0, sizeof(*out)); + out->chunk_x = chunk_x; + out->chunk_z = chunk_z; + + // Precompute column data for base terrain + column_data columns[CHUNK_SIZE][CHUNK_SIZE]; + for (int dx = 0; dx < CHUNK_SIZE; ++dx) { + for (int dz = 0; dz < CHUNK_SIZE; ++dz) { + int gx = chunk_x * CHUNK_SIZE + dx; + int gz = chunk_z * CHUNK_SIZE + dz; + columns[dx][dz] = get_column_data(ctx, gx, gz); + out->heightmap[dx][dz] = (uint16_t)columns[dx][dz].height; + } + } + + // Fill base terrain + for (int dx = 0; dx < CHUNK_SIZE; ++dx) { + for (int dz = 0; dz < CHUNK_SIZE; ++dz) { + column_data cd = columns[dx][dz]; + int world_x = chunk_x * CHUNK_SIZE + dx; + int world_z = chunk_z * CHUNK_SIZE + dz; + for (int y = 0; y < CHUNK_HEIGHT; ++y) { + int id; + if (y == 0) { + id = BLOCK_BEDROCK; + } else if (y < cd.height - 3) { + if (generate_coal(ctx, chunk_x * CHUNK_SIZE + dx, y, chunk_z * CHUNK_SIZE + dz, cd.height, cd.biome)) { + id = BLOCK_COAL; + } else { + id = BLOCK_STONE; + } + } else if (y < cd.height) { + id = BLOCK_DIRT; + } else if (y == cd.height) { + id = select_surface_block(ctx, &cd, world_x, world_z); + } else if (cd.has_water && y <= cd.water_surface && y > cd.height) { + id = BLOCK_WATER; + } else { + id = BLOCK_AIR; + } + out->blocks[y][dx][dz] = (uint16_t)id; + } + } + } + + if (ctx->enable_trails) { + generate_chunk_trails(ctx, chunk_x, chunk_z, columns, out); + } + + generate_chunk_grass(ctx, chunk_x, chunk_z, columns, out); + + // Tree overlay + block_list trees; + block_list_init(&trees); + generate_chunk_trees(ctx, chunk_x, chunk_z, &trees); + for (size_t i = 0; i < trees.count; ++i) { + int lx = trees.items[i].x - chunk_x * CHUNK_SIZE; + int lz = trees.items[i].z - chunk_z * CHUNK_SIZE; + int ly = trees.items[i].y; + if (lx >= 0 && lx < CHUNK_SIZE && lz >= 0 && lz < CHUNK_SIZE && ly >= 0 && ly < CHUNK_HEIGHT) { + out->blocks[ly][lx][lz] = trees.items[i].id; + } + } + block_list_free(&trees); +}