From 61268fb374030b8d1586a27918f1ae0bd816e3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Fri, 2 May 2025 16:11:10 +0200 Subject: [PATCH] Add Structure Building Blocks based Generator (#1227) * Revert "Remove SBBGen" This reverts commit b49048412f21c57d5638144da7f039753a94cafc. * Revert "Remove example SBB" This reverts commit afc5d6fed0ba92b558ccda91893fdb3fc63f69ec. * Revert "Remove blueprint code" This reverts commit 2553950adbdcef9c1c68afe4109f9247b74abb92. * Fix compilation errors * Fix compilation errors #2 * Fix test errors * Fix rotateZ * Resolve structure reference while instantiating SBBGen * Fix formatting issues * Add new trees to forest * Add new trees to grassland * Decrease forest density so you can find new trees * Add degradable paste mode * Remove substitutions * Apply review suggestions * Use lookup table for alignment * Apply suggestions from code review * Update src/server/terrain/simple_structures/_list.zig Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Fix inital SBB offset * Never place air in placeInGeneration * Integrate void block with paste * Revert "Move hashInt and hashCombine to utils" This reverts commit 9bb276f69f60ad60f1170d07ec30ebd02307a36a. * Make PasteMode comptime * Fix remaining issues with void block integration * Fix formatting * Apply review change requests * Remove origin and child blocks while resolving sbbs * Apply review change requests * I hate indexing * Fix example tree models * Use single index for chunk and blueprint in pasteInGeneration * Fix formatting * Extract blueprintOffset * Fix formatting * Apply suggestions from code review Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> * Apply Quantums suggestion for Y and Z * No cast * Use pos instead of chunkOffset * Remove test tree * Apply suggestions from code review --------- Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> --- src/blueprint.zig | 41 +++++++++ .../terrain/simple_structures/SbbGen.zig | 90 +++++++++++++++++++ .../terrain/simple_structures/_list.zig | 1 + .../terrain/structure_building_blocks.zig | 22 ++++- 4 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 src/server/terrain/simple_structures/SbbGen.zig diff --git a/src/blueprint.zig b/src/blueprint.zig index 3ecbf0b3..6c3394c1 100644 --- a/src/blueprint.zig +++ b/src/blueprint.zig @@ -108,9 +108,46 @@ pub const Blueprint = struct { } return .{.success = self}; } + + pub const PasteMode = enum {all, degradable}; + + pub fn pasteInGeneration(self: Blueprint, pos: Vec3i, chunk: *ServerChunk, mode: PasteMode) void { + switch(mode) { + inline else => |comptimeMode| _pasteInGeneration(self, pos, chunk, comptimeMode), + } + } + + fn _pasteInGeneration(self: Blueprint, pos: Vec3i, chunk: *ServerChunk, comptime mode: PasteMode) void { + const indexEndX: i32 = @min(@as(i32, chunk.super.width) - pos[0], @as(i32, @intCast(self.blocks.width))); + const indexEndY: i32 = @min(@as(i32, chunk.super.width) - pos[1], @as(i32, @intCast(self.blocks.depth))); + const indexEndZ: i32 = @min(@as(i32, chunk.super.width) - pos[2], @as(i32, @intCast(self.blocks.height))); + + var indexX: u31 = @max(0, -pos[0]); + while(indexX < indexEndX) : (indexX += chunk.super.pos.voxelSize) { + var indexY: u31 = @max(0, -pos[1]); + while(indexY < indexEndY) : (indexY += chunk.super.pos.voxelSize) { + var indexZ: u31 = @max(0, -pos[2]); + while(indexZ < indexEndZ) : (indexZ += chunk.super.pos.voxelSize) { + const block = self.blocks.get(indexX, indexY, indexZ); + + if(block.typ == voidType) continue; + + const chunkX = indexX + pos[0]; + const chunkY = indexY + pos[1]; + const chunkZ = indexZ + pos[2]; + switch(mode) { + .all => chunk.updateBlockInGeneration(chunkX, chunkY, chunkZ, block), + .degradable => chunk.updateBlockIfDegradable(chunkX, chunkY, chunkZ, block), + } + } + } + } + } + pub const PasteFlags = struct { preserveVoid: bool = false, }; + pub fn paste(self: Blueprint, pos: Vec3i, flags: PasteFlags) void { const startX = pos[0]; const startY = pos[1]; @@ -339,3 +376,7 @@ pub fn registerVoidBlock(block: Block) void { voidType = block.typ; std.debug.assert(voidType != 0); } + +pub fn getVoidBlock() Block { + return Block{.typ = voidType.?, .data = 0}; +} diff --git a/src/server/terrain/simple_structures/SbbGen.zig b/src/server/terrain/simple_structures/SbbGen.zig new file mode 100644 index 00000000..5dd79190 --- /dev/null +++ b/src/server/terrain/simple_structures/SbbGen.zig @@ -0,0 +1,90 @@ +const std = @import("std"); + +const main = @import("main"); +const terrain = main.server.terrain; +const Vec3i = main.vec.Vec3i; +const GenerationMode = terrain.biomes.SimpleStructureModel.GenerationMode; +const CaveMapView = terrain.CaveMap.CaveMapView; +const CaveBiomeMapView = terrain.CaveBiomeMap.CaveBiomeMapView; +const sbb = terrain.structure_building_blocks; +const Blueprint = main.blueprint.Blueprint; +const ZonElement = main.ZonElement; +const Neighbor = main.chunk.Neighbor; +const ServerChunk = main.chunk.ServerChunk; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +pub const id = "cubyz:sbb"; +pub const generationMode = .floor; + +const SbbGen = @This(); + +structureRef: *const sbb.StructureBuildingBlock, +placeMode: Blueprint.PasteMode, + +pub fn getHash(self: SbbGen) u64 { + return std.hash.Wyhash.hash(@intFromEnum(self.placeMode), self.structureRef.id); +} + +pub fn loadModel(arenaAllocator: NeverFailingAllocator, parameters: ZonElement) *SbbGen { + const structureId = parameters.get(?[]const u8, "structure", null) orelse unreachable; + const structureRef = sbb.getByStringId(structureId) orelse { + std.log.err("Could not find structure building block with id '{s}'", .{structureId}); + unreachable; + }; + const self = arenaAllocator.create(SbbGen); + self.* = .{ + .structureRef = structureRef, + .placeMode = std.meta.stringToEnum(Blueprint.PasteMode, parameters.get([]const u8, "placeMode", "degradable")) orelse Blueprint.PasteMode.degradable, + }; + return self; +} + +pub fn generate(self: *SbbGen, _: GenerationMode, x: i32, y: i32, z: i32, chunk: *ServerChunk, _: CaveMapView, _: CaveBiomeMapView, seed: *u64, _: bool) void { + placeSbb(self, self.structureRef, Vec3i{x, y, z}, Neighbor.dirUp, chunk, seed); +} + +fn placeSbb(self: *SbbGen, structure: *const sbb.StructureBuildingBlock, placementPosition: Vec3i, placementDirection: Neighbor, chunk: *ServerChunk, seed: *u64) void { + const origin = structure.blueprints[0].originBlock; + const rotationCount = alignDirections(origin.direction(), placementDirection) catch |err| { + std.log.err("Could not align directions for structure '{s}' for directions '{s}'' and '{s}', error: {s}", .{structure.id, @tagName(origin.direction()), @tagName(placementDirection), @errorName(err)}); + return; + }; + const rotated = &structure.blueprints[rotationCount]; + const rotatedOrigin = rotated.originBlock.pos(); + const pastePosition = placementPosition - rotatedOrigin - placementDirection.relPos(); + + rotated.blueprint.pasteInGeneration(pastePosition, chunk, self.placeMode); + + for(rotated.childBlocks) |childBlock| { + const child = structure.pickChild(childBlock, seed); + placeSbb(self, child, pastePosition + childBlock.pos(), childBlock.direction(), chunk, seed); + } +} + +fn alignDirections(input: Neighbor, desired: Neighbor) !usize { + const Rotation = enum(u3) { + @"0" = 0, + @"90" = 1, + @"180" = 2, + @"270" = 3, + NotPossibleToAlign = 4, + }; + comptime var alignTable: [6][6]Rotation = undefined; + comptime for(Neighbor.iterable) |in| { + for(Neighbor.iterable) |out| blk: { + var current = in; + for(0..4) |i| { + if(current == out) { + alignTable[in.toInt()][out.toInt()] = @enumFromInt(i); + break :blk; + } + current = current.rotateZ(); + } + alignTable[in.toInt()][out.toInt()] = Rotation.NotPossibleToAlign; + } + }; + switch(alignTable[input.toInt()][desired.toInt()]) { + .NotPossibleToAlign => return error.NotPossibleToAlign, + else => |v| return @intFromEnum(v), + } +} diff --git a/src/server/terrain/simple_structures/_list.zig b/src/server/terrain/simple_structures/_list.zig index 0303ffef..3e1d8f91 100644 --- a/src/server/terrain/simple_structures/_list.zig +++ b/src/server/terrain/simple_structures/_list.zig @@ -2,6 +2,7 @@ pub const Boulder = @import("Boulder.zig"); pub const FallenTree = @import("FallenTree.zig"); pub const FlowerPatch = @import("FlowerPatch.zig"); pub const GroundPatch = @import("GroundPatch.zig"); +pub const SbbGen = @import("SbbGen.zig"); pub const SimpleTreeModel = @import("SimpleTreeModel.zig"); pub const SimpleVegetation = @import("SimpleVegetation.zig"); pub const Stalagmite = @import("Stalagmite.zig"); diff --git a/src/server/terrain/structure_building_blocks.zig b/src/server/terrain/structure_building_blocks.zig index 186d6607..6625e7f2 100644 --- a/src/server/terrain/structure_building_blocks.zig +++ b/src/server/terrain/structure_building_blocks.zig @@ -1,6 +1,7 @@ const std = @import("std"); const main = @import("main"); +const Vec3i = main.vec.Vec3i; const ZonElement = main.ZonElement; const Blueprint = main.blueprint.Blueprint; const List = main.List; @@ -40,6 +41,10 @@ const BlueprintEntry = struct { pub inline fn direction(self: StructureBlock) Neighbor { return @enumFromInt(self.data); } + + pub inline fn pos(self: StructureBlock) Vec3i { + return Vec3i{self.x, self.y, self.z}; + } }; fn init(blueprint: Blueprint, stringId: []const u8) !BlueprintEntry { @@ -70,6 +75,7 @@ const BlueprintEntry = struct { .data = block.data, }; hasOrigin = true; + self.blueprint.blocks.set(x, y, z, main.blueprint.getVoidBlock()); } } else if(isChildBlock(block)) { const childBlockLocalId = childBlockNumericIdMap.get(block.typ) orelse return error.ChildBlockNotRecognized; @@ -80,6 +86,7 @@ const BlueprintEntry = struct { .index = childBlockLocalId, .data = block.data, }); + self.blueprint.blocks.set(x, y, z, main.blueprint.getVoidBlock()); } } } @@ -103,6 +110,7 @@ pub fn isOriginBlock(block: Block) bool { } pub const StructureBuildingBlock = struct { + id: []const u8, children: []AliasTable(Child), blueprints: *[4]BlueprintEntry, @@ -116,6 +124,7 @@ pub const StructureBuildingBlock = struct { return error.MissingBlueprint; }; const self = StructureBuildingBlock{ + .id = stringId, .children = arenaAllocator.alloc(AliasTable(Child), childBlockStringId.items.len), .blueprints = blueprints, }; @@ -172,6 +181,7 @@ pub fn registerSBB(structures: *std.StringHashMap(ZonElement)) !void { std.debug.assert(structureCache.capacity() == 0); structureCache.ensureTotalCapacity(arenaAllocator.allocator, structures.count()) catch unreachable; childrenToResolve = .init(main.stackAllocator); + defer childrenToResolve.deinit(); { var iterator = structures.iterator(); while(iterator.next()) |entry| { @@ -195,7 +205,6 @@ pub fn registerSBB(structures: *std.StringHashMap(ZonElement)) !void { std.log.debug("Resolved child structure '{s}'->'{s}'->'{d}' to '{s}'", .{entry.parentId, entry.colorName, entry.childIndex, entry.structureId}); parent.children[entry.colorIndex].items[entry.childIndex].structure = child; } - childrenToResolve.deinit(); } } @@ -221,17 +230,22 @@ pub fn registerBlueprints(blueprints: *std.StringHashMap([]u8)) !void { var iterator = blueprints.iterator(); while(iterator.next()) |entry| { const stringId = entry.key_ptr.*; + // Rotated copies need to be made before initializing BlueprintEntry as it removes origin and child blocks. const blueprint0 = Blueprint.load(arenaAllocator, entry.value_ptr.*) catch |err| { std.log.err("Could not load blueprint '{s}' ({s})", .{stringId, @errorName(err)}); continue; }; + const blueprint90 = blueprint0.rotateZ(arenaAllocator, .@"90"); + const blueprint180 = blueprint0.rotateZ(arenaAllocator, .@"180"); + const blueprint270 = blueprint0.rotateZ(arenaAllocator, .@"270"); const rotatedBlueprints = arenaAllocator.create([4]BlueprintEntry); + rotatedBlueprints.* = .{ BlueprintEntry.init(blueprint0, stringId) catch continue, - BlueprintEntry.init(blueprint0.rotateZ(arenaAllocator, .@"90"), stringId) catch continue, - BlueprintEntry.init(blueprint0.rotateZ(arenaAllocator, .@"180"), stringId) catch continue, - BlueprintEntry.init(blueprint0.rotateZ(arenaAllocator, .@"270"), stringId) catch continue, + BlueprintEntry.init(blueprint90, stringId) catch continue, + BlueprintEntry.init(blueprint180, stringId) catch continue, + BlueprintEntry.init(blueprint270, stringId) catch continue, }; blueprintCache.put(arenaAllocator.allocator, arenaAllocator.dupe(u8, stringId), rotatedBlueprints) catch unreachable;