Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,13 @@ public Builder addCustomProgressLine(RecipeLogic recipeLogic) {
public Builder addRecipeFailReasonLine(RecipeLogic recipeLogic) {
if (!isStructureFormed || !recipeLogic.isIdle())
return this;
var reasons = recipeLogic.getFailureReasons();
if (!reasons.isEmpty()) {
var reason = recipeLogic.getBestFailureReason();
if (reason != null) {
textList.add(Component.translatable("gtceu.recipe_logic.setup_fail").withStyle(ChatFormatting.RED));
for (var reason : reasons) {
var recipe = recipeLogic.getBestFailureRecipe();
if (recipe != null) {
textList.add(Component.literal(" - ").append(recipe).append(": ").append(reason));

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.

Component.literal(" - ") could be made into a static final field to save some allocations. Same for Component.literal(": ")

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.

The dash can't since it's the first component in the list and we mutate it (unless you have a way where we can that saves allocations, but idts), will update with the colon though

} else {
textList.add(Component.literal(" - ").append(reason));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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.api.sync_system.ClassSyncData;
Expand Down Expand Up @@ -91,12 +90,19 @@ public enum Status implements StringRepresentable {
@SyncToClient
private Component waitingReason = null;

@Nullable
@Getter
@SyncToClient
protected Component bestFailureReason;

/** Display name (id) of the recipe {@link #bestFailureReason} belongs to. */
@Nullable
@Getter
@SyncToClient
protected final List<Component> failureReasons = new ArrayList<>();
protected Component bestFailureRecipe;

@Getter
protected final Map<GTRecipe, Component> failureReasonMap = new HashMap<>();
protected double bestFailureScore = Double.NEGATIVE_INFINITY;
/**
* unsafe, it may not be found from {@link RecipeManager}. Do not index it.
*/
Expand All @@ -105,10 +111,7 @@ public enum Status implements StringRepresentable {
@SaveField
@SyncToClient
protected GTRecipe lastRecipe;
@Getter
@SaveField
@SyncToClient
protected int consecutiveRecipes = 0; // Consecutive recipes that have been run

/**
* safe, it is the origin recipe before {@link IRecipeLogicMachine#fullModifyRecipe(GTRecipe)}'
* which can be found
Expand All @@ -118,6 +121,12 @@ public enum Status implements StringRepresentable {
@Getter
@SaveField
protected GTRecipe lastOriginRecipe;

@Getter
@SaveField
@SyncToClient
protected int consecutiveRecipes = 0; // Consecutive recipes that have been run

@SaveField
@Getter
@SyncToClient
Expand Down Expand Up @@ -176,7 +185,7 @@ public void resetRecipeLogic() {
isActive = false;
lastFailedMatches = null;
waitingReason = null;
failureReasons.clear();
clearFailureReason();
if (status != Status.SUSPEND) {
setStatus(Status.IDLE);
}
Expand Down Expand Up @@ -250,10 +259,6 @@ 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 @@ -278,7 +283,7 @@ public boolean checkMatchedRecipeAvailable(GTRecipe match) {
if (recipeMatch.isSuccess()) {
setupRecipe(modified);
} else {
putFailureReason(this, match, recipeMatch.reason());
recordFailureReason(match, recipeMatch.reason(), recipeMatch.score());
}
if (lastRecipe != null && getStatus() == Status.WORKING) {
lastOriginRecipe = match;
Expand Down Expand Up @@ -326,6 +331,8 @@ public void handleRecipeWorking() {
if (getMachine() instanceof MultiblockControllerMachine && !preventPowerFail) {
runAttempt = 0;
setStatus(Status.SUSPEND);
// Keep showing why it suspended (setStatus cleared waitingReason on leaving WAITING).
recordFailureReason(lastRecipe, handleTick.reason(), Double.POSITIVE_INFINITY);
}
}
runDelay = runAttempt * 60;
Expand All @@ -351,20 +358,26 @@ public Iterator<GTRecipe> searchRecipe() {

public void findAndHandleRecipe() {
lastFailedMatches = null;
clearFailureReason();

// 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
failureReasonMap.clear();
lastRecipe = null;
lastOriginRecipe = null;
handleSearchingRecipes(searchRecipe());
GTRecipe last = lastRecipe;
if (!recipeDirty && last != null) {
var lastCheck = checkRecipe(last);
if (lastCheck.isSuccess()) {
lastRecipe = null;
lastOriginRecipe = null;
setupRecipe(last);
recipeDirty = false;
return;
}
recordFailureReason(last, lastCheck.reason(), Double.POSITIVE_INFINITY);
}

// try to find and handle a new recipe
lastRecipe = null;
lastOriginRecipe = null;
handleSearchingRecipes(searchRecipe());
recipeDirty = false;
}

Expand Down Expand Up @@ -415,7 +428,7 @@ public void setupRecipe(GTRecipe recipe) {
if (lastRecipe != null && !recipe.equals(lastRecipe)) {
chanceCaches.clear();
}
failureReasonMap.clear();
clearFailureReason();
recipeDirty = false;
lastRecipe = recipe;
setStatus(Status.WORKING);
Expand Down Expand Up @@ -482,6 +495,7 @@ public boolean isWorkingEnabled() {
@Override
public void setWorkingEnabled(boolean isWorkingAllowed) {
if (!isWorkingAllowed && getStatus() == Status.IDLE) {
clearFailureReason();
setStatus(Status.SUSPEND);
} else {
setSuspendAfterFinish(!isWorkingAllowed);
Expand Down Expand Up @@ -623,17 +637,24 @@ public IGuiTexture getFancyTooltipIcon() {
@Override
public List<Component> getFancyTooltip() {
if (isWaiting() && waitingReason != null) {
Component name = recipeDisplayName(lastRecipe);
if (name != null) {
return List.of(name, waitingReason);
}
return List.of(waitingReason);
}
if (isIdle() && !failureReasons.isEmpty()) {
return failureReasons;
if ((isIdle() || isSuspend()) && bestFailureReason != null) {
if (bestFailureRecipe != null) {
return List.of(bestFailureRecipe, bestFailureReason);
}
return List.of(bestFailureReason);
}
return Collections.emptyList();
}

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

protected IdentityHashMap<RecipeCapability<?>, Object2IntMap<?>> makeChanceCaches() {
Expand Down Expand Up @@ -708,13 +729,32 @@ public static void putFailureReason(Object machine, GTRecipe recipe, Component r
}

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);
logic.recordFailureReason(recipe, reason, 0.0);
}

/**
* Record a failure reason as the one to display, along with the recipe it belongs to. It becomes the displayed
* reason only if it's usable and beats the current best score.
*/
protected void recordFailureReason(@Nullable GTRecipe recipe, @Nullable Component reason, double score) {
if (reason != null && !reason.getString().isBlank()) {
if (score > bestFailureScore) {
bestFailureScore = score;
bestFailureReason = reason;
bestFailureRecipe = recipeDisplayName(recipe);
}
} else {
map.put(recipe, reason);
}
}

/** The display name shown for a recipe in failure tooltips: its id, or {@code null} if the recipe is null. */
protected static @Nullable Component recipeDisplayName(@Nullable GTRecipe recipe) {
return recipe == null ? null : Component.literal(recipe.id.toString());
}

/** Forget the currently-displayed failure reason. */
protected void clearFailureReason() {
bestFailureReason = null;
bestFailureRecipe = null;
bestFailureScore = Double.NEGATIVE_INFINITY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@
/**
* @param isSuccess is action success
* @param reason if fail, fail reason
* @param score how close the recipe was to succeeding, in {@code [0, 1]}. {@code 1} means fully satisfied
* (success), {@code 0} means nothing matched. Used to pick the most-relevant failure reason to
* display. Failures that don't measure contents (conditions, missing capabilities) use {@code 0}.
*/
public record ActionResult(boolean isSuccess, @Nullable Component reason, @Nullable RecipeCapability<?> capability,
@Nullable IO io) {
@Nullable IO io, double score) {

public final static ActionResult SUCCESS = new ActionResult(true, null, null, null);
public final static ActionResult FAIL_NO_REASON = new ActionResult(false, null, null, null);
public final static ActionResult SUCCESS = new ActionResult(true, null, null, null, 1.0);
public final static ActionResult FAIL_NO_REASON = new ActionResult(false, null, null, null, 0.0);
public final static ActionResult PASS_NO_CONTENTS = new ActionResult(true,
Component.translatable("gtceu.recipe_logic.no_contents"), null, null);
Component.translatable("gtceu.recipe_logic.no_contents"), null, null, 1.0);
public final static ActionResult FAIL_NO_CAPABILITIES = new ActionResult(false,
Component.translatable("gtceu.recipe_logic.no_capabilities"), null, null);
Component.translatable("gtceu.recipe_logic.no_capabilities"), null, null, 0.0);

public static ActionResult fail(@Nullable Component component, @Nullable RecipeCapability<?> capability, IO io) {
return new ActionResult(false, component, capability, io);
return fail(component, capability, io, 0.0);
}

public static ActionResult fail(@Nullable Component component, @Nullable RecipeCapability<?> capability, IO io,
double score) {
return new ActionResult(false, component, capability, io, score);
}

public Component reason() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ public static ActionResult handleRecipe(IRecipeCapabilityHolder holder, GTRecipe
}
String key = "gtceu.recipe_logic.insufficient_" + (io == IO.IN ? "in" : "out");
return ActionResult.fail(Component.translatable(key)
.append(": ").append(result.capability().getName()), result.capability(), io);
.append(": ").append(result.capability().getName()), result.capability(), io, result.score());
}

public static ActionResult matchContents(IRecipeCapabilityHolder holder, GTRecipe recipe) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -43,6 +44,10 @@ public class RecipeRunner {
@Getter
private int groupColor;

/** Highest closeness score seen across handler-group attempts, and the leftover map that produced it. */
Comment thread
YoungOnionMC marked this conversation as resolved.
private double bestScore = 0;
private @Nullable Map<RecipeCapability<?>, List<Object>> bestLeftover;

public RecipeRunner(GTRecipe recipe, IO io, boolean isTick,
IRecipeCapabilityHolder holder, Map<RecipeCapability<?>, Object2IntMap<?>> chanceCaches,
boolean simulated) {
Expand Down Expand Up @@ -150,9 +155,15 @@ private ActionResult handleContents() {
}
}
if (io == IO.OUT) {
if (hasAnyNonVoidingContents(res)) continue;
if (hasAnyNonVoidingContents(res)) {
recordLeftover(res);
continue;
}
} else if (io == IO.IN) {
if (!res.isEmpty()) continue;
if (!res.isEmpty()) {
recordLeftover(res);
continue;
}
}
if (!simulated) {
// Actually consume the contents of this handler and also all the bypassed handlers
Expand Down Expand Up @@ -202,9 +213,15 @@ private ActionResult handleContents() {
}

if (io == IO.OUT) {
if (hasAnyNonVoidingContents(copiedRecipeContents)) continue;
if (hasAnyNonVoidingContents(copiedRecipeContents)) {
recordLeftover(copiedRecipeContents);
continue;
}
} else if (io == IO.IN) {
if (!copiedRecipeContents.isEmpty()) continue;
if (!copiedRecipeContents.isEmpty()) {
recordLeftover(copiedRecipeContents);
continue;
}
}
if (simulated) return ActionResult.SUCCESS;
// Start actually removing items.
Expand Down Expand Up @@ -235,7 +252,16 @@ private ActionResult handleContents() {
entry.getValue().clear();
}
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
return ActionResult.fail(null, entry.getKey(), io);
RecipeCapability<?> failedCap = entry.getKey();
if (simulated && bestLeftover != null) {
for (var leftoverEntry : bestLeftover.entrySet()) {
if (leftoverEntry.getValue() != null && !leftoverEntry.getValue().isEmpty() &&
leftoverEntry.getKey() != null) {
failedCap = leftoverEntry.getKey();
}
}
}
return ActionResult.fail(null, failedCap, io, bestScore);
}
}

Expand All @@ -251,7 +277,33 @@ private ActionResult handleContents() {
return ActionResult.PASS_NO_CONTENTS;
}

return ActionResult.FAIL_NO_REASON;
return ActionResult.fail(null, null, io, bestScore);
}

/**
* Records a failed handler-group attempt's leftover map if it's the closest match seen so far. Higher score wins;
* ties keep the earlier attempt. The retained map identifies which capabilities the closest attempt was short on.
*/
private void recordLeftover(Map<RecipeCapability<?>, List<Object>> leftover) {
double score = scoreLeftover(leftover);
if (bestLeftover == null || score > bestScore) {
bestScore = score;
bestLeftover = leftover;
}
}

private double scoreLeftover(Map<RecipeCapability<?>, List<Object>> leftover) {
double sum = 0;
int caps = 0;
for (var entry : searchRecipeContents.entrySet()) {
int total = entry.getValue().size();
if (total <= 0) continue;
caps++;
var leftList = leftover.get(entry.getKey());
int leftCount = leftList == null ? 0 : Math.min(leftList.size(), total);
sum += (double) (total - leftCount) / total;
}
return caps == 0 ? 1.0 : sum / caps;
}

private boolean hasAnyNonVoidingContents(Map<RecipeCapability<?>, List<Object>> contents) {
Expand Down
Loading
Loading