Add world generator sources and binary
This commit is contained in:
446
export_mca.py
Normal file
446
export_mca.py
Normal file
@@ -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()
|
||||
709
mc.py
Normal file
709
mc.py
Normal file
@@ -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
|
||||
83
settings.py
Normal file
83
settings.py
Normal file
@@ -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
|
||||
22
worldgen-c/Makefile
Normal file
22
worldgen-c/Makefile
Normal file
@@ -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
|
||||
BIN
worldgen-c/bin/worldgen
Executable file
BIN
worldgen-c/bin/worldgen
Executable file
Binary file not shown.
14
worldgen-c/include/noise.h
Normal file
14
worldgen-c/include/noise.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef WORLDGEN_NOISE_H
|
||||
#define WORLDGEN_NOISE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
52
worldgen-c/include/worldgen.h
Normal file
52
worldgen-c/include/worldgen.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef WORLDGEN_H
|
||||
#define WORLDGEN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#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
|
||||
818
worldgen-c/src/main.c
Normal file
818
worldgen-c/src/main.c
Normal file
@@ -0,0 +1,818 @@
|
||||
#include "worldgen.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <zlib.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
180
worldgen-c/src/noise.c
Normal file
180
worldgen-c/src/noise.c
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "noise.h"
|
||||
#include <math.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// 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);
|
||||
}
|
||||
1839
worldgen-c/src/worldgen.c
Normal file
1839
worldgen-c/src/worldgen.c
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user