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 0000000..850e7e9 Binary files /dev/null and b/worldgen-c/bin/worldgen differ 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); +}