diff --git a/docs/content/Modpacks/Other-Topics/Machine-Modification.md b/docs/content/Modpacks/Other-Topics/Machine-Modification.md new file mode 100644 index 00000000000..112d3ad160d --- /dev/null +++ b/docs/content/Modpacks/Other-Topics/Machine-Modification.md @@ -0,0 +1,10 @@ +--- +title: Machine Modification +--- + +# Machine Modification +If you want to modify an existing machine's definition, you can do so using the +`GTCEuStartupEvents.machineModification` event. It is fired right before a machine gets registered, so that +its builder is still accessible through the `event.getBuilder()` method. For examples of methods that machine builders +have, see [Custom Machines](Custom-Machines.md). +For a full list of methods, see [source](https://github.com/GregTechCEu/GregTech-Modern/blob/1.20.1/src/main/java/com/gregtechceu/gtceu/api/registry/registrate/MachineBuilder.java). \ No newline at end of file diff --git a/docs/content/Modpacks/Other-Topics/Spoilage.md b/docs/content/Modpacks/Other-Topics/Spoilage.md new file mode 100644 index 00000000000..d3374ddd9d5 --- /dev/null +++ b/docs/content/Modpacks/Other-Topics/Spoilage.md @@ -0,0 +1,158 @@ +--- +title: Spoilage +--- + +**Spoilage** is a mechanic that allows items to *spoil*.
+Spoilable items spoil based on the amount of ticks that passed from their creation, +or, more specifically, from one of these events (due to Minecraft's limitations): + +- The item was in an `IItemHandler` that is a capability of a `BlockEntity` that had `Level#getBlockEntity` called +- The item was crafted in a GregTech recipe +- The item was crafted in a crafting table +- The item was in a player's inventory for at least 1 tick +- The item was dropped +- `SpoilUtils.update(ItemStack, SpoilContext)` was called + +If you want to make an item spoil, you need to attack the `ISpoilableItem` capability to it. +Please note that the spoilage timer still decrements even if the stack is in an unloaded chunk. + +### SpoilContext +A `SpoilContext` is an object that represents the environment in which an item spoils. +It may represent: + +- Nothing (all values are null, obtained by just calling the constructor without arguments) +- A block (contains `Level` and `BlockPos`) +- A block and an item handler (contains `Level`, `BlockPos`, `IItemHandler` and the number of the slot in that `IItemHandler`, that may be `-1`) +- An entity, usually a player (contains `Entity` and the number of the slot in that entity's inventory, that may be `-1`) + +This info is used to spawn entities when the item spoils, or do something more complex. +The `SpoilableBehavior.builder()` can accept a function as a `result`, so you can do whatever you want there :) + +### SpoilableBehavior +`SpoilableBehavior` is a helper class used to make items spoilable. + +`SpoilableBehavior.builder()` is a convenient way to create a `SpoilableBehavior`, currently it has the following methods: + +- `.ticks(long)` + used to specify ticks until spoiled +- `.ticks(Function)` + used to specify a ticks until spoiled value that may depend on the stack itself, for example having more items in the stack may make it spoil slower +- `.result(ItemLike)` + used to specify the resulting item +- `.result(ItemStack)` + used to specify the resulting stack (may be with NBT, but not count) +- `.result(Function)` + used to specify the resulting stack that may depend on the original stack +- `.result(EntityType)` + used to specify the mob into which the item will spoil (it can still spoil into an item as well, but the item has to be specified first) +- `.result(Supplier>` + same as `.result(EntityType)`, exists for convenience +- `.result(SpoilResultProvider)` + used to specify a result function ((`ItemStack`, `SpoilContext`, `simulate`) -> `ItemStack`) for custom spoiling logic +- `.multiplyResult(int)` + multiply all previously specified results (spoil into multiple items, spawn multiple mobs, etc.) +- `.tooltip(Component)` + used to specify things to show in `Spoils into: ...` in the tooltip +- `.tooltip(Function)` + same as `.tooltip(Component)`, but can depend on the stack + +To attach a `SpoilableBehavior` to an item, you can use the `attachTo(ItemLike)` method. +It can be chained if you want to make multiple items spoil using the same behavior. + +### SpoilUtils +`SpoilUtils` is a utility class with some static methods for updating items and blocks: + +- `update` + makes an item start spoiling with the specified `SpoilContext` +- `updateBlock` + updates all items in any found `IItemHandler` capabilities of the specified block, + the `SpoilContext` is generated automatically + +!!! example + ```java + public class Example { + + // Make diamonds spoil into dirt and a dragon in 100 seconds, apples into jigsaws in 35 seconds + public static void attachSpoilables() { + SpoilageBehaviour.builder() + .ticks(20*100) + .result(Items.DIRT) + .result(EntityType.ENDER_DRAGON) + .build().attachTo(Items.DIAMOND); + SpoilableBehaviour.builder() + .ticks(20*35) + .result(Items.JIGSAW) + .build().attachTo(Items.APPLE); + } + + public void getAndSetValuesAndStuff(ItemStack stack) { + ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack); + // If spoilable is null, it means the stack cannot spoil + if (spoilable != null) { + // Get amount of ticks until a completely fresh stack spoils + long totalTicks = spoilable.getSpoilTicks(stack); + // Get amount of ticks until this stack spoils (may be more than the previous value in some cases) + long ticksRemaining = spoilable.getTicksUntilSpoiled(stack); + // Get the stack this stack spoils into + ItemStack spoilResult = spoilable.spoilResult(stack); + // Get whether this stack should START spoiling + boolean shouldStartSpoiling = spoilable.shouldSpoil(stack); + // Get the amount of ticks until this stack spoils (may be more than spoilable.getSpoilTicks(stack)) + spoilable.setTicksUntilSpoiled(stack, 12345); + // Freeze the spoiling progress of this stack + spoilable.freezeSpoiling(stack); + // Unfreeze the spoiling progress of this stack + spoilable.unfreezeSpoiling(stack); + } + } + + public void makeStackStartSpoiling(ItemStack stack, Level level, BlockPos pos) { + // If for some reason the stack still hasn't started spoiling, you can start the spoiling progress using this + // That may happen if it is the result of a non-GT recipe and not a crafting result for example + SpoilUtils.update(stack, new SpoilContext(level, pos)); + } + + public void disableFrozenAndNonFrozenEquality() { + // If you want the player to have frozen stacks in their inventory, do this + // A side effect of this is that filtering by ticks remaining until spoiled will no longer work + SpoilUtils.FROZEN_EQUALITY = false; + } + } + ``` + +!!! warning "Items may spoil in other mods' filters (even if they are phantom slots)." + +### Frozen stacks + +To freeze a stack's spoiling progress, you can use `spoilable.freezeSpoiling(stack)`, and `spoilable.unfreezeSpoiling(stack)` +to unfreeze. A frozen stack's freshness will never be changed unless `spoilable.setTicksUntilSpoiled(stack, value)` is called. +Currently, a stack is frozen only if it is in a phantom slot (in a GregTech filter). +!!! warning "`ItemHandlerHelper.canItemStacksStack` behaviour is completely different for spoilables" + + `ItemHandlerHelper.canItemStacksStack` returns `true` if: + + - Both items are not frozen and: + - They are the same item + - They have the same NBT + - **Their ticks until spoiling are averaged before the equality check** + - One of the items is frozen and: + - They are the same item + - They have the same non-spoilage-related NBT + - **One of them MAY be not frozen, and they will still be equal if `ISpoilableItem.FROZEN_EQUALITY` is `true`** + - **Their ticks until spoiling are NOT modified in any way in this method** + + !!! info "The following only applies if `ISpoilableItem.FROZEN_EQUALITY` is `true`:" + That means that frozen and non-frozen spoilables may stack, this is done mostly to make filtering by remaining ticks possible. + **Please prevent the player from having direct access to frozen stacks, as they could use them to bypass the spoiling system entirely.** + +### Spoilables in recipes + +If a GT recipe that does not have spoilable ingredients outputs a spoilable, it is outputted at full freshness. +If a GT recipe that has spoilable ingredients outputs a spoilable, it outputs it at the freshness level equal to the average +freshness of the ingredients. This can be overridden by setting `keepSpoilingProgress` (a parameter of the GTRecipe) to `false`. +

+Results of crafts in a crafting table are always outputted fully fresh. + +!!! note + + Items will spoil in machine inputs, and there's no way to automatically remove items from inputs. diff --git a/docs/content/Modpacks/Recipes/Adding-and-Removing-Recipes.md b/docs/content/Modpacks/Recipes/Adding-and-Removing-Recipes.md index c8d1e5a807f..21581c7d49c 100644 --- a/docs/content/Modpacks/Recipes/Adding-and-Removing-Recipes.md +++ b/docs/content/Modpacks/Recipes/Adding-and-Removing-Recipes.md @@ -141,7 +141,9 @@ to distinguish them from other recipes in the same machine with similar ingredie running or all at once at recipe start/end. Set to true with `.perTick(true)` to make the recipe builder consider any following input/output calls as per-tick. Remember to set the value to false with `.perTick(false)` after the calls you intend to be per-tick, to prevent behaviour you don't want! - + - `.keepSpoilingProgress()`: + If set to true, spoilable outputs' freshness will depend on the recipe's inputs' freshness (default).
+ If set to false, spoilable outputs of this recipe will always be crafted completely fresh. ### The Research System diff --git a/src/generated/resources/assets/gtceu/lang/en_ud.json b/src/generated/resources/assets/gtceu/lang/en_ud.json index 892d9e3836c..c7ef4e4dfe3 100644 --- a/src/generated/resources/assets/gtceu/lang/en_ud.json +++ b/src/generated/resources/assets/gtceu/lang/en_ud.json @@ -1749,6 +1749,7 @@ "config.gtceu.option.allowDrumsInputFluidsFromOutputSide": "ǝpıSʇndʇnOɯoɹℲspınןℲʇnduIsɯnɹᗡʍoןןɐ", "config.gtceu.option.allowedImageDomains": "suıɐɯoᗡǝbɐɯIpǝʍoןןɐ", "config.gtceu.option.animationTime": "ǝɯı⟘uoıʇɐɯıuɐ", + "config.gtceu.option.aprilFoolsMode": "ǝpoWsןooℲןıɹdɐ", "config.gtceu.option.arcRecyclingYield": "pןǝıʎbuıןɔʎɔǝᴚɔɹɐ", "config.gtceu.option.armorHud": "pnHɹoɯɹɐ", "config.gtceu.option.batchDuration": "uoıʇɐɹnᗡɥɔʇɐq", @@ -3863,13 +3864,22 @@ "gtceu.tool_action.wrench.set_facing": "buıɔɐℲ ʇǝs oʇ ɥɔuǝɹM ǝs∩8§", "gtceu.tooltip.computer_monitor_config": "ɐʇɐp uoıʇɐɹnbıɟuoɔ ɹǝʌoɔ ɹoʇıuoɯ ɹǝʇndɯoɔ buıɹoʇS", "gtceu.tooltip.computer_monitor_data": "%s :ɐʇɐp buıɹoʇS", + "gtceu.tooltip.creation_tick": "%d ʞɔıʇ pןɹoʍɹǝʌo uo pǝʇɐǝɹƆ", "gtceu.tooltip.fluid_pipe_hold_shift": "oɟuI ʇuǝɯuıɐʇuoƆ pınןℲ ʍoɥs oʇ ⟘ℲIHS pןoHㄥ§", "gtceu.tooltip.hold_ctrl": "oɟuı ǝɹoɯ ɹoɟ Ꞁᴚ⟘Ɔ pןoHㄥ§", "gtceu.tooltip.hold_shift": "oɟuı ǝɹoɯ ɹoɟ ⟘ℲIHS pןoHㄥ§", + "gtceu.tooltip.item_handler_data": "%s :ɐʇɐp ɹǝןpuɐH", + "gtceu.tooltip.item_handler_source": "%s :ǝɔɹnos ɹǝןpuɐH", + "gtceu.tooltip.location": ")%d '%d '%d( %s :uoıʇɐɔoꞀ", + "gtceu.tooltip.location_entity": "%s :ʎʇıʇuƎ", + "gtceu.tooltip.location_slot": "%s :ʇoןS", "gtceu.tooltip.player_bind": "%s :ɹǝʎɐןd oʇ punoᗺ", "gtceu.tooltip.potion.each": "ɹ§buıuǝddɐɥ ɟo ǝɔuɐɥɔㄥ§ %s%% ɹ§ɐ ɥʇıʍ sʞɔıʇㄥ§ %s ɹ§ɹoɟㄥ§ %s %s", "gtceu.tooltip.potion.header": ":sʇɔǝɟɟǝ suıɐʇuoƆ9§", "gtceu.tooltip.proxy_bind": "%s %s %s ʇɐ ɹǝɟɟnᗺ uɹǝʇʇɐԀ ɐ oʇ buıpuıᗺɟ§", + "gtceu.tooltip.spoil_time_remaining": "%s :sןıods ןıʇun ǝɯı⟘", + "gtceu.tooltip.spoil_time_total": "%s :ǝɯıʇ ןıods ןɐʇo⟘", + "gtceu.tooltip.spoils_into": "%s :oʇuı sןıodS", "gtceu.tooltip.status.trinary.false": "ǝsןɐℲ", "gtceu.tooltip.status.trinary.true": "ǝnɹ⟘", "gtceu.tooltip.status.trinary.unknown": "uʍouʞu∩", diff --git a/src/generated/resources/assets/gtceu/lang/en_us.json b/src/generated/resources/assets/gtceu/lang/en_us.json index 35d0223ccab..a3a40021344 100644 --- a/src/generated/resources/assets/gtceu/lang/en_us.json +++ b/src/generated/resources/assets/gtceu/lang/en_us.json @@ -1749,6 +1749,7 @@ "config.gtceu.option.allowDrumsInputFluidsFromOutputSide": "allowDrumsInputFluidsFromOutputSide", "config.gtceu.option.allowedImageDomains": "allowedImageDomains", "config.gtceu.option.animationTime": "animationTime", + "config.gtceu.option.aprilFoolsMode": "aprilFoolsMode", "config.gtceu.option.arcRecyclingYield": "arcRecyclingYield", "config.gtceu.option.armorHud": "armorHud", "config.gtceu.option.batchDuration": "batchDuration", @@ -3863,13 +3864,22 @@ "gtceu.tool_action.wrench.set_facing": "§8Use Wrench to set Facing", "gtceu.tooltip.computer_monitor_config": "Storing computer monitor cover configuration data", "gtceu.tooltip.computer_monitor_data": "Storing data: %s", + "gtceu.tooltip.creation_tick": "Created on overworld tick %d", "gtceu.tooltip.fluid_pipe_hold_shift": "§7Hold SHIFT to show Fluid Containment Info", "gtceu.tooltip.hold_ctrl": "§7Hold CTRL for more info", "gtceu.tooltip.hold_shift": "§7Hold SHIFT for more info", + "gtceu.tooltip.item_handler_data": "Handler data: %s", + "gtceu.tooltip.item_handler_source": "Handler source: %s", + "gtceu.tooltip.location": "Location: %s (%d, %d, %d)", + "gtceu.tooltip.location_entity": "Entity: %s", + "gtceu.tooltip.location_slot": "Slot: %s", "gtceu.tooltip.player_bind": "Bound to player: %s", "gtceu.tooltip.potion.each": "%s %s §7for§r %s §7ticks with a§r %s%% §7chance of happening§r", "gtceu.tooltip.potion.header": "§6Contains effects:", "gtceu.tooltip.proxy_bind": "§fBinding to a Pattern Buffer at %s %s %s", + "gtceu.tooltip.spoil_time_remaining": "Time until spoils: %s", + "gtceu.tooltip.spoil_time_total": "Total spoil time: %s", + "gtceu.tooltip.spoils_into": "Spoils into: %s", "gtceu.tooltip.status.trinary.false": "False", "gtceu.tooltip.status.trinary.true": "True", "gtceu.tooltip.status.trinary.unknown": "Unknown", diff --git a/src/main/java/com/gregtechceu/gtceu/api/GTValues.java b/src/main/java/com/gregtechceu/gtceu/api/GTValues.java index 7f2da34899c..2af98b454ed 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/GTValues.java +++ b/src/main/java/com/gregtechceu/gtceu/api/GTValues.java @@ -1,5 +1,7 @@ package com.gregtechceu.gtceu.api; +import com.gregtechceu.gtceu.config.ConfigHolder; + import net.minecraft.util.RandomSource; import java.time.LocalDate; @@ -272,6 +274,7 @@ public static int[] tiersBetween(int minInclusive, int maxInclusive) { 0x7EC3C4, 0x7EB07E, 0xBF74C0, 0x0B5CFE, 0x914E91, 0x488748, 0x8C0000, 0x2828F5 }; // Main colour for each tier + @SuppressWarnings("DataFlowIssue") public static final int[] VCM = new int[] { DARK_GRAY.getColor(), GRAY.getColor(), @@ -308,6 +311,7 @@ public static int[] tiersBetween(int minInclusive, int maxInclusive) { public static boolean HT = false; public static BooleanSupplier FOOLS = () -> { + if (ConfigHolder.INSTANCE != null && ConfigHolder.INSTANCE.client.aprilFoolsMode) return true; var now = LocalDate.now(); return now.getMonth() == Month.APRIL && now.getDayOfMonth() == 1; }; diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapability.java index 66a0aa5c6b3..c23813b6486 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapability.java @@ -1,5 +1,6 @@ package com.gregtechceu.gtceu.api.capability; +import com.gregtechceu.gtceu.api.item.component.ISpoilableItem; import com.gregtechceu.gtceu.api.machine.feature.multiblock.IMaintenanceMachine; import net.minecraftforge.common.capabilities.Capability; @@ -32,6 +33,8 @@ public class GTCapability { .get(new CapabilityToken<>() {}); public static final Capability CAPABILITY_MONITOR_COMPONENT = CapabilityManager .get(new CapabilityToken<>() {}); + public static final Capability CAPABILITY_SPOILABLE_ITEM = CapabilityManager + .get(new CapabilityToken<>() {}); public static final Capability CAPABILITY_MEDICAL_CONDITION_TRACKER = CapabilityManager .get(new CapabilityToken<>() {}); @@ -51,5 +54,6 @@ public static void register(RegisterCapabilitiesEvent event) { event.register(IMedicalConditionTracker.class); event.register(IHazardParticleContainer.class); event.register(IMonitorComponent.class); + event.register(ISpoilableItem.class); } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapabilityHelper.java b/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapabilityHelper.java index ddd096e5fab..d0f71773674 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapabilityHelper.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/GTCapabilityHelper.java @@ -1,5 +1,6 @@ package com.gregtechceu.gtceu.api.capability; +import com.gregtechceu.gtceu.api.item.component.ISpoilableItem; import com.gregtechceu.gtceu.api.machine.feature.multiblock.IMaintenanceMachine; import net.minecraft.core.BlockPos; @@ -126,4 +127,9 @@ private static T getBlockEntityCapability(Capability capability, Level le public static IMedicalConditionTracker getMedicalConditionTracker(@NotNull Entity entity) { return entity.getCapability(GTCapability.CAPABILITY_MEDICAL_CONDITION_TRACKER, null).resolve().orElse(null); } + + @Nullable + public static ISpoilableItem getSpoilable(ItemStack stack) { + return stack.getCapability(GTCapability.CAPABILITY_SPOILABLE_ITEM).resolve().orElse(null); + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java index 6d2a54badc1..0722bb0d16d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/RecipeCapability.java @@ -38,6 +38,9 @@ public abstract class RecipeCapability { public static final Codec, List>> CODEC = new DispatchedMapCodec<>( RecipeCapability.DIRECT_CODEC, RecipeCapability::contentCodec); + public static final Codec, List>> INGREDIENT_CODEC = new DispatchedMapCodec<>( + RecipeCapability.DIRECT_CODEC, + RecipeCapability::ingredientCodec); public static final Comparator> COMPARATOR = Comparator.comparingInt(o -> o.sortIndex); public final String name; @@ -59,6 +62,10 @@ public static Codec> contentCodec(RecipeCapability capability) return Content.codec(capability).listOf(); } + public static Codec> ingredientCodec(RecipeCapability capability) { + return Content.ingredientCodec(capability).listOf(); + } + public Tag contentToNbt(Object value) { return this.serializer.toNbt(this.of(value)); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java index ea051ee971f..268e328fa12 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java @@ -53,7 +53,7 @@ public FilterHandler(ISyncManaged container) { this.container = container; } - protected abstract F loadFilter(ItemStack filterItem); + public abstract F loadFilter(ItemStack filterItem); protected abstract F getEmptyFilter(); diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java index cb269e0aa23..e80eb4bee83 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java @@ -11,7 +11,7 @@ static FilterHandler item(ISyncManaged container) { return new FilterHandler<>(container) { @Override - protected ItemFilter loadFilter(ItemStack filterItem) { + public ItemFilter loadFilter(ItemStack filterItem) { return ItemFilter.loadFilter(filterItem); } @@ -31,7 +31,7 @@ static FilterHandler fluid(ISyncManaged container) { return new FilterHandler<>(container) { @Override - protected FluidFilter loadFilter(ItemStack filterItem) { + public FluidFilter loadFilter(ItemStack filterItem) { return FluidFilter.loadFilter(filterItem); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/SimpleItemFilter.java b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/SimpleItemFilter.java index 408a6656687..58cb218d22e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/SimpleItemFilter.java +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/SimpleItemFilter.java @@ -1,10 +1,11 @@ package com.gregtechceu.gtceu.api.cover.filter; +import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; import com.gregtechceu.gtceu.api.gui.GuiTextures; import com.gregtechceu.gtceu.api.gui.widget.PhantomSlotWidget; import com.gregtechceu.gtceu.api.gui.widget.ToggleButtonWidget; +import com.gregtechceu.gtceu.api.item.component.ISpoilableItem; import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; -import com.gregtechceu.gtceu.utils.GTUtil; import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; @@ -47,6 +48,20 @@ public static SimpleItemFilter loadFilter(ItemStack itemStack) { return loadFilter(itemStack.getOrCreateTag(), filter -> itemStack.setTag(filter.saveFilter())); } + public static SimpleItemFilter forItems(boolean ignoreNbt, ItemStack... items) { + SimpleItemFilter filter = new SimpleItemFilter(); + filter.ignoreNbt = ignoreNbt; + filter.isBlackList = false; + int i = 0; + for (ItemStack item : items) { + filter.matches[i] = item.copy(); + ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(filter.matches[i]); + if (spoilable != null) spoilable.freezeSpoiling(); + i++; + } + return filter; + } + private static SimpleItemFilter loadFilter(CompoundTag tag, Consumer itemWriter) { var handler = new SimpleItemFilter(); handler.itemWriter = itemWriter; @@ -158,7 +173,7 @@ public int getTotalConfiguredItemCount(ItemStack itemStack) { if (ignoreNbt && ItemStack.isSameItem(candidate, itemStack)) { totalCount += candidate.getCount(); } - if (!ignoreNbt && GTUtil.isSameItemSameTags(candidate, itemStack)) { + if (!ignoreNbt && ItemStack.isSameItemSameTags(candidate, itemStack)) { totalCount += candidate.getCount(); } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/events/ModifyMachineEvent.java b/src/main/java/com/gregtechceu/gtceu/api/events/ModifyMachineEvent.java new file mode 100644 index 00000000000..5d607316364 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/events/ModifyMachineEvent.java @@ -0,0 +1,18 @@ +package com.gregtechceu.gtceu.api.events; + +import com.gregtechceu.gtceu.api.registry.registrate.MachineBuilder; + +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.event.IModBusEvent; + +import lombok.Getter; + +public class ModifyMachineEvent extends Event implements IModBusEvent { + + @Getter + private final MachineBuilder builder; + + public ModifyMachineEvent(MachineBuilder builder) { + this.builder = builder; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PhantomSlotWidget.java b/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PhantomSlotWidget.java index 190de3930ff..c607249e8d4 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PhantomSlotWidget.java +++ b/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PhantomSlotWidget.java @@ -1,6 +1,8 @@ package com.gregtechceu.gtceu.api.gui.widget; import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; +import com.gregtechceu.gtceu.api.item.component.ISpoilableItem; import com.lowdragmc.lowdraglib.gui.editor.annotation.ConfigSetter; import com.lowdragmc.lowdraglib.gui.editor.annotation.Configurable; @@ -85,7 +87,10 @@ public PhantomSlotWidget setMaxStackSize(int stackSize) { public boolean mouseClicked(double mouseX, double mouseY, int button) { if (slotReference != null && isMouseOverElement(mouseX, mouseY) && gui != null) { if (isClientSideWidget && !gui.getModularUIContainer().getCarried().isEmpty()) { - slotReference.set(gui.getModularUIContainer().getCarried()); + ItemStack carried = gui.getModularUIContainer().getCarried().copy(); + ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(carried); + if (spoilable != null) spoilable.freezeSpoiling(); + slotReference.set(carried); } else if (button == 1 && clearSlotOnRightClick && !slotReference.getItem().isEmpty()) { slotReference.set(ItemStack.EMPTY); writeClientAction(2, buf -> {}); @@ -248,6 +253,8 @@ private void fillPhantomSlot(Slot slot, ItemStack stackHeld, int mouseButton) { stackSize = slot.getMaxStackSize(); } ItemStack phantomStack = stackHeld.copy(); + ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(phantomStack); + if (spoilable != null) spoilable.freezeSpoiling(); phantomStack.setCount(Math.min(maxStackSize, stackSize)); if (validator.test(phantomStack)) slot.set(phantomStack); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/IMergeableNBTSerializable.java b/src/main/java/com/gregtechceu/gtceu/api/item/IMergeableNBTSerializable.java new file mode 100644 index 00000000000..bc94eac78bd --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/item/IMergeableNBTSerializable.java @@ -0,0 +1,24 @@ +package com.gregtechceu.gtceu.api.item; + +import net.minecraft.nbt.Tag; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.capabilities.CapabilityDispatcher; +import net.minecraftforge.common.util.INBTSerializable; + +/** + * An interface for capability providers to implement if they need to store NBT data + * and have custom comparison logic in {@link ItemStack#areCapsCompatible(CapabilityDispatcher)}. + */ +public interface IMergeableNBTSerializable extends INBTSerializable { + + /** + * Called right before this capability provider is compared to a different one in + * {@link ItemStack#areCapsCompatible(CapabilityDispatcher)}. + * The other capability provider is guaranteed to have the same id as this one, but may be {@code null} if the other + * item does not have this + * capability. + * + * @param other the other capability provider + */ + void prepareForComparisonWith(INBTSerializable other); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/ISpoilableItemStackExtension.java b/src/main/java/com/gregtechceu/gtceu/api/item/ISpoilableItemStackExtension.java new file mode 100644 index 00000000000..72e76e7324a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/item/ISpoilableItemStackExtension.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.item; + +import net.minecraft.world.item.ItemStack; + +public interface ISpoilableItemStackExtension { + + void gtceu$setStack(ItemStack newStack); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/component/ISpoilableItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/component/ISpoilableItem.java new file mode 100644 index 00000000000..22426ce1d8f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/item/component/ISpoilableItem.java @@ -0,0 +1,160 @@ +package com.gregtechceu.gtceu.api.item.component; + +import com.gregtechceu.gtceu.api.gui.widget.PhantomSlotWidget; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.common.item.SpoilableItemStack; +import com.gregtechceu.gtceu.common.item.behavior.SpoilableBehavior; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.ItemHandlerHelper; + +import java.util.Optional; + +/** + * This is a capability! {@link Item} subclasses should not implement this directly! + *
+ * Spoilable items will, as the name implies, spoil (who could've thought). + * Due to Minecraft's limitations, items will only start spoiling only if: + *
    + *
  • It is in an {@link IItemHandler}, which is a capability of a {@link BlockEntity} that had + * {@link Level#getBlockEntity(BlockPos)} called (should cover most cases)
  • + *
  • It is an output of a {@link GTRecipe}
  • + *
  • It enters a player's inventory and gets ticked at least once
  • + *
  • It is dropped (exists as an entity)
  • + *
  • Any other mod calls {@link SpoilUtils#update} on the item
  • + *
+ * If you are a developer of a mod that adds any other way to obtain items, that doesn't involve + * any of the conditions above being true at any tick, consider adding compatibility with this feature :) + *
+ * Items that don't start spoiling will simply not have spoilable NBT, and as a result, won't spoil, + * until any of the above conditions become true. + *

+ * The only exception is if one of the stacks is frozen, in which case it works as normal, except that + * frozen stacks will be equal to non-frozen stacks if all else is equal. This is done to make filtering work correctly. + *

+ *

+ * Spoilable stacks will be frozen if they enter a {@link PhantomSlotWidget}, + * to prevent stacks spoiling in filters. If you are a developer of a mod that adds filters, consider calling + * {@link ISpoilableItem#freezeSpoiling()} on stacks entering these filters for compatibility :) + *

+ *

+ * Note that if an item is spoilable, it does not mean that it spoils in all cases, as you can override + * {@link ISpoilableItem#shouldSpoil()} + * in your own implementation of the {@link ISpoilableItem} interface. + *

+ *

+ * To make an item spoilable, you can simply use {@link SpoilableBehavior#attachTo(ItemLike)}. + *
+ * If you want to implement this interface yourself, please note that {@link SpoilableItemStack} + * calls a mixin method in its {@link ISpoilableItem#updateFreshness} implementation. + *

+ */ +public interface ISpoilableItem { + + /** + * Checks if this stack is supposed to already be spoiled, and spoils it into the + * {@link ISpoilableItem#spoilResult} + * + * @param createTag whether to start spoiling this stack if it didn't start spoiling yet (adds NBT) + */ + void updateFreshness(SpoilContext spoilContext, boolean createTag); + + /** + * Should return the amount of ticks that this item can stay fresh. + * The result of this method shouldn't be based on the freshness of the provided stack + */ + long getSpoilTicks(); + + /** + * @return the amount of ticks left until the provided {@link ItemStack} spoils. + * The ticks still reduce even when the item is unloaded, and only pause if the + * overworld time pauses, as all tick calculations are done with overworld tick time + * @see ISpoilableItem#setTicksUntilSpoiled(long) + */ + long getTicksUntilSpoiled(); + + /** + * Sets the amount of ticks left until the provided {@link ItemStack} spoils. + * This modifies the provided stack's NBT data. + * The provided value may be more than {@link ISpoilableItem#getSpoilTicks()} + * + * @see ISpoilableItem#getTicksUntilSpoiled() + */ + void setTicksUntilSpoiled(long value); + + /** + * Freezes the stack's spoiling progress until it is unfrozen by + * {@link ISpoilableItem#unfreezeSpoiling()}. + * Frozen stacks will NOT spoil, even if {@link ISpoilableItem#getTicksUntilSpoiled()} is {@code <= 0}. + * This method modifies the provided stack's NBT data. + * Calls to {@link ItemHandlerHelper#canItemStacksStack(ItemStack, ItemStack)} with a frozen stack as one of the + * arguments + * will check equality of both stacks' {@link ISpoilableItem#getTicksUntilSpoiled()} values, as well as all + * non-spoilage + * related tags and the equality of the item itself. + * + * @see ISpoilableItem#unfreezeSpoiling() + */ + void freezeSpoiling(); + + /** + * Unfreezes the stack's spoiling progress. If the stack's + * {@link ISpoilableItem#getTicksUntilSpoiled()} is {@code <= 0}, it will spoil + * immediately after this method call. + * This method modifies the provided stack's NBT data. + * + * @see ISpoilableItem#freezeSpoiling() + */ + void unfreezeSpoiling(); + + /** + * @return whether this stack's spoiling is frozen + */ + boolean isFrozen(); + + /** + * This function may have side effects (i.e. spawning an entity) when called with + * {@code simulate = false}. + * + * @return the stack to replace the provided stack with when it spoils + */ + ItemStack spoilResult(SpoilContext spoilContext, boolean simulate); + + /** + * Note: returning {@code false} in this method won't stop the item from spoiling if the spoiling NBT has already + * been initialized + */ + boolean shouldSpoil(); + + /** + * @return the tick on which this item started spoiling, might not actually be the creation tick in some cases + */ + long getCreationTick(); + + /** + * Sets the tick on which this item started spoiling, modifying its spoiling progress accordingly + * + * @param tick the value to set to + */ + void setCreationTick(long tick); + + /** + * Called when {@link ItemHandlerHelper#canItemStacksStack(ItemStack, ItemStack)} is called. + * If this returns an empty optional, {@link ItemHandlerHelper#canItemStacksStack(ItemStack, ItemStack)} will return + * its + * normal value, otherwise it will return the same value as this method. + *
+ * This exists mostly for custom spoilable merging logic. + * + * @return whether these two stacks should be considered equal + */ + default Optional isEqualTo(ItemStack other) { + return Optional.empty(); + }; +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/component/SpoilContext.java b/src/main/java/com/gregtechceu/gtceu/api/item/component/SpoilContext.java new file mode 100644 index 00000000000..d5764fb1a56 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/item/component/SpoilContext.java @@ -0,0 +1,206 @@ +package com.gregtechceu.gtceu.api.item.component; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.server.ServerLifecycleHooks; + +import lombok.With; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class represents the environment in which an item spoils, for example, + * the level and the block's position, or the entity. It also may include a way to get an + * {@link IItemHandler} (an instance of {@link ItemHandlerSource}) in which the item spoiled, and the slot number of the + * item. + * This info is used to, for example, spawn an entity when an item spoils. + */ +@With +public record SpoilContext(@Nullable Level level, + @Nullable BlockPos pos, + @Nullable Entity entity, + @Nullable ItemHandlerSource itemHandlerSource, + @Nullable CompoundTag itemHandlerData, + int slot) { + + /** + * @return the {@link Level} used to determine time to calculate spoilage progress (using + * {@link Level#getGameTime()}). + * This is usually the Overworld. If it is {@code null}, all spoilage updates are ignored. + */ + public static @Nullable Level getDefaultLevel() { + MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); + if (server == null) return null; + return server.overworld(); + } + + public SpoilContext() { + this((Level) null); + } + + public SpoilContext(@Nullable Level level) { + this(level, null); + } + + public SpoilContext(@Nullable Level level, @Nullable BlockPos pos) { + this(level, pos, null, null, null, -1); + } + + public SpoilContext(@NotNull Entity entity) { + this(entity.level(), entity.blockPosition(), entity, null, null, -1); + } + + public SpoilContext(@NotNull Player player, int slot) { + this(player.level(), player.blockPosition(), player, ItemHandlerSource.PLAYER_INVENTORY, null, slot); + } + + public SpoilContext(@NotNull MetaMachine machine) { + this(machine.getLevel(), machine.getBlockPos(), null, null, null, -1); + } + + public boolean isEmpty() { + return level == null; + } + + public @Nullable IItemHandler itemHandler() { + if (itemHandlerSource == null) return null; + return itemHandlerSource.getHandler(this); + } + + public SpoilContext withItemHandlerData(String key, Tag value) { + CompoundTag tag = itemHandlerData == null ? new CompoundTag() : itemHandlerData.copy(); + tag.put(key, value); + return this.withItemHandlerData(tag); + } + + public SpoilContext withItemHandlerSide(Direction side) { + if (side == null) return this.withItemHandlerSource(ItemHandlerSource.BLOCK_CAPABILITY); + return this.withItemHandlerSource(ItemHandlerSource.BLOCK_CAPABILITY) + .withItemHandlerData("side", StringTag.valueOf(side.getSerializedName())); + } + + public CompoundTag serializeNBT() { + CompoundTag tag = new CompoundTag(); + if (level != null) tag.putString("level", level.dimensionTypeId().location().toString()); + if (pos != null) tag.putLong("pos", pos.asLong()); + if (entity != null) tag.putInt("entity", entity.getId()); + if (slot != -1) tag.putInt("slot", slot); + if (itemHandlerSource != null) tag.putString("handlerSource", itemHandlerSource.getId().toString()); + if (itemHandlerData != null) tag.put("handlerData", itemHandlerData); + return tag; + } + + public static SpoilContext deserializeNBT(CompoundTag tag) { + SpoilContext ctx = new SpoilContext(); + if (tag.contains("level")) { + ctx = ctx.withLevel(ServerLifecycleHooks.getCurrentServer().getLevel(ResourceKey.create( + Registries.DIMENSION, + new ResourceLocation(tag.getString("level"))))); + } + if (tag.contains("pos")) { + ctx = ctx.withPos(BlockPos.of(tag.getLong("pos"))); + } + if (tag.contains("entity") && ctx.level != null) { + ctx = ctx.withEntity(ctx.level.getEntity(tag.getInt("entity"))); + } + if (tag.contains("slot")) { + ctx = ctx.withSlot(tag.getInt("slot")); + } + if (tag.contains("handlerSource")) { + ctx = ctx.withItemHandlerSource( + ItemHandlerSource.getById(new ResourceLocation(tag.getString("handlerSource")))); + } + if (tag.contains("handlerData")) { + ctx = ctx.withItemHandlerData(tag.getCompound("handlerData")); + } + return ctx; + } + + /** + * This class represents a way to get an {@link IItemHandler} from a {@link SpoilContext}, optionally + * using {@link SpoilContext#itemHandlerData}. This is used instead of a normal supplier, due to the fact that + * it is serializable. Note that new instances of this class should not be created dynamically. + *
+ * This class is basically equivalent to a serializable {@code Function}. + */ + public static abstract class ItemHandlerSource { + + private static final Map HANDLER_SOURCES = new HashMap<>(); + + /** + * Represents getting an item handler as a capability of a block, with an optional "side" key in + * {@link SpoilContext#itemHandlerData} + */ + public static final ItemHandlerSource BLOCK_CAPABILITY = new ItemHandlerSource(GTCEu.id("block_cap")) { + + @Override + protected @Nullable IItemHandler getHandler(SpoilContext ctx) { + if (ctx.level() == null || ctx.pos() == null || ctx.itemHandlerData() == null) return null; + CompoundTag tag = ctx.itemHandlerData(); + BlockEntity blockEntity = ctx.level().getBlockEntity(ctx.pos()); + if (blockEntity == null) return null; + if (!tag.contains("side")) + return blockEntity.getCapability(ForgeCapabilities.ITEM_HANDLER, null).resolve().orElse(null); + return blockEntity + .getCapability(ForgeCapabilities.ITEM_HANDLER, Direction.byName(tag.getString("side"))) + .resolve().orElse(null); + } + }; + + /** + * Represents getting an item handler as a player's inventory (used if {@link SpoilContext#entity} is a + * {@link Player}) + */ + public static final ItemHandlerSource PLAYER_INVENTORY = new ItemHandlerSource(GTCEu.id("player_inventory")) { + + @Override + protected @Nullable IItemHandler getHandler(SpoilContext ctx) { + if (ctx.entity instanceof Player player) { + return new CustomItemStackHandler(player.getInventory().items); + } else return null; + } + }; + + private static ItemHandlerSource getById(ResourceLocation id) { + return HANDLER_SOURCES.get(id); + } + + private final ResourceLocation id; + + public ItemHandlerSource(ResourceLocation id) { + this.id = id; + HANDLER_SOURCES.put(id, this); + } + + private ResourceLocation getId() { + return id; + } + + @Override + public String toString() { + return id.toString(); + } + + abstract protected @Nullable IItemHandler getHandler(SpoilContext ctx); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/component/SpoilUtils.java b/src/main/java/com/gregtechceu/gtceu/api/item/component/SpoilUtils.java new file mode 100644 index 00000000000..f0355daa490 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/item/component/SpoilUtils.java @@ -0,0 +1,67 @@ +package com.gregtechceu.gtceu.api.item.component; + +import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.items.IItemHandler; + +public class SpoilUtils { + + /** + * Initializes this ItemStack's spoilage timer if it wasn't initialized before. + * Should be called when it finishes crafting, for example. + */ + public static void update(ItemStack stack, SpoilContext spoilContext) { + ISpoilableItem spoilable = GTCapabilityHelper.getSpoilable(stack); + if (spoilable != null) spoilable.updateFreshness(spoilContext, true); + } + + public static void updateBlock(Level level, BlockPos pos) { + updateBlock(level.getBlockEntity(pos)); + } + + public static void updateBlock(BlockEntity blockEntity) { + for (Direction side : Direction.values()) updateBlock(blockEntity, side); + updateBlock(blockEntity, null); + } + + public static void updateBlock(BlockEntity blockEntity, Direction side) { + IItemHandler handler = blockEntity.getCapability(ForgeCapabilities.ITEM_HANDLER, side).resolve().orElse(null); + if (handler != null) { + SpoilContext ctx = new SpoilContext(blockEntity.getLevel(), blockEntity.getBlockPos()) + .withItemHandlerSide(side); + for (int slot = 0; slot < handler.getSlots(); slot++) { + update(handler.getStackInSlot(slot), ctx.withSlot(slot)); + } + } + } + + public static void spawnEntity(SpoilContext spoilContext, EntityType type, int count) { + if (spoilContext.level() instanceof ServerLevel level) { + BlockPos pos = null; + if (spoilContext.entity() != null) pos = spoilContext.entity().blockPosition(); + else if (spoilContext.pos() != null) pos = spoilContext.pos(); + if (pos != null && type != null) { + if (level.getBlockState(pos).isSuffocating(level, pos)) { + for (Direction direction : Direction.values()) { + BlockPos relative = pos.relative(direction); + if (!level.getBlockState(relative).isSuffocating(level, relative)) { + pos = relative; + break; + } + } + } + for (int i = 0; i < count; i++) type.spawn(level, pos, MobSpawnType.SPAWN_EGG); + } + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java index aaa3149cc0a..b8506ae7d7b 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java @@ -11,6 +11,7 @@ import com.gregtechceu.gtceu.api.sync_system.annotations.SyncToClient; import com.gregtechceu.gtceu.api.transfer.fluid.CustomFluidTank; import com.gregtechceu.gtceu.api.transfer.fluid.IFluidHandlerModifiable; +import com.gregtechceu.gtceu.common.data.GTRecipeCapabilities; import com.gregtechceu.gtceu.utils.GTTransferUtils; import net.minecraft.core.Direction; @@ -178,11 +179,15 @@ public List handleRecipeInner(IO io, GTRecipe recipe, List filter) { public void onContentsChanged() { isEmpty = null; + SpoilUtils.updateBlock(Objects.requireNonNull(machine.getLevel()), machine.getBlockPos()); syncDataHolder.markClientSyncFieldDirty("storage"); notifyListeners(); } @@ -158,11 +164,16 @@ public static List handleRecipe(IO io, GTRecipe recipe, List outputModifier = (recipe, object) -> {}; public final GTRecipeCategory recipeCategory; // Lazy fields, since we need the recipe EUt very often @Getter(lazy = true) @@ -61,6 +68,7 @@ public class GTRecipe implements net.minecraft.world.item.crafting.Recipe, List> inputs, @@ -76,10 +84,11 @@ public GTRecipe(GTRecipeType recipeType, @NotNull CompoundTag data, int duration, @NotNull GTRecipeCategory recipeCategory, - int groupColor) { + int groupColor, + RecipeSpoilageData spoilageData) { this(recipeType, null, inputs, outputs, tickInputs, tickOutputs, inputChanceLogics, outputChanceLogics, tickInputChanceLogics, tickOutputChanceLogics, - conditions, ingredientActions, data, duration, recipeCategory, groupColor); + conditions, ingredientActions, data, duration, recipeCategory, groupColor, spoilageData); } public GTRecipe(GTRecipeType recipeType, @@ -96,7 +105,8 @@ public GTRecipe(GTRecipeType recipeType, List ingredientActions, @NotNull CompoundTag data, int duration, - @NotNull GTRecipeCategory recipeCategory, int groupColor) { + @NotNull GTRecipeCategory recipeCategory, int groupColor, + RecipeSpoilageData spoilageData) { this.recipeType = recipeType; this.id = id; @@ -116,6 +126,7 @@ public GTRecipe(GTRecipeType recipeType, this.duration = duration; this.recipeCategory = (recipeCategory != GTRecipeCategory.DEFAULT) ? recipeCategory : recipeType.getCategory(); this.groupColor = groupColor; + this.spoilageData = spoilageData; } public GTRecipe copy() { @@ -133,7 +144,7 @@ public GTRecipe copy(ContentModifier modifier, boolean modifyDuration) { new HashMap<>(inputChanceLogics), new HashMap<>(outputChanceLogics), new HashMap<>(tickInputChanceLogics), new HashMap<>(tickOutputChanceLogics), new ArrayList<>(conditions), - new ArrayList<>(ingredientActions), data, duration, recipeCategory, groupColor); + new ArrayList<>(ingredientActions), data, duration, recipeCategory, groupColor, spoilageData.copy()); if (modifyDuration) { copied.duration = modifier.apply(this.duration); } @@ -252,4 +263,8 @@ public int hashCode() { public String toString() { return id.toString(); } + + public void mutateOutput(Object stack) { + if (this.outputModifier != null) outputModifier.accept(this, stack); + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipeSerializer.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipeSerializer.java index ef1462da6d8..4382256ce00 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipeSerializer.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/GTRecipeSerializer.java @@ -137,10 +137,12 @@ public GTRecipe fromNetwork(@NotNull ResourceLocation id, @NotNull FriendlyByteB GTRecipeType type = (GTRecipeType) BuiltInRegistries.RECIPE_TYPE.get(recipeType); GTRecipeCategory category = GTRegistries.RECIPE_CATEGORIES.get(categoryLoc); + RecipeSpoilageData spoilageData = RecipeSpoilageData.readFromNetwork(buf); + GTRecipe recipe = new GTRecipe(type, id, inputs, outputs, tickInputs, tickOutputs, inputChanceLogics, outputChanceLogics, tickInputChanceLogics, tickOutputChanceLogics, - conditions, ingredientActions, data, duration, category, groupColor); + conditions, ingredientActions, data, duration, category, groupColor, spoilageData); recipe.recipeCategory.addRecipe(recipe); @@ -208,14 +210,15 @@ private static Codec makeCodec(boolean isKubeLoaded) { CompoundTag.CODEC.optionalFieldOf("data", new CompoundTag()).forGetter(val -> val.data), ExtraCodecs.NON_NEGATIVE_INT.fieldOf("duration").forGetter(val -> val.duration), GTRegistries.RECIPE_CATEGORIES.codec().optionalFieldOf("category", GTRecipeCategory.DEFAULT).forGetter(val -> val.recipeCategory), - Codec.INT.optionalFieldOf("groupColor", -1).forGetter(val -> val.groupColor)) + Codec.INT.optionalFieldOf("groupColor", -1).forGetter(val -> val.groupColor), + RecipeSpoilageData.CODEC.fieldOf("spoilageData").forGetter(val -> val.spoilageData)) .apply(instance, (type, inputs, outputs, tickInputs, tickOutputs, inputChanceLogics, outputChanceLogics, tickInputChanceLogics, tickOutputChanceLogics, - conditions, data, duration, recipeCategory, groupColor) -> + conditions, data, duration, recipeCategory, groupColor, spoilageData) -> new GTRecipe(type, inputs, outputs, tickInputs, tickOutputs, inputChanceLogics, outputChanceLogics, tickInputChanceLogics, tickOutputChanceLogics, - conditions, List.of(), data, duration, recipeCategory, groupColor))); + conditions, List.of(), data, duration, recipeCategory, groupColor, spoilageData))); } else { return RecordCodecBuilder.create(instance -> instance.group( GTRegistries.RECIPE_TYPES.codec().fieldOf("type").forGetter(val -> val.recipeType), @@ -236,7 +239,8 @@ private static Codec makeCodec(boolean isKubeLoaded) { CompoundTag.CODEC.optionalFieldOf("data", new CompoundTag()).forGetter(val -> val.data), ExtraCodecs.NON_NEGATIVE_INT.fieldOf("duration").forGetter(val -> val.duration), GTRegistries.RECIPE_CATEGORIES.codec().optionalFieldOf("category", GTRecipeCategory.DEFAULT).forGetter(val -> val.recipeCategory), - Codec.INT.optionalFieldOf("groupColor", -1).forGetter(val -> val.groupColor)) + Codec.INT.optionalFieldOf("groupColor", -1).forGetter(val -> val.groupColor), + RecipeSpoilageData.CODEC.fieldOf("spoilageData").forGetter(val -> val.spoilageData)) .apply(instance, GTRecipe::new)); } // spotless:on diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/RecipeSpoilageData.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/RecipeSpoilageData.java new file mode 100644 index 00000000000..909bdfe9a20 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/RecipeSpoilageData.java @@ -0,0 +1,61 @@ +package com.gregtechceu.gtceu.api.recipe; + +import com.gregtechceu.gtceu.api.capability.recipe.RecipeCapability; + +import net.minecraft.network.FriendlyByteBuf; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@AllArgsConstructor +public class RecipeSpoilageData { + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + RecipeCapability.INGREDIENT_CODEC.optionalFieldOf("consumedInputs", new HashMap<>()) + .forGetter(RecipeSpoilageData::getConsumedInputs), + Codec.BOOL.fieldOf("keepSpoilingProgress").forGetter(RecipeSpoilageData::keepSpoilingProgress)) + .apply(instance, RecipeSpoilageData::new)); + + /** + * Populated after the inputs are already consumed, but the recipe didn't start yet. + * For use in {@link GTRecipe#outputModifier} + */ + @Getter(AccessLevel.PRIVATE) + private Map, List> consumedInputs; + @Accessors(fluent = true) + @Getter + private boolean keepSpoilingProgress; + + public RecipeSpoilageData(boolean keepSpoilingProgress) { + this(new HashMap<>(), keepSpoilingProgress); + } + + public RecipeSpoilageData copy() { + return new RecipeSpoilageData(new HashMap<>(consumedInputs), keepSpoilingProgress); + } + + public void addConsumedInput(RecipeCapability recipeCapability, T t) { + // noinspection unchecked why can't I just add whatever I want to a List + ((List) consumedInputs.computeIfAbsent(recipeCapability, cap -> new ArrayList<>())).add(t); + } + + @Unmodifiable + public List getConsumedInputs(RecipeCapability recipeCapability) { + // noinspection unchecked + return (List) consumedInputs.getOrDefault(recipeCapability, List.of()); + } + + public static RecipeSpoilageData readFromNetwork(FriendlyByteBuf buf) { + return new RecipeSpoilageData(new HashMap<>(), buf.readBoolean()); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/content/Content.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/content/Content.java index 59015b68e6e..1736bd0177b 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/content/Content.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/content/Content.java @@ -43,9 +43,13 @@ public Content(Object content, int chance, int maxChance, int tierChanceBoost) { this.tierChanceBoost = fixBoost(tierChanceBoost); } + public static Codec ingredientCodec(RecipeCapability capability) { + return capability.serializer.codec(); + } + public static Codec codec(RecipeCapability capability) { return RecordCodecBuilder.create(instance -> instance.group( - capability.serializer.codec().fieldOf("content").forGetter(val -> capability.of(val.content)), + ingredientCodec(capability).fieldOf("content").forGetter(val -> capability.of(val.content)), ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("chance", ChanceLogic.getMaxChancedValue()) .forGetter(val -> val.chance), ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("maxChance", ChanceLogic.getMaxChancedValue()) diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ModifierFunction.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ModifierFunction.java index 35067a36053..98ea13142d3 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ModifierFunction.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/modifier/ModifierFunction.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; /** * Represents a function that accepts a GTRecipe and returns a modified version of the GTRecipe, or null. @@ -132,6 +133,7 @@ final class FunctionBuilder { private ContentModifier outputModifier = ContentModifier.IDENTITY; private ContentModifier tickInputModifier = ContentModifier.IDENTITY; private ContentModifier tickOutputModifier = ContentModifier.IDENTITY; + private BiConsumer actualOutputModifier = (recipe, object) -> {}; private final List> addedConditions = new ArrayList<>(); public FunctionBuilder() {} @@ -159,6 +161,11 @@ public FunctionBuilder durationMultiplier(double multiplier) { return this; } + public FunctionBuilder modifyItemOutputs(BiConsumer 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 entityType) { + return result(() -> entityType); + } + + public Builder result(Supplier> entityType) { + SpoilResultProvider previousResult = result; + Function previousTooltip = tooltip; + return result((stack, spoilContext, simulate) -> { + if (!simulate) { + EntityType type = entityType.get(); + SpoilUtils.spawnEntity(spoilContext, type, stack.getCount()); + } + return previousResult.getSpoilResult(stack, spoilContext, simulate); + }).tooltip(stack -> { + EntityType 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);