Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ pub fn build(b: *std.Build) void {

const chunk_debug_enable = b.option([]const u8, "chunk-debug-enable", "Re-enable one subsystem in chunk-debug-mode (lod, water, caves, decorations)") orelse "";
options.addOption([]const u8, "chunk_debug_enable", chunk_debug_enable);
const world_worldgen_options = b.addOptions();
world_worldgen_options.addOption(bool, "chunk_debug_mode", chunk_debug_mode);
world_worldgen_options.addOption([]const u8, "chunk_debug_enable", chunk_debug_enable);
const auto_world = b.option([]const u8, "auto-world", "Auto-open a world generator directly (normal, overworld, flat)") orelse "";
const auto_world = b.option([]const u8, "auto-world", "Auto-open a world generator directly by id or alias (normal, overworld, flat, test)") orelse "";
options.addOption([]const u8, "auto_world", auto_world);

const auto_preset = b.option([]const u8, "auto-preset", "Graphics preset to apply for auto-world launches (low, medium, high, ultra, extreme)") orelse "";
Expand Down Expand Up @@ -73,7 +70,6 @@ pub fn build(b: *std.Build) void {

const shadow_test_variant = b.option([]const u8, "shadow-test-variant", "Shadow test scene variant (dug-cave, bend)") orelse "dug-cave";
options.addOption([]const u8, "shadow_test_variant", shadow_test_variant);
world_worldgen_options.addOption([]const u8, "shadow_test_variant", shadow_test_variant);

const benchmark = b.option(bool, "benchmark", "Enable benchmark mode") orelse false;
options.addOption(bool, "benchmark", benchmark);
Expand Down Expand Up @@ -142,6 +138,11 @@ pub fn build(b: *std.Build) void {
const engine_lighting = b.createModule(.{ .root_source_file = b.path("modules/engine-lighting/src/root.zig"), .target = target, .optimize = optimize });
const engine_ui = b.createModule(.{ .root_source_file = b.path("modules/engine-ui/src/root.zig"), .target = target, .optimize = optimize });
const world_core = b.createModule(.{ .root_source_file = b.path("modules/world-core/src/root.zig"), .target = target, .optimize = optimize });
const worldgen_api = b.createModule(.{ .root_source_file = b.path("modules/worldgen-api/src/root.zig"), .target = target, .optimize = optimize });
const worldgen_common = b.createModule(.{ .root_source_file = b.path("modules/worldgen-common/src/root.zig"), .target = target, .optimize = optimize });
const worldgen_overworld = b.createModule(.{ .root_source_file = b.path("modules/worldgen-overworld/src/root.zig"), .target = target, .optimize = optimize });
const worldgen_flat = b.createModule(.{ .root_source_file = b.path("modules/worldgen-flat/src/root.zig"), .target = target, .optimize = optimize });
const worldgen_test = b.createModule(.{ .root_source_file = b.path("modules/worldgen-test/src/root.zig"), .target = target, .optimize = optimize });
const world_worldgen = b.createModule(.{ .root_source_file = b.path("modules/world-worldgen/src/root.zig"), .target = target, .optimize = optimize });
const world_meshing = b.createModule(.{ .root_source_file = b.path("modules/world-meshing/src/root.zig"), .target = target, .optimize = optimize });
const world_lod = b.createModule(.{ .root_source_file = b.path("modules/world-lod/src/root.zig"), .target = target, .optimize = optimize });
Expand Down Expand Up @@ -227,6 +228,31 @@ pub fn build(b: *std.Build) void {
addSharedImports(world_core, zig_math, zig_noise, fs_module, sync_module, c_module, options);
world_core.addImport("engine-core", engine_core);
world_core.addImport("engine-math", engine_math);
addSharedImports(worldgen_api, zig_math, zig_noise, fs_module, sync_module, c_module, options);
worldgen_api.addImport("world-core", world_core);
addSharedImports(worldgen_common, zig_math, zig_noise, fs_module, sync_module, c_module, options);
worldgen_common.addImport("world-core", world_core);
const worldgen_overworld_options = b.addOptions();
worldgen_overworld_options.addOption(bool, "chunk_debug_mode", chunk_debug_mode);
worldgen_overworld_options.addOption([]const u8, "chunk_debug_enable", chunk_debug_enable);
addSharedImports(worldgen_overworld, zig_math, zig_noise, fs_module, sync_module, c_module, options);
worldgen_overworld.addImport("engine-core", engine_core);
worldgen_overworld.addImport("engine-rhi", engine_rhi);
worldgen_overworld.addImport("world-core", world_core);
worldgen_overworld.addImport("worldgen-api", worldgen_api);
worldgen_overworld.addImport("worldgen-common", worldgen_common);
worldgen_overworld.addOptions("worldgen_overworld_options", worldgen_overworld_options);
addSharedImports(worldgen_flat, zig_math, zig_noise, fs_module, sync_module, c_module, options);
worldgen_flat.addImport("world-core", world_core);
worldgen_flat.addImport("worldgen-api", worldgen_api);
worldgen_flat.addImport("worldgen-common", worldgen_common);
const worldgen_test_options = b.addOptions();
worldgen_test_options.addOption([]const u8, "shadow_test_variant", shadow_test_variant);
addSharedImports(worldgen_test, zig_math, zig_noise, fs_module, sync_module, c_module, options);
worldgen_test.addImport("world-core", world_core);
worldgen_test.addImport("worldgen-api", worldgen_api);
worldgen_test.addImport("worldgen-common", worldgen_common);
worldgen_test.addOptions("worldgen_test_options", worldgen_test_options);
addSharedImports(world_worldgen, zig_math, zig_noise, fs_module, sync_module, c_module, options);
addSharedImports(world_meshing, zig_math, zig_noise, fs_module, sync_module, c_module, options);
world_meshing.addImport("engine-core", engine_core);
Expand All @@ -249,7 +275,11 @@ pub fn build(b: *std.Build) void {
world_worldgen.addImport("engine-core", engine_core);
world_worldgen.addImport("engine-rhi", engine_rhi);
world_worldgen.addImport("world-core", world_core);
world_worldgen.addOptions("world_worldgen_options", world_worldgen_options);
world_worldgen.addImport("worldgen-api", worldgen_api);
world_worldgen.addImport("worldgen-common", worldgen_common);
world_worldgen.addImport("worldgen-overworld", worldgen_overworld);
world_worldgen.addImport("worldgen-flat", worldgen_flat);
world_worldgen.addImport("worldgen-test", worldgen_test);

addSharedImports(world_runtime, zig_math, zig_noise, fs_module, sync_module, c_module, options);
world_runtime.addImport("engine-core", engine_core);
Expand Down
13 changes: 12 additions & 1 deletion modules/game-ui/src/screens/world_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const WorldScreen = @import("world.zig").WorldScreen;
const log = @import("engine-core").log;
const fs = @import("fs");
const text_input = @import("game-core").text_input;
const registry = @import("world-worldgen").registry;

fn getenv(name: [:0]const u8) ?[]const u8 {
const value = std.c.getenv(name) orelse return null;
Expand All @@ -36,11 +37,13 @@ pub const LevelDat = struct {
};

pub fn writeLevelDat(allocator: std.mem.Allocator, save_dir: fs.Dir, name: []const u8, seed: u64, generator_index: usize, last_played: i64) !void {
const generator_id = if (generator_index < registry.getGeneratorCount()) registry.getGeneratorId(generator_index) else registry.getGeneratorId(0);
const payload = .{
.name = name,
.seed = seed,
.last_played = last_played,
.generator_index = generator_index,
.generator_id = generator_id,
};
const json_str = try std.json.Stringify.valueAlloc(allocator, payload, .{ .whitespace = .indent_2 });
defer allocator.free(json_str);
Expand All @@ -60,6 +63,7 @@ pub fn readLevelDat(allocator: std.mem.Allocator, save_dir: fs.Dir) ?LevelDat {
const name_val = obj.get("name") orelse return null;
const seed_val = obj.get("seed") orelse return null;
const gen_val = obj.get("generator_index") orelse return null;
const gen_id_val = obj.get("generator_id");
const last_val = obj.get("last_played");
const name_str = switch (name_val) {
.string => |s| s,
Expand All @@ -73,10 +77,17 @@ pub fn readLevelDat(allocator: std.mem.Allocator, save_dir: fs.Dir) ?LevelDat {
.integer => |i| i,
else => 0,
} else 0;
const generator_index: usize = switch (gen_val) {
var generator_index: usize = switch (gen_val) {
.integer => |i| @intCast(i),
else => 0,
};
const generator_id_source = if (gen_id_val) |giv| switch (giv) {
.string => |s| s,
else => "",
} else "";
if (generator_id_source.len > 0) {
generator_index = registry.findGeneratorIndex(generator_id_source) orelse generator_index;
}
const name_copy = allocator.dupe(u8, name_str) catch return null;
errdefer allocator.free(name_copy);
return .{
Expand Down
192 changes: 17 additions & 175 deletions modules/world-worldgen/src/generator_interface.zig
Original file line number Diff line number Diff line change
@@ -1,175 +1,17 @@
const std = @import("std");
const world_core = @import("world-core");
const Chunk = world_core.Chunk;
const LODLevel = world_core.LODLevel;
const LODSimplifiedData = world_core.LODSimplifiedData;

const region_pkg = @import("region.zig");
const RegionInfo = region_pkg.RegionInfo;
const BiomeId = @import("world-core").BiomeId;

pub const ColumnInfo = struct {
height: i32,
biome: BiomeId,
is_ocean: bool,
temperature: f32,
humidity: f32,
continentalness: f32,
};

/// Options for controlling generation detail level
pub const GenerationOptions = struct {
/// LOD level - higher = more simplified
lod_level: LODLevel = .lod0,

/// Enable cave generation (worm + noise caves)
enable_caves: bool = true,

/// Enable worm caves specifically (expensive neighbor checks)
enable_worm_caves: bool = true,

/// Enable decorations (trees, flowers, grass)
enable_decorations: bool = true,

/// Enable ore generation
enable_ores: bool = true,

/// Enable lighting calculation
enable_lighting: bool = true,

/// Noise octave reduction (0 = full detail, higher = fewer octaves)
octave_reduction: u8 = 0,

/// Skip biome edge blending
skip_biome_blending: bool = false,

/// Create options from LOD level with sensible defaults
pub fn fromLOD(lod: LODLevel) GenerationOptions {
const level = @intFromEnum(lod);
return .{
.lod_level = lod,
.enable_caves = level <= 1,
.enable_worm_caves = level == 0,
.enable_decorations = level <= 1,
.enable_ores = level == 0,
.enable_lighting = level == 0,
.octave_reduction = @intCast(level),
.skip_biome_blending = level > 0,
};
}
};

pub const GeneratorInfo = struct {
/// Human-readable name of the generator (e.g. "Overworld").
/// Displayed in the UI. Should be kept relatively short (under 32 chars recommended).
name: []const u8,
/// Description of the generator's features and purpose.
description: []const u8,
};

/// Pluggable generator interface.
/// Uses a VTable for runtime polymorphism.
pub const Generator = struct {
ptr: *anyopaque,
vtable: *const VTable,
info: GeneratorInfo,

pub const VTable = struct {
/// Generate a full chunk of terrain.
/// Implementations MUST:
/// 1. Set chunk.generated = false at the very beginning of the function.
/// 2. Respect the stop_flag (if provided) by returning early without setting chunk.generated = true.
/// 3. Set chunk.generated = true ONLY after ALL generation steps (terrain, ores, features, lighting) are complete.
/// Note: If stop_flag is used to return early, the chunk may be left in a partially modified state.
/// The chunk.generated = false flag ensures that other systems (like rendering) do not process this incomplete data.
generate: *const fn (ptr: *anyopaque, chunk: *Chunk, stop_flag: ?*const bool) void,

/// Generate heightmap-only data for LOD levels.
generateHeightmapOnly: *const fn (ptr: *anyopaque, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel) void,

/// Periodically check if internal caches should be recentered around the player.
/// Returns true if any cache was recentered.
maybeRecenterCache: *const fn (ptr: *anyopaque, player_x: i32, player_z: i32) bool,

/// Get the world seed used by this generator
getSeed: *const fn (ptr: *anyopaque) u64,

/// Get region info for a specific world position
getRegionInfo: *const fn (ptr: *anyopaque, world_x: i32, world_z: i32) RegionInfo,

/// Get detailed column information for a world position (used for mapping)
getColumnInfo: *const fn (ptr: *anyopaque, wx: f32, wz: f32) ColumnInfo,

/// Clean up generator resources.
/// This MUST be called to free any memory or resources allocated by the generator.
deinit: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator) void,
};

pub fn generate(self: Generator, chunk: *Chunk, stop_flag: ?*const bool) void {
self.vtable.generate(self.ptr, chunk, stop_flag);
}

pub fn generateHeightmapOnly(self: Generator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel) void {
self.vtable.generateHeightmapOnly(self.ptr, data, region_x, region_z, lod_level);
}

pub fn maybeRecenterCache(self: Generator, player_x: i32, player_z: i32) bool {
return self.vtable.maybeRecenterCache(self.ptr, player_x, player_z);
}

pub fn getSeed(self: Generator) u64 {
return self.vtable.getSeed(self.ptr);
}

pub fn getRegionInfo(self: Generator, world_x: i32, world_z: i32) RegionInfo {
return self.vtable.getRegionInfo(self.ptr, world_x, world_z);
}

pub fn getColumnInfo(self: Generator, wx: f32, wz: f32) ColumnInfo {
return self.vtable.getColumnInfo(self.ptr, wx, wz);
}

pub fn deinit(self: Generator, allocator: std.mem.Allocator) void {
self.vtable.deinit(self.ptr, allocator);
}
};

pub const IChunkGenerator = struct {
generator: Generator,

pub fn generate(self: IChunkGenerator, chunk: *Chunk, stop_flag: ?*const bool) void {
self.generator.generate(chunk, stop_flag);
}
};

pub const ILODHeightmapGenerator = struct {
generator: Generator,

pub fn generateHeightmapOnly(self: ILODHeightmapGenerator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel) void {
self.generator.generateHeightmapOnly(data, region_x, region_z, lod_level);
}
};

pub const IGeneratorInfoProvider = struct {
generator: Generator,

pub fn getSeed(self: IGeneratorInfoProvider) u64 {
return self.generator.getSeed();
}

pub fn getRegionInfo(self: IGeneratorInfoProvider, world_x: i32, world_z: i32) RegionInfo {
return self.generator.getRegionInfo(world_x, world_z);
}

pub fn getColumnInfo(self: IGeneratorInfoProvider, wx: f32, wz: f32) ColumnInfo {
return self.generator.getColumnInfo(wx, wz);
}
};

pub const ICacheRecenterable = struct {
generator: Generator,

pub fn maybeRecenterCache(self: ICacheRecenterable, player_x: i32, player_z: i32) bool {
return self.generator.maybeRecenterCache(player_x, player_z);
}
};
const worldgen_api = @import("worldgen-api");

pub const ColumnInfo = worldgen_api.ColumnInfo;
pub const CreateContext = worldgen_api.CreateContext;
pub const FeatureFocus = worldgen_api.FeatureFocus;
pub const GenerationOptions = worldgen_api.GenerationOptions;
pub const Generator = worldgen_api.Generator;
pub const GeneratorDescriptor = worldgen_api.GeneratorDescriptor;
pub const GeneratorInfo = worldgen_api.GeneratorInfo;
pub const ICacheRecenterable = worldgen_api.ICacheRecenterable;
pub const IChunkGenerator = worldgen_api.IChunkGenerator;
pub const IGeneratorInfoProvider = worldgen_api.IGeneratorInfoProvider;
pub const ILODHeightmapGenerator = worldgen_api.ILODHeightmapGenerator;
pub const RegionInfo = worldgen_api.RegionInfo;
pub const RegionMood = worldgen_api.RegionMood;
pub const RegionRole = worldgen_api.RegionRole;
pub const RegistryError = worldgen_api.RegistryError;
Loading
Loading