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>
This commit is contained in:
Krzysztof Wiśniewski 2025-05-02 16:11:10 +02:00 committed by GitHub
parent 3ba4d5fd14
commit 61268fb374
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 150 additions and 4 deletions

View File

@ -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};
}

View File

@ -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),
}
}

View File

@ -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");

View File

@ -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;