Add list palettes and item palette (#1190)

* Add list palettes and item palette

* Fix block-item assignments

* Remove initEmpty

* Replace This() with Palette

* Update src/assets.zig

Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>

* Fix duplicated keys in legacy palette issue

* Remove redundant errdefer

* Some more figuring out order of registering

* Remove assertion breaking migrations 2to1

* Apply review suggestions

* Add error log for block and item ID conflict

* Remove migrations file

* Update src/assets.zig

---------

Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
This commit is contained in:
Krzysztof Wiśniewski 2025-03-18 21:25:04 +01:00 committed by GitHub
parent 9226d51a8f
commit f0eb9f4f5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 188 additions and 78 deletions

View File

@ -260,7 +260,7 @@ pub fn readAssets(
readAllObjFilesInAddonsHashmap(externalAllocator, addons, addonNames, "models", models);
}
fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !*items_zig.BaseItem {
fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void {
var split = std.mem.splitScalar(u8, id, ':');
const mod = split.first();
var texturePath: []const u8 = &[0]u8{};
@ -271,7 +271,7 @@ fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !*item
texturePath = try std.fmt.bufPrint(&buf1, "{s}/{s}/items/textures/{s}", .{assetFolder, mod, texture});
replacementTexturePath = try std.fmt.bufPrint(&buf2, "assets/{s}/items/textures/{s}", .{mod, texture});
}
return items_zig.register(assetFolder, texturePath, replacementTexturePath, id, zon);
_ = items_zig.register(assetFolder, texturePath, replacementTexturePath, id, zon);
}
fn registerTool(assetFolder: []const u8, id: []const u8, zon: ZonElement) void {
@ -279,13 +279,21 @@ fn registerTool(assetFolder: []const u8, id: []const u8, zon: ZonElement) void {
}
fn registerBlock(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void {
const block = blocks_zig.register(assetFolder, id, zon);
blocks_zig.meshes.register(assetFolder, id, zon);
if(zon == .null) std.log.err("Missing block: {s}. Replacing it with default block.", .{id});
if(zon.get(bool, "hasItem", true)) {
const item = try registerItem(assetFolder, id, zon.getChild("item"));
item.block = block;
}
_ = blocks_zig.register(assetFolder, id, zon);
blocks_zig.meshes.register(assetFolder, id, zon);
}
fn assignBlockItem(stringId: []const u8) !void {
const block = blocks_zig.getTypeById(stringId);
const item = items_zig.getByID(stringId) orelse unreachable;
item.block = block;
}
fn registerBiome(numericId: u32, stringId: []const u8, zon: ZonElement) void {
if(zon == .null) std.log.err("Missing biome: {s}. Replacing it with default biome.", .{stringId});
biomes_zig.register(stringId, numericId, zon);
}
fn registerRecipesFromZon(zon: ZonElement) void {
@ -294,29 +302,68 @@ fn registerRecipesFromZon(zon: ZonElement) void {
pub const Palette = struct { // MARK: Palette
palette: main.List([]const u8),
pub fn init(allocator: NeverFailingAllocator, zon: ZonElement, firstElement: ?[]const u8) !*Palette {
const self = switch(zon) {
.object => try loadFromZonLegacy(allocator, zon),
.array, .null => try loadFromZon(allocator, zon),
else => return error.InvalidPaletteFormat,
};
if(firstElement) |elem| {
if(self.palette.items.len == 0) {
self.palette.append(allocator.dupe(u8, elem));
}
if(!std.mem.eql(u8, self.palette.items[0], elem)) {
return error.FistItemMismatch;
}
}
return self;
}
fn loadFromZon(allocator: NeverFailingAllocator, zon: ZonElement) !*Palette {
const items = zon.toSlice();
const self = allocator.create(Palette);
self.* = Palette{
.palette = .init(allocator),
.palette = .initCapacity(allocator, items.len),
};
errdefer self.deinit();
if(zon != .object or zon.object.count() == 0) {
if(firstElement) |elem| self.palette.append(allocator.dupe(u8, elem));
} else {
const palette = main.stackAllocator.alloc(?[]const u8, zon.object.count());
defer main.stackAllocator.free(palette);
for(palette) |*val| {
val.* = null;
}
var iterator = zon.object.iterator();
while(iterator.next()) |entry| {
palette[entry.value_ptr.as(usize, std.math.maxInt(usize))] = entry.key_ptr.*;
}
if(firstElement) |elem| std.debug.assert(std.mem.eql(u8, palette[0].?, elem));
for(palette) |val| {
std.log.info("palette[{}]: {s}", .{self.palette.items.len, val.?});
self.palette.append(allocator.dupe(u8, val orelse return error.MissingKeyInPalette));
for(items) |name| {
const stringId = name.as(?[]const u8, null) orelse return error.InvalidPaletteFormat;
self.palette.appendAssumeCapacity(allocator.dupe(u8, stringId));
}
return self;
}
fn loadFromZonLegacy(allocator: NeverFailingAllocator, zon: ZonElement) !*Palette {
// Using zon.object.count() here has the implication that array can not be sparse.
const paletteLength = zon.object.count();
const translationPalette = main.stackAllocator.alloc(?[]const u8, paletteLength);
defer main.stackAllocator.free(translationPalette);
@memset(translationPalette, null);
var iterator = zon.object.iterator();
while(iterator.next()) |entry| {
const numericId = entry.value_ptr.as(?usize, null) orelse return error.InvalidPaletteFormat;
const name = entry.key_ptr.*;
if(numericId >= translationPalette.len) {
std.log.err("ID {} ('{s}') out of range. This can be caused by palette having missing block IDs.", .{numericId, name});
return error.SparsePaletteNotAllowed;
}
translationPalette[numericId] = name;
}
const self = allocator.create(Palette);
self.* = Palette{
.palette = .initCapacity(allocator, paletteLength),
};
errdefer self.deinit();
for(translationPalette) |val| {
self.palette.appendAssumeCapacity(allocator.dupe(u8, val orelse return error.MissingKeyInPalette));
std.log.info("palette[{}]: {s}", .{self.palette.items.len, val.?});
}
return self;
}
@ -334,11 +381,13 @@ pub const Palette = struct { // MARK: Palette
self.palette.append(self.palette.allocator.dupe(u8, id));
}
pub fn save(self: *Palette, allocator: NeverFailingAllocator) ZonElement {
const zon = ZonElement.initObject(allocator);
errdefer zon.free(allocator);
for(self.palette.items, 0..) |item, i| {
zon.put(item, i);
pub fn storeToZon(self: *Palette, allocator: NeverFailingAllocator) ZonElement {
const zon = ZonElement.initArray(allocator);
zon.array.ensureCapacity(self.palette.items.len);
for(self.palette.items) |item| {
zon.append(item);
}
return zon;
}
@ -355,7 +404,7 @@ pub const Palette = struct { // MARK: Palette
var loadedAssets: bool = false;
pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, biomePalette: *Palette) !void { // MARK: loadWorldAssets()
pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPalette: *Palette, biomePalette: *Palette) !void { // MARK: loadWorldAssets()
if(loadedAssets) return; // The assets already got loaded by the server.
loadedAssets = true;
@ -402,30 +451,81 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, biomePal
_ = main.models.registerModel(entry.key_ptr.*, entry.value_ptr.*);
}
// blocks:
blocks_zig.meshes.registerBlockBreakingAnimation(assetFolder);
for(blockPalette.palette.items) |id| {
const nullValue = blocks.get(id);
var zon: ZonElement = undefined;
if(nullValue) |value| {
zon = value;
} else {
std.log.err("Missing block: {s}. Replacing it with default block.", .{id});
zon = .null;
}
try registerBlock(assetFolder, id, zon);
}
var iterator = blocks.iterator();
while(iterator.next()) |entry| {
if(blocks_zig.hasRegistered(entry.key_ptr.*)) continue;
try registerBlock(assetFolder, entry.key_ptr.*, entry.value_ptr.*);
blockPalette.add(entry.key_ptr.*);
// Blocks:
// First blocks from the palette to enforce ID values.
for(blockPalette.palette.items) |stringId| {
try registerBlock(assetFolder, stringId, blocks.get(stringId) orelse .null);
}
// items:
// Then all the blocks that were missing in palette but are present in the game.
var iterator = blocks.iterator();
while(iterator.next()) |entry| {
const stringId = entry.key_ptr.*;
const zon = entry.value_ptr.*;
if(blocks_zig.hasRegistered(stringId)) continue;
try registerBlock(assetFolder, stringId, zon);
blockPalette.add(stringId);
}
// Items:
// First from the palette to enforce ID values.
for(itemPalette.palette.items) |stringId| {
std.debug.assert(!items_zig.hasRegistered(stringId));
// Some items are created automatically from blocks.
if(blocks.get(stringId)) |zon| {
if(!zon.get(bool, "hasItem", true)) continue;
try registerItem(assetFolder, stringId, zon.getChild("item"));
if(items.get(stringId) != null) {
std.log.err("Item {s} appears as standalone item and as block item.", .{stringId});
}
continue;
}
// Items not related to blocks should appear in items hash map.
if(items.get(stringId)) |zon| {
try registerItem(assetFolder, stringId, zon);
continue;
}
std.log.err("Missing item: {s}. Replacing it with default item.", .{stringId});
try registerItem(assetFolder, stringId, .null);
}
// Then missing block-items to keep backwards compatibility of ID order.
for(blockPalette.palette.items) |stringId| {
const zon = blocks.get(stringId) orelse .null;
if(!zon.get(bool, "hasItem", true)) continue;
if(items_zig.hasRegistered(stringId)) continue;
try registerItem(assetFolder, stringId, zon.getChild("item"));
itemPalette.add(stringId);
}
// And finally normal items.
iterator = items.iterator();
while(iterator.next()) |entry| {
_ = try registerItem(assetFolder, entry.key_ptr.*, entry.value_ptr.*);
const stringId = entry.key_ptr.*;
const zon = entry.value_ptr.*;
if(items_zig.hasRegistered(stringId)) continue;
std.debug.assert(zon != .null);
try registerItem(assetFolder, stringId, zon);
itemPalette.add(stringId);
}
// After we have registered all items and all blocks, we can assign block references to those that come from blocks.
for(blockPalette.palette.items) |stringId| {
const zon = blocks.get(stringId) orelse .null;
if(!zon.get(bool, "hasItem", true)) continue;
std.debug.assert(items_zig.hasRegistered(stringId));
try assignBlockItem(stringId);
}
// tools:
@ -443,25 +543,17 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, biomePal
}
// Biomes:
var i: u32 = 0;
var nextBiomeNumericId: u32 = 0;
for(biomePalette.palette.items) |id| {
const nullValue = biomes.get(id);
var zon: ZonElement = undefined;
if(nullValue) |value| {
zon = value;
} else {
std.log.err("Missing biomes: {s}. Replacing it with default biomes.", .{id});
zon = .null;
}
biomes_zig.register(id, i, zon);
i += 1;
registerBiome(nextBiomeNumericId, id, biomes.get(id) orelse .null);
nextBiomeNumericId += 1;
}
iterator = biomes.iterator();
while(iterator.next()) |entry| {
if(biomes_zig.hasRegistered(entry.key_ptr.*)) continue;
biomes_zig.register(entry.key_ptr.*, i, entry.value_ptr.*);
registerBiome(nextBiomeNumericId, entry.key_ptr.*, entry.value_ptr.*);
biomePalette.add(entry.key_ptr.*);
i += 1;
nextBiomeNumericId += 1;
}
biomes_zig.finishLoading();

View File

@ -645,6 +645,7 @@ pub const World = struct { // MARK: World
spawn: Vec3f = undefined,
connected: bool = true,
blockPalette: *assets.Palette = undefined,
itemPalette: *assets.Palette = undefined,
biomePalette: *assets.Palette = undefined,
itemDrops: ClientItemDropManager = undefined,
playerBiome: Atomic(*const main.server.terrain.biomes.Biome) = undefined,
@ -682,6 +683,7 @@ pub const World = struct { // MARK: World
main.threadPool.clear();
self.itemDrops.deinit();
self.blockPalette.deinit();
self.itemPalette.deinit();
self.biomePalette.deinit();
self.manager.deinit();
main.server.stop();
@ -701,9 +703,11 @@ pub const World = struct { // MARK: World
errdefer self.blockPalette.deinit();
self.biomePalette = try assets.Palette.init(main.globalAllocator, zon.getChild("biomePalette"), null);
errdefer self.biomePalette.deinit();
self.itemPalette = try assets.Palette.init(main.globalAllocator, zon.getChild("itemPalette"), null);
errdefer self.itemPalette.deinit();
self.spawn = zon.get(Vec3f, "spawn", .{0, 0, 0});
try assets.loadWorldAssets("serverAssets", self.blockPalette, self.biomePalette);
try assets.loadWorldAssets("serverAssets", self.blockPalette, self.itemPalette, self.biomePalette);
Player.id = zon.get(u32, "player_id", std.math.maxInt(u32));
Player.inventory = Inventory.init(main.globalAllocator, 32, .normal, .{.playerInventory = Player.id});
Player.loadFrom(zon.getChild("player"));

View File

@ -756,6 +756,10 @@ pub var itemListSize: u16 = 0;
var recipeList: main.List(Recipe) = undefined;
pub fn hasRegistered(id: []const u8) bool {
return reverseIndices.contains(id);
}
pub fn toolTypeIterator() std.StringHashMap(ToolType).ValueIterator {
return toolTypes.valueIterator();
}

View File

@ -682,8 +682,9 @@ pub const Protocols = struct {
zonObject.put("player", conn.user.?.player.save(main.stackAllocator));
zonObject.put("player_id", conn.user.?.id);
zonObject.put("spawn", main.server.world.?.spawn);
zonObject.put("blockPalette", main.server.world.?.blockPalette.save(main.stackAllocator));
zonObject.put("biomePalette", main.server.world.?.biomePalette.save(main.stackAllocator));
zonObject.put("blockPalette", main.server.world.?.blockPalette.storeToZon(main.stackAllocator));
zonObject.put("itemPalette", main.server.world.?.itemPalette.storeToZon(main.stackAllocator));
zonObject.put("biomePalette", main.server.world.?.biomePalette.storeToZon(main.stackAllocator));
const outData = zonObject.toStringEfficient(main.stackAllocator, &[1]u8{stepServerData});
defer main.stackAllocator.free(outData);

View File

@ -20,6 +20,7 @@ const terrain = server.terrain;
const server = @import("server.zig");
const User = server.User;
const Entity = server.Entity;
const Palette = main.assets.Palette;
const storage = @import("storage.zig");
@ -415,6 +416,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld
itemDropManager: ItemDropManager = undefined,
blockPalette: *main.assets.Palette = undefined,
itemPalette: *main.assets.Palette = undefined,
biomePalette: *main.assets.Palette = undefined,
chunkManager: ChunkManager = undefined,
@ -510,35 +512,37 @@ pub const ServerWorld = struct { // MARK: ServerWorld
}
self.wio = WorldIO.init(try files.openDir(try std.fmt.bufPrint(&buf, "saves/{s}", .{name})), self);
errdefer self.wio.deinit();
const blockPaletteZon = files.readToZon(arenaAllocator, try std.fmt.bufPrint(&buf, "saves/{s}/palette.zig.zon", .{name})) catch .null;
self.blockPalette = try main.assets.Palette.init(main.globalAllocator, blockPaletteZon, "cubyz:air");
errdefer self.blockPalette.deinit();
std.log.info(
"Loaded save block palette with {} blocks.",
.{self.blockPalette.size()},
);
std.log.info("Loaded save block palette with {} blocks.", .{self.blockPalette.size()});
const itemPaletteZon = files.readToZon(arenaAllocator, try std.fmt.bufPrint(&buf, "saves/{s}/item_palette.zig.zon", .{name})) catch .null;
self.itemPalette = try main.assets.Palette.init(main.globalAllocator, itemPaletteZon, null);
errdefer self.itemPalette.deinit();
std.log.info("Loaded save item palette with {} items.", .{self.itemPalette.size()});
const biomePaletteZon = files.readToZon(arenaAllocator, try std.fmt.bufPrint(&buf, "saves/{s}/biome_palette.zig.zon", .{name})) catch .null;
self.biomePalette = try main.assets.Palette.init(main.globalAllocator, biomePaletteZon, null);
errdefer self.biomePalette.deinit();
std.log.info(
"Loaded save biome palette with {} biomes.",
.{self.biomePalette.size()},
);
std.log.info("Loaded save biome palette with {} biomes.", .{self.biomePalette.size()});
errdefer main.assets.unloadAssets();
if(self.wio.hasWorldData()) {
self.seed = try self.wio.loadWorldSeed();
self.generated = true;
try main.assets.loadWorldAssets(try std.fmt.bufPrint(&buf, "saves/{s}/assets/", .{name}), self.blockPalette, self.biomePalette);
try main.assets.loadWorldAssets(try std.fmt.bufPrint(&buf, "saves/{s}/assets/", .{name}), self.blockPalette, self.itemPalette, self.biomePalette);
} else {
self.seed = main.random.nextInt(u48, &main.seed);
try main.assets.loadWorldAssets(try std.fmt.bufPrint(&buf, "saves/{s}/assets/", .{name}), self.blockPalette, self.biomePalette);
try main.assets.loadWorldAssets(try std.fmt.bufPrint(&buf, "saves/{s}/assets/", .{name}), self.blockPalette, self.itemPalette, self.biomePalette);
try self.wio.saveWorldData();
}
// Store the block palette now that everything is loaded.
try files.writeZon(try std.fmt.bufPrint(&buf, "saves/{s}/palette.zig.zon", .{name}), self.blockPalette.save(arenaAllocator));
try files.writeZon(try std.fmt.bufPrint(&buf, "saves/{s}/biome_palette.zig.zon", .{name}), self.biomePalette.save(arenaAllocator));
try files.writeZon(try std.fmt.bufPrint(&buf, "saves/{s}/palette.zig.zon", .{name}), self.blockPalette.storeToZon(arenaAllocator));
try files.writeZon(try std.fmt.bufPrint(&buf, "saves/{s}/biome_palette.zig.zon", .{name}), self.biomePalette.storeToZon(arenaAllocator));
try files.writeZon(try std.fmt.bufPrint(&buf, "saves/{s}/item_palette.zig.zon", .{name}), self.itemPalette.storeToZon(arenaAllocator));
var gamerules = files.readToZon(arenaAllocator, try std.fmt.bufPrint(&buf, "saves/{s}/gamerules.zig.zon", .{name})) catch ZonElement.initObject(arenaAllocator);
@ -567,6 +571,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld
self.chunkManager.deinit();
self.itemDropManager.deinit();
self.blockPalette.deinit();
self.itemPalette.deinit();
self.biomePalette.deinit();
self.wio.deinit();
main.globalAllocator.free(self.name);

View File

@ -250,6 +250,10 @@ pub const ZonElement = union(enum) { // MARK: Zon
}
}
pub fn append(self: *const ZonElement, value: anytype) void {
self.array.append(createElementFromRandomType(value, self.array.allocator.allocator));
}
pub fn put(self: *const ZonElement, key: []const u8, value: anytype) void {
const result = createElementFromRandomType(value, self.object.allocator);