Blueprint asset loading (#1207)

* SBB

* Fix rotation code

* Remove rotate command

* Fix segfault during generation

* Delete format.bat

* Pregenerate all rotated blueprints

* Apply review suggestions

* Apply review suggestions

* Change child block storage model

* Resolve child structures during load

* Move structure_building_blocks.zig to terrain

* Update src/rotation.zig

* Apply review suggestions

* Remove sbb blocks

* Remove SBBGen

* Remove example SBB

* Remove blueprint code

* Remove createAssetStringID

* Apply review suggestions

* Apply review suggestions

* Add asset ID generation

* Revert "Add asset ID generation"

This reverts commit ffe8fd281486f2124ab83b0e614b6a7db97a020d.

* Remove SBB loading

* Apply review requests
This commit is contained in:
Krzysztof Wiśniewski 2025-03-26 23:05:49 +01:00 committed by GitHub
parent 0e9ffda1b1
commit 1ec150d624
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 216 additions and 3 deletions

View File

@ -3,9 +3,12 @@ const std = @import("std");
const blocks_zig = @import("blocks.zig");
const items_zig = @import("items.zig");
const migrations_zig = @import("migrations.zig");
const blueprints_zig = @import("blueprint.zig");
const Blueprint = blueprints_zig.Blueprint;
const ZonElement = @import("zon.zig").ZonElement;
const main = @import("main.zig");
const biomes_zig = main.server.terrain.biomes;
const sbb = main.server.terrain.structure_building_blocks;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
var arena: main.heap.NeverFailingArenaAllocator = undefined;
@ -18,6 +21,7 @@ var commonBiomes: std.StringHashMap(ZonElement) = undefined;
var commonBiomeMigrations: std.StringHashMap(ZonElement) = undefined;
var commonRecipes: std.StringHashMap(ZonElement) = undefined;
var commonModels: std.StringHashMap([]const u8) = undefined;
var commonBlueprints: std.StringHashMap([]u8) = undefined;
pub fn init() void {
biomes_zig.init();
@ -33,6 +37,7 @@ pub fn init() void {
commonBiomeMigrations = .init(arenaAllocator.allocator);
commonRecipes = .init(arenaAllocator.allocator);
commonModels = .init(arenaAllocator.allocator);
commonBlueprints = .init(arenaAllocator.allocator);
readAssets(
arenaAllocator,
@ -45,11 +50,12 @@ pub fn init() void {
&commonBiomeMigrations,
&commonRecipes,
&commonModels,
&commonBlueprints,
);
std.log.info(
"Finished assets init with {} blocks ({} migrations), {} items, {} tools. {} biomes ({} migrations), {} recipes",
.{commonBlocks.count(), commonBlockMigrations.count(), commonItems.count(), commonTools.count(), commonBiomes.count(), commonBiomeMigrations.count(), commonRecipes.count()},
"Finished assets init with {} blocks ({} migrations), {} items, {} tools, {} biomes ({} migrations), {} recipes, and {} blueprints",
.{commonBlocks.count(), commonBlockMigrations.count(), commonItems.count(), commonTools.count(), commonBiomes.count(), commonBiomeMigrations.count(), commonRecipes.count(), commonBlueprints.count()},
);
}
@ -149,6 +155,7 @@ pub fn readAllZonFilesInAddons(
}
}
}
fn createAssetStringID(
externalAllocator: NeverFailingAllocator,
addonName: []const u8,
@ -173,6 +180,45 @@ fn createAssetStringID(
return assetId;
}
pub fn readAllBlueprintFilesInAddons(
externalAllocator: NeverFailingAllocator,
addons: main.List(std.fs.Dir),
addonNames: main.List([]const u8),
subPath: []const u8,
output: *std.StringHashMap([]u8),
) void {
for(addons.items, addonNames.items) |addon, addonName| {
var dir = addon.openDir(subPath, .{.iterate = true}) catch |err| {
if(err != error.FileNotFound) {
std.log.err("Could not open addon directory {s}: {s}", .{subPath, @errorName(err)});
}
continue;
};
defer dir.close();
var walker = dir.walk(main.stackAllocator.allocator) catch unreachable;
defer walker.deinit();
while(walker.next() catch |err| blk: {
std.log.err("Got error while iterating addon directory {s}: {s}", .{subPath, @errorName(err)});
break :blk null;
}) |entry| {
if(entry.kind != .file) continue;
if(std.ascii.startsWithIgnoreCase(entry.basename, "_defaults")) continue;
if(!std.ascii.endsWithIgnoreCase(entry.basename, ".blp")) continue;
if(std.ascii.startsWithIgnoreCase(entry.basename, "_migrations")) continue;
const stringId: []u8 = createAssetStringID(externalAllocator, addonName, entry.path);
const data = main.files.Dir.init(dir).read(externalAllocator, entry.path) catch |err| {
std.log.err("Could not open {s}/{s}: {s}", .{subPath, entry.path, @errorName(err)});
continue;
};
output.put(stringId, data) catch unreachable;
}
}
}
/// Reads obj files recursively from all subfolders.
pub fn readAllObjFilesInAddonsHashmap(
externalAllocator: NeverFailingAllocator,
@ -221,6 +267,7 @@ pub fn readAssets(
biomeMigrations: *std.StringHashMap(ZonElement),
recipes: *std.StringHashMap(ZonElement),
models: *std.StringHashMap([]const u8),
blueprints: *std.StringHashMap([]u8),
) void {
var addons = main.List(std.fs.Dir).init(main.stackAllocator);
defer addons.deinit();
@ -258,6 +305,7 @@ pub fn readAssets(
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "biomes", true, biomes, biomeMigrations);
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "recipes", false, recipes, null);
readAllObjFilesInAddonsHashmap(externalAllocator, addons, addonNames, "models", models);
readAllBlueprintFilesInAddons(externalAllocator, addons, addonNames, "blueprints", blueprints);
}
fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void {
@ -424,6 +472,8 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
defer recipes.clearAndFree();
var models = commonModels.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
defer models.clearAndFree();
var blueprints = commonBlueprints.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
defer blueprints.clearAndFree();
readAssets(
arenaAllocator,
@ -436,6 +486,7 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
&biomeMigrations,
&recipes,
&models,
&blueprints,
);
errdefer unloadAssets();
@ -542,6 +593,8 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
registerRecipesFromZon(entry.value_ptr.*);
}
try sbb.registerBlueprints(&blueprints);
// Biomes:
var nextBiomeNumericId: u32 = 0;
for(biomePalette.palette.items) |id| {
@ -577,7 +630,7 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
}
std.log.info(
"Finished registering assets with {} blocks ({} migrations), {} items {} tools. {} biomes ({} migrations), {} recipes and {} models",
"Finished registering assets with {} blocks ({} migrations), {} items {} tools, {} biomes ({} migrations), {} recipes and {} models",
.{blocks.count(), blockMigrations.count(), items.count(), tools.count(), biomes.count(), biomeMigrations.count(), recipes.count(), models.count()},
);
}
@ -586,6 +639,7 @@ pub fn unloadAssets() void { // MARK: unloadAssets()
if(!loadedAssets) return;
loadedAssets = false;
sbb.reset();
blocks_zig.reset();
items_zig.reset();
biomes_zig.reset();

View File

@ -16,10 +16,12 @@ const rotation = @import("rotation.zig");
const RotationMode = rotation.RotationMode;
const Degrees = rotation.Degrees;
const Entity = main.server.Entity;
const sbb = main.server.terrain.structure_building_blocks;
pub const BlockTag = enum(u32) {
air = 0,
fluid = 1,
sbbChild = 2,
_,
var tagList: main.List([]const u8) = .init(allocator);
@ -142,6 +144,12 @@ pub fn register(_: []const u8, id: []const u8, zon: ZonElement) u16 {
_blockTags[size] = BlockTag.loadFromZon(allocator, zon.getChild("tags"));
if(_blockTags[size].len == 0) std.log.err("Block {s} is missing 'tags' field", .{id});
for(_blockTags[size]) |tag| {
if(tag == BlockTag.sbbChild) {
sbb.registerChildBlock(@intCast(size), _id[size]);
break;
}
}
_light[size] = zon.get(u32, "emittedLight", 0);
_absorption[size] = zon.get(u32, "absorbedLight", 0xffffff);
_degradable[size] = zon.get(bool, "degradable", false);

View File

@ -0,0 +1,149 @@
const std = @import("std");
const main = @import("root");
const ZonElement = main.ZonElement;
const Blueprint = main.blueprint.Blueprint;
const List = main.List;
const ListUnmanaged = main.ListUnmanaged;
const AliasTable = main.utils.AliasTable;
const Neighbor = main.chunk.Neighbor;
const Block = main.blocks.Block;
const Degrees = main.rotation.Degrees;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
var arena = main.heap.NeverFailingArenaAllocator.init(main.globalAllocator);
const arenaAllocator = arena.allocator();
var blueprintCache: std.StringHashMapUnmanaged(*[4]BlueprintEntry) = .{};
var childrenToResolve: List(struct {parentId: []const u8, colorName: []const u8, colorIndex: usize, childIndex: usize, structureId: []const u8}) = undefined;
const originBlockStringId = "cubyz:sbb/origin";
var originBlockNumericId: u16 = 0;
// Maps global child block numeric ID to index used to locally represent that child block.
var childBlockNumericIdMap: std.AutoHashMapUnmanaged(u16, u16) = .{};
var childBlockStringId: ListUnmanaged([]const u8) = .{};
const BlueprintEntry = struct {
blueprint: Blueprint,
originBlock: StructureBlock,
childBlocks: []StructureBlock,
const StructureBlock = struct {
x: u16,
y: u16,
z: u16,
index: u16,
data: u16,
pub inline fn direction(self: StructureBlock) Neighbor {
return @enumFromInt(self.data);
}
};
fn init(blueprint: Blueprint, stringId: []const u8) !BlueprintEntry {
var self: BlueprintEntry = .{
.blueprint = blueprint,
.originBlock = undefined,
.childBlocks = undefined,
};
var hasOrigin = false;
var childBlocks: ListUnmanaged(StructureBlock) = .{};
defer childBlocks.deinit(main.stackAllocator);
for(0..blueprint.blocks.width) |x| {
for(0..blueprint.blocks.depth) |y| {
for(0..blueprint.blocks.height) |z| {
const block = blueprint.blocks.get(x, y, z);
if(isOriginBlock(block)) {
if(hasOrigin) {
std.log.err("[{s}] Multiple origin blocks found.", .{stringId});
return error.MultipleOriginBlocks;
} else {
self.originBlock = StructureBlock{
.x = @intCast(x),
.y = @intCast(y),
.z = @intCast(z),
.index = std.math.maxInt(u16),
.data = block.data,
};
hasOrigin = true;
}
} else if(isChildBlock(block)) {
const childBlockLocalId = childBlockNumericIdMap.get(block.typ) orelse return error.ChildBlockNotRecognized;
childBlocks.append(main.stackAllocator, .{
.x = @intCast(x),
.y = @intCast(y),
.z = @intCast(z),
.index = childBlockLocalId,
.data = block.data,
});
}
}
}
}
if(!hasOrigin) {
std.log.err("[{s}] No origin block found.", .{stringId});
return error.NoOriginBlock;
}
self.childBlocks = arenaAllocator.dupe(StructureBlock, childBlocks.items);
return self;
}
};
pub fn isChildBlock(block: Block) bool {
return childBlockNumericIdMap.contains(block.typ);
}
pub fn isOriginBlock(block: Block) bool {
return block.typ == originBlockNumericId;
}
pub fn registerChildBlock(numericId: u16, stringId: []const u8) void {
const index: u16 = @intCast(childBlockNumericIdMap.count());
childBlockNumericIdMap.put(arenaAllocator.allocator, numericId, index) catch unreachable;
// Take only color name from the ID.
var iterator = std.mem.splitBackwardsScalar(u8, stringId, '/');
const colorName = iterator.first();
childBlockStringId.append(arenaAllocator, arenaAllocator.dupe(u8, colorName));
std.log.debug("Structure child block '{s}' {} ('{s}' {}) ", .{colorName, index, stringId, numericId});
}
pub fn registerBlueprints(blueprints: *std.StringHashMap([]u8)) !void {
std.debug.assert(blueprintCache.capacity() == 0);
originBlockNumericId = main.blocks.parseBlock(originBlockStringId).typ;
std.log.debug("Origin block numeric id: {}", .{originBlockNumericId});
blueprintCache.ensureTotalCapacity(arenaAllocator.allocator, blueprints.count()) catch unreachable;
var iterator = blueprints.iterator();
while(iterator.next()) |entry| {
const stringId = entry.key_ptr.*;
const blueprint0 = Blueprint.load(arenaAllocator, entry.value_ptr.*) catch |err| {
std.log.err("Could not load blueprint {s}: {s}", .{stringId, @errorName(err)});
continue;
};
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,
};
blueprintCache.put(arenaAllocator.allocator, arenaAllocator.dupe(u8, stringId), rotatedBlueprints) catch unreachable;
std.log.debug("Registered blueprint: {s}", .{stringId});
}
}
pub fn reset() void {
childBlockNumericIdMap = .{};
childBlockStringId = .{};
blueprintCache = .{};
_ = arena.reset(.free_all);
}

View File

@ -20,6 +20,8 @@ pub const CaveMap = @import("CaveMap.zig");
pub const StructureMap = @import("StructureMap.zig");
pub const structure_building_blocks = @import("structure_building_blocks.zig");
/// A generator for setting the actual Blocks in each Chunk.
pub const BlockGenerator = struct {
init: *const fn(parameters: ZonElement) void,