Skip to content

Conversation

@Wunka
Copy link
Contributor

@Wunka Wunka commented Jan 19, 2026

Extracted from: #2500 (comment)

Fixes: #2497

  • fix the check for deserialize (As already mentioned in DepositToAny slice for the destination #2469 (comment) I am not that knowledgable on the reader code so if anybody has a better idea here, it would be very welcome)
  • find out if any mechanic was removed (what scenario made Drop call tryCrafting?)

This PR also replaces #2483 as its features are also done here.
CraftFrom accepts as source a slice to prepare for #2324 or/and making the user able to craft from items in a chest

@Argmaster
Copy link
Collaborator

Ok, so I am getting a bit confused by the description. What exactly is getting client sided here? Recipe resolution right? Not whole item transformation?
I admit my knowledge is a little bit outdated due to recent refactors I didn't have time to review, so please forgive my stupid questions.

@Wunka
Copy link
Contributor Author

Wunka commented Jan 20, 2026

Ok, so I am getting a bit confused by the description. What exactly is getting client sided here? Recipe resolution right? Not whole item transformation? I admit my knowledge is a little bit outdated due to recent refactors I didn't have time to review, so please forgive my stupid questions.

For reference see: #2500 . There Quantum introduced the ClientInventory this can either be synced with a Normal inventory on the server or can be completly be handeld on client side. This PR introduces the CraftFrom sync so that the server doesn't have to have access to the crafting inventories (which are displayed in the crafting menu). Which makes it possible to make them also only client side as is the creative menu through #2500

@Argmaster
Copy link
Collaborator

Aight, since you are clearly keeping up with the topic, could you please answer for sake of documentation: Can the client invoke arbitrary item transformation that was not defined by server side recipe table? Will the server reject such transformation?

src/sync.zig Outdated
Comment on lines 1075 to 1089
// check if recipe is valid
const found = blk: {
outer: for(main.items.recipes()) |*recipe| {
if(recipe.resultItem != resultStack.item.baseItem) continue;
if(recipe.resultAmount != resultStack.amount) continue;
if(recipe.sourceItems.len != sourceStacksSize) continue;
for(recipe.sourceItems, recipe.sourceAmounts, sourceStacks) |recipeItem, recipeAmount, sourceStack| {
if(recipeItem != sourceStack.item.baseItem) continue :outer;
if(recipeAmount != sourceStack.amount) continue :outer;
}
break :blk true;
}
break :blk false;
};
if(!found) return error.Invalid;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks if the recipe actually exist, but I have not yet tested if there is a flaw in my logic here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I am just trying to understand the intentions

src/sync.zig Outdated
}

fn deserialize(reader: *BinaryReader, side: Side, user: ?*main.server.User) !CraftFrom {
const destinationsSize = try reader.readVarInt(usize);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, varInt. (This comment mainly to addressed @IntegratedQuantum) How often is this message send so this varInt makes sense here? It's saving 6? 7? Bytes. Out of how large impact over eg. Hour of average gameplay? Anyway, my skepticism aside, shouldn't we make the enums varInts too? Since we care about those few bytes, I am almost sure we are wasting double that when all the entries are summed up.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw this is destinationCount (possibly with plural destinations), size has to strong size-in-bytes connotations in this context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, let me point you to the contributing guidelines, using varint is non-pessimization. Of course in this case it makes no difference, but if we save a few bytes in each packet, then suddenly we are looking at different dimensions.
And there is no reason not to use varint, it's just 3 characters more to write.

You are also correct about the enums here, since they (and all non-exhaustive enums) are indices, it would make sense to serialize them as varints as well, and this might actually make a meaningful enough difference for item drop/entitiy/projectile ids. So maybe we should think about automatically doing this in the background.

src/sync.zig Outdated
for(self.sources) |source| if(source.type != .normal) return;

// Can we even craft it?
outer: for(self.sourceStacks) |requiredStack| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect we have to wait with improvements for next PR?
This cries for a helper function. Abstraction level in here is too low and the function is too long overall.
This is clearly a separate function that would be better off with lower indentation level and a proper name, rather than "outer" label.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ye without a proper abstraction level adjustment I am not reviewing that, but I know a person who will.

src/sync.zig Outdated
const sourceStacksSize = try reader.readVarInt(usize);
if(sourceStacksSize == 0) return error.Invalid;
std.log.info("sourceStacksSize: {d}, sourceStacksSize*@sizeOf(ItemStack): {d}, remaining: {d}\n", .{sourceStacksSize, sourceStacksSize*@sizeOf(ItemStack), reader.remaining.len});
// if(sourceStacksSize*@sizeOf(ItemStack) > reader.remaining.len) return error.Invalid; // this check sadly fails
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm >=?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When writting the code first I changed to > because this is the last read so if its maybe = then this would still be ok.

Copy link
Collaborator

@Argmaster Argmaster Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway my suggestion is to read until it fails and hard cap max count we can process, as described In nearby comment.

src/sync.zig Outdated
}
}

fn deserialize(reader: *BinaryReader, side: Side, user: ?*main.server.User) !CraftFrom {
Copy link
Collaborator

@Argmaster Argmaster Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could unit test this fairly easily. We would have to be naught and add mock entry to Side enums that result in getInventory being noop and reserve a sentinel InventoryId, but we could get serialization edge case coverage from that. Alternatively we can just implement declarative serialization C: nvm we can't easily make a dymmy CraftFrom object.

src/sync.zig Outdated
fn deserialize(reader: *BinaryReader, side: Side, user: ?*main.server.User) !CraftFrom {
const destinationsSize = try reader.readVarInt(usize);
if(destinationsSize == 0) return error.Invalid;
if(destinationsSize*@sizeOf(InventoryId) >= reader.remaining.len) return error.Invalid;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to check this ahead of time? We can just for loop for destinationCount times and try reading. Reader should report error when it runs out of buffer data I think? We then catch it and possibly transform to Invalid or whatever.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, then I guess it's bad I missed that one. Suggestion still remains valid IMO, unless proven otherwise.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tho I admit that IMO a best solution would be an automagical serialization for structs, but Quantum disagrees.

src/sync.zig Outdated
sourceStack.* = try ItemStack.fromBytes(reader);
}

// check if recipe is valid
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very pretty comment. Now make it a function name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

@Wunka Wunka mentioned this pull request Jan 21, 2026
@bagggage bagggage moved this to In review in PRs to review Jan 23, 2026
IntegratedQuantum pushed a commit that referenced this pull request Jan 24, 2026
This is extracted from:
#2506 (comment)

Initially this was only supposed to be a helper function, but as I
wanted to improve the abstraction it grew into a struct to avoid
namespace pollution.

This will also be helpful if #2506 is not accepted, as I have at least
one other slice refactor that currently copies this code (#2494), and we
may extend additional functions to slices or make more operations use
the same priority system.
@Wunka
Copy link
Contributor Author

Wunka commented Jan 24, 2026

  • I have updated this to use the put_items_into.
  • I also looked what scenario made Drop call tryCraftingTo and I didn't find any scenario, so the remove seems to not remove any mechanic
  • I see for now no conclusion to the deserialize problem, so I didn't change anything there.
  • In my opinion the run function from CraftFrom has now engough abstraction but if there are different opinions I can move more code to helper functions (There is also this comment: PutItemsInto Helper #2515 (comment) so they may be changes to this order in the future in a follow up PR)

@Wunka
Copy link
Contributor Author

Wunka commented Jan 25, 2026

this is now updated to use #2524.
I also want to mention that I had an idea. The last read where I get the problems for the check is about the recipe. But we know all recipes, so should I maybe just add a variable to recipe.zig for maxRecipeSourceCount wich while loading the recipes tracks the max amount then I can just compare it to the user given source amount. I think it probably needs a better solution in the future (at least for the inventory fromBytes) but that would at least solve for now the problem that I have no check there.

@IntegratedQuantum
Copy link
Member

Hmm, I'm not sure. I think for now it would be better to have one clear pattern: Check if the reader has enough data left before allocating.

Copy link
Member

@IntegratedQuantum IntegratedQuantum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add the check to fromBytes please


// Can we even craft it?
for (self.recipe.sourceItems, self.recipe.sourceAmounts) |requiredItem, requiredAmount| {
self.sources.removeItems(ctx, requiredAmount, requiredItem);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please first check if we have the needed ingredients, before removing anything (you accidentally removed that code)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readded. It seems I removed it when introduced the remove_items namespace...

Wunka and others added 3 commits January 26, 2026 18:07
Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
@Wunka
Copy link
Contributor Author

Wunka commented Jan 26, 2026

So now we go back to the problem I described at the beginning of the PR. The check for recipe fromBytes fails. So if you would run this currently the game crashes when crafting something.

src/items.zig Outdated
const resultAmount = try reader.readVarInt(u16);
const sourceCount = try reader.readVarInt(usize);
if (sourceCount == 0) return error.Invalid;
if (sourceCount*@sizeOf(Recipe) > reader.remaining.len) return error.Invalid; // this check fails
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be @sizeOf(u16) + @sizeOf(BaseItemIndex), but even that doesn't account for the compression of the varint.

So instead, I'd suggest to just use a pair of Lists, that are maybe preallocated to @min(256, sourceCount).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood correctly what you wanted. Its now there. (but please no merge It seems I removed a check so now if you click a sourceitem the game crashes)

src/sync.zig Outdated
// There might be duplicate entries:
for (self.recipe.sourceItems, self.recipe.sourceAmounts) |otherItem, otherAmount| {
if (std.meta.eql(requiredItem, otherItem))
amount += otherAmount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wunka and others added 2 commits January 27, 2026 17:46
Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
Wunka and others added 2 commits January 27, 2026 18:29
Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
@Wunka
Copy link
Contributor Author

Wunka commented Jan 27, 2026

check is revived.

src/gui/gui.zig Outdated

if (itemSlot.inventory.type == .crafting and itemSlot.mode == .takeOnly and mainGuiButton.pressed and (recipeItem != .null or itemSlot.pressed)) {
if (itemSlot.inventory.type == .crafting and mainGuiButton.pressed and (recipeItem != .null or itemSlot.pressed)) {
if (itemSlot.mode != .takeOnly) return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this change?

Copy link
Contributor Author

@Wunka Wunka Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actual thought this was also a case where I needed to add the check again. But yeah... it also works without this change so I reverted that.

@IntegratedQuantum
Copy link
Member

I'm also still wondering about the other change from that "check is revived" part, why did it need to change?

@Wunka
Copy link
Contributor Author

Wunka commented Jan 28, 2026

I'm also still wondering about the other change from that "check is revived" part, why did it need to change?

When you click the left "source" inventories of the recipe menu you are in a recipe inventory but its not takeOnly. so we trigger a depositOrSwap with a non sharedInventory which triggers the assert. Before in depositOrSwap tryCrafting would be called.

Copy link
Member

@IntegratedQuantum IntegratedQuantum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few more things

src/items.zig Outdated
const resultItem = try reader.readEnum(BaseItemIndex);
const resultAmount = try reader.readVarInt(u16);
const sourceCount = try reader.readVarInt(usize);
if (sourceCount == 0) return error.Invalid;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't dismiss this so quickly. The rest of the engine allows it, and there is no reason not to allow addon creators to define recipes like this. This might in fact make more sense once tool durability and special crafting stations are added to the mix.

src/items.zig Outdated
var sourceAmounts: main.List(u16) = .initCapacity(main.stackAllocator, @min(256, sourceCount));
defer sourceAmounts.deinit();

while (reader.remaining.len > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add most add sourceCount items. Otherwise we may cause problems if there is other stuff afterwards that expects to read things from the same reader.

Wunka and others added 2 commits January 28, 2026 19:29
Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
Copy link
Member

@IntegratedQuantum IntegratedQuantum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this, I think it's a great improvement both in terms of readability and feature consistency.
If you are up to it, the next step in that direction would be #2529, it would be amazing if we could get rid of all the edge cases that came with the server-shared inventory type.

@IntegratedQuantum IntegratedQuantum merged commit 146ed64 into PixelGuys:master Jan 28, 2026
1 check passed
@Wunka
Copy link
Contributor Author

Wunka commented Jan 28, 2026

If you are up to it, the next step in that direction would be #2529

maybe. I will have to read into the tool logic a little bit before to understand whats exactly going on there.
If I take it up I will again start first with the proposal for the structure of the Sync Operation like I did for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Inventory crafting should first take items from partial stacks instead of always targettting the first stack

3 participants