func) {
+ actualOutputModifier = actualOutputModifier.andThen(func);
+ return this;
+ }
+
/**
* Builds the ModifierFunction from this builder.
*
@@ -183,7 +190,7 @@ public ModifierFunction build() {
new HashMap<>(recipe.inputChanceLogics), new HashMap<>(recipe.outputChanceLogics),
new HashMap<>(recipe.tickInputChanceLogics), new HashMap<>(recipe.tickOutputChanceLogics),
newConditions, new ArrayList<>(recipe.ingredientActions),
- recipe.data, recipe.duration, recipe.recipeCategory, recipe.groupColor);
+ recipe.data, recipe.duration, recipe.recipeCategory, recipe.groupColor, recipe.spoilageData);
copied.parallels = recipe.parallels * parallels;
copied.subtickParallels = recipe.subtickParallels * subtickParallels;
copied.ocLevel = recipe.ocLevel + addOCs;
@@ -198,6 +205,7 @@ public ModifierFunction build() {
EnergyStack eut = EURecipeCapability.CAP.copyWithModifier(preEUt.stack(), eutModifier);
EURecipeCapability.putEUContent(preEUt.isInput() ? copied.tickInputs : copied.tickOutputs, eut);
}
+ copied.outputModifier = actualOutputModifier;
return copied;
};
}
diff --git a/src/main/java/com/gregtechceu/gtceu/api/registry/registrate/MachineBuilder.java b/src/main/java/com/gregtechceu/gtceu/api/registry/registrate/MachineBuilder.java
index c1b59704aa3..214d343d273 100644
--- a/src/main/java/com/gregtechceu/gtceu/api/registry/registrate/MachineBuilder.java
+++ b/src/main/java/com/gregtechceu/gtceu/api/registry/registrate/MachineBuilder.java
@@ -6,6 +6,7 @@
import com.gregtechceu.gtceu.api.blockentity.BlockEntityCreationInfo;
import com.gregtechceu.gtceu.api.capability.recipe.RecipeCapability;
import com.gregtechceu.gtceu.api.data.RotationState;
+import com.gregtechceu.gtceu.api.events.ModifyMachineEvent;
import com.gregtechceu.gtceu.api.gui.editor.EditableMachineUI;
import com.gregtechceu.gtceu.api.item.MetaMachineItem;
import com.gregtechceu.gtceu.api.machine.MachineDefinition;
@@ -27,6 +28,8 @@
import com.gregtechceu.gtceu.common.data.models.GTMachineModels;
import com.gregtechceu.gtceu.config.ConfigHolder;
import com.gregtechceu.gtceu.data.model.builder.MachineModelBuilder;
+import com.gregtechceu.gtceu.integration.kjs.GTCEuStartupEvents;
+import com.gregtechceu.gtceu.integration.kjs.events.ModifyMachineEventJS;
import com.gregtechceu.gtceu.utils.data.RuntimeBlockstateProvider;
import net.minecraft.MethodsReturnNonnullByDefault;
@@ -46,6 +49,7 @@
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraftforge.client.model.generators.BlockModelBuilder;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import com.tterrag.registrate.AbstractRegistrate;
import com.tterrag.registrate.builders.BlockBuilder;
@@ -572,6 +576,15 @@ public TYPE recipeModifier(RecipeModifier recipeModifier) {
return getThis();
}
+ public TYPE 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 getThis();
+ }
+
public TYPE recipeModifier(RecipeModifier recipeModifier, boolean alwaysTryModifyRecipe) {
this.alwaysTryModifyRecipe = alwaysTryModifyRecipe;
return this.recipeModifier(recipeModifier);
@@ -641,6 +654,11 @@ protected void setupStateDefinition(MachineDefinition definition) {
@HideFromJS
public DEFINITION register() {
+ ModifyMachineEvent event = new ModifyMachineEvent(this);
+ FMLJavaModLoadingContext.get().getModEventBus().post(event);
+ if (GTCEu.Mods.isKubeJSLoaded()) {
+ KJSCallWrapper.fireKJSEvent(event);
+ }
this.registrate.object(name);
var definition = createDefinition();
@@ -813,5 +831,9 @@ public static void generateAssetJsons(@Nullable As
generator.itemModel(id, gen -> gen.parent(id.withPrefix("block/machine/").toString()));
}
}
+
+ public static void fireKJSEvent(ModifyMachineEvent event) {
+ GTCEuStartupEvents.MACHINE_MODIFICATION.post(new ModifyMachineEventJS(event));
+ }
}
}
diff --git a/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java b/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java
index 68b203df551..9eb06547162 100644
--- a/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java
+++ b/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java
@@ -16,6 +16,7 @@
import com.gregtechceu.gtceu.api.data.worldgen.WorldGenLayers;
import com.gregtechceu.gtceu.api.data.worldgen.generator.IndicatorGenerators;
import com.gregtechceu.gtceu.api.data.worldgen.generator.VeinGenerators;
+import com.gregtechceu.gtceu.api.events.ModifyMachineEvent;
import com.gregtechceu.gtceu.api.gui.factory.CoverUIFactory;
import com.gregtechceu.gtceu.api.gui.factory.GTUIEditorFactory;
import com.gregtechceu.gtceu.api.gui.factory.MachineUIFactory;
@@ -237,6 +238,11 @@ private static void initMaterials() {
/* End Material Registration */
}
+ @SubscribeEvent
+ public void addSpoilTransferModifier(ModifyMachineEvent event) {
+ event.getBuilder().addRecipeModifier(GTRecipeModifiers.SPOILAGE_TRANSFER);
+ }
+
@SubscribeEvent
public void register(RegisterEvent event) {
if (event.getRegistryKey().equals(BuiltInRegistries.LOOT_FUNCTION_TYPE.key()))
diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java
index cea3cf40f93..3fd62b90b76 100644
--- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java
+++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeModifiers.java
@@ -1,8 +1,12 @@
package com.gregtechceu.gtceu.common.data;
import com.gregtechceu.gtceu.api.GTValues;
+import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper;
import com.gregtechceu.gtceu.api.capability.recipe.EURecipeCapability;
import com.gregtechceu.gtceu.api.data.medicalcondition.MedicalCondition;
+import com.gregtechceu.gtceu.api.item.component.ISpoilableItem;
+import com.gregtechceu.gtceu.api.item.component.SpoilContext;
+import com.gregtechceu.gtceu.api.item.component.SpoilUtils;
import com.gregtechceu.gtceu.api.machine.MetaMachine;
import com.gregtechceu.gtceu.api.machine.feature.IOverclockMachine;
import com.gregtechceu.gtceu.api.machine.multiblock.CoilWorkableElectricMultiblockMachine;
@@ -22,6 +26,8 @@
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.crafting.Ingredient;
import org.jetbrains.annotations.NotNull;
@@ -50,6 +56,31 @@ public class GTRecipeModifiers {
public static final RecipeModifier OC_NON_PERFECT = ELECTRIC_OVERCLOCK.apply(NON_PERFECT_OVERCLOCK);
public static final RecipeModifier OC_PERFECT_SUBTICK = ELECTRIC_OVERCLOCK.apply(PERFECT_OVERCLOCK_SUBTICK);
public static final RecipeModifier OC_NON_PERFECT_SUBTICK = ELECTRIC_OVERCLOCK.apply(NON_PERFECT_OVERCLOCK_SUBTICK);
+ public static final RecipeModifier SPOILAGE_TRANSFER = (machine, recipe) -> ModifierFunction.builder()
+ .modifyItemOutputs((r, stackObject) -> {
+ if (!(stackObject instanceof ItemStack stack)) return;
+ ISpoilableItem outputSpoilable = GTCapabilityHelper.getSpoilable(stack);
+ if (outputSpoilable == null) return;
+ SpoilUtils.update(stack, new SpoilContext(machine));
+ if (!r.spoilageData.keepSpoilingProgress()) return;
+ double spoilProgress = 0;
+ int spoilableCount = 0;
+ for (Object inObject : r.spoilageData.getConsumedInputs(GTRecipeCapabilities.ITEM)) {
+ if (!(inObject instanceof Ingredient ingredient)) continue;
+ if (ingredient.getItems().length == 0) continue;
+ ItemStack in = ingredient.getItems()[0];
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(in);
+ if (spoilable != null && spoilable.shouldSpoil()) {
+ spoilableCount += in.getCount();
+ spoilProgress += in.getCount() * (double) spoilable.getTicksUntilSpoiled() /
+ spoilable.getSpoilTicks();
+ }
+ }
+ if (outputSpoilable.shouldSpoil() && spoilableCount > 0) {
+ double spoiled = spoilProgress / spoilableCount;
+ outputSpoilable.setTicksUntilSpoiled((long) (spoiled * outputSpoilable.getSpoilTicks()));
+ }
+ }).build();
public static final BiFunction ENVIRONMENT_REQUIREMENT = Util
.memoize((condition, maxAllowedStrength) -> (machine, recipe) -> {
diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/SpoilableItemStack.java b/src/main/java/com/gregtechceu/gtceu/common/item/SpoilableItemStack.java
new file mode 100644
index 00000000000..4ab49c4fbb7
--- /dev/null
+++ b/src/main/java/com/gregtechceu/gtceu/common/item/SpoilableItemStack.java
@@ -0,0 +1,302 @@
+package com.gregtechceu.gtceu.common.item;
+
+import com.gregtechceu.gtceu.GTCEu;
+import com.gregtechceu.gtceu.api.capability.GTCapability;
+import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper;
+import com.gregtechceu.gtceu.api.item.IMergeableNBTSerializable;
+import com.gregtechceu.gtceu.api.item.ISpoilableItemStackExtension;
+import com.gregtechceu.gtceu.api.item.component.*;
+import com.gregtechceu.gtceu.common.item.behavior.SpoilableBehavior;
+import com.gregtechceu.gtceu.utils.FormattingUtil;
+
+import net.minecraft.ChatFormatting;
+import net.minecraft.core.Direction;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.util.FastColor;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.TooltipFlag;
+import net.minecraft.world.level.ItemLike;
+import net.minecraft.world.level.Level;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.ICapabilityProvider;
+import net.minecraftforge.common.util.INBTSerializable;
+import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.event.AttachCapabilitiesEvent;
+
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * This class is a basic implementation of the {@link ISpoilableItem} capability,
+ * to be attached to an item in an {@link AttachCapabilitiesEvent} listener.
+ * It leaves some methods unimplemented, such as {@link ISpoilableItem#getSpoilTicks()} and
+ * {@link ISpoilableItem#spoilResult(SpoilContext, boolean)}.
+ *
+ * @implNote this class uses a mixin in its {@link ISpoilableItem#updateFreshness} implementation
+ *
+ * @see SpoilableBehavior
+ * @see SpoilableBehavior#attachTo(ItemLike)
+ */
+public abstract class SpoilableItemStack implements ISpoilableItem, IAddInformation, IDurabilityBar,
+ IMergeableNBTSerializable, ICapabilityProvider {
+
+ public static final String SPOIL_CONTEXT_KEY = "spoilContext";
+ public static final String FROZEN_TICKS_KEY = "frozenRemainingTicks";
+ public static final String CREATION_TICK_KEY = "creationTick";
+ /**
+ * Consider frozen and non-frozen spoilables equal. This is done to allow filtering by ticks remaining until
+ * spoiled.
+ * If you want the player to have frozen stacks in their inventory, set this to {@code false} to prevent players
+ * from
+ * entirely bypassing the spoilage system.
+ */
+ public static boolean FROZEN_EQUALITY = true;
+ @Getter
+ private final ItemStack stack;
+
+ private final Item originalItem;
+
+ private boolean initialized;
+ @Getter
+ private boolean frozen = false;
+ private long frozenTicks;
+ @Getter
+ private long creationTick = 0;
+ @Getter
+ @Setter
+ private SpoilContext spoilContext = new SpoilContext();
+
+ public SpoilableItemStack(ItemStack stack) {
+ this.stack = stack;
+ this.originalItem = stack.getItem();
+ }
+
+ public void setCreationTick(long creationTick) {
+ this.initialized = true;
+ this.creationTick = creationTick;
+ }
+
+ /**
+ * Checks if this item should've already spoiled, and calls
+ * {@link ISpoilableItem#spoilResult(SpoilContext, boolean)}
+ * with {@link SpoilableItemStack#getSpoilContext()}
+ * and replaces this item with its return value if so.
+ * Also sets the {@link SpoilContext} stored in the mixin to the provided
+ * context if it is non-empty (determined by {@link SpoilContext#isEmpty()}).
+ *
+ * If {@code createTag = true} and the spoilage tag did not exist, creates
+ * the tag and sets the creation tick to this tick.
+ * If {@code createTag = false} and the spoilage tag isn't present, does nothing.
+ *
+ * @param createTag Whether to create a spoilage tag if it wasn't present.
+ * Usually {@code true} for stacks that are present in-world,
+ * and {@code false} for stacks in XEI, icons, quests, etc.
+ *
+ * @implNote This method is injected into all of {@link ItemStack}'s getters to
+ * be called with an empty {@link SpoilContext} and {@code createTag = false}.
+ */
+ public void updateFreshness(SpoilContext spoilContext, boolean createTag) {
+ if (!stack.is(originalItem)) return;
+ if (!createTag && !initialized || frozen) return;
+ if (!spoilContext.isEmpty()) setSpoilContext(spoilContext);
+ Level level = SpoilContext.getDefaultLevel();
+ if (level != null && createTag && !initialized) {
+ setCreationTick(level.getGameTime());
+ }
+ if (level != null && this.shouldSpoil()) {
+ long spoilTicks = this.getSpoilTicks();
+ long timeDifference = level.getGameTime() - creationTick - spoilTicks;
+ if (timeDifference >= 0) {
+ ItemStack newStack = this.spoilResult(getSpoilContext(), GTCEu.isClientThread());
+ ((ISpoilableItemStackExtension) (Object) stack).gtceu$setStack(newStack);
+ onItemChanged();
+ ISpoilableItem newSpoilable = GTCapabilityHelper.getSpoilable(stack);
+ if (newSpoilable != null) {
+ newSpoilable.setCreationTick(level.getGameTime() - timeDifference);
+ try {
+ newSpoilable.updateFreshness(spoilContext, false);
+ } catch (StackOverflowError ignored) {
+ // if items spoil in a giant chain or a loop
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public long getTicksUntilSpoiled() {
+ updateFreshness(new SpoilContext(), false);
+ if (!initialized) return this.getSpoilTicks();
+ if (frozen) return frozenTicks;
+ Level level = SpoilContext.getDefaultLevel();
+ if (level != null) {
+ return this.getSpoilTicks() - level.getGameTime() +
+ this.getCreationTick();
+ }
+ return this.getSpoilTicks();
+ }
+
+ @Override
+ public void setTicksUntilSpoiled(long value) {
+ updateFreshness(new SpoilContext(), false);
+ Level level = SpoilContext.getDefaultLevel();
+ if (level != null && initialized)
+ setCreationTick(level.getGameTime() - this.getSpoilTicks() + value);
+ }
+
+ private void setFreezeSpoiling(boolean freeze) {
+ if (freeze) {
+ updateFreshness(new SpoilContext(), true);
+ frozenTicks = getTicksUntilSpoiled();
+ frozen = true;
+ } else {
+ if (initialized && frozen) {
+ setTicksUntilSpoiled(frozenTicks);
+ frozen = false;
+ }
+ }
+ }
+
+ @Override
+ public void freezeSpoiling() {
+ setFreezeSpoiling(true);
+ }
+
+ @Override
+ public void unfreezeSpoiling() {
+ setFreezeSpoiling(false);
+ }
+
+ @Override
+ public boolean shouldSpoil() {
+ return true;
+ }
+
+ @Override
+ public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltipComponents,
+ TooltipFlag isAdvanced) {
+ tooltipComponents.add(Component.translatable(
+ "gtceu.tooltip.spoil_time_remaining",
+ Component.literal(FormattingUtil.formatTime(getTicksUntilSpoiled()))
+ .withStyle(ChatFormatting.DARK_AQUA)));
+ tooltipComponents.add(Component.translatable(
+ "gtceu.tooltip.spoils_into", getSpoilResultTooltip()));
+ if (isAdvanced.isAdvanced()) {
+ tooltipComponents.add(Component.translatable(
+ "gtceu.tooltip.spoil_time_total",
+ Component.literal(FormattingUtil.formatTime(getSpoilTicks()))
+ .withStyle(ChatFormatting.GREEN)));
+ tooltipComponents.add(Component.translatable(
+ "gtceu.tooltip.creation_tick",
+ getCreationTick()));
+ SpoilContext ctx = getSpoilContext();
+ if (ctx.level() != null && ctx.pos() != null)
+ tooltipComponents.add(Component.translatable("gtceu.tooltip.location",
+ ctx.level().dimensionTypeId().location().toString(),
+ ctx.pos().getX(), ctx.pos().getY(), ctx.pos().getZ()));
+ if (ctx.entity() != null) tooltipComponents.add(Component.translatable("gtceu.tooltip.location_entity",
+ ctx.entity().getType().getDescription()));
+ if (ctx.itemHandlerSource() != null) tooltipComponents
+ .add(Component.translatable("gtceu.tooltip.item_handler_source", ctx.itemHandlerSource()));
+ if (ctx.itemHandlerData() != null)
+ tooltipComponents.add(Component.translatable("gtceu.tooltip.item_handler_data", ctx.itemHandlerData()));
+ if (ctx.slot() != -1)
+ tooltipComponents.add(Component.translatable("gtceu.tooltip.location_slot", ctx.slot()));
+ }
+ }
+
+ protected Component getSpoilResultTooltip() {
+ return spoilResult(new SpoilContext(), false).getDisplayName();
+ }
+
+ @Override
+ public int getBarColor(ItemStack stack) {
+ return FastColor.ARGB32.color(255, 255, 255, 255);
+ }
+
+ @Override
+ public boolean doDamagedStateColors(ItemStack itemStack) {
+ return false;
+ }
+
+ @Override
+ public @Nullable IntIntPair getDurabilityColorsForDisplay(ItemStack itemStack) {
+ return IntIntPair.of(getBarColor(itemStack), getBarColor(itemStack));
+ }
+
+ @Override
+ public float getDurabilityForDisplay(ItemStack stack) {
+ return (float) getTicksUntilSpoiled() / getSpoilTicks();
+ }
+
+ @Override
+ public Tag serializeNBT() {
+ if (!initialized) return new CompoundTag();
+ CompoundTag tag = new CompoundTag();
+ tag.put(SPOIL_CONTEXT_KEY, getSpoilContext().serializeNBT());
+ tag.putLong(CREATION_TICK_KEY, getCreationTick());
+ if (isFrozen())
+ tag.putLong(FROZEN_TICKS_KEY, frozenTicks);
+ return tag;
+ }
+
+ @Override
+ public void deserializeNBT(Tag nbt) {
+ if (nbt instanceof CompoundTag tag && !tag.isEmpty()) {
+ initialized = true;
+ spoilContext = SpoilContext.deserializeNBT(tag.getCompound(SPOIL_CONTEXT_KEY));
+ creationTick = tag.getLong(CREATION_TICK_KEY);
+ if (tag.contains(FROZEN_TICKS_KEY, Tag.TAG_LONG)) {
+ frozenTicks = tag.getLong(FROZEN_TICKS_KEY);
+ frozen = true;
+ } else frozen = false;
+ } else initialized = false;
+ }
+
+ /**
+ * This method averages the spoil progress of the two stacks (or, more
+ * accurately, their {@link ISpoilableItem#getCreationTick()}). If {@link SpoilableItemStack#FROZEN_EQUALITY}
+ * is {@code true}, this method will ignore the frozen/not frozen status of stacks.
+ *
+ * @implNote This implementation may lead to spoil progress averaging in situations other
+ * than stack merging, though I don't think this will lead to any big user-facing bugs.
+ */
+ @Override
+ public void prepareForComparisonWith(INBTSerializable other) {
+ if (other instanceof SpoilableItemStack spoilable) {
+ if (!initialized || !spoilable.initialized) return;
+ if (isFrozen() || spoilable.isFrozen()) {
+ return;
+ }
+ long tick1 = getCreationTick();
+ long tick2 = spoilable.getCreationTick();
+ if (tick1 != tick2) {
+ long average;
+ if (!stack.isEmpty() && !spoilable.stack.isEmpty()) {
+ average = (tick1 * stack.getCount() + tick2 * spoilable.stack.getCount()) /
+ (stack.getCount() + spoilable.stack.getCount());
+ } else average = tick1;
+ this.setCreationTick(average);
+ spoilable.setCreationTick(average);
+ }
+ }
+ }
+
+ @Override
+ public @NotNull LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) {
+ return GTCapability.CAPABILITY_SPOILABLE_ITEM.orEmpty(cap, LazyOptional.of(() -> this));
+ }
+
+ /**
+ * Called when the stack has spoiled and transformed into a different item.
+ */
+ protected void onItemChanged() {}
+}
diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/behavior/SpoilableBehavior.java b/src/main/java/com/gregtechceu/gtceu/common/item/behavior/SpoilableBehavior.java
new file mode 100644
index 00000000000..83b60f00138
--- /dev/null
+++ b/src/main/java/com/gregtechceu/gtceu/common/item/behavior/SpoilableBehavior.java
@@ -0,0 +1,183 @@
+package com.gregtechceu.gtceu.common.item.behavior;
+
+import com.gregtechceu.gtceu.GTCEu;
+import com.gregtechceu.gtceu.api.item.component.SpoilContext;
+import com.gregtechceu.gtceu.api.item.component.SpoilUtils;
+import com.gregtechceu.gtceu.common.item.SpoilableItemStack;
+
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.ItemLike;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.event.AttachCapabilitiesEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class SpoilableBehavior {
+
+ private final Function ticks;
+ private final SpoilResultProvider spoilResult;
+ private final Function spoilsIntoTooltip;
+ private final List- attachedTo = new ArrayList<>();
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private SpoilableBehavior(Function ticks, SpoilResultProvider spoilResult,
+ Function spoilsIntoTooltip) {
+ this.ticks = ticks;
+ this.spoilResult = spoilResult;
+ this.spoilsIntoTooltip = spoilsIntoTooltip;
+ }
+
+ public SpoilableBehavior attachTo(ItemLike item) {
+ if (attachedTo.isEmpty()) {
+ MinecraftForge.EVENT_BUS.register(this);
+ }
+ attachedTo.add(item.asItem());
+ return this;
+ }
+
+ @SubscribeEvent
+ public void attachCapability(AttachCapabilitiesEvent event) {
+ ItemStack stack = event.getObject();
+ if (attachedTo.stream().anyMatch(stack::is)) {
+ event.addCapability(GTCEu.id("spoilable"), new SpoilableBehaviourStack(stack));
+ }
+ }
+
+ public class SpoilableBehaviourStack extends SpoilableItemStack {
+
+ private SpoilableBehaviourStack(ItemStack stack) {
+ super(stack);
+ }
+
+ @Override
+ public long getSpoilTicks() {
+ return ticks.apply(getStack());
+ }
+
+ @Override
+ public ItemStack spoilResult(SpoilContext spoilContext, boolean simulate) {
+ return spoilResult.getSpoilResult(getStack(), spoilContext, simulate);
+ }
+
+ @Override
+ protected Component getSpoilResultTooltip() {
+ return spoilsIntoTooltip.apply(getStack());
+ }
+ }
+
+ @FunctionalInterface
+ public interface SpoilResultProvider {
+
+ ItemStack getSpoilResult(@NotNull ItemStack stack, @NotNull SpoilContext spoilContext, boolean simulate);
+ }
+
+ public static class Builder {
+
+ private Function ticks;
+ private SpoilResultProvider result;
+ private Function tooltip;
+
+ private Builder() {
+ ticks(100);
+ result(ItemStack.EMPTY);
+ }
+
+ public SpoilableBehavior build() {
+ return new SpoilableBehavior(ticks, result, tooltip);
+ }
+
+ public Builder ticks(Function ticks) {
+ this.ticks = ticks;
+ return this;
+ }
+
+ public Builder result(SpoilResultProvider result) {
+ this.result = result;
+ return this;
+ }
+
+ public Builder tooltip(Function tooltip) {
+ this.tooltip = tooltip;
+ return this;
+ }
+
+ public Builder ticks(long ticks) {
+ return ticks(stack -> ticks);
+ }
+
+ public Builder result(ItemLike itemLike) {
+ return result(stack -> itemLike.asItem().getDefaultInstance().copyWithCount(stack.getCount()));
+ }
+
+ public Builder result(ItemStack stack) {
+ return result(stack1 -> stack.copyWithCount(stack1.getCount()));
+ }
+
+ public Builder result(Function result) {
+ return result((stack, spoilContext, simulate) -> result.apply(stack))
+ .tooltip(stack -> {
+ ItemStack resultStack = result.apply(stack);
+ if (resultStack.isEmpty()) return Component.empty();
+ return resultStack.getHoverName();
+ });
+ }
+
+ public Builder result(EntityType extends Mob> entityType) {
+ return result(() -> entityType);
+ }
+
+ public Builder result(Supplier extends EntityType extends Mob>> entityType) {
+ SpoilResultProvider previousResult = result;
+ Function previousTooltip = tooltip;
+ return result((stack, spoilContext, simulate) -> {
+ if (!simulate) {
+ EntityType extends Mob> type = entityType.get();
+ SpoilUtils.spawnEntity(spoilContext, type, stack.getCount());
+ }
+ return previousResult.getSpoilResult(stack, spoilContext, simulate);
+ }).tooltip(stack -> {
+ EntityType extends Mob> type = entityType.get();
+ MutableComponent component = type.getDescription().copy();
+ Component previous = previousTooltip.apply(stack);
+ if (!previous.getString().isEmpty()) component.append(", ").append(previous);
+ return component;
+ });
+ }
+
+ public Builder multiplyResult(int mult) {
+ SpoilResultProvider prevResult = result;
+ Function previousTooltip = tooltip;
+ return result((stack, spoilContext, simulate) -> {
+ ItemStack total = prevResult.getSpoilResult(stack, spoilContext, simulate);
+ for (int i = 1; i < mult; i++) {
+ ItemStack temp = prevResult.getSpoilResult(stack, spoilContext, simulate);
+ if (ItemStack.isSameItemSameTags(total, temp)) total.grow(temp.getCount());
+ }
+ return total;
+ }).tooltip(stack -> {
+ MutableComponent component = Component.literal("(");
+ component.append(previousTooltip.apply(stack));
+ component.append(") x").append(Integer.toString(mult));
+ return component;
+ });
+ }
+
+ public Builder tooltip(Component tooltip) {
+ return tooltip(stack -> tooltip);
+ }
+ }
+}
diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/DistillationTowerMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/DistillationTowerMachine.java
index f783a7d5d99..64449b222d9 100644
--- a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/DistillationTowerMachine.java
+++ b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/DistillationTowerMachine.java
@@ -160,7 +160,7 @@ private static GTRecipe modifyOutputs(GTRecipe recipe, ContentModifier cm) {
recipe.outputChanceLogics,
recipe.tickInputChanceLogics, recipe.tickOutputChanceLogics, recipe.conditions,
recipe.ingredientActions,
- recipe.data, recipe.duration, recipe.recipeCategory, recipe.groupColor);
+ recipe.data, recipe.duration, recipe.recipeCategory, recipe.groupColor, recipe.spoilageData);
}
public static class DistillationTowerLogic extends RecipeLogic {
diff --git a/src/main/java/com/gregtechceu/gtceu/config/ConfigHolder.java b/src/main/java/com/gregtechceu/gtceu/config/ConfigHolder.java
index 3248f0c11be..8f67f2436d9 100644
--- a/src/main/java/com/gregtechceu/gtceu/config/ConfigHolder.java
+++ b/src/main/java/com/gregtechceu/gtceu/config/ConfigHolder.java
@@ -755,6 +755,8 @@ public static class ClientConfigs {
"Disable if experiencing performance issues.", "Default: true" })
public boolean machinesHaveBERsByDefault = true;
@Configurable
+ public boolean aprilFoolsMode = false;
+ @Configurable
@Configurable.Comment({ "Whether or not sounds should be played when using tools outside of crafting.",
"Default: true" })
public boolean toolUseSounds = true;
diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/CapabilityDispatcherAccessor.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/CapabilityDispatcherAccessor.java
new file mode 100644
index 00000000000..ca830d7e97f
--- /dev/null
+++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/CapabilityDispatcherAccessor.java
@@ -0,0 +1,18 @@
+package com.gregtechceu.gtceu.core.mixins;
+
+import net.minecraft.nbt.Tag;
+import net.minecraftforge.common.capabilities.CapabilityDispatcher;
+import net.minecraftforge.common.util.INBTSerializable;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(value = CapabilityDispatcher.class, remap = false)
+public interface CapabilityDispatcherAccessor {
+
+ @Accessor
+ INBTSerializable[] getWriters();
+
+ @Accessor
+ String[] getNames();
+}
diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/CapabilityDispatcherMixin.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/CapabilityDispatcherMixin.java
new file mode 100644
index 00000000000..da2506098f3
--- /dev/null
+++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/CapabilityDispatcherMixin.java
@@ -0,0 +1,60 @@
+package com.gregtechceu.gtceu.core.mixins;
+
+import com.gregtechceu.gtceu.api.item.IMergeableNBTSerializable;
+
+import net.minecraft.nbt.Tag;
+import net.minecraftforge.common.capabilities.CapabilityDispatcher;
+import net.minecraftforge.common.util.INBTSerializable;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Mixin(value = CapabilityDispatcher.class, remap = false)
+public abstract class CapabilityDispatcherMixin {
+
+ @Unique
+ private static void gtceu$beforeComparison(@NotNull Object first, @Nullable Object second) {
+ Map> map = new HashMap<>();
+ Map> map2 = new HashMap<>();
+ CapabilityDispatcherAccessor accessor = (CapabilityDispatcherAccessor) first;
+ CapabilityDispatcherAccessor otherAccessor = (CapabilityDispatcherAccessor) second;
+ if (otherAccessor != null) {
+ for (int i = 0; i < otherAccessor.getWriters().length; i++) {
+ map.put(otherAccessor.getNames()[i], otherAccessor.getWriters()[i]);
+ }
+ }
+ for (int i = 0; i < accessor.getWriters().length; i++) {
+ map2.put(accessor.getNames()[i], accessor.getWriters()[i]);
+ INBTSerializable writer = accessor.getWriters()[i];
+ String name = accessor.getNames()[i];
+ if (writer instanceof IMergeableNBTSerializable mergeable) {
+ mergeable.prepareForComparisonWith(map.get(name));
+ }
+ }
+ if (otherAccessor != null) {
+ for (int i = 0; i < otherAccessor.getWriters().length; i++) {
+ INBTSerializable writer = otherAccessor.getWriters()[i];
+ String name = otherAccessor.getNames()[i];
+ if (writer instanceof IMergeableNBTSerializable mergeable) {
+ mergeable.prepareForComparisonWith(map2.get(name));
+ }
+ }
+ }
+ }
+
+ @Inject(at = @At(value = "INVOKE",
+ target = "Lnet/minecraftforge/common/capabilities/CapabilityDispatcher;serializeNBT()Lnet/minecraft/nbt/CompoundTag;",
+ ordinal = 0),
+ method = "areCompatible")
+ private void gtceu$areCompatible(CapabilityDispatcher other, CallbackInfoReturnable cir) {
+ gtceu$beforeComparison(this, other);
+ }
+}
diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/ItemStackMixin.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/ItemStackMixin.java
new file mode 100644
index 00000000000..6b708b8754a
--- /dev/null
+++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/ItemStackMixin.java
@@ -0,0 +1,210 @@
+package com.gregtechceu.gtceu.core.mixins;
+
+import com.gregtechceu.gtceu.api.GTValues;
+import com.gregtechceu.gtceu.api.capability.GTCapability;
+import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper;
+import com.gregtechceu.gtceu.api.item.ISpoilableItemStackExtension;
+import com.gregtechceu.gtceu.api.item.component.*;
+import com.gregtechceu.gtceu.utils.FormattingUtil;
+
+import net.minecraft.ChatFormatting;
+import net.minecraft.core.Holder;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.util.FastColor;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.TooltipFlag;
+import net.minecraft.world.level.ItemLike;
+import net.minecraft.world.level.Level;
+import net.minecraftforge.registries.ForgeRegistries;
+
+import org.jetbrains.annotations.NotNull;
+import org.spongepowered.asm.mixin.*;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+@Mixin(ItemStack.class)
+public abstract class ItemStackMixin implements ISpoilableItemStackExtension {
+
+ // ************************* //
+ // Shadow fields and methods //
+ // ************************* //
+
+ @Shadow
+ @Mutable
+ @Final
+ @Nullable
+ private Item item;
+
+ @Shadow(remap = false)
+ @Mutable
+ @Final
+ @Nullable
+ private Holder.Reference
- delegate;
+
+ @Shadow(remap = false)
+ protected abstract void forgeInit();
+
+ // ************* //
+ // Unique fields //
+ // ************* //
+
+ @Shadow
+ @Nullable
+ private CompoundTag tag;
+ @Shadow
+ private int count;
+ /**
+ * Whether {@link ItemStackMixin#gtceu$updateFreshness(SpoilContext, boolean)}
+ * was called and did not return yet.
+ *
+ * Used to prevent stack overflows.
+ */
+ @Unique
+ private boolean gtceu$isUpdating = false;
+
+ /**
+ * Whether to display a fake "spoils into" tooltip for an item if it's not spoilable.
+ *
+ * Has a 10% chance of being {@code true} for any stack on april fools.
+ * The chance is rolled once in the constructor (injected by {@link ItemStackMixin#gtceu$injectFakeTooltipInit}).
+ */
+ @Unique
+ private boolean gtceu$fakeTooltip;
+
+ @Unique
+ private ItemStack gtceu$self() {
+ return (ItemStack) (Object) this;
+ }
+
+ // ************************* //
+ // Interface implementations //
+ // ************************* //
+
+ @Unique
+ @Override
+ public void gtceu$setStack(ItemStack newStack) {
+ item = newStack.getItem();
+ delegate = ForgeRegistries.ITEMS.getDelegateOrThrow(item);
+ count = newStack.getCount();
+ tag = newStack.getTag();
+ forgeInit();
+ }
+
+ @Unique
+ public void gtceu$updateFreshness(@NotNull SpoilContext spoilContext, boolean createTag) {
+ if (!gtceu$isUpdating) {
+ gtceu$isUpdating = true;
+ gtceu$self().getCapability(GTCapability.CAPABILITY_SPOILABLE_ITEM)
+ .ifPresent(spoilable -> spoilable.updateFreshness(spoilContext, createTag));
+ gtceu$isUpdating = false;
+ }
+ }
+
+ // ********* //
+ // Injectors //
+ // ********* //
+
+ @Inject(at = @At("HEAD"), method = { "getItem", "getCount" })
+ private void gtceu$injectedFreshnessUpdate(CallbackInfoReturnable- cir) {
+ if (gtceu$self().getEntityRepresentation() != null)
+ gtceu$updateFreshness(new SpoilContext(gtceu$self().getEntityRepresentation()), true);
+ else gtceu$updateFreshness(new SpoilContext(), false);
+ }
+
+ @Inject(at = @At("HEAD"), method = "inventoryTick")
+ private void gtceu$tickFreshness(Level level, Entity entity, int inventorySlot, boolean isCurrentItem,
+ CallbackInfo ci) {
+ if (entity instanceof Player player) gtceu$updateFreshness(new SpoilContext(player, inventorySlot), true);
+ else gtceu$updateFreshness(new SpoilContext(entity), true);
+ }
+
+ @Inject(at = @At("HEAD"), method = "onCraftedBy")
+ private void gtceu$updateFreshnessOnCraft(Level level, Player player, int amount, CallbackInfo ci) {
+ gtceu$updateFreshness(new SpoilContext(player, -1), true);
+ }
+
+ @Inject(at = @At("RETURN"),
+ method = "(Lnet/minecraft/world/level/ItemLike;ILnet/minecraft/nbt/CompoundTag;)V")
+ private void gtceu$injectFakeTooltipInit(ItemLike item, int count, CompoundTag tag, CallbackInfo ci) {
+ gtceu$fakeTooltip = GTValues.FOOLS.getAsBoolean() && GTValues.RNG.nextFloat() < .1f;
+ }
+
+ /**
+ * Allows {@link ISpoilableItem} subclasses that implement {@link IAddInformation} to
+ * actually display the added tooltip.
+ */
+ @Inject(at = @At(value = "INVOKE",
+ target = "Lnet/minecraft/world/item/Item;appendHoverText(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/level/Level;Ljava/util/List;Lnet/minecraft/world/item/TooltipFlag;)V"),
+ method = "getTooltipLines",
+ locals = LocalCapture.CAPTURE_FAILSOFT)
+ private void gtceu$spoilageTooltip(Player player, TooltipFlag isAdvanced,
+ CallbackInfoReturnable
> cir,
+ List list) {
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(gtceu$self());
+ if (spoilable instanceof IAddInformation addInformation) {
+ addInformation.appendHoverText(gtceu$self(), player == null ? null : player.level(), list,
+ isAdvanced);
+ } else if (gtceu$fakeTooltip) {
+ list.add(Component.translatable(
+ "gtceu.tooltip.spoil_time_remaining",
+ Component.literal(FormattingUtil.formatTime(100)).withStyle(ChatFormatting.DARK_AQUA)));
+ list.add(Component.translatable(
+ "gtceu.tooltip.spoils_into",
+ Items.DIRT.getDefaultInstance().getDisplayName()));
+ }
+ }
+
+ /**
+ * Allows {@link ISpoilableItem} subclasses that implement {@link IDurabilityBar} to
+ * actually display the bar.
+ */
+ @Inject(at = @At("HEAD"), method = "isBarVisible", cancellable = true)
+ private void gtceu$spoilageBarVisible(CallbackInfoReturnable cir) {
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(gtceu$self());
+ if (spoilable instanceof IDurabilityBar durabilityBar) {
+ cir.setReturnValue(durabilityBar.isBarVisible(gtceu$self()));
+ } else if (gtceu$fakeTooltip) {
+ cir.setReturnValue(true);
+ }
+ }
+
+ /**
+ * Allows {@link ISpoilableItem} subclasses that implement {@link IDurabilityBar} to
+ * actually display the bar.
+ */
+ @Inject(at = @At("HEAD"), method = "getBarColor", cancellable = true)
+ private void gtceu$spoilageBarColor(CallbackInfoReturnable cir) {
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(gtceu$self());
+ if (spoilable instanceof IDurabilityBar durabilityBar) {
+ cir.setReturnValue(durabilityBar.getBarColor(gtceu$self()));
+ } else if (gtceu$fakeTooltip) {
+ cir.setReturnValue(FastColor.ARGB32.color(255, 255, 255, 255));
+ }
+ }
+
+ /**
+ * Allows {@link ISpoilableItem} subclasses that implement {@link IDurabilityBar} to
+ * actually display the bar.
+ */
+ @Inject(at = @At("HEAD"), method = "getBarWidth", cancellable = true)
+ private void gtceu$spoilageBarWidth(CallbackInfoReturnable cir) {
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(gtceu$self());
+ if (spoilable instanceof IDurabilityBar durabilityBar) {
+ cir.setReturnValue(durabilityBar.getBarWidth(gtceu$self()));
+ } else if (gtceu$fakeTooltip) {
+ cir.setReturnValue(13);
+ }
+ }
+}
diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/LevelMixin.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/LevelMixin.java
index d095baa8fa6..ddbc32f309a 100644
--- a/src/main/java/com/gregtechceu/gtceu/core/mixins/LevelMixin.java
+++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/LevelMixin.java
@@ -1,5 +1,6 @@
package com.gregtechceu.gtceu.core.mixins;
+import com.gregtechceu.gtceu.api.item.component.SpoilUtils;
import com.gregtechceu.gtceu.api.pattern.MultiblockState;
import com.gregtechceu.gtceu.api.pattern.MultiblockWorldSavedData;
@@ -30,6 +31,9 @@
@Mixin(Level.class)
public abstract class LevelMixin implements LevelAccessor {
+ @Unique
+ private static boolean gtceu$updatingSpoilage = false;
+
@Shadow
@Final
public boolean isClientSide;
@@ -64,6 +68,15 @@ public abstract class LevelMixin implements LevelAccessor {
}
}
+ @Inject(method = "getBlockEntity", at = @At("RETURN"))
+ private void gtceu$updateSpoilage(BlockPos pos, CallbackInfoReturnable cir) {
+ if (cir.getReturnValue() != null && !gtceu$updatingSpoilage) {
+ gtceu$updatingSpoilage = true;
+ SpoilUtils.updateBlock(cir.getReturnValue());
+ gtceu$updatingSpoilage = false;
+ }
+ }
+
@SuppressWarnings("ConstantValue")
@Inject(method = "markAndNotifyBlock",
at = @At(value = "INVOKE",
diff --git a/src/main/java/com/gregtechceu/gtceu/data/lang/LangHandler.java b/src/main/java/com/gregtechceu/gtceu/data/lang/LangHandler.java
index f36f4b91036..5922f08fad2 100644
--- a/src/main/java/com/gregtechceu/gtceu/data/lang/LangHandler.java
+++ b/src/main/java/com/gregtechceu/gtceu/data/lang/LangHandler.java
@@ -1756,6 +1756,15 @@ public static void init(RegistrateLangProvider provider) {
"Place the cover on the target block, right-click it with a data stick and put that data stick into a data access hatch in the multiblock.",
"Then select the data access hatch as the target, and set the slot index of your data stick in the number field that appeared.");
provider.add("gtceu.tooltip.player_bind", "Bound to player: %s");
+ provider.add("gtceu.tooltip.spoil_time_remaining", "Time until spoils: %s");
+ provider.add("gtceu.tooltip.spoil_time_total", "Total spoil time: %s");
+ provider.add("gtceu.tooltip.spoils_into", "Spoils into: %s");
+ provider.add("gtceu.tooltip.creation_tick", "Created on overworld tick %d");
+ provider.add("gtceu.tooltip.location", "Location: %s (%d, %d, %d)");
+ provider.add("gtceu.tooltip.location_entity", "Entity: %s");
+ provider.add("gtceu.tooltip.item_handler_source", "Handler source: %s");
+ provider.add("gtceu.tooltip.item_handler_data", "Handler data: %s");
+ provider.add("gtceu.tooltip.location_slot", "Slot: %s");
}
/**
diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/GTRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/GTRecipeBuilder.java
index 0cc2e4328c3..cf071435988 100644
--- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/GTRecipeBuilder.java
+++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/GTRecipeBuilder.java
@@ -107,6 +107,8 @@ public class GTRecipeBuilder {
private boolean itemMaterialInfo = false;
private boolean fluidMaterialInfo = false;
private boolean removePreviousMatInfo = false;
+ @Setter
+ public boolean keepSpoilingProgress = true;
public GTRecipeCategory recipeCategory;
@Setter
public @Nullable BiConsumer> onSave;
@@ -1805,7 +1807,8 @@ public GTRecipe buildRawRecipe() {
return new GTRecipe(recipeType, id.withPrefix(recipeType.registryName.getPath() + "/"),
input, output, tickInput, tickOutput,
inputChanceLogic, outputChanceLogic, tickInputChanceLogic, tickOutputChanceLogic,
- conditions, List.of(), data, duration, recipeCategory, -1);
+ conditions, List.of(), data, duration, recipeCategory, -1,
+ new RecipeSpoilageData(keepSpoilingProgress));
}
protected void warnTooManyIngredients(RecipeCapability> capability,
diff --git a/src/main/java/com/gregtechceu/gtceu/integration/kjs/GTCEuStartupEvents.java b/src/main/java/com/gregtechceu/gtceu/integration/kjs/GTCEuStartupEvents.java
index 59764783f61..004ea63db29 100644
--- a/src/main/java/com/gregtechceu/gtceu/integration/kjs/GTCEuStartupEvents.java
+++ b/src/main/java/com/gregtechceu/gtceu/integration/kjs/GTCEuStartupEvents.java
@@ -5,6 +5,7 @@
import com.gregtechceu.gtceu.integration.kjs.events.CraftingComponentsEventJS;
import com.gregtechceu.gtceu.integration.kjs.events.GTRegistryEventJS;
import com.gregtechceu.gtceu.integration.kjs.events.MaterialModificationEventJS;
+import com.gregtechceu.gtceu.integration.kjs.events.ModifyMachineEventJS;
import dev.latvian.mods.kubejs.event.EventGroup;
import dev.latvian.mods.kubejs.event.EventHandler;
@@ -28,4 +29,5 @@ private static boolean validateRegistry(Object o) {
EventHandler REGISTRY = GROUP.startup("registry", () -> GTRegistryEventJS.class).extra(REGISTRY_EXTRA);
EventHandler MATERIAL_MODIFICATION = GROUP.startup("materialModification", () -> MaterialModificationEventJS.class);
EventHandler CRAFTING_COMPONENTS = GROUP.startup("craftingComponents", () -> CraftingComponentsEventJS.class);
+ EventHandler MACHINE_MODIFICATION = GROUP.startup("machineModification", () -> ModifyMachineEventJS.class);
}
diff --git a/src/main/java/com/gregtechceu/gtceu/integration/kjs/events/ModifyMachineEventJS.java b/src/main/java/com/gregtechceu/gtceu/integration/kjs/events/ModifyMachineEventJS.java
new file mode 100644
index 00000000000..a4a4b695925
--- /dev/null
+++ b/src/main/java/com/gregtechceu/gtceu/integration/kjs/events/ModifyMachineEventJS.java
@@ -0,0 +1,19 @@
+package com.gregtechceu.gtceu.integration.kjs.events;
+
+import com.gregtechceu.gtceu.api.events.ModifyMachineEvent;
+import com.gregtechceu.gtceu.api.registry.registrate.MachineBuilder;
+
+import dev.latvian.mods.kubejs.event.EventJS;
+
+public class ModifyMachineEventJS extends EventJS {
+
+ private final ModifyMachineEvent event;
+
+ public ModifyMachineEventJS(ModifyMachineEvent event) {
+ this.event = event;
+ }
+
+ public MachineBuilder, ?> getBuilder() {
+ return this.event.getBuilder();
+ }
+}
diff --git a/src/main/java/com/gregtechceu/gtceu/utils/FormattingUtil.java b/src/main/java/com/gregtechceu/gtceu/utils/FormattingUtil.java
index 493f7798fb9..b42906d4a97 100644
--- a/src/main/java/com/gregtechceu/gtceu/utils/FormattingUtil.java
+++ b/src/main/java/com/gregtechceu/gtceu/utils/FormattingUtil.java
@@ -254,6 +254,16 @@ public static String formatBuckets(long mB) {
return formatNumberReadable(mB, true, DECIMAL_FORMAT_2F, "B");
}
+ public static String formatTime(long ticks) {
+ long sec = ticks / 20;
+ String out = "";
+ out = (sec % 60) + "s" + out;
+ if (sec > 60) out = ((sec / 60) % 60) + "m " + out;
+ if (sec > 3600) out = ((sec / 3600) % 24) + "h " + out;
+ if (sec > 24 * 3600) out = (sec / (3600 * 24)) + "d " + out;
+ return out;
+ }
+
@NotNull
public static String formatNumber2Places(float number) {
return DECIMAL_FORMAT_2F.format(number);
diff --git a/src/main/resources/gtceu.mixins.json b/src/main/resources/gtceu.mixins.json
index c7352d5caf9..b00f2ce470e 100644
--- a/src/main/resources/gtceu.mixins.json
+++ b/src/main/resources/gtceu.mixins.json
@@ -38,12 +38,15 @@
"BlockBehaviourAccessor",
"BlockMixin",
"BlockPropertiesAccessor",
+ "CapabilityDispatcherAccessor",
+ "CapabilityDispatcherMixin",
"ChunkGeneratorMixin",
"EntityMixin",
"GrowingPlantBlockAccessor",
"IngredientAccessor",
"IntegerPropertyAccessor",
"InventoryMixin",
+ "ItemStackMixin",
"ItemValueAccessor",
"LevelMixin",
"LootDataManagerMixin",
diff --git a/src/test/java/com/gregtechceu/gtceu/common/item/SpoilableBehaviourTest.java b/src/test/java/com/gregtechceu/gtceu/common/item/SpoilableBehaviourTest.java
new file mode 100644
index 00000000000..8fbba605454
--- /dev/null
+++ b/src/test/java/com/gregtechceu/gtceu/common/item/SpoilableBehaviourTest.java
@@ -0,0 +1,342 @@
+package com.gregtechceu.gtceu.common.item;
+
+import com.gregtechceu.gtceu.GTCEu;
+import com.gregtechceu.gtceu.api.GTValues;
+import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper;
+import com.gregtechceu.gtceu.api.cover.filter.SimpleItemFilter;
+import com.gregtechceu.gtceu.api.item.component.ISpoilableItem;
+import com.gregtechceu.gtceu.api.item.component.SpoilContext;
+import com.gregtechceu.gtceu.api.item.component.SpoilUtils;
+import com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine;
+import com.gregtechceu.gtceu.api.recipe.GTRecipeType;
+import com.gregtechceu.gtceu.common.cover.ConveyorCover;
+import com.gregtechceu.gtceu.common.data.GTItems;
+import com.gregtechceu.gtceu.common.data.GTMachines;
+import com.gregtechceu.gtceu.common.data.GTRecipeTypes;
+import com.gregtechceu.gtceu.common.item.behavior.SpoilableBehavior;
+import com.gregtechceu.gtceu.common.machine.multiblock.part.FluidHatchPartMachine;
+import com.gregtechceu.gtceu.common.machine.multiblock.part.ItemBusPartMachine;
+import com.gregtechceu.gtceu.common.machine.storage.CrateMachine;
+import com.gregtechceu.gtceu.gametest.util.TestUtils;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.gametest.framework.BeforeBatch;
+import net.minecraft.gametest.framework.GameTest;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.animal.Pig;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.phys.AABB;
+import net.minecraftforge.gametest.GameTestHolder;
+import net.minecraftforge.gametest.PrefixGameTestTemplate;
+import net.minecraftforge.items.IItemHandler;
+import net.minecraftforge.items.ItemHandlerHelper;
+
+import java.util.List;
+import java.util.Objects;
+
+@PrefixGameTestTemplate(false)
+@GameTestHolder(GTCEu.MOD_ID)
+public class SpoilableBehaviourTest {
+
+ private static GTRecipeType LCR_RECIPE_TYPE;
+
+ @BeforeBatch(batch = "spoilageTests")
+ public static void prepare(Level ignoredLevel) {
+ LCR_RECIPE_TYPE = TestUtils.createRecipeType("spoilage_lcr_tests", GTRecipeTypes.LARGE_CHEMICAL_RECIPES);
+ LCR_RECIPE_TYPE.getAdditionHandler().beginStaging();
+ LCR_RECIPE_TYPE.getAdditionHandler().addStaging(LCR_RECIPE_TYPE
+ .recipeBuilder(GTCEu.id("test_spoilage_transfers"))
+ .inputItems(new ItemStack(Items.JIGSAW))
+ .outputItems(new ItemStack(Items.STRUCTURE_BLOCK))
+ .EUt(GTValues.V[GTValues.HV])
+ .duration(20)
+ .buildRawRecipe());
+ LCR_RECIPE_TYPE.getAdditionHandler().addStaging(LCR_RECIPE_TYPE
+ .recipeBuilder(GTCEu.id("test_spoilage_doesnt_transfer"))
+ .inputItems(new ItemStack(Items.APPLE))
+ .outputItems(new ItemStack(Items.STRUCTURE_BLOCK))
+ .EUt(GTValues.V[GTValues.HV])
+ .duration(20)
+ .keepSpoilingProgress(false)
+ .buildRawRecipe());
+ LCR_RECIPE_TYPE.getAdditionHandler().completeStaging();
+ attachSpoilables();
+ }
+
+ private static void attachSpoilables() {
+ SpoilableBehavior.builder()
+ .ticks(10)
+ .result(Items.DIRT)
+ .build()
+ .attachTo(Items.JIGSAW);
+ SpoilableBehavior.builder()
+ .ticks(10)
+ .result(Items.STRUCTURE_BLOCK)
+ .build()
+ .attachTo(Items.APPLE);
+ SpoilableBehavior.builder()
+ .ticks(40)
+ .result(Items.STRUCTURE_VOID)
+ .build()
+ .attachTo(Items.STRUCTURE_BLOCK);
+ SpoilableBehavior.builder()
+ .ticks(10)
+ .result(Items.JIGSAW)
+ .build()
+ .attachTo(Items.STRUCTURE_VOID);
+ SpoilableBehavior.builder()
+ .ticks(10)
+ .result(Items.DRAGON_EGG)
+ .result(EntityType.PIG)
+ .multiplyResult(3)
+ .build()
+ .attachTo(Items.EGG);
+ }
+
+ private static BusHolder getBussesAndForm(GameTestHelper helper) {
+ WorkableMultiblockMachine controller = (WorkableMultiblockMachine) helper.getBlockEntity(new BlockPos(1, 2, 0));
+ assert controller != null;
+ TestUtils.formMultiblock(controller);
+ controller.setRecipeType(LCR_RECIPE_TYPE);
+ ItemBusPartMachine inputBus1 = (ItemBusPartMachine) helper.getBlockEntity(new BlockPos(2, 1, 0));
+ ItemBusPartMachine inputBus2 = (ItemBusPartMachine) helper.getBlockEntity(new BlockPos(2, 2, 0));
+ ItemBusPartMachine outputBus1 = (ItemBusPartMachine) helper.getBlockEntity(new BlockPos(0, 1, 0));
+ FluidHatchPartMachine outputHatch1 = (FluidHatchPartMachine) helper.getBlockEntity(new BlockPos(0, 2, 0));
+ return new BusHolder(inputBus1, inputBus2, outputBus1, outputHatch1, controller);
+ }
+
+ private record BusHolder(ItemBusPartMachine inputBus1, ItemBusPartMachine inputBus2, ItemBusPartMachine outputBus1,
+ FluidHatchPartMachine outputHatch1, WorkableMultiblockMachine controller) {}
+
+ @GameTest(template = "empty_5x5", batch = "spoilageTests")
+ public static void itemSpoilsInChest(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ helper.setBlock(1, 1, 1, Blocks.CHEST);
+ IItemHandler itemHandler = TestUtils.getItemHandler(helper, new BlockPos(1, 1, 1));
+ itemHandler.insertItem(0, Items.JIGSAW.getDefaultInstance().copyWithCount(23), false);
+ TestUtils.getItemHandler(helper, new BlockPos(1, 1, 1));
+ helper.runAtTickTime(9, () -> {
+ ItemStack stack = TestUtils.getItemHandler(helper, new BlockPos(1, 1, 1)).getStackInSlot(0);
+ helper.assertTrue(TestUtils.isItemStackEqual(
+ Items.JIGSAW.getDefaultInstance().copyWithCount(23),
+ stack), "jigsaw spoiled 1 tick earlier");
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ TestUtils.assertNotNull(helper, spoilable, "spoilable was null when shouldn't have");
+ helper.assertTrue(spoilable.shouldSpoil(), "shouldSpoil returned false on spoilable item");
+ TestUtils.assertEqual(helper, spoilable.getTicksUntilSpoiled(), 1,
+ "spoilable didn't return correct ticks until spoiled amount");
+ helper.assertTrue(spoilable.getSpoilTicks() == 10,
+ "spoilable didn't return correct total tick amount");
+ });
+ helper.runAtTickTime(10, () -> helper.assertTrue(TestUtils.isItemStackEqual(
+ Items.DIRT.getDefaultInstance().copyWithCount(23),
+ TestUtils.getItemHandler(helper, new BlockPos(1, 1, 1)).getStackInSlot(0)),
+ "jigsaw didn't spoil when should have"));
+ }
+
+ @GameTest(template = "empty_5x5", batch = "spoilageTests")
+ public static void itemSpoilsRecursively(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ helper.setBlock(1, 1, 1, Blocks.CHEST);
+ IItemHandler itemHandler = TestUtils.getItemHandler(helper, new BlockPos(1, 1, 1));
+ ItemStack in = Items.APPLE.getDefaultInstance().copyWithCount(41);
+ SpoilUtils.update(in, new SpoilContext(
+ helper.getLevel(),
+ helper.absolutePos(new BlockPos(1, 1, 1)))
+ .withItemHandlerSide(null)
+ .withSlot(0));
+ itemHandler.insertItem(0, in, false);
+ helper.runAtTickTime(70, () -> helper.assertTrue(TestUtils.isItemStackEqual(
+ Items.DIRT.getDefaultInstance().copyWithCount(41),
+ itemHandler.getStackInSlot(0)),
+ "apple didn't spoil recursively (apple -> structure block -> structure void -> jigsaw -> dirt), got " +
+ itemHandler.getStackInSlot(0) + " instead"));
+ }
+
+ @GameTest(template = "empty_5x5", batch = "spoilageTests")
+ public static void itemSpoilsInCrate(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ CrateMachine crate = (CrateMachine) TestUtils.setMachine(helper, new BlockPos(1, 1, 1), GTMachines.STEEL_CRATE);
+ crate.inventory.insertItem(0, Items.JIGSAW.getDefaultInstance().copyWithCount(23), false);
+ helper.runAtTickTime(9, () -> {
+ ItemStack stack = crate.inventory.getStackInSlot(0);
+ helper.assertTrue(TestUtils.isItemStackEqual(
+ Items.JIGSAW.getDefaultInstance().copyWithCount(23),
+ stack), "jigsaw spoiled 1 tick earlier");
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ TestUtils.assertNotNull(helper, spoilable, "spoilable was null when shouldn't have");
+ helper.assertTrue(spoilable.shouldSpoil(), "shouldSpoil returned false on spoilable item");
+ helper.assertTrue(spoilable.getTicksUntilSpoiled() == 1,
+ "spoilable didn't return correct ticks until spoiled amount");
+ helper.assertTrue(spoilable.getSpoilTicks() == 10,
+ "spoilable didn't return correct total tick amount");
+ });
+ helper.runAtTickTime(10, () -> helper.assertTrue(TestUtils.isItemStackEqual(
+ Items.DIRT.getDefaultInstance().copyWithCount(23),
+ crate.inventory.getStackInSlot(0)), "jigsaw didn't spoil when should have"));
+ }
+
+ @GameTest(template = "empty", batch = "spoilageTests")
+ public static void spoilageFreeze(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ CrateMachine crate = (CrateMachine) TestUtils.setMachine(helper, new BlockPos(1, 1, 1), GTMachines.STEEL_CRATE);
+ crate.inventory.insertItem(0, Items.JIGSAW.getDefaultInstance().copyWithCount(23), false);
+ helper.runAtTickTime(4, () -> {
+ ItemStack stack = crate.inventory.getStackInSlot(0);
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ TestUtils.assertNotNull(helper, spoilable, "spoilable was null when shouldn't have (check #1)");
+ TestUtils.assertEqual(helper, spoilable.getTicksUntilSpoiled(), 6, "incorrect ticks until spoiled");
+ spoilable.freezeSpoiling();
+ });
+ helper.runAtTickTime(9, () -> {
+ ItemStack stack = crate.inventory.getStackInSlot(0);
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ TestUtils.assertNotNull(helper, spoilable, "spoilable was null when shouldn't have (check #2)");
+ TestUtils.assertEqual(helper, spoilable.getTicksUntilSpoiled(), 6,
+ "ticks until spoiled changed while frozen");
+ spoilable.unfreezeSpoiling();
+ });
+ helper.runAtTickTime(13, () -> {
+ ItemStack stack = crate.inventory.getStackInSlot(0);
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ TestUtils.assertNotNull(helper, spoilable, "spoilable was null when shouldn't have (check #3)");
+ TestUtils.assertEqual(helper, spoilable.getTicksUntilSpoiled(), 2,
+ "incorrect ticks until spoiled after unfreeze");
+ });
+ }
+
+ @GameTest(template = "empty", batch = "spoilageTests")
+ public static void entitySpoilage(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ CrateMachine crate = (CrateMachine) TestUtils.setMachine(helper, new BlockPos(1, 1, 1), GTMachines.STEEL_CRATE);
+ crate.inventory.insertItem(0, Items.EGG.getDefaultInstance().copyWithCount(2), false);
+ helper.runAtTickTime(10, () -> {
+ TestUtils.assertEqual(helper, crate.inventory.getStackInSlot(0),
+ Items.DRAGON_EGG.getDefaultInstance().copyWithCount(6));
+ List pigs = helper.getLevel().getEntities(EntityType.PIG,
+ new AABB(helper.absolutePos(new BlockPos(1, 0, 1))), Entity::isAlive);
+ TestUtils.assertEqual(helper, pigs.size(), 6, "incorrect amount of entities spawned");
+ });
+ }
+
+ @GameTest(template = "lcr_input_separation", batch = "spoilageTests")
+ public static void spoilageTransfersInRecipe(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ BusHolder busHolder = getBussesAndForm(helper);
+ ItemStack input = new ItemStack(Items.JIGSAW);
+ SpoilUtils.update(input, new SpoilContext());
+ Objects.requireNonNull(GTCapabilityHelper.getSpoilable(input)).setTicksUntilSpoiled(8);
+ busHolder.inputBus1.getInventory().setStackInSlot(0, input);
+ helper.runAtTickTime(21, () -> {
+ ItemStack stack = busHolder.outputBus1.getInventory().getStackInSlot(0);
+ helper.assertTrue(TestUtils.isItemStackEqual(
+ stack,
+ new ItemStack(Items.STRUCTURE_BLOCK)),
+ "incorrect recipe output (%s != %s)".formatted(stack.toString(),
+ new ItemStack(Items.STRUCTURE_BLOCK).toString()));
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ TestUtils.assertNotNull(helper, spoilable, "recipe output was not spoilable");
+ TestUtils.assertEqual(helper, spoilable.getTicksUntilSpoiled(), 27,
+ "recipe output didn't have correct ticks until spoiled");
+ });
+ }
+
+ @GameTest(template = "lcr_input_separation", batch = "spoilageTests")
+ public static void spoilageDoesntTransferInRecipe(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ BusHolder busHolder = getBussesAndForm(helper);
+ ItemStack input = new ItemStack(Items.APPLE);
+ SpoilUtils.update(input, new SpoilContext());
+ Objects.requireNonNull(GTCapabilityHelper.getSpoilable(input)).setTicksUntilSpoiled(8);
+ busHolder.inputBus1.getInventory().setStackInSlot(0, input);
+ helper.runAtTickTime(21, () -> {
+ ItemStack stack = busHolder.outputBus1.getInventory().getStackInSlot(0);
+ helper.assertTrue(TestUtils.isItemStackEqual(
+ stack,
+ new ItemStack(Items.STRUCTURE_BLOCK)),
+ "incorrect recipe output (%s != %s)".formatted(stack.toString(),
+ new ItemStack(Items.STRUCTURE_BLOCK).toString()));
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ TestUtils.assertNotNull(helper, spoilable, "recipe output was not spoilable");
+ TestUtils.assertEqual(helper, spoilable.getTicksUntilSpoiled(), 40,
+ "recipe output didn't have correct ticks until spoiled");
+ });
+ }
+
+ @GameTest(template = "empty_5x5", batch = "spoilageTests")
+ public static void droppedItemSpoils(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ ItemEntity item = helper.spawnItem(Items.JIGSAW, new BlockPos(1, 1, 1));
+ helper.runAtTickTime(10, () -> helper.assertTrue(TestUtils.isItemStackEqual(
+ item.getItem(),
+ Items.DIRT.getDefaultInstance()), "item didn't spoil when dropped"));
+ }
+
+ @GameTest(template = "empty_5x5", batch = "spoilageTests")
+ public static void itemSpoilsInInventory(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ Player player = helper.makeMockPlayer();
+ player.getInventory().setItem(0, Items.JIGSAW.getDefaultInstance());
+ player.tick();
+ helper.runAtTickTime(10, () -> helper.assertTrue(TestUtils.isItemStackEqual(
+ player.getInventory().getItem(0),
+ Items.DIRT.getDefaultInstance()),
+ "item didn't spoil in a player inventory (%s != %s)".formatted(player.getInventory().getItem(0),
+ Items.DIRT.getDefaultInstance())));
+ }
+
+ @GameTest(template = "empty_5x5", batch = "spoilageTests")
+ public static void spoilableFiltering(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ CrateMachine crate1 = (CrateMachine) TestUtils.setMachine(helper, new BlockPos(1, 1, 1),
+ GTMachines.STEEL_CRATE);
+ CrateMachine crate2 = (CrateMachine) TestUtils.setMachine(helper, new BlockPos(1, 2, 1),
+ GTMachines.STEEL_CRATE);
+ ConveyorCover cover = (ConveyorCover) TestUtils.placeCover(helper, crate1, GTItems.CONVEYOR_MODULE_HV.asStack(),
+ Direction.UP);
+ CompoundTag filterTag = SimpleItemFilter.forItems(true, Items.STRUCTURE_BLOCK.getDefaultInstance())
+ .saveFilter();
+ ItemStack filter = GTItems.ITEM_FILTER.asStack();
+ filter.setTag(filterTag);
+ cover.getFilterHandler().loadFilter(filter);
+ cover.setWorkingEnabled(false);
+ crate1.inventory.setStackInSlot(0, Items.STRUCTURE_BLOCK.getDefaultInstance());
+ helper.runAtTickTime(10, () -> cover.setWorkingEnabled(true));
+ helper.runAtTickTime(20, () -> {
+ ItemStack stack = crate2.inventory.getStackInSlot(0);
+ ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack);
+ helper.assertTrue(TestUtils.isItemStackEqual(stack, Items.STRUCTURE_BLOCK.getDefaultInstance()),
+ "wrong item");
+ TestUtils.assertNotNull(helper, spoilable, "spoilable was null");
+ TestUtils.assertEqual(helper, 20, spoilable.getTicksUntilSpoiled(), "wrong ticks until spoiled");
+ });
+ }
+
+ @GameTest(template = "empty_5x5", batch = "spoilageTests")
+ public static void spoilableMerging(GameTestHelper helper) {
+ TestUtils.succeedAfterTest(helper);
+ ItemStack stack1 = Items.JIGSAW.getDefaultInstance().copyWithCount(9);
+ ItemStack stack2 = Items.JIGSAW.getDefaultInstance().copyWithCount(5);
+ SpoilUtils.update(stack1, new SpoilContext());
+ SpoilUtils.update(stack2, new SpoilContext());
+ ISpoilableItem spoilable1 = GTCapabilityHelper.getSpoilable(stack1);
+ ISpoilableItem spoilable2 = GTCapabilityHelper.getSpoilable(stack2);
+ TestUtils.assertNotNull(helper, spoilable1, "spoilable1 was null");
+ TestUtils.assertNotNull(helper, spoilable2, "spoilable2 was null");
+ spoilable1.setTicksUntilSpoiled(7);
+ spoilable2.setTicksUntilSpoiled(4);
+ helper.assertTrue(ItemHandlerHelper.canItemStacksStack(stack1, stack2), "stacks were not equal");
+ // (9*7 + 5*4)/(9 + 5) = 5.928
+ TestUtils.assertEqual(helper, spoilable1.getTicksUntilSpoiled(), 5, "spoilable1 had wrong ticks until spoiled");
+ TestUtils.assertEqual(helper, spoilable2.getTicksUntilSpoiled(), 5, "spoilable2 has wrong ticks until spoiled");
+ }
+}
diff --git a/src/test/java/com/gregtechceu/gtceu/gametest/util/TestUtils.java b/src/test/java/com/gregtechceu/gtceu/gametest/util/TestUtils.java
index 6c2840a8c17..d9f21f61adc 100644
--- a/src/test/java/com/gregtechceu/gtceu/gametest/util/TestUtils.java
+++ b/src/test/java/com/gregtechceu/gtceu/gametest/util/TestUtils.java
@@ -2,6 +2,7 @@
import com.gregtechceu.gtceu.GTCEu;
import com.gregtechceu.gtceu.api.GTValues;
+import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper;
import com.gregtechceu.gtceu.api.capability.recipe.FluidRecipeCapability;
import com.gregtechceu.gtceu.api.capability.recipe.IO;
import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability;
@@ -28,7 +29,9 @@
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RedstoneLampBlock;
import net.minecraftforge.fluids.FluidStack;
+import net.minecraftforge.items.IItemHandler;
+import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -226,8 +229,7 @@ public static CoverBehavior placeCover(GameTestHelper helper, MetaMachine machin
}
}
}
- helper.assertTrue(coverDefinition != null, "attempted to place cover with item that is not a cover");
- assert coverDefinition != null;
+ TestUtils.assertNotNull(helper, coverDefinition, "attempted to place cover with item that is not a cover");
helper.assertTrue(shouldFail ^ machine.getCoverContainer().placeCoverOnSide(
direction, stack, coverDefinition, null), "failed to place cover");
return machine.getCoverContainer().getCoverAtSide(direction);
@@ -244,6 +246,16 @@ public static void assertEqual(GameTestHelper helper, List tex
"strings not equal: \"%s\" != \"%s\"".formatted(component.toString(), s));
}
+ public static void assertEqual(GameTestHelper helper, long a, long b, String message) {
+ helper.assertTrue(a == b, "%s (%d != %d)".formatted(message, a, b));
+ }
+
+ public static void assertEqual(GameTestHelper helper, ItemStack stack1, ItemStack stack2, String message) {
+ helper.assertTrue(
+ isItemStackEqual(stack1, stack2),
+ "%s (%s != %s)".formatted(message, stack1, stack2));
+ }
+
public static void assertEqual(GameTestHelper helper, ItemStack stack1, ItemStack stack2) {
helper.assertTrue(isItemStackEqual(stack1, stack2),
"Item stacks not equal: \"%s\" != \"%s\"".formatted(stack1.toString(), stack2.toString()));
@@ -283,10 +295,23 @@ public static void succeedAfterTest(GameTestHelper helper, long timeout) {
helper.runAtTickTime(timeout, helper::succeed);
}
+ public static IItemHandler getItemHandler(GameTestHelper helper, BlockPos pos) {
+ return GTCapabilityHelper.getItemHandler(helper.getLevel(), helper.absolutePos(pos), null);
+ }
+
public static void assertEqual(GameTestHelper helper, @Nullable BlockPos pos1, @Nullable BlockPos pos2) {
helper.assertTrue(pos1 != null && pos1.equals(pos2), "Expected %s to equal to %s".formatted(pos1, pos2));
}
+ /**
+ * Use this instead of {@code helper.assertTrue(obj != null, ...)} to stop IntelliJ from complaining
+ * about nullability.
+ */
+ @Contract("_, null, _ -> fail")
+ public static void assertNotNull(GameTestHelper helper, Object object, String failureMessage) {
+ helper.assertTrue(object != null, failureMessage);
+ }
+
public static void assertRedstone(GameTestHelper helper, BlockPos pos, int min, int max) {
BlockPos absolutePos = helper.absolutePos(pos);
int strength = helper.getLevel().getBestNeighborSignal(absolutePos);