Refactor how recipes are handled:

- They now have a fixed source type
- The server is responsible for filling the recipe items, disabling use of fillFromCreative for recipes
- The server now checks if the recipe is known (→ recipes cannot be exploited to get unobtainable items)
- The inventory is now cached on the client, instead of being recreated every time the recipes update (→less traffic)
This commit is contained in:
IntegratedQuantum 2025-02-09 17:11:21 +01:00
parent 2b3f38827e
commit c9bac6377e
3 changed files with 68 additions and 17 deletions

View File

@ -302,7 +302,7 @@ pub const Sync = struct { // MARK: Sync
fn createInventory(user: *main.server.User, clientId: u32, len: usize, typ: Inventory.Type, source: Source) void { fn createInventory(user: *main.server.User, clientId: u32, len: usize, typ: Inventory.Type, source: Source) void {
main.utils.assertLocked(&mutex); main.utils.assertLocked(&mutex);
switch(source) { switch(source) {
.sharedTestingInventory => { .sharedTestingInventory, .recipe => {
for(inventories.items) |*inv| { for(inventories.items) |*inv| {
if(std.meta.eql(inv.source, source)) { if(std.meta.eql(inv.source, source)) {
inv.addUser(user, clientId); inv.addUser(user, clientId);
@ -332,6 +332,14 @@ pub const Sync = struct { // MARK: Sync
inventory.inv.loadFromZon(inventoryZon); inventory.inv.loadFromZon(inventoryZon);
}, },
.recipe => |recipe| {
for(0..recipe.sourceAmounts.len) |i| {
inventory.inv._items[i].amount = recipe.sourceAmounts[i];
inventory.inv._items[i].item = .{.baseItem = recipe.sourceItems[i]};
}
inventory.inv._items[inventory.inv._items.len - 1].amount = recipe.resultAmount;
inventory.inv._items[inventory.inv._items.len - 1].item = .{.baseItem = recipe.resultItem};
},
.other => {}, .other => {},
.alreadyFreed => unreachable, .alreadyFreed => unreachable,
} }
@ -1063,10 +1071,17 @@ pub const Command = struct { // MARK: Command
.playerInventory, .hand => |val| { .playerInventory, .hand => |val| {
std.mem.writeInt(u32, data.addMany(4)[0..4], val, .big); std.mem.writeInt(u32, data.addMany(4)[0..4], val, .big);
}, },
else => {} .recipe => |val| {
std.mem.writeInt(u16, data.addMany(2)[0..2], val.resultAmount, .big);
data.appendSlice(val.resultItem.id);
data.append(0);
for(0..val.sourceItems.len) |i| {
std.mem.writeInt(u16, data.addMany(2)[0..2], val.sourceAmounts[i], .big);
data.appendSlice(val.sourceItems[i].id);
data.append(0);
} }
switch(self.source) { },
.playerInventory, .sharedTestingInventory, .hand, .other => {}, .sharedTestingInventory, .other => {},
.alreadyFreed => unreachable, .alreadyFreed => unreachable,
} }
} }
@ -1082,6 +1097,32 @@ pub const Command = struct { // MARK: Command
.playerInventory => .{.playerInventory = std.mem.readInt(u32, data[14..18], .big)}, .playerInventory => .{.playerInventory = std.mem.readInt(u32, data[14..18], .big)},
.sharedTestingInventory => .{.sharedTestingInventory = {}}, .sharedTestingInventory => .{.sharedTestingInventory = {}},
.hand => .{.hand = std.mem.readInt(u32, data[14..18], .big)}, .hand => .{.hand = std.mem.readInt(u32, data[14..18], .big)},
.recipe => .{.recipe = blk: {
var itemList = main.List(struct{amount: u16, item: *const main.items.BaseItem}).initCapacity(main.stackAllocator, len);
defer itemList.deinit();
var index: usize = 14;
while(index + 2 < data.len) {
const resultAmount = std.mem.readInt(u16, data[index..][0..2], .big);
index += 2;
const resultItem = if(std.mem.indexOfScalarPos(u8, data, index, 0)) |endIndex| blk2: {
const itemId = data[index..endIndex];
index = endIndex + 1;
break :blk2 main.items.getByID(itemId) orelse return error.Invalid;
} else return error.Invalid;
itemList.append(.{.amount = resultAmount, .item = resultItem});
}
if(itemList.items.len != len) return error.Invalid;
// Find the recipe in our list:
outer: for(main.items.recipes()) |*recipe| {
if(recipe.resultAmount == itemList.items[0].amount and recipe.resultItem == itemList.items[0].item and recipe.sourceItems.len == itemList.items.len - 1) {
for(itemList.items[1..], 0..) |item, i| {
if(item.amount != recipe.sourceAmounts[i] or item.item != recipe.sourceItems[i]) continue :outer;
}
break :blk recipe;
}
}
return error.Invalid;
}},
.other => .{.other = {}}, .other => .{.other = {}},
.alreadyFreed => unreachable, .alreadyFreed => unreachable,
}; };
@ -1655,6 +1696,7 @@ const SourceType = enum(u8) {
playerInventory = 1, playerInventory = 1,
sharedTestingInventory = 2, sharedTestingInventory = 2,
hand = 3, hand = 3,
recipe = 4,
other = 0xff, // TODO: List every type separately here. other = 0xff, // TODO: List every type separately here.
}; };
const Source = union(SourceType) { const Source = union(SourceType) {
@ -1662,6 +1704,7 @@ const Source = union(SourceType) {
playerInventory: u32, playerInventory: u32,
sharedTestingInventory: void, sharedTestingInventory: void,
hand: u32, hand: u32,
recipe: *const main.items.Recipe,
other: void, other: void,
}; };

View File

@ -80,9 +80,6 @@ fn findAvailableRecipes(list: *VerticalList) bool {
_ = availableItems.swapRemove(i); _ = availableItems.swapRemove(i);
} }
} }
for(inventories.items) |inv| {
inv.deinit(main.globalAllocator);
}
inventories.clearRetainingCapacity(); inventories.clearRetainingCapacity();
// Find all recipes the player can make: // Find all recipes the player can make:
outer: for(items.recipes()) |*recipe| { outer: for(items.recipes()) |*recipe| {
@ -95,7 +92,10 @@ fn findAvailableRecipes(list: *VerticalList) bool {
continue :outer; // Ingredient not found. continue :outer; // Ingredient not found.
} }
// All ingredients found: Add it to the list. // All ingredients found: Add it to the list.
const inv = Inventory.init(main.globalAllocator, recipe.sourceItems.len + 1, .crafting, .other); if(recipe.cachedInventory == null) {
recipe.cachedInventory = Inventory.init(main.globalAllocator, recipe.sourceItems.len + 1, .crafting, .{.recipe = recipe});
}
const inv = recipe.cachedInventory.?;
inventories.append(inv); inventories.append(inv);
const rowList = HorizontalList.init(); const rowList = HorizontalList.init();
const maxColumns: u32 = 4; const maxColumns: u32 = 4;
@ -107,14 +107,12 @@ fn findAvailableRecipes(list: *VerticalList) bool {
if(col < remainder) itemsThisColumn += 1; if(col < remainder) itemsThisColumn += 1;
const columnList = VerticalList.init(.{0, 0}, std.math.inf(f32), 0); const columnList = VerticalList.init(.{0, 0}, std.math.inf(f32), 0);
for(0..itemsThisColumn) |_| { for(0..itemsThisColumn) |_| {
inv.fillAmountFromCreative(i, .{.baseItem = recipe.sourceItems[i]}, recipe.sourceAmounts[i]);
columnList.add(ItemSlot.init(.{0, 0}, inv, i, .immutable, .immutable)); columnList.add(ItemSlot.init(.{0, 0}, inv, i, .immutable, .immutable));
i += 1; i += 1;
} }
columnList.finish(.center); columnList.finish(.center);
rowList.add(columnList); rowList.add(columnList);
} }
inv.fillAmountFromCreative(@intCast(recipe.sourceItems.len), recipe.resultItem.item, recipe.resultItem.amount);
rowList.add(Icon.init(.{8, 0}, .{32, 32}, arrowTexture, false)); rowList.add(Icon.init(.{8, 0}, .{32, 32}, arrowTexture, false));
const itemSlot = ItemSlot.init(.{8, 0}, inv, @intCast(recipe.sourceItems.len), .craftingResult, .takeOnly); const itemSlot = ItemSlot.init(.{8, 0}, inv, @intCast(recipe.sourceItems.len), .craftingResult, .takeOnly);
rowList.add(itemSlot); rowList.add(itemSlot);
@ -156,9 +154,6 @@ pub fn onClose() void {
} }
availableItems.deinit(); availableItems.deinit();
itemAmount.deinit(); itemAmount.deinit();
for(inventories.items) |inv| {
inv.deinit(main.globalAllocator);
}
inventories.deinit(); inventories.deinit();
} }

View File

@ -1184,10 +1184,12 @@ pub const ItemStack = struct { // MARK: ItemStack
} }
}; };
const Recipe = struct { // MARK: Recipe pub const Recipe = struct { // MARK: Recipe
sourceItems: []*BaseItem, sourceItems: []*BaseItem,
sourceAmounts: []u16, sourceAmounts: []u16,
resultItem: ItemStack, resultItem: *BaseItem,
resultAmount: u16,
cachedInventory: ?Inventory = null,
}; };
var arena: main.utils.NeverFailingArenaAllocator = undefined; var arena: main.utils.NeverFailingArenaAllocator = undefined;
@ -1244,7 +1246,8 @@ fn parseRecipe(zon: ZonElement) !Recipe {
const recipe = Recipe { const recipe = Recipe {
.sourceItems = arena.allocator().alloc(*BaseItem, inputs.len), .sourceItems = arena.allocator().alloc(*BaseItem, inputs.len),
.sourceAmounts = arena.allocator().alloc(u16, inputs.len), .sourceAmounts = arena.allocator().alloc(u16, inputs.len),
.resultItem = output, .resultItem = output.item.?.baseItem,
.resultAmount = output.amount,
}; };
errdefer { errdefer {
arena.allocator().free(recipe.sourceAmounts); arena.allocator().free(recipe.sourceAmounts);
@ -1267,6 +1270,11 @@ pub fn registerRecipes(zon: ZonElement) void {
pub fn reset() void { pub fn reset() void {
reverseIndices.clearAndFree(); reverseIndices.clearAndFree();
for(recipeList.items) |recipe| {
if(recipe.cachedInventory) |inv| {
inv.deinit(main.globalAllocator);
}
}
recipeList.clearAndFree(); recipeList.clearAndFree();
itemListSize = 0; itemListSize = 0;
_ = arena.reset(.free_all); _ = arena.reset(.free_all);
@ -1274,6 +1282,11 @@ pub fn reset() void {
pub fn deinit() void { pub fn deinit() void {
reverseIndices.clearAndFree(); reverseIndices.clearAndFree();
for(recipeList.items) |recipe| {
if(recipe.cachedInventory) |inv| {
inv.deinit(main.globalAllocator);
}
}
recipeList.clearAndFree(); recipeList.clearAndFree();
arena.deinit(); arena.deinit();
Inventory.Sync.ClientSide.deinit(); Inventory.Sync.ClientSide.deinit();