Skip to content

Commit 8158701

Browse files
committed
run_async() and small run() documentation fix
1 parent ac72645 commit 8158701

File tree

3 files changed

+144
-8
lines changed

3 files changed

+144
-8
lines changed

docs/scarpet/api/Auxiliary.md

+25-8
Original file line numberDiff line numberDiff line change
@@ -406,19 +406,20 @@ read_file('foo', 'shared_text') => ['one', 'two', 'three', '', 'four', '', '
406406

407407
### `run(expr)`
408408

409-
Runs a vanilla command from the string result of the `expr` and returns a triple of 0 (unused after success count removal),
410-
intercepted list of output messages, and error message if the command resulted in a failure.
411-
Successful commands return `null` as their error.
409+
Runs a vanilla command from the string result of the `expr` and returns a triple of the exit code (usually 0 or 1),
410+
intercepted list of output messages, and last synchronous error message if the command resulted in a failure.
411+
The exit code may be null if a non command caused error occurred while running the command,
412+
with the error message field giving the error message.
412413

413414
The command return `null` if the command was not run immediately, but was scheduled for later execution
414-
due to being requested while command was executed. This happens most commonly
415-
when running `run` from a `/script run/invoke` command since that always results in piling up command to run while `script` is being executed.
415+
due to being requested while command was executed. This happens most commonly when running `run` from a `/script run/invoke`
416+
command or from a custom app command callback function since that always results in piling up command to run while `script` is being executed.
416417

417-
The mitigation for this is to use `run` in a separate scheduled function, or use `run` in a `tick` event, or literally in
418-
any other way than directly from a `/script` command, which will ensure that the command runs immediately.
418+
The mitigation for this is to use `run` in a separate scheduled function, or use `run` in a `tick` event, or in other ways than
419+
directly from a `/script` command or custom app command callback, which will ensure that the command runs immediately.
419420

420421
<pre>
421-
run('fill 1 1 1 10 10 10 air') -> [123, ["Successfully filled 123 blocks"], null]
422+
run('fill 1 1 1 10 10 10 air') -> [123, ["Successfully filled 123 block(s)"], null]
422423
run('give @s stone 4') -> [1, ["Gave 4 [Stone] to gnembon"], null]
423424
run('seed') -> [-170661413, [Seed: [4031384495743822299]], null]
424425
run('sed') -> [-1, [], "sed<--[HERE]"] // wrong command
@@ -427,6 +428,22 @@ run('sed') -> [-1, [], "sed<--[HERE]"] // wrong command
427428
/script run schedule(0, _() -> print(run('setblock 0 0 0 stone'))) -> [1, [Changed the block at 0, 0, 0], null]
428429
</pre>
429430

431+
### `run_async(expr, callback)`
432+
433+
Runs a vanilla command from the string result of the `expr` and calls `callback` with a triple of the exit code (usually 0 or 1),
434+
intercepted list of output messages, and first synchronous error message if the command resulted in a failure.
435+
The exit code may be null if a non command caused error occurred while running the command,
436+
with the error message field giving the error message.
437+
438+
<pre>
439+
run_async('fill 1 1 1 10 10 10 air', _(result) -> print(result)) -> [123, ["Successfully filled 123 block(s)"], null]
440+
run_async('give @s stone 4', _(result) -> print(result)) -> [1, ["Gave 4 [Stone] to gnembon"], null]
441+
run_async('seed', _(result) -> print(result)) -> [-170661413, [Seed: [4031384495743822299]], null]
442+
run_async('sed', _(result) -> print(result)) -> [-1, [], "sed<--[HERE]"] // wrong command
443+
444+
/script run run_async('setblock 0 0 0 stone', _(result) -> print(result)) -> [1, [Changed the block at 0, 0, 0], null]
445+
</pre>
446+
430447
### `save()`
431448

432449
Performs autosave, saves all chunks, player data, etc. Useful for programs where autosave is disabled due to

src/main/java/carpet/script/api/Auxiliary.java

+100
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,106 @@ else if (!interactable && targetBlock == null)
706706
}
707707
});
708708

709+
expression.addContextFunction("run_async", 1, (c, t, lv) ->
710+
{
711+
CommandSourceStack source = ((CarpetContext) c).source();
712+
FunctionArgument callback = FunctionArgument.findIn(c, expression.module, lv, 1, true, false);
713+
String command = lv.get(0).getString();
714+
if(callback.function == null)
715+
{
716+
try
717+
{
718+
Component[] errorOutput = {null};
719+
List<Component> output = new ArrayList<>();
720+
OptionalLong[] returnValue = {OptionalLong.empty()};
721+
source.getServer().getCommands().performPrefixedCommand(
722+
new SnoopyCommandSource(source, errorOutput, output, returnValue),
723+
command
724+
);
725+
}
726+
catch (Exception ignored)
727+
{
728+
}
729+
return Value.NULL;
730+
}
731+
else if (callback.function.getArguments().size() < 1)
732+
{
733+
throw new InternalExpressionException("callback requires 1 argument for command output");
734+
}
735+
boolean[] isCallbackConsumed = {false};
736+
try
737+
{
738+
boolean[] hasCommandError = {false};
739+
Integer[] commandExitCode = {null};
740+
Component[] errorOutput = {null};
741+
List<Component> output = new ArrayList<>();
742+
OptionalLong[] ignoredReturnedValue = {OptionalLong.empty()};
743+
744+
SnoopyCommandSource snoopyCommandSource = (SnoopyCommandSource) new SnoopyCommandSource(source, errorOutput, output, ignoredReturnedValue)
745+
.withCallback((success, exitCode) -> {
746+
if(isCallbackConsumed[0])
747+
{
748+
return;
749+
}
750+
isCallbackConsumed[0] = true;
751+
// The CommandSourceStack.sendFailure are sent after the resultConsumer, so the error output is always empty in
752+
// this resultConsumer. This workaround is using the resultConsumer in tandem with an errorCallback.
753+
if(!success)
754+
{
755+
hasCommandError[0] = true;
756+
commandExitCode[0] = exitCode;
757+
return;
758+
}
759+
List<Value> args = List.of(ListValue.of(
760+
NumericValue.of(exitCode),
761+
ListValue.wrap(output.stream().map(FormattedTextValue::new)),
762+
FormattedTextValue.of(errorOutput[0])
763+
));
764+
callback.function.callInContext(c, t, args);
765+
});
766+
// To catch errors that don't call the resultConsumer and make sure the error output is set for those that do.
767+
snoopyCommandSource.setErrorCallback((ignored) -> {
768+
if(isCallbackConsumed[0])
769+
{
770+
if(!hasCommandError[0])
771+
{
772+
return;
773+
}
774+
List<Value> args = List.of(ListValue.of(
775+
NumericValue.of(commandExitCode[0]),
776+
ListValue.wrap(output.stream().map(FormattedTextValue::new)),
777+
FormattedTextValue.of(errorOutput[0])
778+
));
779+
callback.function.callInContext(c, t, args);
780+
return;
781+
}
782+
isCallbackConsumed[0] = true;
783+
List<Value> args = List.of(ListValue.of(
784+
Value.NULL,
785+
ListValue.of(),
786+
FormattedTextValue.of(errorOutput[0])
787+
));
788+
callback.function.callInContext(c, t, args);
789+
});
790+
791+
source.getServer().getCommands().performPrefixedCommand(snoopyCommandSource, command);
792+
}
793+
catch (Exception exc)
794+
{
795+
if(!isCallbackConsumed[0])
796+
{
797+
isCallbackConsumed[0] = true;
798+
List<Value> args = List.of(ListValue.of(
799+
Value.NULL,
800+
ListValue.of(),
801+
new FormattedTextValue(Component.literal(exc.getMessage()))
802+
));
803+
callback.function.callInContext(c, t, args);
804+
}
805+
}
806+
return Value.NULL;
807+
});
808+
709809
expression.addContextFunction("save", 0, (c, t, lv) ->
710810
{
711811
CommandSourceStack s = ((CarpetContext) c).source();

src/main/java/carpet/script/utils/SnoopyCommandSource.java

+19
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.OptionalLong;
2323
import java.util.function.BinaryOperator;
24+
import java.util.function.Consumer;
2425
import java.util.function.Supplier;
2526

2627
public class SnoopyCommandSource extends CommandSourceStack
@@ -44,6 +45,9 @@ public class SnoopyCommandSource extends CommandSourceStack
4445

4546
private final TaskChainer taskChainer;
4647

48+
// sendFailure() callback
49+
@Nullable private Consumer<Component> errorCallback = null;
50+
4751
public SnoopyCommandSource(CommandSourceStack original, Component[] error, List<Component> chatOutput, OptionalLong[] returnValue)
4852
{
4953
super(CommandSource.NULL, original.getPosition(), original.getRotation(), original.getLevel(), Vanilla.MinecraftServer_getRunPermissionLevel(original.getServer()),
@@ -197,11 +201,26 @@ public CommandSourceStack facing(Vec3 position)
197201
public void sendFailure(Component message)
198202
{
199203
error[0] = message;
204+
if(this.errorCallback != null)
205+
{
206+
this.errorCallback.accept(message);
207+
}
200208
}
201209

202210
@Override
203211
public void sendSuccess(Supplier<Component> message, boolean broadcastToOps)
204212
{
205213
chatOutput.add(message.get());
206214
}
215+
216+
/**
217+
* Added for the new queue system for commands, since some early errors don't trigger the resultConsumer and all sendFailures calls
218+
* are executed after the call resultConsumer when they do.
219+
* @param errorCallback A consumer that will be called upon sendFailure
220+
*/
221+
public void setErrorCallback(Consumer<Component> errorCallback)
222+
{
223+
this.errorCallback = errorCallback;
224+
}
225+
207226
}

0 commit comments

Comments
 (0)