Skip to content
25 changes: 25 additions & 0 deletions docs/content/Modpacks/Changes/v7.5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,31 @@ Previously, lamps were not useable with the terminal in multiblocks. There are n
The predicate to use all lamps is: `Predicates.anyLamp()`
The predicate to use a specific color is: `Predicates.lampsByColor(DyeColor.DYE_COLOR)`. Where DYE_COLOR is the name of the color you want.

## Machine Recipe Failure Diagnostics
Machines now provide detailed diagnostics explaining why recipes fail to start.

Failure reasons are displayed in:
- the sidebar of simple machines
- the UI of multiblock machines
- the Jade tooltip for all machines

You are supposed to replace `ModifierFunction.NULL` with `ModifierFunction.cancel(reason)` to tell the machine why the recipe fails to start.
```patch
- return ModifierFunction.NULL;
+ return ModifierFunction.cancel(Component.translatable("gtceu.recipe_modifier.xxx"));
```

Also, if you cancel a recipe in `IRecipeLogicMachine#beforeWorking()`, you are supposed to add some custom reason info like:
```
@Override
public boolean beforeWorking(@Nullable GTRecipe recipe) {
if(xxx){
RecipeLogic.putFailureReason(this, recipe, Component.literal("Machine spirit is displeased"));
return false
}
return true;
}
```
## Painted Output Buses/Hatches
For some time, Spray Paint cans could be used to paint Input and Output Buses/Hatches on multiblock machines.
Version 7.0.0 added the feature that Painted Input Buses/Hatches of different colors would not share their ingredients with
Expand Down
7 changes: 7 additions & 0 deletions src/generated/resources/assets/gtceu/lang/en_ud.json
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,7 @@
"config.gtceu.option.casingsPerCraft": "ʇɟɐɹƆɹǝԀsbuısɐɔ",
"config.gtceu.option.cleanMultiblocks": "sʞɔoןqıʇןnWuɐǝןɔ",
"config.gtceu.option.client": "ʇuǝıןɔ",
"config.gtceu.option.coloredMaterialBlockOutline": "ǝuıןʇnOʞɔoןᗺןɐıɹǝʇɐWpǝɹoןoɔ",
"config.gtceu.option.coloredTieredMachineOutline": "ǝuıןʇnOǝuıɥɔɐWpǝɹǝı⟘pǝɹoןoɔ",
"config.gtceu.option.coloredWireOutline": "ǝuıןʇnOǝɹıMpǝɹoןoɔ",
"config.gtceu.option.compat": "ʇɐdɯoɔ",
Expand Down Expand Up @@ -3762,8 +3763,14 @@
"gtceu.recipe_logic.insufficient_out": "sʇndʇnO ʇuǝıɔıɟɟnsuI",
"gtceu.recipe_logic.no_capabilities": "sǝıʇıןıqɐdɐƆ ou sɐɥ ǝuıɥɔɐW",
"gtceu.recipe_logic.no_contents": "sʇuǝʇuoƆ ou sɐɥ ǝdıɔǝᴚ",
"gtceu.recipe_logic.recipe_waiting": " :buıʇıɐM ǝdıɔǝᴚ",
"gtceu.recipe_logic.setup_fail": " :ǝdıɔǝɹ dnʇǝs oʇ ןıɐℲ",
"gtceu.recipe_memory_widget.tooltip.0": "pıɹb buıʇɟɐɹɔ ǝɥʇ oʇuı ǝdıɔǝɹ sıɥʇ ʇnduı ʎןןɐɔıʇɐɯoʇnɐ oʇ ʞɔıןɔ ʇɟǝꞀㄥ§",
"gtceu.recipe_memory_widget.tooltip.1": "ǝdıɔǝɹ sıɥʇ ʞɔoןun/ʞɔoן oʇ ʞɔıןɔ ʇɟıɥSㄥ§",
"gtceu.recipe_modifier.coil_temperature_too_low": "ʍoꞀ oo⟘ ǝɹnʇɐɹǝdɯǝ⟘ ןıoƆ",
"gtceu.recipe_modifier.default_fail": "ןıɐℲ ɹǝıɟıpoW ǝdıɔǝᴚ",
"gtceu.recipe_modifier.insufficient_eu_to_start_fusion": "uoıʇɔɐǝᴚ uoısnℲ ǝʇɐıʇıuI oʇ ʎbɹǝuƎ ʇuǝıɔıɟɟnsuI",
"gtceu.recipe_modifier.insufficient_voltage": "ʍoꞀ oo⟘ ɹǝı⟘ ǝbɐʇןoΛ",
"gtceu.recipe_type.show_recipes": "sǝdıɔǝᴚ ʍoɥS",
"gtceu.rei.group.potion_fluids": "spınןℲ uoıʇoԀ",
"gtceu.research_station": "uoıʇɐʇS ɥɔɹɐǝsǝᴚ",
Expand Down
7 changes: 7 additions & 0 deletions src/generated/resources/assets/gtceu/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,7 @@
"config.gtceu.option.casingsPerCraft": "casingsPerCraft",
"config.gtceu.option.cleanMultiblocks": "cleanMultiblocks",
"config.gtceu.option.client": "client",
"config.gtceu.option.coloredMaterialBlockOutline": "coloredMaterialBlockOutline",
"config.gtceu.option.coloredTieredMachineOutline": "coloredTieredMachineOutline",
"config.gtceu.option.coloredWireOutline": "coloredWireOutline",
"config.gtceu.option.compat": "compat",
Expand Down Expand Up @@ -3762,8 +3763,14 @@
"gtceu.recipe_logic.insufficient_out": "Insufficient Outputs",
"gtceu.recipe_logic.no_capabilities": "Machine has no Capabilities",
"gtceu.recipe_logic.no_contents": "Recipe has no Contents",
"gtceu.recipe_logic.recipe_waiting": "Recipe Waiting: ",
"gtceu.recipe_logic.setup_fail": "Fail to setup recipe: ",
"gtceu.recipe_memory_widget.tooltip.0": "§7Left click to automatically input this recipe into the crafting grid",
"gtceu.recipe_memory_widget.tooltip.1": "§7Shift click to lock/unlock this recipe",
"gtceu.recipe_modifier.coil_temperature_too_low": "Coil Temperature Too Low",
"gtceu.recipe_modifier.default_fail": "Recipe Modifier Fail",
"gtceu.recipe_modifier.insufficient_eu_to_start_fusion": "Insufficient Energy to Initiate Fusion Reaction",
"gtceu.recipe_modifier.insufficient_voltage": "Voltage Tier Too Low",
"gtceu.recipe_type.show_recipes": "Show Recipes",
"gtceu.rei.group.potion_fluids": "Potion Fluids",
"gtceu.research_station": "Research Station",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,19 @@ public Builder addCustomProgressLine(RecipeLogic recipeLogic) {
return this;
}

public Builder addRecipeFailReasonLine(RecipeLogic recipeLogic) {
if (!isStructureFormed || !recipeLogic.isIdle())
return this;
var reasons = recipeLogic.getFailureReasons();
if (!reasons.isEmpty()) {
textList.add(Component.translatable("gtceu.recipe_logic.setup_fail").withStyle(ChatFormatting.RED));
for (var reason : reasons) {
textList.add(Component.literal(" - ").append(reason));
}
}
return this;
}

public Builder addBatchModeLine(boolean batchEnabled, int batchAmount) {
if (batchEnabled && batchAmount > 0) {
Component runs = Component.literal(FormattingUtil.formatNumbers(batchAmount))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public void addDisplayText(List<Component> textList) {
.addBatchModeLine(isBatchEnabled(), batchParallels)
.addWorkingStatusLine()
.addProgressLine(recipeLogic)
.addRecipeFailReasonLine(recipeLogic)
.addOutputLines(recipeLogic.getLastRecipe());
getDefinition().getAdditionalDisplay().accept(this, textList);
IDisplayUIMachine.super.addDisplayText(textList);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.gregtechceu.gtceu.api.recipe.ActionResult;
import com.gregtechceu.gtceu.api.recipe.GTRecipe;
import com.gregtechceu.gtceu.api.recipe.RecipeHelper;
import com.gregtechceu.gtceu.api.recipe.modifier.ModifierFunction;
import com.gregtechceu.gtceu.api.registry.GTRegistries;
import com.gregtechceu.gtceu.api.sound.AutoReleasedSound;
import com.gregtechceu.gtceu.common.cover.MachineControllerCover;
Expand Down Expand Up @@ -80,10 +81,18 @@ public enum Status implements StringRepresentable {
@UpdateListener(methodName = "onActiveSynced")
protected boolean isActive;

@Getter
@Nullable
@Persisted
@DescSynced
private Component waitingReason = null;

@Getter
@DescSynced
protected final List<Component> failureReasons = new ArrayList<>();

@Getter
protected final Map<GTRecipe, Component> failureReasonMap = new HashMap<>();
/**
* unsafe, it may not be found from {@link RecipeManager}. Do not index it.
*/
Expand Down Expand Up @@ -158,6 +167,8 @@ public void resetRecipeLogic() {
duration = 0;
isActive = false;
lastFailedMatches = null;
waitingReason = null;
failureReasons.clear();
if (status != Status.SUSPEND) {
setStatus(Status.IDLE);
}
Expand Down Expand Up @@ -225,7 +236,10 @@ public void serverTick() {
// No recipes available and the machine wants to unsubscribe until notified
unsubscribe = true;
}

if (isIdle()) {
failureReasons.clear();
failureReasons.addAll(failureReasonMap.values());
}
if (unsubscribe && subscription != null) {
subscription.unsubscribe();
subscription = null;
Expand All @@ -249,6 +263,8 @@ public boolean checkMatchedRecipeAvailable(GTRecipe match) {
var recipeMatch = checkRecipe(modified);
if (recipeMatch.isSuccess()) {
setupRecipe(modified);
} else {
putFailureReason(this, match, recipeMatch.reason());
}
if (lastRecipe != null && getStatus() == Status.WORKING) {
lastOriginRecipe = match;
Expand Down Expand Up @@ -321,13 +337,16 @@ protected void regressRecipe() {

public void findAndHandleRecipe() {
lastFailedMatches = null;

// try to execute last recipe if possible
if (!recipeDirty && lastRecipe != null && checkRecipe(lastRecipe).isSuccess()) {
GTRecipe recipe = lastRecipe;
lastRecipe = null;
lastOriginRecipe = null;
setupRecipe(recipe);
} else { // try to find and handle a new recipe
} else {
// try to find and handle a new recipe
failureReasonMap.clear();
lastRecipe = null;
lastOriginRecipe = null;
handleSearchingRecipes(searchRecipe());
Expand Down Expand Up @@ -383,6 +402,7 @@ public void setupRecipe(GTRecipe recipe) {
if (lastRecipe != null && !recipe.equals(lastRecipe)) {
chanceCaches.clear();
}
failureReasonMap.clear();
recipeDirty = false;
lastRecipe = recipe;
setStatus(Status.WORKING);
Expand Down Expand Up @@ -522,9 +542,6 @@ public void onRecipeFinish() {
setupRecipe(lastRecipe);
} else {
setStatus(Status.IDLE);
if (recipeCheck.io() != IO.IN || recipeCheck.capability() == EURecipeCapability.CAP) {
waitingReason = recipeCheck.reason();
}
consecutiveRecipes = 0;
progress = 0;
duration = 0;
Expand Down Expand Up @@ -590,23 +607,26 @@ public void updateSound() {

@Override
public IGuiTexture getFancyTooltipIcon() {
if (waitingReason != null) {
if (showFancyTooltip()) {
return GuiTextures.INSUFFICIENT_INPUT;
}
return IGuiTexture.EMPTY;
}

@Override
public List<Component> getFancyTooltip() {
if (waitingReason != null) {
if (isWaiting() && waitingReason != null) {
return List.of(waitingReason);
}
if (isIdle() && !failureReasons.isEmpty()) {
return failureReasons;
}
return Collections.emptyList();
}

@Override
public boolean showFancyTooltip() {
return waitingReason != null;
return waitingReason != null || !failureReasons.isEmpty();
}

protected Map<RecipeCapability<?>, Object2IntMap<?>> makeChanceCaches() {
Expand Down Expand Up @@ -667,4 +687,21 @@ public void loadCustomPersistedData(@NotNull CompoundTag tag) {
});
tag.put("chance_cache", chanceCache);
}

public static void putFailureReason(Object machine, GTRecipe recipe, Component reason) {
if (machine instanceof IRecipeLogicMachine rlm) {
putFailureReason(rlm.getRecipeLogic(), recipe, reason);
}
}

public static void putFailureReason(RecipeLogic logic, GTRecipe recipe, Component reason) {
var map = logic.getFailureReasonMap();
if (map.containsKey(recipe)) {
if (reason != ModifierFunction.DEFAULT_FAILURE) {
map.put(recipe, reason);
}
} else {
map.put(recipe, reason);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import com.gregtechceu.gtceu.api.recipe.chance.logic.ChanceLogic;
import com.gregtechceu.gtceu.api.recipe.content.Content;

import net.minecraft.network.chat.Component;

import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import lombok.Getter;
Expand Down Expand Up @@ -119,7 +121,11 @@ private void fillContentMatchList(Map<RecipeCapability<?>, List<Content>> entrie
private ActionResult handleContents() {
if (recipeContents.isEmpty()) return ActionResult.SUCCESS;
if (!capabilityProxies.containsKey(io)) {
return ActionResult.FAIL_NO_CAPABILITIES;
return ActionResult.fail(
Component.translatable("gtceu.recipe_logic.no_capabilities")
.append(Component.literal(": "))
.append(Component.translatable(io.tooltip)),
null, io);
}

List<RecipeHandlerList> handlers = capabilityProxies.getOrDefault(io, Collections.emptyList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import com.gregtechceu.gtceu.api.recipe.content.ContentModifier;
import com.gregtechceu.gtceu.api.recipe.ingredient.EnergyStack;

import net.minecraft.network.chat.Component;

import lombok.Setter;
import lombok.experimental.Accessors;
import org.jetbrains.annotations.Contract;
Expand All @@ -35,6 +37,7 @@
@FunctionalInterface
public interface ModifierFunction {

// TODO: Add reasons for any NULL ModifierFunction (replace them with cancel)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this finished within this codebase?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not yet. There are still multiple usages of ModifierFunction.NULL in the codebase. Some of them were not replaced because I am not sure about reasons. I have only replaced the more common cases where the fail reason is explicit.

/**
* Use this static to denote that the recipe should be cancelled
*/
Expand All @@ -44,6 +47,21 @@ public interface ModifierFunction {
*/
ModifierFunction IDENTITY = ModifierFunction.builder().build();

static ModifierFunction cancel(Component reason) {
return new ModifierFunction() {

@Override
public @Nullable GTRecipe apply(@NotNull GTRecipe recipe) {
return null;
}

@Override
public Component getFailReason() {
return reason;
}
};
}

/**
* Applies this modifier to the passed recipe
*
Expand Down Expand Up @@ -79,6 +97,12 @@ private GTRecipe applySafe(@Nullable GTRecipe recipe) {
return apply(recipe);
}

static final Component DEFAULT_FAILURE = Component.translatable("gtceu.recipe_modifier.default_fail");

default Component getFailReason() {
return DEFAULT_FAILURE;
}

/**
* Creates a FunctionBuilder to easily build a ModifierFunction that modifies parts of a recipe.
* <p>
Expand Down
Loading