Skip to content

Conversation

@TarLaboratories
Copy link
Contributor

@TarLaboratories TarLaboratories commented Sep 7, 2025

What

Spoilage yay!
Added API to modify item outputs and read item inputs in recipe modifiers.
Also made an event that fires just before a machine is registered to allow modifying it's parameters from addons (most notably add recipe modifiers)

Implementation Details

Stores the creation tick of a spoilable item in its nbt, thus bypassing the need to actively tick it.
Should be noted that the item has to be updated at least once after it finished crafting for it to start spoiling (done by updating items inserted into CustomItemStackHandler)
Basically everything is done via a mixin into ItemStack lols
Doesn't actually make any items spoilable, only adds the functionality to do so.

Outcome

GLEBA

@TarLaboratories TarLaboratories requested a review from a team as a code owner September 7, 2025 11:37
@TarLaboratories TarLaboratories added type: feature New feature or request bundled for a 0.X.0 Update 1.20.1 Release: Major - 0.X.0 Releases focused on Content, changes to gameplay; While maintaining mostly API stability. labels Sep 7, 2025
@github-actions github-actions bot added the Tests: Failed Game Tests have failed on this PR label Sep 7, 2025
@github-actions github-actions bot added Tests: Passed Game Tests have passed on this PR and removed Tests: Failed Game Tests have failed on this PR labels Sep 7, 2025
Copy link
Contributor

@jurrejelle jurrejelle left a comment

Choose a reason for hiding this comment

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

Very quick review before I have to go do other things, these are my initial thoughts

Comment on lines 137 to 154
@Inject(at = @At("HEAD"), method = "isSameItemSameTags", cancellable = true)
private static void mergeSpoilables(ItemStack stack, ItemStack other, CallbackInfoReturnable<Boolean> cir) {
CompoundTag tag1 = stack.getTagElement("GTCEu_spoilable");
CompoundTag tag2 = other.getTagElement("GTCEu_spoilable");
boolean isSameItem = ItemStack.isSameItem(stack, other) && stack.areCapsCompatible(other);
if (tag1 != null && tag2 != null) {
long tick1 = tag1.getLong("creation_tick");
long tick2 = tag2.getLong("creation_tick");
if (tick1 != tick2) {
long avg = (tick1 * stack.getCount() + tick2 * other.getCount()) /
(stack.getCount() + other.getCount());
tag1.putLong("creation_tick", avg);
tag2.putLong("creation_tick", avg);
}
}
cir.setReturnValue(isSameItem && Objects.equals(stack.getTag(), other.getTag()));
cir.cancel();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this done in isSameItemSameTags? Now, when we e.g. compare two itemstacks for recipe checking but the recipe can't run, it will still assign the average spoil value

Copy link
Contributor

Choose a reason for hiding this comment

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

To be fair, it's very difficult to know when to merge spoilability status and items don't really have a "merge" function. I'm not sure how we would implement this

@jurrejelle
Copy link
Contributor

Some thoughts I had laid out in the discord, ramble style, that I'd like to document here:

  1. The thing with isItemStackEqual being used as a "merge" check when there might be a case that we're just checking without merging, although I have no clue how to solve that, any ideas welcome
  2. The fact that we have to deeply modify GTRecipe and NotifiableItemStackHandler means our API isn't flexible enough to do these things without. Ideally, when implementing content features, we setup API and then use said API instead of messing with internals. Probably this can be done as a RecipeModifier and then we add API to attach to existing machines? then use a new or existing forge event to let users modify all machines to attach the new RecipeModifier? More input needed. But we definitely should do / be able to do this with public API, otherwise we should improve the API until we can.
  3. The fact that when things spoil, the item just gets turned in the spot. Gameplay wise you'd hope it'd get put somewhere else like in the output bus, or at least it's possible to extract them (which is impossible with input busses since the items are inaccessable)
  4. I'm a little worried at the overhead of checking/updating the state at each of the following: method = { "getItem", "getCount", "getTag", "getOrCreateTag", "getTagElement", "getOrCreateTagElement","getItemHolder" }. Maybe we can profile that or have others look at the method when reviewing
  5. (tiny thought) Injecting into ItemStack this heavily might break compatibility with other mods, not 100% sure, I'm not super well known on mixins
  6. With how many internal methods this touches I would not be comfortable having this in this release cycle without it being in testing / snapshot for a while, although that might be mitigated with the "testing release" that we're planning. More input needed by others too

@jurrejelle jurrejelle added Release: API - X.0.0 Major Breaking Refactors that MUST be in a API-Breaking Release and removed Release: Major - 0.X.0 Releases focused on Content, changes to gameplay; While maintaining mostly API stability. labels Sep 8, 2025
@github-actions github-actions bot added Tests: Failed Game Tests have failed on this PR and removed Tests: Passed Game Tests have passed on this PR labels Nov 17, 2025
@github-actions github-actions bot added Tests: Passed Game Tests have passed on this PR and removed Tests: Failed Game Tests have failed on this PR labels Nov 17, 2025
@TarLaboratories TarLaboratories changed the base branch from 1.20.1 to 1.20.1-v8.0.0 December 13, 2025 05:42
@github-actions github-actions bot added Tests: Failed Game Tests have failed on this PR and removed Tests: Passed Game Tests have passed on this PR labels Dec 30, 2025
Copy link
Contributor

@jurrejelle jurrejelle left a comment

Choose a reason for hiding this comment

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

Some initial questions/notes. Maybe also make an v8.0.0 change docs where you mention you added spoilage and link here, see gustavo's sync PR for how to do that.

Comment on lines +25 to +27
- A block and an item handler (contains `Level`, `BlockPos`, `IItemHandler` and an `int` slot, that may be `-1`)
- An entity, usually a player (contains `Entity` and an `int` slot, that may be `-1`)

Copy link
Contributor

Choose a reason for hiding this comment

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

What does the int slot represent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The slot of the item in the IItemHandler

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The slot of the item in the IItemHandler

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe consider adding that to the docs then

- `.result(Function<ItemStack, ItemStack>)`
used to specify the resulting stack that may depend on the original stack
- `.result(EntityType<? extends Mob>)`
used to specify the mob into which the item will spoil (it can still spoil into an item as well, but it has to be specified first)
Copy link
Contributor

Choose a reason for hiding this comment

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

What is "it" here? Does the ItemStack have to be specified first, or the EntityType?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The stack has to be specified first, ig i should make that more clear

Comment on lines +90 to +93
ItemStack carried = gui.getModularUIContainer().getCarried().copy();
ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(carried);
if (spoilable != null) spoilable.freezeSpoiling();
slotReference.set(carried);
Copy link
Contributor

Choose a reason for hiding this comment

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

Will need to eventually do stuff like this in MUI2 in GTM too when that's implemented

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep

Comment on lines +469 to +477
public MachineBuilder<DEFINITION> addRecipeModifier(RecipeModifier recipeModifier) {
if (this.recipeModifier instanceof RecipeModifierList list) {
this.recipeModifier = new RecipeModifierList(ArrayUtils.add(list.getModifiers(), recipeModifier));
} else {
this.recipeModifier = new RecipeModifierList(this.recipeModifier, recipeModifier);
}
return this;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a similar method in MultiblockMachineBuilder since the return type of that is different

import javax.annotation.Nullable;

@Mixin(ItemStack.class)
public abstract class ItemStackMixin implements ISpoilableItemStackExtension {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not really an actionable point, but having 13 mixins into ItemStack for spoilage is making my skin crawl and I do not look forward to all the conflicts and problems this will cause with minecraft let alone other mods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well it shouldn't cause any conflicts with anything really, since all it does is call the upsate function

Comment on lines +72 to +76
ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
if (spoilable != null && spoilable.shouldSpoil() && spoilableCount > 0) {
double spoiled = spoilProgress / spoilableCount;
spoilable.setTicksUntilSpoiled((long) (spoiled * spoilable.getSpoilTicks()));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Early return if stack doesn't have a spoilable higher up in the function maybe?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also what happens if you start a recipe with spoiled inputs + outputs, shut down the world, start it up again? Does it just make a full freshness item?

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

Labels

1.20.1 Release: API - X.0.0 Major Breaking Refactors that MUST be in a API-Breaking Release Tests: Failed Game Tests have failed on this PR type: feature New feature or request bundled for a 0.X.0 Update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants