diff --git a/.gitignore b/.gitignore index d7b740f..6d26a02 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ # Project-specific output worldgen-c/test_out/ +worldgen-c/bin/worldgen-scan mc.zip lakeland-update-*/ untitled\ folder/ diff --git a/README.md b/README.md index e69de29..efba26d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,101 @@ +MC Worldgen +=========== + +Procedural Minecraft Java region exporter. The C exporter is the maintained path: +it builds without Python dependencies and writes `.mca` region files that can be +copied into an existing world's `region/` folder. + +Requirements +------------ + +- GCC or Clang +- `make` +- zlib development headers (`zlib1g-dev` on Debian/Ubuntu) +- pthreads, normally included with libc toolchains + +Build +----- + +```sh +cd worldgen-c +make +``` + +Quick smoke test: + +```sh +./bin/worldgen --radius 0 --out /tmp/mcworldgen-smoke +``` + +That should create `/tmp/mcworldgen-smoke/r.0.0.mca`. + +Diagnostic feature scan: + +```sh +make scan +./bin/worldgen-scan --radius 2 --center-x -3200 --center-z 3200 +``` + +The scanner prints counts for cabin blocks, gravel/path columns, redwood biome +columns, and titan-height tree trunks. + +Generate Regions +---------------- + +Small area around chunk 0,0: + +```sh +./bin/worldgen --radius 4 --trails --out output +``` + +Specific chunk rectangle: + +```sh +./bin/worldgen --min-x -8 --max-x 8 --min-z -8 --max-z 8 --trails --out output +``` + +Useful options: + +- `--radius N`: generate chunks from `center - N` through `center + N`. +- `--center-x X --center-z Z`: center chunk for radius mode. +- `--min-x --max-x --min-z --max-z`: explicit chunk bounds. +- `--threads N`: worker thread count. +- `--seed S`: deterministic world seed. +- `--sea-level L`: sea level, default `70`. +- `--snow-line H`: snow threshold, default `sea-level + 38`. +- `--trails`: enable trail and cabin prepass. +- `--format mca|bin`: write Minecraft region files or raw debug chunks. +- `--out DIR`: output directory. + +Features +-------- + +- Redwoods are enabled automatically. The biome classifier marks high-rainfall, + higher-relief areas as redwood forest, then places titan trees, dirt/mulch + floor cover, tall grass, and occasional fallen logs. +- Cabins are enabled automatically. With `--trails`, cabins only try to spawn on + chunks crossed by the trail network so their spur paths can connect to roads. + Without `--trails`, cabins can still spawn, but they are rarer and unconnected. +- Trails are optional because they need a prepass over the target area. Use + `--trails` when you want roads and better-connected cabins. + +Minecraft Version +----------------- + +The exporter writes Java Edition 1.17.1 chunk data (`DataVersion` 2730). That +version is chosen because the generator includes copper ore. Open the generated +regions in Minecraft Java 1.17.1 or a newer version that can upgrade old worlds. + +The exporter writes region files only. To use them in game, create or choose a +Java world, close Minecraft, back up the save, and replace or add files under: + +```text +/region/ +``` + +Python Exporter +--------------- + +`export_mca.py` is the older prototype exporter. It now shares the same 1.17.1 +data version and maps generated coal correctly, but the C exporter is faster and +has the current terrain features. diff --git a/export_mca.py b/export_mca.py index 1bf920c..cc020d6 100644 --- a/export_mca.py +++ b/export_mca.py @@ -27,7 +27,7 @@ TAG_COMPOUND = 10 TAG_INT_ARRAY = 11 TAG_LONG_ARRAY = 12 -DATA_VERSION = 2586 # Minecraft 1.15.2 +DATA_VERSION = 2730 # Minecraft Java Edition 1.17.1; matches copper-era block IDs. DEFAULT_RADIUS = 256 # ~512 block diameter (~0.5 km) CHUNK_HEIGHT = 256 SECTION_COUNT = CHUNK_HEIGHT // 16 @@ -54,6 +54,7 @@ def build_state_mapping(): 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"}) + set_state("coal", "minecraft:coal_ore") return mapping diff --git a/worldgen-c/Makefile b/worldgen-c/Makefile index 8e6a98c..ecf1e30 100644 --- a/worldgen-c/Makefile +++ b/worldgen-c/Makefile @@ -7,16 +7,22 @@ OBJ := $(SRC:.c=.o) BIN_DIR := bin TARGET := $(BIN_DIR)/worldgen +SCAN_TARGET := $(BIN_DIR)/worldgen-scan all: $(TARGET) $(TARGET): $(OBJ) | $(BIN_DIR) $(CC) $(CFLAGS) $(OBJ) -o $@ $(LDFLAGS) +$(SCAN_TARGET): tools/worldgen_scan.o src/worldgen.o src/noise.o | $(BIN_DIR) + $(CC) $(CFLAGS) tools/worldgen_scan.o src/worldgen.o src/noise.o -o $@ $(LDFLAGS) + $(BIN_DIR): mkdir -p $(BIN_DIR) clean: - rm -f $(OBJ) $(TARGET) + rm -f $(OBJ) tools/worldgen_scan.o $(TARGET) $(SCAN_TARGET) -.PHONY: all clean +scan: $(SCAN_TARGET) + +.PHONY: all clean scan diff --git a/worldgen-c/bin/worldgen b/worldgen-c/bin/worldgen index 4730b1d..529d512 100755 Binary files a/worldgen-c/bin/worldgen and b/worldgen-c/bin/worldgen differ diff --git a/worldgen-c/include/worldgen.h b/worldgen-c/include/worldgen.h index 3b3ccd9..2e7c749 100644 --- a/worldgen-c/include/worldgen.h +++ b/worldgen-c/include/worldgen.h @@ -80,5 +80,7 @@ void worldgen_init(worldgen_ctx *ctx, int world_seed, int sea_level, int snow_li void worldgen_generate_chunk(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *out); void worldgen_prepass(worldgen_ctx *ctx, int min_x, int max_x, int min_z, int max_z); void worldgen_free_trails(worldgen_ctx *ctx); +int worldgen_debug_biome(worldgen_ctx *ctx, int x, int z); +double worldgen_debug_redwood_mask(worldgen_ctx *ctx, int x, int z); #endif diff --git a/worldgen-c/src/main.c b/worldgen-c/src/main.c index 574a672..e40aea7 100644 --- a/worldgen-c/src/main.c +++ b/worldgen-c/src/main.c @@ -97,6 +97,7 @@ static const kv_pair PROPS_LADDER_S[] = {{"facing", "south"}, {"waterlogged", "f static const kv_pair PROPS_LADDER_W[] = {{"facing", "west"}, {"waterlogged", "false"}}; static const kv_pair PROPS_LADDER_E[] = {{"facing", "east"}, {"waterlogged", "false"}}; static const size_t BLOCK_STATE_CAP = 256; +static const int32_t DATA_VERSION = 2730; /* Java Edition 1.17.1; includes copper ore. */ static const block_state BLOCK_STATE_TABLE[] = { [BLOCK_BEDROCK] = {"minecraft:bedrock", NULL, 0}, [BLOCK_STONE] = {"minecraft:stone", NULL, 0}, @@ -422,7 +423,7 @@ static void build_chunk_nbt(const chunk_data *chunk, buf *out) { pack_heightmap(chunk, heightmap, 36); nbt_start_compound(out, ""); - nbt_write_int(out, "DataVersion", 2586); + nbt_write_int(out, "DataVersion", DATA_VERSION); nbt_start_compound(out, "Level"); nbt_write_string_tag(out, "Status", "full"); diff --git a/worldgen-c/src/worldgen.c b/worldgen-c/src/worldgen.c index 206fbe0..5cc8347 100644 --- a/worldgen-c/src/worldgen.c +++ b/worldgen-c/src/worldgen.c @@ -158,14 +158,19 @@ static column_data get_column_data(worldgen_ctx *ctx, int x, int z); static int generate_coal(worldgen_ctx *ctx, int x, int y, int z, int column_height, biome_id biome); static uint32_t hash_coords(int x, int z, uint32_t seed) { - uint32_t h = (uint32_t)(x * 374761393 + z * 668265263) ^ seed; + uint32_t ux = (uint32_t)x; + uint32_t uz = (uint32_t)z; + uint32_t h = (ux * 374761393u + uz * 668265263u) ^ seed; h = (h ^ (h >> 13)) * 1274126177u; return h ^ (h >> 16); } static uint32_t hash_coords3(int x, int y, int z, uint32_t seed) { - uint32_t h = (uint32_t)(x * 374761393 + y * 668265263 + z * 362827313) ^ seed; - h ^= (uint32_t)y * 0x9E3779B9u; + uint32_t ux = (uint32_t)x; + uint32_t uy = (uint32_t)y; + uint32_t uz = (uint32_t)z; + uint32_t h = (ux * 374761393u + uy * 668265263u + uz * 362827313u) ^ seed; + h ^= uy * 0x9E3779B9u; h = (h ^ (h >> 13)) * 1274126177u; return h ^ (h >> 16); } @@ -417,6 +422,18 @@ static void set_block_with_height(chunk_data *chunk, int local_x, int local_z, i } } +static int is_cabin_structure_block(uint16_t block) { + return block == BLOCK_SPRUCE_PLANKS || block == BLOCK_OAK_PLANKS || + block == BLOCK_OAK_LOG_X || block == BLOCK_OAK_LOG_Z || + block == BLOCK_SPRUCE_LOG_X || block == BLOCK_SPRUCE_LOG_Z || + block == BLOCK_GLASS_PANE || block == BLOCK_SPRUCE_STAIRS_E || block == BLOCK_SPRUCE_STAIRS_W || + block == BLOCK_LADDER_N || block == BLOCK_LADDER_S || block == BLOCK_LADDER_E || block == BLOCK_LADDER_W || + block == BLOCK_SPRUCE_DOOR_N_LOWER || block == BLOCK_SPRUCE_DOOR_N_UPPER || + block == BLOCK_SPRUCE_DOOR_S_LOWER || block == BLOCK_SPRUCE_DOOR_S_UPPER || + block == BLOCK_SPRUCE_DOOR_E_LOWER || block == BLOCK_SPRUCE_DOOR_E_UPPER || + block == BLOCK_SPRUCE_DOOR_W_LOWER || block == BLOCK_SPRUCE_DOOR_W_UPPER; +} + static double worley_distance(int x, int z, double scale, uint32_t seed) { double px = x * scale; double pz = z * scale; @@ -527,16 +544,16 @@ static int local_relief(worldgen_ctx *ctx, int x, int z, int center_height) { } static biome_id classify_biome(worldgen_ctx *ctx, int x, int z, int column_height) { + int slope = local_relief(ctx, x, z, column_height); + double redwood = redwood_biome_mask(ctx, x, z, column_height, slope); + if (redwood > 0.18) { + return BIOME_REDWOOD_FOREST; + } double old_growth = old_growth_plains_mask(ctx, x, z); if (old_growth > 0.6) { return BIOME_OLD_GROWTH_PLAINS; } double blend = region_blend(ctx, x, z); - int slope = local_relief(ctx, x, z, column_height); - double redwood = redwood_biome_mask(ctx, x, z, column_height, slope); - if (redwood > 0.62) { - return BIOME_REDWOOD_FOREST; - } double slope_score = clamp01((double)slope / 10.0); double relief = clamp01((double)(column_height - ctx->sea_level) / 45.0); double jitter = simplex_noise2(&ctx->noise, (x - 17000) * 0.001, (z + 17000) * 0.001) * 0.1; @@ -547,6 +564,15 @@ static biome_id classify_biome(worldgen_ctx *ctx, int x, int z, int column_heigh return BIOME_WEST_KY_COALFIELDS; } +int worldgen_debug_biome(worldgen_ctx *ctx, int x, int z) { + return (int)classify_biome(ctx, x, z, column_height(ctx, x, z)); +} + +double worldgen_debug_redwood_mask(worldgen_ctx *ctx, int x, int z) { + int height = column_height(ctx, x, z); + return redwood_biome_mask(ctx, x, z, height, local_relief(ctx, x, z, height)); +} + static void block_list_init(block_list *list) { list->items = NULL; list->count = 0; @@ -2049,6 +2075,14 @@ static void build_cabin_rect(worldgen_ctx *ctx, const cabin_blueprint *bp, const } } } + for (int wz = z0; wz <= z1; ++wz) { + for (int wx = x0; wx <= x1; ++wx) { + int lx = wx - chunk_origin_x; + int lz = wz - chunk_origin_z; + if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) continue; + set_block_with_height(chunk, lx, lz, base_floor_y, foundation_block); + } + } int ladder_hole_x = INT_MIN; int ladder_hole_z = INT_MIN; @@ -2249,13 +2283,19 @@ static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_da if (rect_overlaps_mask(occupancy, chunk_origin_x, chunk_origin_z, x0, x1, z0, z1, 3)) { return 0; } + for (int wz = z0 - 2; wz <= z1 + 2; ++wz) { + for (int wx = x0 - 2; wx <= x1 + 2; ++wx) { + if (wx < chunk_origin_x || wx >= chunk_origin_x + CHUNK_SIZE) continue; + if (wz < chunk_origin_z || wz >= chunk_origin_z + CHUNK_SIZE) continue; + if (column_has_manmade_surface(chunk, chunk_x, chunk_z, wx, wz)) return 0; + } + } for (int wz = z0; wz <= z1; ++wz) { for (int wx = x0; wx <= x1; ++wx) { int lx = wx - chunk_origin_x; int lz = wz - chunk_origin_z; column_data col = columns[lx][lz]; if (col.has_water && col.height < col.water_surface) return 0; - if (column_has_manmade_surface(chunk, chunk_x, chunk_z, wx, wz)) return 0; if (col.height < min_h) min_h = col.height; if (col.height > max_h) max_h = col.height; } @@ -2438,8 +2478,8 @@ static void connect_cabin_to_trail(worldgen_ctx *ctx, int chunk_x, int chunk_z, int door_x, int door_z, int door_side, int path_width) { int step_x = (door_side == 2) ? -1 : (door_side == 3) ? 1 : 0; int step_z = (door_side == 0) ? -1 : (door_side == 1) ? 1 : 0; - int start_x = door_x + step_x; - int start_z = door_z + step_z; + int start_x = door_x + step_x * 3; + int start_z = door_z + step_z * 3; int spur_len = 24; if (path_width < 2) path_width = 2; carve_cabin_path(ctx, chunk_x, chunk_z, chunk, columns, start_x, start_z, step_x, step_z, spur_len, path_width); @@ -2568,20 +2608,9 @@ static int column_has_manmade_surface(chunk_data *chunk, int chunk_x, int chunk_ if (local_x < 0 || local_x >= CHUNK_SIZE || local_z < 0 || local_z >= CHUNK_SIZE) return 0; for (int y = CHUNK_HEIGHT - 2; y >= 1; --y) { uint16_t block = chunk->blocks[y][local_x][local_z]; - if (block == BLOCK_AIR) continue; - if (block == BLOCK_WATER) return 0; - if (block == BLOCK_GRAVEL || block == BLOCK_SPRUCE_PLANKS || block == BLOCK_OAK_PLANKS || - block == BLOCK_OAK_LOG_X || block == BLOCK_OAK_LOG_Z || - block == BLOCK_SPRUCE_LOG_X || block == BLOCK_SPRUCE_LOG_Z || - block == BLOCK_GLASS_PANE || block == BLOCK_SPRUCE_STAIRS_E || block == BLOCK_SPRUCE_STAIRS_W || - block == BLOCK_LADDER_N || block == BLOCK_LADDER_S || block == BLOCK_LADDER_E || block == BLOCK_LADDER_W || - block == BLOCK_SPRUCE_DOOR_N_LOWER || block == BLOCK_SPRUCE_DOOR_N_UPPER || - block == BLOCK_SPRUCE_DOOR_S_LOWER || block == BLOCK_SPRUCE_DOOR_S_UPPER || - block == BLOCK_SPRUCE_DOOR_E_LOWER || block == BLOCK_SPRUCE_DOOR_E_UPPER || - block == BLOCK_SPRUCE_DOOR_W_LOWER || block == BLOCK_SPRUCE_DOOR_W_UPPER) { + if (block == BLOCK_GRAVEL || is_cabin_structure_block(block)) { return 1; } - return 0; } return 0; } @@ -3153,6 +3182,12 @@ static void place_trail_column(chunk_data *out, column_data columns[CHUNK_SIZE][ int original_height = columns[local_x][local_z].height; if (original_height < 1) original_height = target_height; + for (int y = 1; y < CHUNK_HEIGHT; ++y) { + if (is_cabin_structure_block(out->blocks[y][local_x][local_z])) { + return; + } + } + for (int y = target_height - 1; y >= 1; --y) { uint16_t block = out->blocks[y][local_x][local_z]; if (block == BLOCK_AIR || block == BLOCK_WATER) { diff --git a/worldgen-c/tools/worldgen_scan.c b/worldgen-c/tools/worldgen_scan.c new file mode 100644 index 0000000..bc7b7ba --- /dev/null +++ b/worldgen-c/tools/worldgen_scan.c @@ -0,0 +1,146 @@ +#include "worldgen.h" + +#include +#include +#include +#include + +static int is_cabin_block(uint16_t block) { + return block == BLOCK_SPRUCE_PLANKS || block == BLOCK_OAK_PLANKS || + block == BLOCK_OAK_LOG_X || block == BLOCK_OAK_LOG_Z || + block == BLOCK_SPRUCE_LOG_X || block == BLOCK_SPRUCE_LOG_Z || + block == BLOCK_GLASS_PANE || block == BLOCK_SPRUCE_STAIRS_E || block == BLOCK_SPRUCE_STAIRS_W || + block == BLOCK_LADDER_N || block == BLOCK_LADDER_S || block == BLOCK_LADDER_E || block == BLOCK_LADDER_W || + block == BLOCK_SPRUCE_DOOR_N_LOWER || block == BLOCK_SPRUCE_DOOR_N_UPPER || + block == BLOCK_SPRUCE_DOOR_S_LOWER || block == BLOCK_SPRUCE_DOOR_S_UPPER || + block == BLOCK_SPRUCE_DOOR_E_LOWER || block == BLOCK_SPRUCE_DOOR_E_UPPER || + block == BLOCK_SPRUCE_DOOR_W_LOWER || block == BLOCK_SPRUCE_DOOR_W_UPPER; +} + +static long parse_long(const char *s) { + char *end = NULL; + long v = strtol(s, &end, 10); + if (!end || *end != '\0') { + fprintf(stderr, "Invalid number: %s\n", s); + exit(1); + } + return v; +} + +static void usage(const char *prog) { + fprintf(stderr, "Usage: %s [--radius N] [--center-x X] [--center-z Z] [--seed S] [--sea-level L] [--snow-line H] [--trails]\n", prog); +} + +int main(int argc, char **argv) { + int radius = 16; + int center_x = 0; + int center_z = 0; + int seed = 123456; + int sea_level = 70; + int snow_line = INT_MIN; + int trails = 0; + + 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], "--seed") == 0 && i + 1 < argc) { + 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], "--trails") == 0) { + trails = 1; + } else if (strcmp(argv[i], "--help") == 0) { + usage(argv[0]); + return 0; + } else { + usage(argv[0]); + return 1; + } + } + if (snow_line == INT_MIN) snow_line = sea_level + 38; + + worldgen_ctx ctx; + worldgen_init(&ctx, seed, sea_level, snow_line); + ctx.enable_trails = trails; + if (trails) { + worldgen_prepass(&ctx, (center_x - radius) * CHUNK_SIZE, (center_x + radius) * CHUNK_SIZE + CHUNK_SIZE - 1, + (center_z - radius) * CHUNK_SIZE, (center_z + radius) * CHUNK_SIZE + CHUNK_SIZE - 1); + } + + long chunks = 0; + long cabin_blocks = 0; + long cabin_columns = 0; + long gravel_columns = 0; + long cabin_gravel_columns = 0; + long tall_log_columns = 0; + long max_oak_log_run = 0; + long biome_columns[4] = {0, 0, 0, 0}; + double max_redwood_mask = 0.0; + + for (int cx = center_x - radius; cx <= center_x + radius; ++cx) { + for (int cz = center_z - radius; cz <= center_z + radius; ++cz) { + chunk_data chunk; + worldgen_generate_chunk(&ctx, cx, cz, &chunk); + ++chunks; + for (int lx = 0; lx < CHUNK_SIZE; ++lx) { + for (int lz = 0; lz < CHUNK_SIZE; ++lz) { + int wx = cx * CHUNK_SIZE + lx; + int wz = cz * CHUNK_SIZE + lz; + int biome = worldgen_debug_biome(&ctx, wx, wz); + if (biome >= 0 && biome < 4) ++biome_columns[biome]; + double redwood_mask = worldgen_debug_redwood_mask(&ctx, wx, wz); + if (redwood_mask > max_redwood_mask) max_redwood_mask = redwood_mask; + int has_cabin = 0; + int has_gravel = 0; + int current_run = 0; + int best_run = 0; + for (int y = 1; y < CHUNK_HEIGHT; ++y) { + uint16_t block = chunk.blocks[y][lx][lz]; + if (is_cabin_block(block)) { + has_cabin = 1; + ++cabin_blocks; + } + if (block == BLOCK_GRAVEL) { + has_gravel = 1; + } + if (block == BLOCK_OAK_LOG) { + ++current_run; + if (current_run > best_run) best_run = current_run; + } else { + current_run = 0; + } + } + if (best_run > max_oak_log_run) max_oak_log_run = best_run; + if (best_run >= 40) ++tall_log_columns; + if (has_cabin) ++cabin_columns; + if (has_gravel) ++gravel_columns; + if (has_cabin && has_gravel) { + ++cabin_gravel_columns; + } + } + } + } + } + + printf("chunks=%ld\n", chunks); + printf("cabin_blocks=%ld\n", cabin_blocks); + printf("cabin_columns=%ld\n", cabin_columns); + printf("gravel_columns=%ld\n", gravel_columns); + printf("cabin_gravel_columns=%ld\n", cabin_gravel_columns); + printf("tall_oak_log_columns=%ld\n", tall_log_columns); + printf("max_oak_log_run=%ld\n", max_oak_log_run); + printf("biome_west_ky=%ld\n", biome_columns[0]); + printf("biome_east_ky=%ld\n", biome_columns[1]); + printf("biome_old_growth=%ld\n", biome_columns[2]); + printf("biome_redwood=%ld\n", biome_columns[3]); + printf("max_redwood_mask=%.6f\n", max_redwood_mask); + + worldgen_free_trails(&ctx); + return 0; +}