From 0b1f531d68fcdcab62f31dd3d8dd4ab05214dad1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 11:59:53 +0000 Subject: [PATCH] Translate script values back to NBT when an operator expects NBT Only compound tags keep their type when they are translated to a script: all other tags become plain script values, and an absent NBT value becomes null. Passing such a value to an operator again therefore failed, because it was translated back to the value type it looked like. A list tag that came out of an NBT path filter expression came back as a list, so nbtAsTagList(idContext.ops.stringNbtPathMatchFirst(...)) failed with a wrong type error instead of listing the matches, and null values did not translate at all. Operator arguments are now translated against the value type the operator expects for them, so script values reaching an NBT input are converted to NBT, and null becomes an absent NBT value. Related to #67 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JCW1HmWhLT27jh57t9d7B6 --- .../translator/OperatorProxyExecutable.java | 36 +++++- .../NbtOperatorArgumentsJavaScriptTests.java | 106 ++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/cyclops/integratedscripting/evaluate/translation/NbtOperatorArgumentsJavaScriptTests.java diff --git a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java index 39cdaf6cb..370fce901 100644 --- a/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java +++ b/src/main/java/org/cyclops/integratedscripting/evaluate/translation/translator/OperatorProxyExecutable.java @@ -1,9 +1,11 @@ package org.cyclops.integratedscripting.evaluate.translation.translator; import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integrateddynamics.api.evaluate.operator.IOperator; import org.cyclops.integrateddynamics.api.evaluate.variable.IValue; import org.cyclops.integrateddynamics.api.evaluate.variable.IVariable; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeNbt; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeOperator; import org.cyclops.integrateddynamics.core.evaluate.variable.Variable; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypes; @@ -16,6 +18,8 @@ import org.graalvm.polyglot.Value; import org.graalvm.polyglot.proxy.ProxyExecutable; +import javax.annotation.Nullable; + /** * A Graal proxy executable for operator values. * @author rubensworks @@ -45,14 +49,42 @@ public IValueType getProxiedValueType() { @Override public Object execute(Value... args) { try { + IOperator operator = value.getRawValue(); + IValueType[] inputTypes = operator.getInputTypes(); IVariable[] variables = new IVariable[args.length]; for (int i = 0; i < args.length; i++) { - variables[i] = new Variable<>(ValueTranslators.REGISTRY.translateFromGraal(context, args[i], exceptionFactory, valueDeseralizationContext)); + variables[i] = new Variable<>(translateArgument(args[i], i < inputTypes.length ? inputTypes[i] : null)); } - return ValueTranslators.REGISTRY.translateToGraal(context, value.getRawValue().evaluate(variables), exceptionFactory, valueDeseralizationContext); + return ValueTranslators.REGISTRY.translateToGraal(context, operator.evaluate(variables), exceptionFactory, valueDeseralizationContext); } catch (EvaluationException e) { ScriptHelpers.sneakyThrow(e); return null; } } + + /** + * Translate a single argument of this operator. + * + * Only compound tags keep their NBT type when they are translated to a script, + * all other tags become plain script values, and an absent NBT value becomes null. + * These are translated back to NBT here, so that they can be passed to operators again. + * + * @param arg A script value. + * @param inputType The value type the operator expects for this argument, if known. + * @return The translated value. + * @throws EvaluationException If translation failed. + */ + protected IValue translateArgument(Value arg, @Nullable IValueType inputType) throws EvaluationException { + if (inputType == ValueTypes.NBT) { + if (arg.isNull()) { + return ValueTypeNbt.ValueNbt.of(); + } + IValue value = ValueTranslators.REGISTRY.translateFromGraal(context, arg, exceptionFactory, valueDeseralizationContext); + if (value.getType() != ValueTypes.NBT) { + return ValueTypeNbt.ValueNbt.of(ValueTranslators.REGISTRY.translateToNbt(context, value, exceptionFactory)); + } + return value; + } + return ValueTranslators.REGISTRY.translateFromGraal(context, arg, exceptionFactory, valueDeseralizationContext); + } } diff --git a/src/test/java/org/cyclops/integratedscripting/evaluate/translation/NbtOperatorArgumentsJavaScriptTests.java b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/NbtOperatorArgumentsJavaScriptTests.java new file mode 100644 index 000000000..50b2e1dc1 --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/evaluate/translation/NbtOperatorArgumentsJavaScriptTests.java @@ -0,0 +1,106 @@ +package org.cyclops.integratedscripting.evaluate.translation; + +import net.minecraft.nbt.CompoundTag; +import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeNbt; +import org.cyclops.integratedscripting.api.evaluate.translation.IEvaluationExceptionFactory; +import org.cyclops.integratedscripting.evaluate.ScriptHelpers; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for passing values to operators that expect NBT. + * + * NBT values other than compound tags reach scripts as plain JS values, + * so they must be converted back to NBT when they are passed into an operator again. + * + * @author rubensworks + */ +public class NbtOperatorArgumentsJavaScriptTests { + + private static ValueDeseralizationContext VDC = null; + private static Context CTX = null; + private static IEvaluationExceptionFactory EF = ScriptHelpers.getDummyEvaluationExceptionFactory(); + + @BeforeAll + public static void beforeAll() throws EvaluationException { + VDC = ValueDeseralizationContextMocked.get(); + CTX = ScriptHelpers.createPopulatedContext(null, VDC); + } + + private static Value nbtValue() throws EvaluationException { + CompoundTag levels = new CompoundTag(); + levels.putInt("a", 5); + levels.putInt("b", 1); + levels.putInt("c", 7); + CompoundTag tag = new CompoundTag(); + tag.put("levels", levels); + return ValueTranslators.REGISTRY.translateToGraal(CTX, ValueTypeNbt.ValueNbt.of(tag), EF, VDC); + } + + @Test + public void testListTagMatchPassedBackToNbtOperator() throws EvaluationException { + // A filter expression matches into a list tag, which reaches the script as a plain array. + Value function = CTX.eval("js", """ + (function (nbt) { + const matched = idContext.ops.stringNbtPathMatchFirst('["levels"][?(@ >= 3)]', nbt); + return [Array.isArray(matched), idContext.ops.nbtAsTagList(matched).length]; + }) + """); + Value result = function.execute(nbtValue()); + assertThat(result.getArrayElement(0).asBoolean(), is(true)); + assertThat(result.getArrayElement(1).asInt(), is(2)); + } + + @Test + public void testArrayPassedToNbtOperator() { + Value function = CTX.eval("js", """ + (function () { + return idContext.ops.nbtAsTagList([1, 2, 3]).length; + }) + """); + assertThat(function.execute().asInt(), is(3)); + } + + @Test + public void testNullPassedToNbtOperator() throws EvaluationException { + // An NBT path that matches nothing yields null, which stays usable as an NBT value. + Value function = CTX.eval("js", """ + (function (nbt) { + const matched = idContext.ops.stringNbtPathMatchFirst('["absent"]', nbt); + return [matched === null, idContext.ops.nbtAsTagList(matched).length]; + }) + """); + Value result = function.execute(nbtValue()); + assertThat(result.getArrayElement(0).asBoolean(), is(true)); + assertThat(result.getArrayElement(1).asInt(), is(0)); + } + + @Test + public void testCompoundTagPassedBackToNbtOperator() throws EvaluationException { + // Compound tags were already passed back unchanged, and must keep working. + Value function = CTX.eval("js", """ + (function (nbt) { + const levels = idContext.ops.stringNbtPathMatchFirst('["levels"]', nbt); + return idContext.ops.nbtSize(levels); + }) + """); + assertThat(function.execute(nbtValue()).asInt(), is(3)); + } + + @Test + public void testNonNbtOperatorArgumentsAreUnaffected() { + Value function = CTX.eval("js", """ + (function () { + return idContext.ops.stringLength("abcd"); + }) + """); + assertThat(function.execute().asInt(), is(4)); + } +}