diff --git a/assets/cubyz/ui/inventory/sort_icon.png b/assets/cubyz/ui/inventory/sort_icon.png new file mode 100644 index 0000000000..0e93266217 Binary files /dev/null and b/assets/cubyz/ui/inventory/sort_icon.png differ diff --git a/src/Inventory.zig b/src/Inventory.zig index 3654bcd0a5..0a4971e3ec 100644 --- a/src/Inventory.zig +++ b/src/Inventory.zig @@ -7,6 +7,7 @@ const Item = main.items.Item; const ItemStack = main.items.ItemStack; const ProceduralItem = main.items.ProceduralItem; const utils = main.utils; +const Tag = main.Tag; const BinaryWriter = utils.BinaryWriter; const BinaryReader = utils.BinaryReader; const NeverFailingAllocator = main.heap.NeverFailingAllocator; @@ -531,6 +532,67 @@ pub const ClientInventory = struct { // MARK: ClientInventory main.sync.client.executeCommand(.{.craftProceduralItem = .init(destinations, workbenchInv)}); } + pub fn sortItems(source: ClientInventory, ignoredSlotCount: usize) void { + compressItems(source); + const ctx: SortContext = .{.inv = source}; + std.sort.insertionContext(ignoredSlotCount, source.super._items.len, ctx); + } + + pub fn compressItems(source: ClientInventory) void { + for (source.super._items, 0..) |invStack, slot| { + for (source.super._items, 0..) |checkedInvStack, checkedSlot| { + if (checkedSlot < slot) continue; + if (std.meta.eql(invStack.item, checkedInvStack.item)) { + main.sync.client.executeCommand(.{.deposit = .{.dest = .{.inv = source.super, .slot = @intCast(checkedSlot)}, .source = .{.inv = source.super, .slot = @intCast(slot)}, .amount = checkedInvStack.amount}}); + } + } + } + } + + const SortContext = struct { + inv: ClientInventory, + + pub fn lessThan(ctx: @This(), a: usize, b: usize) bool { + const itemA: Item = ctx.inv.getItem(a); + const itemB: Item = ctx.inv.getItem(b); + + if (itemA == .null) return false; + if (itemB == .null) return true; + if ((itemA != .proceduralItem) and (itemB == .proceduralItem)) return false; + if ((itemA == .proceduralItem) and (itemB != .proceduralItem)) return true; + + const itemATags = getTagsFromItem(itemA); + const itemBTags = getTagsFromItem(itemB); + + for (0..@min(itemATags.len, itemBTags.len)) |i| { + if (itemATags[i] == itemBTags[i]) continue; + return std.mem.lessThan(u8, itemATags[i].getName(), itemBTags[i].getName()); + } + if (itemATags.len != itemBTags.len) return itemATags.len < itemBTags.len; + if ((itemA == .proceduralItem) and (itemB == .proceduralItem)) { + std.log.debug("checking durability", .{}); + return (itemA.proceduralItem.durability > itemB.proceduralItem.durability); + } + + return std.mem.lessThan(u8, itemA.id().?, itemB.id().?); + } + + pub fn swap(ctx: @This(), a: usize, b: usize) void { + main.sync.client.executeCommand(.{.depositOrSwap = .{ + .dest = .{.inv = ctx.inv.super, .slot = @intCast(a)}, + .source = .{.inv = ctx.inv.super, .slot = @intCast(b)}, + }}); + } + }; + + pub fn getTagsFromItem(givenItem: Item) []const Tag { + switch (givenItem) { + .null => return &[_]Tag{}, + .proceduralItem => return givenItem.proceduralItem.type.tags(), + .baseItem => return givenItem.baseItem.tags(), + } + } + pub fn placeBlock(self: ClientInventory, slot: u32) void { std.debug.assert(self.type == .serverShared); main.renderer.MeshSelection.placeBlock(self, slot); diff --git a/src/assets.zig b/src/assets.zig index c2147751fa..5185818b04 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -420,6 +420,7 @@ fn assignBlockItem(stringId: []const u8) !void { const index = items.BaseItemIndex.fromId(stringId) orelse unreachable; const item = &items.itemList[@intFromEnum(index)]; item.block = block; + item.tags = blocks.parseBlock(stringId).tags(); } fn registerBiome(numericId: u32, stringId: []const u8, zon: ZonElement) void { diff --git a/src/blocks.zig b/src/blocks.zig index 304ad6edb3..6f11f3dd0e 100644 --- a/src/blocks.zig +++ b/src/blocks.zig @@ -173,7 +173,7 @@ pub fn register(_: []const u8, id: []const u8, zon: ZonElement) u16 { const rotation_tags = _mode[size].getBlockTags(); const block_tags = Tag.loadTagsFromZon(main.stackAllocator, zon.getChild("tags")); defer main.stackAllocator.free(block_tags); - _tags[size] = std.mem.concat(main.worldArena.allocator, Tag, &.{rotation_tags, block_tags}) catch unreachable; + _tags[size] = std.mem.concat(main.worldArena.allocator, Tag, &.{block_tags, rotation_tags}) catch unreachable; if (_tags[size].len == 0) std.log.err("Block {s} is missing 'tags' field", .{id}); for (_tags[size]) |tag| { diff --git a/src/gui/windows/inventory.zig b/src/gui/windows/inventory.zig index d459acd6a6..af89e0ccad 100644 --- a/src/gui/windows/inventory.zig +++ b/src/gui/windows/inventory.zig @@ -30,9 +30,11 @@ pub var window = GuiWindow{ const padding: f32 = 8; var craftingIcon: Texture = undefined; +var sortIcon: Texture = undefined; pub fn init() void { craftingIcon = Texture.initFromFile("assets/cubyz/ui/inventory/crafting_icon.png"); + sortIcon = Texture.initFromFile("assets/cubyz/ui/inventory/sort_icon.png"); } pub fn deinit() void { @@ -41,14 +43,23 @@ pub fn deinit() void { var itemSlots: [20]*ItemSlot = undefined; +pub fn sortItems(target: main.items.Inventory.ClientInventory) void { + target.sortItems(12); +} + + pub fn onOpen() void { + const sortCallback: main.callbacks.SimpleCallback = .{.inner = @ptrCast(&sortItems), .data = &Player.inventory}; + window.titleBar = HorizontalList.init(); + window.titleBar.?.add(Button.initIcon(.{0, 0}, .{9, 9}, sortIcon, false, .{.onAction = sortCallback})); + const list = VerticalList.init(.{padding, padding + 16}, 300, 0); // Some miscellanious slots and buttons: // TODO: armor slots, backpack slot + stack-based backpack inventory, other items maybe? { const row = HorizontalList.init(); blk: { - row.add(GuiComponent.BagSlot.init(.{0, 0}, main.entity.components.@"cubyz:bag".client.getBag(main.game.Player.id) orelse break :blk)); + row.add(GuiComponent.BagSlot.init(.{32, 0}, main.entity.components.@"cubyz:bag".client.getBag(main.game.Player.id) orelse break :blk)); } row.add(Button.initIcon(.{32, 0}, .{32, 32}, craftingIcon, true, .{.onAction = gui.openWindowCallback("inventory_crafting")})); list.add(row);