diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8e6a98c --- /dev/null +++ b/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/bin/worldgen b/bin/worldgen new file mode 100755 index 0000000..0988b3c Binary files /dev/null and b/bin/worldgen differ diff --git a/include/noise.h b/include/noise.h new file mode 100644 index 0000000..e7dc1ad --- /dev/null +++ b/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/include/worldgen.h b/include/worldgen.h new file mode 100644 index 0000000..007059a --- /dev/null +++ b/include/worldgen.h @@ -0,0 +1,41 @@ +#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 +}; + +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; +} worldgen_ctx; + +void worldgen_init(worldgen_ctx *ctx, int world_seed, int sea_level); +void worldgen_generate_chunk(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *out); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..31cbccc --- /dev/null +++ b/src/main.c @@ -0,0 +1,799 @@ +#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; + pthread_mutex_t *log_mu; + 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} +}; + +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] [--format mca|bin] [--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); + 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; + 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], "--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], "--out") == 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 (!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, + .log_mu = &log_mu, + .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/src/main.o b/src/main.o new file mode 100644 index 0000000..a2b6981 Binary files /dev/null and b/src/main.o differ diff --git a/src/noise.c b/src/noise.c new file mode 100644 index 0000000..45bd0d9 --- /dev/null +++ b/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/src/noise.o b/src/noise.o new file mode 100644 index 0000000..a886ccb Binary files /dev/null and b/src/noise.o differ diff --git a/src/worldgen.c b/src/worldgen.c new file mode 100644 index 0000000..a9d8cb7 --- /dev/null +++ b/src/worldgen.c @@ -0,0 +1,463 @@ +#include "worldgen.h" + +#include +#include +#include +#include +#include + +typedef struct { + int height; + int water_surface; + int has_water; +} 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; + +// --------------------------------------------------------------------------- +// 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); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +static inline double clamp01(double v) { + if (v < 0.0) return 0.0; + if (v > 1.0) return 1.0; + return v; +} + +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 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 height = ctx->sea_level - 5; + height += simplex_noise2(&ctx->noise, warp2_x * 0.0005, warp2_z * 0.0005) * 80.0; + height += simplex_noise2(&ctx->noise, warp2_x * 0.002, warp2_z * 0.002) * 40.0; + height += simplex_noise2(&ctx->noise, warp2_x * 0.005, warp2_z * 0.005) * 25.0; + height += simplex_noise2(&ctx->noise, warp2_x * 0.01, warp2_z * 0.01) * 15.0; + height += simplex_noise2(&ctx->noise, warp2_x * 0.02, warp2_z * 0.02) * 5.0; + height += simplex_noise2(&ctx->noise, warp2_x * 0.05, warp2_z * 0.05) * 2.0; + height += simplex_noise2(&ctx->noise, warp2_x * 0.1, warp2_z * 0.1) * 1.0; + 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; + } else if (data.height < ctx->sea_level) { + data.has_water = 1; + data.water_surface = ctx->sea_level; + } else { + data.has_water = 0; + data.water_surface = 0; + } + return data; +} + +// --------------------------------------------------------------------------- +// Ore generation (coal seams) +// --------------------------------------------------------------------------- +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 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, const char *biome) { + (void)biome; + if (check_seam_generic(ctx, x, y, z, 12, 5, 0.02, 2.0, 0.03, 1.0, -0.7)) return 1; + if (check_seam_generic(ctx, x, y, z, 32, 4, 0.02, 2.0, 0.03, 1.0, -0.7)) return 1; + if (check_seam_generic(ctx, x, y, z, 85, 3, 0.02, 2.0, 0.03, 1.0, -0.7)) return 1; + if (check_seam_generic(ctx, x, y, z, 45, 2, 0.015, 1.5, 0.04, 0.5, -0.6)) return 1; + if (check_seam_generic(ctx, x, y, z, 25, 3, 0.015, 1.5, 0.04, 0.5, -0.6)) return 1; + if (check_flat_ceiling_seam(ctx, x, y, z, column_height, 5, 1, -0.3)) return 1; + if (check_terrain_relative_seam(ctx, x, y, z, column_height, 8, 2, 0.03, 0.8, -0.65)) return 1; + return 0; +} + +// --------------------------------------------------------------------------- +// Terrain block generation +// --------------------------------------------------------------------------- +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, "appalachian")) { + return BLOCK_COAL; + } + return BLOCK_STONE; + } + if (y < data.height) return BLOCK_DIRT; + if (y == data.height) return BLOCK_GRASS; + 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 int can_place_tree(worldgen_ctx *ctx, int x, int y, int z, int height) { + int ground_block = sample_block(ctx, x, y - 1, z); + if (ground_block != BLOCK_GRASS) return 0; + for (int cy = y; cy < y + height + 3 && cy < CHUNK_HEIGHT; ++cy) { + if (sample_block(ctx, x, cy, z) != BLOCK_AIR) return 0; + } + return 1; +} + +static void generate_oak_tree(worldgen_ctx *ctx, int x, int y, int z, int variation, rng_state *rng, block_list *out) { + (void)ctx; + int trunk_height = 4 + variation + rng_range_inclusive(rng, 0, 2); + for (int dy = 0; dy < trunk_height; ++dy) { + block_list_push(out, x, y + dy, z, BLOCK_OAK_LOG); + } + int crown_start = y + trunk_height - 2; + int crown_top = y + trunk_height + 2; + for (int cy = crown_start; cy < crown_top; ++cy) { + int dist = abs(cy - (crown_start + 2)); + int radius = 1; + if (dist == 0 || dist == 1) radius = 2; + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + if (abs(dx) == radius && abs(dz) == radius) { + if (rng_next_f64(rng) > 0.3) continue; + } + if (dx == 0 && dz == 0 && cy < y + trunk_height) continue; + block_list_push(out, x + dx, cy, z + dz, BLOCK_OAK_LEAVES); + } + } + } +} + +static void generate_birch_tree(worldgen_ctx *ctx, int x, int y, int z, rng_state *rng, block_list *out) { + (void)ctx; + int trunk_height = 5 + rng_range_inclusive(rng, 0, 3); + for (int dy = 0; dy < trunk_height; ++dy) { + block_list_push(out, x, y + dy, z, BLOCK_BIRCH_LOG); + } + int crown_start = y + trunk_height - 1; + int crown_top = y + trunk_height + 2; + for (int cy = crown_start; cy < crown_top; ++cy) { + int distance_from_top = crown_top - cy; + int radius = (distance_from_top <= 1) ? 1 : 2; + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + if (abs(dx) + abs(dz) > radius + 1) continue; + if (dx == 0 && dz == 0 && cy < y + trunk_height) continue; + block_list_push(out, x + dx, cy, z + dz, BLOCK_BIRCH_LEAVES); + } + } + } +} + +static void generate_tree(worldgen_ctx *ctx, int x, int y, int z, const char *type, int variation, rng_state *rng, block_list *out) { + if (strcmp(type, "oak") == 0) { + if (variation < 0) variation = rng_range_inclusive(rng, 0, 2); + if (!can_place_tree(ctx, x, y, z, 4 + variation)) return; + generate_oak_tree(ctx, x, y, z, variation, rng, out); + } else if (strcmp(type, "birch") == 0) { + if (!can_place_tree(ctx, x, y, z, 6)) return; + generate_birch_tree(ctx, x, y, z, rng, out); + } +} + +static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, block_list *out) { + const int grid = 6; + const int margin = 3; + 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; + + 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 (altitude_factor <= 0.0) continue; + + double density = tree_density_mask(ctx, candidate_x, candidate_z); + double spawn_prob = 0.5 * (0.6 + 0.4 * density) * altitude_factor; + if (rng_next_f64(&rng) > spawn_prob) continue; + + const char *tree_type = "oak"; + if (data.height > ctx->sea_level + 35) { + tree_type = (rng_next_f64(&rng) < 0.7) ? "birch" : "oak"; + } else if (data.height > ctx->sea_level + 15) { + tree_type = (rng_next_f64(&rng) < 0.7) ? "oak" : "birch"; + } + + block_list tmp; + block_list_init(&tmp); + generate_tree(ctx, candidate_x, data.height + 1, candidate_z, tree_type, -1, &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); + } + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- +void worldgen_init(worldgen_ctx *ctx, int world_seed, int sea_level) { + ctx->world_seed = world_seed; + ctx->sea_level = sea_level; + 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]; + 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, "appalachian")) { + id = BLOCK_COAL; + } else { + id = BLOCK_STONE; + } + } else if (y < cd.height) { + id = BLOCK_DIRT; + } else if (y == cd.height) { + id = BLOCK_GRASS; + } 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; + } + } + } + + // 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); +} diff --git a/src/worldgen.o b/src/worldgen.o new file mode 100644 index 0000000..f639136 Binary files /dev/null and b/src/worldgen.o differ