From cc71c1bd17522510ed4a8fd7e4c45e5525f76bcb Mon Sep 17 00:00:00 2001 From: yello Date: Sun, 23 Aug 2026 19:59:33 +0200 Subject: [PATCH 1/2] Show loading overlay on inventory slots until their contents arrive --- src/Inventory.zig | 38 +++++++++++++++++++++++++++++++++ src/gui/components/ItemSlot.zig | 5 +++++ src/sync.zig | 10 ++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Inventory.zig b/src/Inventory.zig index a9706099a6..f664eb9a80 100644 --- a/src/Inventory.zig +++ b/src/Inventory.zig @@ -26,15 +26,18 @@ pub const client = struct { // MARK: client var maxId: InventoryId = @enumFromInt(0); var freeIdList: main.List(InventoryId) = .empty; var serverToClientMap: std.AutoHashMap(InventoryId, Inventory) = undefined; + var loadingInventories: std.AutoHashMap(InventoryId, u32) = undefined; pub fn init() void { serverToClientMap = .init(main.globalAllocator.allocator); + loadingInventories = .init(main.globalAllocator.allocator); } pub fn deinit() void { std.debug.assert(freeIdList.items.len == @intFromEnum(maxId)); // leak freeIdList.clearAndFree(main.globalAllocator); serverToClientMap.deinit(); + loadingInventories.deinit(); } fn nextId() InventoryId { @@ -65,6 +68,7 @@ pub const client = struct { // MARK: client pub fn unmapServerIdByClientId(clientId: InventoryId) void { main.sync.client.mutex.assertLocked(); + _ = loadingInventories.remove(clientId); const serverId = blk: { var it = serverToClientMap.iterator(); while (it.next()) |entry| { @@ -75,6 +79,34 @@ pub const client = struct { // MARK: client unmapServerId(serverId, clientId); } + fn startLoadTracking(clientId: InventoryId) void { + main.sync.client.mutex.lock(); + defer main.sync.client.mutex.unlock(); + loadingInventories.put(clientId, 1) catch unreachable; + } + + pub fn setExpectedItemCount(clientId: InventoryId, count: u32) void { + main.sync.client.mutex.assertLocked(); + if (count == 0) { + _ = loadingInventories.remove(clientId); + } else { + loadingInventories.put(clientId, count) catch unreachable; + } + } + + pub fn recordInitialItemReceived(clientId: InventoryId) void { + main.sync.client.mutex.assertLocked(); + const entry = loadingInventories.getPtr(clientId) orelse return; + entry.* -= 1; + if (entry.* == 0) _ = loadingInventories.remove(clientId); + } + + fn isLoaded(clientId: InventoryId) bool { + main.sync.client.mutex.lock(); + defer main.sync.client.mutex.unlock(); + return !loadingInventories.contains(clientId); + } + fn getInventory(serverId: InventoryId) ?Inventory { main.sync.client.mutex.assertLocked(); return serverToClientMap.get(serverId); @@ -428,11 +460,17 @@ pub const ClientInventory = struct { // MARK: ClientInventory .type = clientType, }; if (clientType == .serverShared) { + client.startLoadTracking(self.super.id); sync.client.executeCommand(.{.open = .{.inv = self.super, .source = source}}); } return self; } + pub fn isLoaded(self: ClientInventory) bool { + if (self.type != .serverShared) return true; + return client.isLoaded(self.super.id); + } + pub fn deinit(self: ClientInventory, allocator: NeverFailingAllocator) void { if (main.game.world.?.connected) { sync.client.executeCommand(.{.close = .{.inv = self.super, .allocator = allocator}}); diff --git a/src/gui/components/ItemSlot.zig b/src/gui/components/ItemSlot.zig index 9407c860ac..b461b46ef2 100644 --- a/src/gui/components/ItemSlot.zig +++ b/src/gui/components/ItemSlot.zig @@ -143,6 +143,11 @@ pub fn render(self: *ItemSlot, _: Vec2f) void { self.text.render(self.pos[0] + self.size[0] - self.textSize[0] - border, self.pos[1] + self.size[1] - self.textSize[1] - border, 8); } } + if (!self.inventory.isLoaded()) { + const oldColor = draw.setColor(0x80000000); + defer draw.restoreColor(oldColor); + draw.rect(self.pos, self.size); + } if (self.mode != .immutable) { if (self.hovered) { self.hovered = false; diff --git a/src/sync.zig b/src/sync.zig index e7947f7db9..5021ed5f32 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -379,6 +379,7 @@ pub const Command = struct { // MARK: Command create.inv.ref().amount += create.amount; create.inv.inv.update(); + Inventory.client.recordInitialItemReceived(create.inv.inv.id); }, .delete => |delete| { if (delete.inv.ref().amount < delete.amount) { @@ -856,12 +857,19 @@ pub const Command = struct { // MARK: Command if (reader.remaining.len != 0) { const serverId = try reader.readEnum(InventoryId); Inventory.client.mapServerId(serverId, self.inv); + const itemCount = try reader.readInt(u32); + Inventory.client.setExpectedItemCount(self.inv.id, itemCount); } } fn confirmationData(self: Open, allocator: NeverFailingAllocator) []const u8 { - var writer = BinaryWriter.initCapacity(allocator, 4); + var writer = BinaryWriter.initCapacity(allocator, 8); writer.writeEnum(InventoryId, self.inv.id); + var itemCount: u32 = 0; + for (self.inv._items) |stack| { + if (stack.item != .null) itemCount += 1; + } + writer.writeInt(u32, itemCount); return writer.data.toOwnedSlice(); } From e72cd433e810ac0e8699dc8aa7032c82aeb30416 Mon Sep 17 00:00:00 2001 From: yello Date: Wed, 26 Aug 2026 22:25:05 +0200 Subject: [PATCH 2/2] Make chest opening server-initiated --- src/Inventory.zig | 72 +++++++++++------------ src/callbacks/block/client/open_chest.zig | 7 +-- src/game.zig | 2 + src/gui/components/ItemSlot.zig | 5 -- src/gui/windows/chest.zig | 7 +++ src/network/protocols.zig | 66 +++++++++++++++++++++ src/server/server.zig | 20 +++++++ src/sync.zig | 10 +--- 8 files changed, 133 insertions(+), 56 deletions(-) diff --git a/src/Inventory.zig b/src/Inventory.zig index f664eb9a80..a72bc044d8 100644 --- a/src/Inventory.zig +++ b/src/Inventory.zig @@ -26,21 +26,18 @@ pub const client = struct { // MARK: client var maxId: InventoryId = @enumFromInt(0); var freeIdList: main.List(InventoryId) = .empty; var serverToClientMap: std.AutoHashMap(InventoryId, Inventory) = undefined; - var loadingInventories: std.AutoHashMap(InventoryId, u32) = undefined; pub fn init() void { serverToClientMap = .init(main.globalAllocator.allocator); - loadingInventories = .init(main.globalAllocator.allocator); } pub fn deinit() void { std.debug.assert(freeIdList.items.len == @intFromEnum(maxId)); // leak freeIdList.clearAndFree(main.globalAllocator); serverToClientMap.deinit(); - loadingInventories.deinit(); } - fn nextId() InventoryId { + pub fn nextId() InventoryId { main.sync.client.mutex.lock(); defer main.sync.client.mutex.unlock(); if (freeIdList.popOrNull()) |id| { @@ -68,7 +65,6 @@ pub const client = struct { // MARK: client pub fn unmapServerIdByClientId(clientId: InventoryId) void { main.sync.client.mutex.assertLocked(); - _ = loadingInventories.remove(clientId); const serverId = blk: { var it = serverToClientMap.iterator(); while (it.next()) |entry| { @@ -79,46 +75,54 @@ pub const client = struct { // MARK: client unmapServerId(serverId, clientId); } - fn startLoadTracking(clientId: InventoryId) void { - main.sync.client.mutex.lock(); - defer main.sync.client.mutex.unlock(); - loadingInventories.put(clientId, 1) catch unreachable; + fn getInventory(serverId: InventoryId) ?Inventory { + main.sync.client.mutex.assertLocked(); + return serverToClientMap.get(serverId); } - pub fn setExpectedItemCount(clientId: InventoryId, count: u32) void { + fn getInventoryByClientId(clientId: InventoryId) ?Inventory { main.sync.client.mutex.assertLocked(); - if (count == 0) { - _ = loadingInventories.remove(clientId); - } else { - loadingInventories.put(clientId, count) catch unreachable; + var it = serverToClientMap.valueIterator(); + while (it.next()) |inv| { + if (inv.id == clientId) return inv.*; } + return null; } - pub fn recordInitialItemReceived(clientId: InventoryId) void { - main.sync.client.mutex.assertLocked(); - const entry = loadingInventories.getPtr(clientId) orelse return; - entry.* -= 1; - if (entry.* == 0) _ = loadingInventories.remove(clientId); + var pendingChestOpen: ?ClientInventory = null; + + fn buildFromResponse(clientId: InventoryId, source: Source, len: usize, reader: *BinaryReader) Inventory { + const inv: Inventory = .{ + ._items = main.globalAllocator.alloc(ItemStack, len), + .id = clientId, + .source = source, + .callbacks = .{}, + }; + for (inv._items) |*item| item.* = .{}; + inv.fromBytes(reader); + return inv; } - fn isLoaded(clientId: InventoryId) bool { + pub fn receiveChestOpenResponse(clientId: InventoryId, pos: Vec3i, serverId: InventoryId, reader: *BinaryReader) void { main.sync.client.mutex.lock(); defer main.sync.client.mutex.unlock(); - return !loadingInventories.contains(clientId); + const inv = buildFromResponse(clientId, .{.blockInventory = pos}, main.block_entity.BlockEntityTypes.@"cubyz:chest".inventorySize, reader); + mapServerId(serverId, inv); + pendingChestOpen = .{.super = inv, .type = .serverShared}; } - fn getInventory(serverId: InventoryId) ?Inventory { - main.sync.client.mutex.assertLocked(); - return serverToClientMap.get(serverId); + pub fn cancelChestOpen(clientId: InventoryId) void { + main.sync.client.mutex.lock(); + defer main.sync.client.mutex.unlock(); + freeId(clientId); + std.log.err("Server rejected request to open chest.", .{}); } - fn getInventoryByClientId(clientId: InventoryId) ?Inventory { - main.sync.client.mutex.assertLocked(); - var it = serverToClientMap.valueIterator(); - while (it.next()) |inv| { - if (inv.id == clientId) return inv.*; - } - return null; + pub fn takePendingChestOpen() ?ClientInventory { + main.sync.client.mutex.lock(); + defer main.sync.client.mutex.unlock(); + defer pendingChestOpen = null; + return pendingChestOpen; } }; @@ -460,17 +464,11 @@ pub const ClientInventory = struct { // MARK: ClientInventory .type = clientType, }; if (clientType == .serverShared) { - client.startLoadTracking(self.super.id); sync.client.executeCommand(.{.open = .{.inv = self.super, .source = source}}); } return self; } - pub fn isLoaded(self: ClientInventory) bool { - if (self.type != .serverShared) return true; - return client.isLoaded(self.super.id); - } - pub fn deinit(self: ClientInventory, allocator: NeverFailingAllocator) void { if (main.game.world.?.connected) { sync.client.executeCommand(.{.close = .{.inv = self.super, .allocator = allocator}}); diff --git a/src/callbacks/block/client/open_chest.zig b/src/callbacks/block/client/open_chest.zig index a4846117ab..19e1ce5cbb 100644 --- a/src/callbacks/block/client/open_chest.zig +++ b/src/callbacks/block/client/open_chest.zig @@ -17,11 +17,8 @@ pub fn run(_: *anyopaque, params: main.callbacks.ClientBlockCallback.Params) mai } main.network.protocols.blockEntityUpdate.sendClientDataUpdateToServer(main.game.world.?.conn, params.blockPos); - const inventory = main.items.Inventory.ClientInventory.init(main.globalAllocator, main.block_entity.BlockEntityTypes.@"cubyz:chest".inventorySize, .serverShared, .{.blockInventory = params.blockPos}, .{}); - - main.gui.windowlist.chest.setInventory(inventory); - main.gui.openWindow("chest"); - main.Window.setMouseGrabbed(false); + const clientId = main.items.Inventory.client.nextId(); + main.network.protocols.chestOpen.sendRequest(main.game.world.?.conn, clientId, params.blockPos); return .handled; } diff --git a/src/game.zig b/src/game.zig index 89e6b9273d..565b94b78a 100644 --- a/src/game.zig +++ b/src/game.zig @@ -585,6 +585,8 @@ pub fn update(deltaTime: f64) void { // MARK: update() restart(); } + main.gui.windowlist.chest.checkPendingOpen(); + physics.calculateVolumeProperties(.client, &Player.volumeProperties, Player.super.pos, Player.outerBoundingBox, physics.playerAirTerminalVelocity); if (Player.isFlying.load(.monotonic)) { Player.friction = .{.current = 20, .mobile = 20}; diff --git a/src/gui/components/ItemSlot.zig b/src/gui/components/ItemSlot.zig index b461b46ef2..9407c860ac 100644 --- a/src/gui/components/ItemSlot.zig +++ b/src/gui/components/ItemSlot.zig @@ -143,11 +143,6 @@ pub fn render(self: *ItemSlot, _: Vec2f) void { self.text.render(self.pos[0] + self.size[0] - self.textSize[0] - border, self.pos[1] + self.size[1] - self.textSize[1] - border, 8); } } - if (!self.inventory.isLoaded()) { - const oldColor = draw.setColor(0x80000000); - defer draw.restoreColor(oldColor); - draw.rect(self.pos, self.size); - } if (self.mode != .immutable) { if (self.hovered) { self.hovered = false; diff --git a/src/gui/windows/chest.zig b/src/gui/windows/chest.zig index f41dd7cbcc..a39e2282cf 100644 --- a/src/gui/windows/chest.zig +++ b/src/gui/windows/chest.zig @@ -39,6 +39,13 @@ pub fn setInventory(selectedInventory: main.items.Inventory.ClientInventory) voi openInventory = selectedInventory; } +pub fn checkPendingOpen() void { + const inv = main.items.Inventory.client.takePendingChestOpen() orelse return; + setInventory(inv); + main.gui.openWindow("chest"); + main.Window.setMouseGrabbed(false); +} + pub fn onOpen() void { const list = VerticalList.init(.{padding, padding + 16}, 300, 0); diff --git a/src/network/protocols.zig b/src/network/protocols.zig index 6201e1f66e..04045cfc00 100644 --- a/src/network/protocols.zig +++ b/src/network/protocols.zig @@ -1105,3 +1105,69 @@ pub const EntityComponentUpdate = struct { // MARK: EntityComponentUpdate conn.send(.secure, id, writer.data.items); } }; + +pub const chestOpen = struct { + pub const id: u8 = 16; + + fn clientReceive(_: *Connection, reader: *utils.BinaryReader) !void { + const InventoryId = items.Inventory.InventoryId; + const success = try reader.readInt(u8); + const clientId = try reader.readEnum(InventoryId); + if (success == 0) { + items.Inventory.client.cancelChestOpen(clientId); + return; + } + const pos = try reader.readVec(Vec3i); + const serverId = try reader.readEnum(InventoryId); + items.Inventory.client.receiveChestOpenResponse(clientId, pos, serverId, reader); + } + fn serverReceive(conn: *Connection, reader: *utils.BinaryReader) !void { + const user = conn.user.?; + user.receiveChestOpenRequest(reader.remaining); + } + + pub fn sendRequest(conn: *Connection, clientId: items.Inventory.InventoryId, pos: Vec3i) void { + std.debug.assert(conn.user == null); + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 16); + defer writer.deinit(); + writer.writeEnum(items.Inventory.InventoryId, clientId); + writer.writeVec(Vec3i, pos); + conn.send(.secure, id, writer.data.items); + } + + pub fn process(user: *main.server.User, reader: *utils.BinaryReader) void { + const InventoryId = items.Inventory.InventoryId; + const clientId = reader.readEnum(InventoryId) catch return; + const pos = reader.readVec(Vec3i) catch return; + main.items.Inventory.server.createInventory(user, clientId, main.block_entity.BlockEntityTypes.@"cubyz:chest".inventorySize, .{.blockInventory = pos}) catch { + sendFailure(user.conn, clientId); + return; + }; + const inv = main.items.Inventory.server.getInventory(user, clientId) orelse { + sendFailure(user.conn, clientId); + return; + }; + sendResponse(user.conn, clientId, pos, inv); + } + + fn sendResponse(conn: *Connection, clientId: items.Inventory.InventoryId, pos: Vec3i, inv: items.Inventory) void { + std.debug.assert(conn.isServerSide()); + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 32); + defer writer.deinit(); + writer.writeInt(u8, 1); + writer.writeEnum(items.Inventory.InventoryId, clientId); + writer.writeVec(Vec3i, pos); + writer.writeEnum(items.Inventory.InventoryId, inv.id); + inv.toBytes(&writer); + conn.send(.secure, id, writer.data.items); + } + + fn sendFailure(conn: *Connection, clientId: items.Inventory.InventoryId) void { + std.debug.assert(conn.isServerSide()); + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 8); + defer writer.deinit(); + writer.writeInt(u8, 0); + writer.writeEnum(items.Inventory.InventoryId, clientId); + conn.send(.secure, id, writer.data.items); + } +}; diff --git a/src/server/server.zig b/src/server/server.zig index 0fbc82d540..7ff553c66b 100644 --- a/src/server/server.zig +++ b/src/server/server.zig @@ -147,6 +147,7 @@ pub const User = struct { // MARK: User mutex: main.utils.Mutex = .{}, inventoryCommands: main.List([]const u8) = .empty, + chestOpenRequests: main.List([]const u8) = .empty, pub const State = enum { awaitingKeyVerification, connectedVerified, awaitingReloadVerified }; @@ -229,6 +230,10 @@ pub const User = struct { // MARK: User main.globalAllocator.free(commandData); } self.inventoryCommands.deinit(main.globalAllocator); + for (self.chestOpenRequests.items) |requestData| { + main.globalAllocator.free(requestData); + } + self.chestOpenRequests.deinit(main.globalAllocator); self.jobQueue.deinit(); } @@ -496,6 +501,9 @@ pub const User = struct { // MARK: User const commands = self.inventoryCommands; defer commands.deinit(main.globalAllocator); self.inventoryCommands = .empty; + const chestOpenRequests = self.chestOpenRequests; + defer chestOpenRequests.deinit(main.globalAllocator); + self.chestOpenRequests = .empty; self.mutex.unlock(); for (commands.items) |commandData| { @@ -512,6 +520,12 @@ pub const User = struct { // MARK: User }; } + for (chestOpenRequests.items) |requestData| { + defer main.globalAllocator.free(requestData); + var reader: BinaryReader = .init(requestData); + main.network.protocols.chestOpen.process(self, &reader); + } + self.mutex.lock(); defer self.mutex.unlock(); var time = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% main.settings.entityLookback; @@ -536,6 +550,12 @@ pub const User = struct { // MARK: User self.inventoryCommands.append(main.globalAllocator, main.globalAllocator.dupe(u8, commandData)); } + pub fn receiveChestOpenRequest(self: *User, requestData: []const u8) void { + self.mutex.lock(); + defer self.mutex.unlock(); + self.chestOpenRequests.append(main.globalAllocator, main.globalAllocator.dupe(u8, requestData)); + } + pub fn receiveData(self: *User, reader: *BinaryReader) !void { self.mutex.lock(); defer self.mutex.unlock(); diff --git a/src/sync.zig b/src/sync.zig index 5021ed5f32..e7947f7db9 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -379,7 +379,6 @@ pub const Command = struct { // MARK: Command create.inv.ref().amount += create.amount; create.inv.inv.update(); - Inventory.client.recordInitialItemReceived(create.inv.inv.id); }, .delete => |delete| { if (delete.inv.ref().amount < delete.amount) { @@ -857,19 +856,12 @@ pub const Command = struct { // MARK: Command if (reader.remaining.len != 0) { const serverId = try reader.readEnum(InventoryId); Inventory.client.mapServerId(serverId, self.inv); - const itemCount = try reader.readInt(u32); - Inventory.client.setExpectedItemCount(self.inv.id, itemCount); } } fn confirmationData(self: Open, allocator: NeverFailingAllocator) []const u8 { - var writer = BinaryWriter.initCapacity(allocator, 8); + var writer = BinaryWriter.initCapacity(allocator, 4); writer.writeEnum(InventoryId, self.inv.id); - var itemCount: u32 = 0; - for (self.inv._items) |stack| { - if (stack.item != .null) itemCount += 1; - } - writer.writeInt(u32, itemCount); return writer.data.toOwnedSlice(); }