diff --git a/src/main/java/org/cyclops/integratedscripting/IntegratedScripting.java b/src/main/java/org/cyclops/integratedscripting/IntegratedScripting.java index b1281162d..d97174697 100644 --- a/src/main/java/org/cyclops/integratedscripting/IntegratedScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/IntegratedScripting.java @@ -37,6 +37,7 @@ import org.cyclops.integratedscripting.command.CommandTestScript; import org.cyclops.integratedscripting.component.DataComponentDiskIdConfig; import org.cyclops.integratedscripting.core.client.model.ScriptingVariableModelProviders; +import org.cyclops.integratedscripting.core.evaluate.ScriptEvaluationStatuses; import org.cyclops.integratedscripting.core.evaluate.ScriptVariableFacadeHandler; import org.cyclops.integratedscripting.core.packageddependencies.PackagedDependenciesLoader; import org.cyclops.integratedscripting.core.language.LanguageHandlerRegistry; @@ -128,6 +129,7 @@ protected void onServerStopping(ServerStoppingEvent event) { } this.scriptingData = null; EvaluationExceptionResolutionHelpers.reset(); + ScriptEvaluationStatuses.reset(); } @SubscribeEvent diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index 8fab0583e..826d5b8c0 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -20,6 +20,7 @@ import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.StringUtil; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; @@ -556,7 +557,19 @@ protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { Component bindingLabel = getBindingLabel(); if (bindingLabel != null) { int right = getErrorX() - 2; - guiGraphics.drawString(font, bindingLabel, right - font.width(bindingLabel), getErrorY() + 2, 4210752, false); + int left = right - font.width(bindingLabel); + guiGraphics.drawString(font, bindingLabel, left, getErrorY() + 2, 4210752, false); + + // Show the last evaluation of the bound card + if (getMenu().getCardMember() != null && pendingDialog == null + && mouseX >= this.leftPos + left && mouseX < this.leftPos + right + && mouseY >= this.topPos + getErrorY() + 1 && mouseY < this.topPos + getErrorY() + 2 + font.lineHeight) { + List lines = Lists.newArrayList(); + for (MutableComponent line : getMenu().getCardEvaluation()) { + lines.addAll(font.split(line, 200)); + } + this.setTooltipForNextRenderPass(lines); + } } displayErrors.drawForeground(guiGraphics.pose(), getMenu().getReadErrors(), getErrorX(), getErrorY(), mouseX, mouseY, this, this.leftPos, this.topPos); diff --git a/src/main/java/org/cyclops/integratedscripting/core/evaluate/ScriptEvaluationStatuses.java b/src/main/java/org/cyclops/integratedscripting/core/evaluate/ScriptEvaluationStatuses.java new file mode 100644 index 000000000..b7d6407b9 --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/core/evaluate/ScriptEvaluationStatuses.java @@ -0,0 +1,107 @@ +package org.cyclops.integratedscripting.core.evaluate; + +import com.google.common.collect.Lists; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.tuple.Triple; +import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; +import org.cyclops.integratedscripting.IntegratedScripting; +import org.cyclops.integratedscripting.core.network.ScriptingData; + +import javax.annotation.Nullable; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The last evaluation results of script members, so they can be shown while editing. + * @author rubensworks + */ +public class ScriptEvaluationStatuses { + + private static final int MAX_VALUE_LENGTH = 100; + private static final Map, Status> STATUSES = new ConcurrentHashMap<>(); + + public static void recordValue(int disk, Path path, String member, IValue value) { + Status status = getOrCreate(disk, path, member); + status.value = value; + status.readError = null; + } + + public static void recordReadError(int disk, Path path, String member, MutableComponent error) { + Status status = getOrCreate(disk, path, member); + status.value = null; + status.readError = error; + // Read errors are also created by the exception factory, so don't show them twice + status.callError = null; + } + + public static void recordCallError(int disk, Path path, String member, MutableComponent error) { + getOrCreate(disk, path, member).callError = error; + } + + private static Status getOrCreate(int disk, Path path, String member) { + String script = getScript(disk, path); + Status status = STATUSES.get(Triple.of(disk, path, member)); + if (status == null || !Objects.equals(status.script, script)) { + status = new Status(script); + STATUSES.put(Triple.of(disk, path, member), status); + } + return status; + } + + @Nullable + private static String getScript(int disk, Path path) { + // Scripting data is not available outside of a running server, such as in unit tests + ScriptingData scriptingData = IntegratedScripting._instance == null ? null : IntegratedScripting._instance.scriptingData; + return scriptingData == null ? null : scriptingData.getScripts(disk).get(path); + } + + /** + * @return Lines describing the last evaluation of the member since its script last changed. + */ + public static List describe(int disk, Path path, String member) { + Status status = STATUSES.get(Triple.of(disk, path, member)); + List lines = Lists.newArrayList(); + if (status == null || !Objects.equals(status.script, getScript(disk, path)) || (status.value == null && status.readError == null)) { + lines.add(Component.translatable("gui.integratedscripting.evaluation.none").withStyle(ChatFormatting.GRAY)); + return lines; + } + if (status.readError != null) { + lines.add(Component.translatable("gui.integratedscripting.evaluation.error", status.readError.copy()).withStyle(ChatFormatting.RED)); + } else { + String value = status.value.getType().toCompactString(status.value).getString(); + if (value.length() > MAX_VALUE_LENGTH) { + value = value.substring(0, MAX_VALUE_LENGTH) + "…"; + } + lines.add(Component.translatable("gui.integratedscripting.evaluation.value", Component.literal(value).withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY)); + } + if (status.callError != null) { + lines.add(Component.translatable("gui.integratedscripting.evaluation.call_error", status.callError.copy()).withStyle(ChatFormatting.RED)); + } + return lines; + } + + public static void reset() { + STATUSES.clear(); + } + + private static class Status { + @Nullable + private final String script; + @Nullable + private IValue value; + @Nullable + private MutableComponent readError; + @Nullable + private MutableComponent callError; + + private Status(@Nullable String script) { + this.script = script; + } + } + +} diff --git a/src/main/java/org/cyclops/integratedscripting/core/evaluate/ScriptVariable.java b/src/main/java/org/cyclops/integratedscripting/core/evaluate/ScriptVariable.java index 1b0c1d7ff..379c22b7a 100644 --- a/src/main/java/org/cyclops/integratedscripting/core/evaluate/ScriptVariable.java +++ b/src/main/java/org/cyclops/integratedscripting/core/evaluate/ScriptVariable.java @@ -44,6 +44,17 @@ public IValueType getType() { @Override public IValue getValue() throws EvaluationException { + try { + IValue value = this.evaluate(); + ScriptEvaluationStatuses.recordValue(this.disk, this.path, this.member, value); + return value; + } catch (EvaluationException e) { + ScriptEvaluationStatuses.recordReadError(this.disk, this.path, this.member, e.getErrorMessage()); + throw e; + } + } + + protected IValue evaluate() throws EvaluationException { if (this.lastEvaluationException != null) { throw this.lastEvaluationException; } diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java b/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java index 997f90eff..1766fa08a 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/ScriptHelpers.java @@ -8,6 +8,7 @@ import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeOperator; import org.cyclops.integratedscripting.GeneralConfig; import org.cyclops.integratedscripting.api.evaluate.translation.IEvaluationExceptionFactory; +import org.cyclops.integratedscripting.core.evaluate.ScriptEvaluationStatuses; import org.cyclops.integratedscripting.core.packageddependencies.UnsafeHelper; import org.cyclops.integratedscripting.evaluate.translation.ValueTranslators; import org.graalvm.polyglot.*; @@ -144,9 +145,12 @@ public static IEvaluationExceptionFactory getDummyEvaluationExceptionFactory() { public static IEvaluationExceptionFactory getEvaluationExceptionFactory(int disk, Path path, String member) { EvaluationExceptionResolutionHelpers.expungeStaleEvaluationExceptions(); - return message -> EvaluationExceptionResolutionHelpers.resolveOnScriptChange( - new EvaluationException(Component.translatable("script.integratedscripting.error.script_exec", member, path.toString(), disk, message)), - disk, path); + return message -> { + ScriptEvaluationStatuses.recordCallError(disk, path, member, message); + return EvaluationExceptionResolutionHelpers.resolveOnScriptChange( + new EvaluationException(Component.translatable("script.integratedscripting.error.script_exec", member, path.toString(), disk, message)), + disk, path); + }; } } diff --git a/src/main/java/org/cyclops/integratedscripting/gametest/GameTestsScripts.java b/src/main/java/org/cyclops/integratedscripting/gametest/GameTestsScripts.java index bb61adbdd..4738b45b9 100644 --- a/src/main/java/org/cyclops/integratedscripting/gametest/GameTestsScripts.java +++ b/src/main/java/org/cyclops/integratedscripting/gametest/GameTestsScripts.java @@ -6,6 +6,8 @@ import net.minecraft.gametest.framework.GameTestAssertException; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.contents.TranslatableContents; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.neoforged.neoforge.gametest.GameTestHolder; @@ -21,12 +23,14 @@ import org.cyclops.integratedscripting.IntegratedScripting; import org.cyclops.integratedscripting.Reference; import org.cyclops.integratedscripting.api.network.IScriptingData; +import org.cyclops.integratedscripting.core.evaluate.ScriptEvaluationStatuses; import org.cyclops.integratedscripting.core.network.ScriptingNetworkHelpers; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import static org.cyclops.integrateddynamics.gametest.GameTestHelpersIntegratedDynamics.*; import static org.cyclops.integratedscripting.gametest.GameTestHelpersIntegratedScripting.createBasicNetwork; @@ -192,6 +196,58 @@ public void testScriptsDisplayScriptInfiniteLoop(GameTestHelper helper) { }); } + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testScriptsEvaluationStatus(GameTestHelper helper) { + GameTestHelpersIntegratedScripting.NetworkPositions positions = createBasicNetwork(helper, POS); + Path path = Path.of("script0.js"); + ScriptingNetworkHelpers.getScriptingData().setScript(positions.diskId(), path, "const abc = 3", IScriptingData.ChangeLocation.MEMORY); + helper.assertTrue(getEvaluationStatusKey(positions.diskId(), path, "abc").equals("gui.integratedscripting.evaluation.none"), "Script must not be evaluated yet"); + + ItemStack variableScript = createVariableForScript(helper.getLevel(), positions.diskId(), path, "abc"); + placeVariableInDisplayPanel(helper.getLevel(), positions.displayPanel(), variableScript); + + helper.startSequence() + .thenWaitUntil(() -> { + helper.assertTrue(getEvaluationStatusKey(positions.diskId(), path, "abc").equals("gui.integratedscripting.evaluation.value"), "Value must be recorded"); + helper.assertTrue(ScriptEvaluationStatuses.describe(positions.diskId(), path, "abc").get(0).getString().contains("3"), "Value must be 3"); + }) + .thenExecute(() -> ScriptingNetworkHelpers.getScriptingData().setScript(positions.diskId(), path, "const abc = doesNotExist();", IScriptingData.ChangeLocation.MEMORY)) + .thenWaitUntil(() -> helper.assertTrue(getEvaluationStatusKey(positions.diskId(), path, "abc").equals("gui.integratedscripting.evaluation.error"), "Error must be recorded")) + .thenSucceed(); + } + + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testScriptsEvaluationStatusCallError(GameTestHelper helper) { + GameTestHelpersIntegratedScripting.NetworkPositions positions = createBasicNetwork(helper, POS); + Path path = Path.of("script0.js"); + ScriptingNetworkHelpers.getScriptingData().setScript(positions.diskId(), path, "function abc(a, b) { throw new Error('boom ' + (a + b)); }", IScriptingData.ChangeLocation.MEMORY); + + // Apply the function in a display panel + ItemStack variableScript = createVariableForScript(helper.getLevel(), positions.diskId(), path, "abc"); + ItemStack variableConst1 = createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, ValueTypeInteger.ValueInteger.of(3)); + ItemStack variableConst2 = createVariableForValue(helper.getLevel(), ValueTypes.INTEGER, ValueTypeInteger.ValueInteger.of(8)); + positions.variableStore().getInventory().setItem(0, variableScript); + positions.variableStore().getInventory().setItem(1, variableConst1); + positions.variableStore().getInventory().setItem(2, variableConst2); + ItemStack variableApplied = createVariableForOperator(helper.getLevel(), Operators.OPERATOR_APPLY_2, new int[]{ + getVariableFacade(helper.getLevel(), variableScript).getId(), + getVariableFacade(helper.getLevel(), variableConst1).getId(), + getVariableFacade(helper.getLevel(), variableConst2).getId() + }); + placeVariableInDisplayPanel(helper.getLevel(), positions.displayPanel(), variableApplied); + + helper.succeedWhen(() -> { + List lines = ScriptEvaluationStatuses.describe(positions.diskId(), path, "abc"); + helper.assertTrue(lines.size() == 2, "The value and call error must be described"); + helper.assertTrue(((TranslatableContents) lines.get(1).getContents()).getKey().equals("gui.integratedscripting.evaluation.call_error"), "Call error must be recorded"); + helper.assertTrue(lines.get(1).getString().contains("boom 11"), "Call error must contain the thrown message"); + }); + } + + private static String getEvaluationStatusKey(int disk, Path path, String member) { + return ((TranslatableContents) ScriptEvaluationStatuses.describe(disk, path, member).get(0).getContents()).getKey(); + } + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) public void testScriptsDisplayScriptErrorResolvedAfterExternalFix(GameTestHelper helper) { GameTestHelpersIntegratedScripting.NetworkPositions positions = createBasicNetwork(helper, POS); diff --git a/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java index 21ce2e97a..736dcf3f9 100644 --- a/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/inventory/container/ContainerTerminalScripting.java @@ -40,6 +40,7 @@ import org.cyclops.integratedscripting.api.language.ScriptSyntaxError; import org.cyclops.integratedscripting.api.network.IScriptingData; import org.cyclops.integratedscripting.api.network.IScriptingNetwork; +import org.cyclops.integratedscripting.core.evaluate.ScriptEvaluationStatuses; import org.cyclops.integratedscripting.core.evaluate.ScriptVariableFacadeHandler; import org.cyclops.integratedscripting.core.item.ScriptVariableFacade; import org.cyclops.integratedscripting.core.language.LanguageHandlers; @@ -80,6 +81,7 @@ public class ContainerTerminalScripting extends InventoryContainer implements ID private final int activeDiskId; private final int activeScriptPathId; private final int selectionId; + private final int cardEvaluationId; @Nullable private BiConsumer remoteScriptChangeListener; private final Map, ScriptSyntaxError> syntaxErrors = Maps.newHashMap(); @@ -119,6 +121,7 @@ public ContainerTerminalScripting(int id, Inventory playerInventory, setActiveDisk(this.availableDisks.isEmpty() ? -1 : this.availableDisks.getInt(0)); this.activeScriptPathId = getNextValueId(); this.selectionId = getNextValueId(); + this.cardEvaluationId = getNextValueId(); } @@ -235,9 +238,21 @@ public void broadcastChanges() { }); this.checkActiveScriptSyntax(); + + // Send the last evaluation of the bound card + IScriptVariableFacade cardFacade = getCardFacade(); + ValueNotifierHelpers.setValue(this, cardEvaluationId, cardFacade == null ? Collections.emptyList() + : ScriptEvaluationStatuses.describe(cardFacade.getDisk(), cardFacade.getPath(), cardFacade.getMember())); } } + /** + * @return Lines describing the last evaluation of the bound card. + */ + public List getCardEvaluation() { + return ValueNotifierHelpers.getValueTextComponentList(this, cardEvaluationId); + } + /** * Check the active script for syntax errors once it hasn't changed for a while, and send the result to the client. */ @@ -427,13 +442,19 @@ public boolean isReservedWord(String word) { */ @Nullable public String getCardMember() { + IScriptVariableFacade facade = getCardFacade(); + return facade != null ? facade.getMember() : null; + } + + @Nullable + protected IScriptVariableFacade getCardFacade() { ItemStack itemStack = getContainerInventory().getItem(0); if (itemStack.isEmpty()) { return null; } IVariableFacadeHandlerRegistry registry = IntegratedDynamics._instance.getRegistryManager().getRegistry(IVariableFacadeHandlerRegistry.class); IVariableFacade variableFacade = registry.handle(ValueDeseralizationContext.of(getLevel()), itemStack); - return variableFacade instanceof IScriptVariableFacade scriptVariableFacade ? scriptVariableFacade.getMember() : null; + return variableFacade instanceof IScriptVariableFacade scriptVariableFacade ? scriptVariableFacade : null; } @Nullable diff --git a/src/main/resources/assets/integratedscripting/lang/en_us.json b/src/main/resources/assets/integratedscripting/lang/en_us.json index 549d7aeeb..dbb372e1f 100644 --- a/src/main/resources/assets/integratedscripting/lang/en_us.json +++ b/src/main/resources/assets/integratedscripting/lang/en_us.json @@ -51,6 +51,10 @@ "gui.integratedscripting.error.reserved_member": "\"%s\" is a keyword, so it can not be bound. Select a function name or variable name instead.", "gui.integratedscripting.binding.next": "Binds to: %s", "gui.integratedscripting.binding.card": "Bound to: %s", + "gui.integratedscripting.evaluation.none": "Not evaluated by the network since the script last changed", + "gui.integratedscripting.evaluation.value": "Last value: %s", + "gui.integratedscripting.evaluation.error": "Last error: %s", + "gui.integratedscripting.evaluation.call_error": "Last error when called: %s", "key.categories.integratedscripting": "Integrated Scripting", "key.integratedscripting.undo": "Undo",