Request: Add function to interrupt walk by a player's input and double-clicking

Started by blexx, Tue 20/08/2024 01:21:04

Previous topic - Next topic

eri0o

Uhm... Callbacks was my first thought on how to implement this and chaining callbacks is how I did this in a JS Engine. But, for this exact case, what about something like

Code: ags
cEgo.Walk(x,y,eNoBlock)
    ?.Say("I will say this when I reach x and y!");
    ?.ChangeRoom(5)

The idea here is the following, the Walk will return the character itself on success and null on failure, same for Say. The ? character is a magical thing that if it's not null it happens and if it's null it ends - c# has something like this.

Edit: ah crap no it won't work because it's not blocking.

Well the only way I know how to get away with this is to hack some way to do await/join or some other way to schedule things after the other is finished, but this will require lambdas in all the things I can imagine.

Edit2: in a script module it may be quite trivial to introduce some "scripting" that can behind the scenes schedule these things in a state machine.

Then you either write the code in this using string - but you lose syntax highlighting and auto complete. Or you push the sequence with command objects like M.Push(Command(eCmdWalk, int param1, int param2))

Edit2: I once did this in Lua, it's using some meta Lu's reinterpretation but bear in mind this entire game and the tools to make were done in 48h (and I also learned Lua on the spot) - https://github.com/VacaRoxa/CodenameLT/blob/1af638b96b0665edb1771abbd9c53a2432604f54/project/src/states/Game.lua#L251 and this https://github.com/VacaRoxa/CodenameLT/blob/1af638b96b0665edb1771abbd9c53a2432604f54/project/map/level0.lua#L635 in the map

Crimson Wizard

I think that implementing coroutines in AGS script may be a solution to this:
https://en.wikipedia.org/wiki/Coroutine

Any kind of command sequencing or making scheduled command list has a number of problems:
- for sequencing you'd need every function to have same return value type, which will allow to sequence next function; If not that, then they have to be wrapped into another function that has this standard return type (or lambda).
- for both sequencing and scheduling it will be more complicated to add anything besides function calls: conditions, loops, operations etc - these will have to be wrapped into function calls too (or lambdas), making syntax more complicated.
- specifically for scheduling, there's a problem of getting current values from global variables if they are used as arguments, because they may change before a scheduled command is called.


Coroutines are, to put it simply, functions that may be paused at certain points.
This is of course not a real plan, and this may be more difficult than I say here,
but in a very crude sense, we'd need a mechanism that
- allows to mark the script function as "coroutine", e.g. some keyword;
- engine runs such function on a dedicated "thread" (internally in ags this is called "script instance fork").
- when such function issues a "waiting for" command, it generates some kind of notification trigger for itself and passes into the engine, then suspends and waits for notification from the engine;
- engine has a list of these triggers somewhere, it checks them during game update, and raises notification for those that achieved result.
- when a "script thread" which runs a coroutine gets a notification, it continues running its function from the point where it paused last time.

Snarky

Quote from: Crimson Wizard on Thu 22/08/2024 09:58:38I might sound quite rude saying this, but looking at these lambdas makes me shudder. I think both of the examples posted above will only make everyone confused. I mean, beginners are often confused by much simpler things, like having to write commands inside functions, having to put brackets after ifs, or writing their own functions.

I don't disagree that lambda syntax in imperative languages is both ugly and pretty much incomprehensible, and that we should prioritize simplicity.

If the brackets around the block can be omitted, that's great. I was a bit unsure about whether it would be possible to do so in general without breaking the parsing in interesting ways. For example:

Code: ags
function crossChasm()
{
  if(cEgo.HasInventory(iHoverBoard))
  {
    waitfor(cEgo.Walk(EXIT_X, EXIT_Y, eNoBlock))
    cEgo.ChangeRoom(42);
    return;
  }
  waitfor(cEgo.Walk(EDGE_X, EDGE_Y, eNoBlock))
  cEgo.Say("I'm not getting across that on foot!");
}

Will the bit after the if-block run if cEgo has an iHoverBoard? Typically in AGS script you would expect that it doesn't because of the return statement, but if the script actually only runs the waitfor(cEgo.Walk) statement now and schedules the rest of the if-block for later, it won't hit that point, and the rest will run. There are also questions about local variables (both scope and value).

Anyway, while we're throwing ideas at the wall, how about this?

Introduce an event type and a delegate type that can be registered as event handlers. A delegate can be created either from a named function or an inline anonymous one. The syntax could for example be:

Code: ags
  // Add the on_event function as an event handler (delegate) for Entity.Event
  Entity.Event += new delegate on_event;

  // Add an an anonymous function as an event handler for Entity.Event
  Entity.Event += new delegate(Entity e, EventData d) {
    // Event handler body
  };

There's a Character.OnAction event that gets raised whenever an asynchronous action (e.g. a non-blocking walk) completes, running all registered event handler delegates. (Specifically, a "OneTimeEvent" event type that unregisters all event handlers after it's triggered once.) Character.Walk returns a pointer to this event, which makes it convenient to register an event handler for when the non-blocking walk completes.

So you could do:

Code: ags
  cEgo.Walk(x,y, eNoBlock) += new delegate(bool complete)
  {
    if(complete)
    {
      cEgo.Say("I will say this when I reach x and y!");
      cEgo.ChangeRoom(5);
    }
  };

As a piece of syntactic sugar, there's a keyword (or set of keywords) that simplifies this syntax somewhat, for example:

Code: ags
  waitForComplete(cEgo.Walk(x,y,eNoBlock));
  cEgo.Say("I will say this when I reach x and y!");
  cEgo.ChangeRoom(5);

OR if it turns out that this creates too many issues:

Code: ags
  waitForComplete(cEgo.Walk(x,y,eNoBlock))
  {
    cEgo.Say("I will say this when I reach x and y!");
    cEgo.ChangeRoom(5);
  }

Possible keywords could include "waitFor" (wait until the event is raised, unconditionally) and "waitForComplete" (wait until the event is raised, and only if the first, boolean argument is true).

I think this gives a good balance of simplicity and flexibility/power.

Crimson Wizard

Quote from: Snarky on Thu 22/08/2024 13:02:21Will the bit after the if-block run if cEgo has an iHoverBoard? Typically in AGS script you would expect that it doesn't because of the return statement, but if the script actually only runs the waitfor(cEgo.Walk) statement now and schedules the rest of the if-block for later, it won't hit that point, and the rest will run.

No, it won't run, why would it? The idea was that execution halts at "waitfor" and then proceeds with the next line.


The event-delegate idea is interesting as an optional ability of attaching a function to the "waiting completed" event.

However, if this is used as a replacement for waitForComplete, then I'm afraid that some constructs become difficult to resolve. For example, what if waitForComplete is inside "if"?

Code: ags
if (condition)
{
   waitForComplete(action);
   more commands
}

other commands

Here one part of statements is always run after event, and second part may run either instantly or after event.

Or if waitForComplete is inside a loop.

Code: ags
while (condition)
{
   waitForComplete(action);
}

Will that become a recursive delegate?


What I might propose instead, following the previous idea of Coroutine, is to let blocking function return some kind of a "WaitingObject" type, which in turn may be used in 2 ways (also simultaneously):
- returned into the engine, using specific keyword such as "waitfor" or "yield", where engine tracks its condition and resumes the coroutine when its complete.
- attached a custom callback using syntax like "WaitingObject.OnComplete += func".

Snarky

Quote from: Crimson Wizard on Thu 22/08/2024 14:08:43No, it won't run, why would it? The idea was that execution halts at "waitfor" and then proceeds with the next line.

I think we're coming at this from two different directions. My thinking was to simplify the way this problem is typically solved in AGS today (putting all the things that have to wait for a non-blocking action to complete in a separate function, and then wait for a signal that it's time to run it; currently by polling in repeatedly_execute), but keep essentially the same logic. So my assumption was not that the script would really halt at the "waitfor" line (but rather mark the rest of the block as a delegate to be run on callback).

I'm not sure I quite understand how that (i.e. halting) would work. In this talk about coroutines, what level is the coroutine? IOW, how much of the engine/script actually blocks? If this happens in an on_key_press handler, for example, will the program stop processing keypresses until the action completes and the script resumes? (Presumably no.)

Quote from: Crimson Wizard on Thu 22/08/2024 14:08:43Here one part of statements is always run after event, and second part may run either instantly or after event.

No, if waitfor is just syntactic sugar for passing the rest of the block as a callback delegate to an event, the rest of the function will run right away.

Quote from: Crimson Wizard on Thu 22/08/2024 14:08:43Or if waitForComplete is inside a loop.

Code: ags
while (condition)
{
  waitForComplete(action);
}

Will that become a recursive delegate?

Under my proposal, it will assign a no-op function (given that there are no lines in the same block of code below waitForComplete) as an event handler for when the action completes, and then begin running the action. Then it will loop, assign another empty event handler, and immediately begin running the action again. In AGS, this will typically interrupt/cancel the ongoing action, and if we're using waitForComplete, the completed event will not trigger, so the first event handler is discarded. It will continue doing this in a loop, all within the same game cycle, for as long as the condition holds. If there is nothing in the loop to change the condition, it will go into an infinite loop and error out. If it eventually breaks out of the loop, we'll end up with one (empty) event handler assigned as a callback for when the action completes.

Basically, this would not be a reasonable thing to write.

If we modify it slightly to:

Code: ags
while (condition)
{
  waitFor(action);
  someOtherFunction();
}

... it might have the same effect as simply:

Code: ags
  while(condition)
    someOtherFunction();

Since each time it reaches the action line it will start that action and interrupt the ongoing one, raising the OnAction(Interrupted) event. Though the exact effect depends on how events being triggered and callbacks called is scheduled internally. Regardless, it's still probably not a reasonable thing to write.

But there is another problem that I mentioned earlier that has to be considered. Whichever approach one would go with, if a script can be paused/deferred while other scripts continue running, and then resumed, there are going to be a bunch of concurrency issues. Take for example:

Code: ags
int x;

void someFunction()
{
  int y = Random(200);
  waitfor(cEgo.Walk(x, y, eNoBlock));
  cEgo.Say("Arrived at %d,%d), x, y);
}

Now you would expect the (x,y) the character Says to be the same they walked to, but this might not be the case. While the function waited for cEgo.Walk to complete, some other script may have changed the value of x.

eri0o

I thought that it meant that there would be an additional "type" of executing a script instance: blocking, non-blocking and co-routine. Not sure how to actually implement ... But here's python example that looks easy to understand: https://docs.python.org/3/library/asyncio-task.html

Crimson Wizard

The following is not the final plan, but an outline of an idea.

The idea of a coroutine that I had is to let ags script vm support a function which could be paused, saving state, and then restored and resumed.
Coroutine's state includes stack (local data) and execution pointer.

Pausing coroutine effectively exits them, but this cannot be done with a regular "return" statement, because "return" unwinds the stack and ends the function.
So there has to be a separate statement for pausing (e.g. "yield" or "waitfor").

Paused coroutines do not prevent engine to update, any events to happen or any script functions to run. They just stay "saved" in the engine's memory, until resumed again under certain conditions. If game is restored from a save file, restarted or exited while there are paused coroutines, these coroutines are erased from memory. (In regards to saves, please see a note in the end of this post)

I imply that in AGS coroutines must return a special type of object when returning with pause. Different languages implement this differently. As a draft I assume some kind of a "wait object" that contains:
- a reference to certain background action that has a completion flag;
- a reference to the coroutine (internal id?), because engine will need to know which coroutine to resume when the action is complete.

This hypothetical WaitObject may be a parent class for other objects, such as WalkingAction or AnimationAction etc, if necessary, but I won't expand this further there. It may also host a "on completed" event to attach user's delegates, if delegates are eventually implemented in AGS.

I suppose that functions that can start non-blocking actions will return this WaitObject or any of its extended types.

When such function is called in coroutine, you can take the returned WaitObject, and use it to pause coroutine, using "return with pause" statement, e.g:

Code: ags
WaitObject MyCutscene()
{
    // initial statements
    
    WaitObject o = player.Walk(100, 200, eNoBlock);
    waitfor o;
    
    // or just
    
    waitfor player.Walk(50, 150, eNoBlock);
    
    // more statements
}

Coroutine may have any number of "pauses", also nested into conditional blocks or loops. It finishes only when reaches real "return" (either explicit statement, or end of function).

Now, the important question is, where does coroutine returns to when pausing.
The short answer here: if it's a first pause, then it returns to where to was called from, whether engine or user's function, but is kept saved in the engine's memory.
Any following pause or return after the first one returns directly to the engine.
But this must be clarified.

Suppose you called a coroutine from another function:

Code: ags
WaitObject MyCutscene()
{
    waitfor player.Walk(100, 200, eNoBlock);
    waitfor player.Walk(50, 150, eNoBlock);
}

function on_mouse_click(MouseButton btn)
{
   if (btn == eMouseLeft)
   {
      MyCutscene();
   }
}

What happens here is this:
1. on_mouse_click is run.
2. MyCutscene() is run.
3. first non-blocking Walk is called.
4. MyCutscene pauses and returns.
5. on_mouse_click continues, and exits.
6. engine updates the game as usual.
7. player.Walk command completes.
8. Engine resumes MyCutscene(), MyCutscene continues to run.
9. second non-blocking Walk is called.
10. MyCutscene pauses and returns.
11. engine updates the game as usual.
12. player.Walk command completes.
13. Engine resumes MyCutscene(), MyCutscene continues to run.
14. MyCutscene reaches its end, and ends normally.

It should be possible to have nested coroutines.
It should be possible to call waitfor on a coroutine's return value, which effectively turns calling function into coroutine too.

There's possibly a number of unanswered problems here. And this suggestion may be different from coroutine implementation in other languages. So consider this to be a draft of idea.

In the context of AGS, there's one question that I have not yet thought through, that's saving game while there are paused coroutines.
AGS does not save the game when a function is run, but if coroutine is paused then it is not running. On another hand, saving with a paused coroutine means the coroutine's state must also be written to a save and loaded after.



Snarky

One thing not quite clear from the description: Does this mean that any function that includes a waitfor statement must have WaitObject as its return type?

Crimson Wizard

Quote from: Snarky on Fri 23/08/2024 11:32:43One thing not quite clear from the description: Does this mean that any function that includes a waitfor statement must have WaitObject as its return type?

Apparently so, because this "waitfor" has to pause the current function and get out of it, returning a wait object, that would let resume same function.
But what else I forgot is that there has to be 2 types of "waits":
- one that pauses this function and returns a WaitObject back to external caller (that's what I've been talking about earlier);
- one that actually waits in place until an action is completed (aka "synchronous wait").

The latter is a advanced equivalent of this classic snippet which we use to block a function while using non-blocking commands:
Code: ags
while (player.Walking) {
   Wait(1);
}

So, perhaps "waitfor" keyword that I proposed earlier is not optimal, or perhaps it should mean the second kind of command instead, and the first (pause and exit coroutine) will need a better name.

Snarky

I think I was the one who suggested waitfor, and I agree that it's probably not the best name.

I'm still not quite ready to abandon the idea of doing this in terms of events and anonymous event handlers. Here's a revised proposal for syntax I think wouldn't be too difficult for users to grasp:

Code: ags
void TakeThingy()
{
  oncomplete(cEgo.Walk(oThingy.x, oThingy.y, eNoBlock))
  {
    if(cEgo.x == oThingy.x && cEgo.y == oThingy.y)
    {
      cEgo.Say("That looks useful!");
      cEgo.PickUp(oThingy);
    }
  }
  Display("This will display before cEgo starts walking");
}

Here, the oncomplete keyword (replacing "waitfor") marks the following block as an event handler for the event that is raised once the non-blocking function completes. So that whole block is deferred, and the script continues to the next line below (here Display).

(There is a question of whether the event handler will be able to access local variables from the function in which it was declared. If so, I guess they have to be copied and stashed somewhere. It's probably easier to say no.)

Crimson Wizard

Quote from: Snarky on Sat 07/09/2024 17:11:37(There is a question of whether the event handler will be able to access local variables from the function in which it was declared. If so, I guess they have to be copied and stashed somewhere. It's probably easier to say no.)

No, it won't, as callback is a separate function with a separate local stack.
In normal programming languages lambdas allow a "capture" syntax, where you explicitly tell which values to copy.
I'm not sure how this is implemented, but from user's perspective this is almost like passing a custom list of function arguments.

eri0o

Code: ags
 if(cEgo.x == oThingy.x && cEgo.y == oThingy.y)

This should never be done, there's no guarantee that the character will reach an exact position - I see this in many many things. Beyond this relying on the walkable areas making sense, there's also the issue of things being solid. Things should have a point for where the character "goes" but you should test only if the best effort was done (is somewhere around the area and finished their walk).

I think you would probably not do this in an actual game but someone with less experience may look at this and think this would actually work.

Snarky

Quote from: Crimson Wizard on Sat 07/09/2024 17:14:38No, it won't, as callback is a separate function with a separate local stack.

I realize that it won't unless AGS makes a special effort to make it happen, but if it was decided that this would be essential to make the feature usable, it would be possible to always make a copy of the local stack and attach it to the other function. But as I hinted, I don't think that's necessarily a good idea. (For one thing, there could be a lot of data in the local stack, for example arrays; and for another, if we think about generalizing this functionality there might be cases where the event never happens, so that the stack copy is never unwound.)

A way to explicitly copy data into the event handler, like the "capture" mechanism you mention, would probably be better. It might also be useful to be able to get a return value from the non-blocking function. The syntax might for example look like:

Code: ags
void TakeThingy(Object* thingy)
{
  oncomplete(bool arrived = cEgo.Walk(thingy.x, thingy.y, eNoBlock), Object* thingy = thingy)
  {
    if(arrived)
    {
      cEgo.Say("That looks useful!");
      cEgo.PickUp(thingy);
    }
  }
  Display("This will display before cEgo starts walking");
}

This assumes that Walk() now returns a bool for whether it reached its destination (i.e. wasn't either interrupted or met an obstacle). Alternatively, functions with a non-blocking option could all return some custom type; probably either an enum or a struct.

(And yes, I realize that the syntax here is a bit "dirty"/non-standard, and that there could be difficulties with the parsing since each "argument" is actually a variable assignment. Suggestions welcome.)

Crimson Wizard

@blexx, in regards to solving a cancellable interaction in the current version of AGS,
Looking back at my first reply in this thread now, I found that my proposed solution is actually not the optimal one. Not sure what I was thinking, somehow I was observing this as a individual situation in the game.

The generic solution would be different. In very simple terms:
- In "on_mouse_click" dont run interaction right away (so not ProcessClick, etc),
- instead save the clicked object, verb and "walkto" destination in variables, and issue non-blocking Walk,
- "walkto" destination for all objects and hotspots may be done using Custom Properties, for instance,
- in rep-exec test if character is Moving, and when it's not moving AND has a saved interaction, AND is close enough to the saved "walkto" destination, then run that interaction using RunInteraction function (these functions may be checked in the manual)
- if another on_mouse_click is run during this walk, reset previously saved interaction variables
- also, these variables has to be reset in case something interrupted player's walk, like a region that triggers a cutscene. Probably there should be a convenient script function that clears them up (like ClearPlayerInteraction) and then you may call that function whenever a cutscene starts. Not sure if this may be automated.


@Scavenger mentioned this "NoBlock" module on Discord recently:
https://www.adventuregamestudio.co.uk/forums/modules-plugins-tools/module-noblock-v0-7-cancelable-interactions/
it supposedly does what I described above, more or less.
It's old (from 2006), but I suppose may be fixed for the use in the recent version.
Or maybe someone could write a new (better?) one for the current version.
But this is rather a topic for "Technical Support" forum, rather than "Editor development".


EDIT: There's also another, simpler "GotThere" module, made by different principle:
https://www.adventuregamestudio.co.uk/forums/modules-plugins-tools/module-gotthere-0-5/



Danvzare

I still much prefer my solution.

Run a function which checks if you walked there or cancelled out.
If you walked there, run the interaction as normal. If you cancelled out, do nothing.

As for the function itself.
Issue a non-blocking WalkTo using the coordinates passed to the function.
While the playing is moving, just keep waiting a single frame. The game will appear to progress as normal.
If the player presses a mouse button, then return a variable saying it was cancelled.
Otherwise after the player has stopped moving, double check to make sure the player reached the correct coordinates, and return the fact that it ran successfully.

Throw in a change of the cursor's wait mode graphic so it doesn't disappear, along with a check for the mouse button being released, so only a new mouse down cancels, and there you go.

It's quick, simple, and easy to follow. Not only that, but it can be used in just about anything without much issue.
It doesn't even require custom properties or anything like that. To the user, it's as simple as using a normal walk to command, just nested in an IF statement instead.
Just slot it in, and let it do the work (I've not had any problems with it so far). I'll have to see about cleaning it up and making it into a module.  :-\

Crimson Wizard

Quote from: Danvzare on Fri 20/09/2024 12:44:44While the playing is moving, just keep waiting a single frame. The game will appear to progress as normal.

This approach is implemented in the Khris's "GotThere" module, linked above.

Unfortunately, topic starter have never mentioned requirements for this feature, so I cannot tell whether this will suit them or not.
As I noted multiple times in this thread, this approach prevents player from interacting with anything else during the walk (other objects, GUIs, etc). Player may only click or press a key to abort current move, nothing else. If that's acceptable, then this may be a way to go.

OTOH, it may be possible to generalize this method by waiting in on_mouse_click instead. This way you do not have to insert this "wait" command in every interaction event. But you must provide "walkto" point in custom properties.
This is a trade off: write more in script, and have a chance to forget, which leads to certain action being non-interruptible, so less consistent,
or have to specify custom properties, but each action will be guaranteed to be handled the same way.

Crimson Wizard

So, speaking of coroutines discussed above in this thread (among other things),
I've been rewriting the script executor class for the AGS 4 engine, for unrelated reasons, when it came to me that these changes add a potential to implement coroutines.

So I did a very quick and dirty test, and although done wrong, it works in practice (with lots of restrictions).

https://github.com/ivan-mogilko/ags-refactoring/tree/experiment--dumb-coroutine

Normally this would require new script syntax and keyword(s) (like "yield", "async" or aforementioned "waitfor"), but here I did not want to bother with compiler so added WaitAsync(loops) function that converts current script into "coroutine".

In principle it works like this:

- WaitAsync is called, it sets "async wait counter" and asks executor to suspend;
- script executor suspends current script thread and returns;
- engine checks execution result, finds that thread was suspended, copies its full state to the special "coroutine thread";
- game continues to run as normal, with all the player input, GUI, etc;
- engine updates "async wait counter" on each game tick;
- as soon as that reaches zero, it tells script executor to run the previously saved "coroutine thread";
- more suspends are possible, coroutine thread may be suspended again and again after each start, until it finishes.

The current implementation cannot have multiple simultaneous coroutines, any new one will overwrite previous completely, but that's just because I was lazy. Normally it should have a list of wait counters mapped to coroutine threads, or something similar.

Example of script that I used to test this:

Code: ags
function RunCoroutineTest()
{
    Display("RunCoroutineTest: 1");
    WaitAsync(60);
    Display("RunCoroutineTest: 2");
    WaitAsync(60);
    Display("RunCoroutineTest: 3");
    WaitAsync(60);
    Display("RunCoroutineTest: 4");
}

function on_key_press(eKeyCode key)
{
    if (key == eKeyW) RunCoroutineTest();
}

So what happens here is that after you press W, the sequence of Displays is going to run one by one with delays, but at the same time the rest of the game goes on normally, character walks, GUI works, other scripts run, etc.


EDIT:
Maybe we should start a separate forum thread for this, as the original question was about interrupting blocking action rather than scheduling asynchronous actions.

Danvzare

That's awesome Crimson Wizard.  8-0
Even I can think of a couple of scenarios from the top of my head where I could use that functionality, so I can't imagine how useful that would be to others.

eri0o

Quote from: Crimson Wizard on Tue 07/01/2025 18:59:22Normally this would require new script syntax and keyword(s) (like "yield", "async" or aforementioned "waitfor")

I tried to read the async docs from C# and thought it was complex (it carries an additional Task thing that I guess is something like promise from JS).

But the result looks like something that can make much easier to accomplish mostly of what I think people use Timer for.

I guess if there is an already speech or display call, when the executor wait time expires, they still would to wait for the blocking thing to end so it can spawn a new blocking thing (like Display("RunCoroutineTest: 2");).

Edit: actually I am not sure I get this, does doing this would require a WalkAsync to wait for the blocking walk to finish or using regular Walk with async would work? The issue I see here is a "cancel" action would still cause the next line after the walk to execute.

Crimson Wizard

Quote from: eri0o on Wed 08/01/2025 01:16:04I guess if there is an already speech or display call, when the executor wait time expires, they still would to wait for the blocking thing to end so it can spawn a new blocking thing (like Display("RunCoroutineTest: 2");).

It depends on how the coroutine update is run, if they are made to run during blocking actions or not.
There's also a question of whether there should be a difference between coroutine started from the normal event and from rep-exec-always.

Quote from: eri0o on Wed 08/01/2025 01:16:04Edit: actually I am not sure I get this, does doing this would require a WalkAsync to wait for the blocking walk to finish or using regular Walk with async would work? The issue I see here is a "cancel" action would still cause the next line after the walk to execute.

WaitAsync is the most primitive implementation for the test. Normally this would require having all the functions that start actions to return "action identifier", and let coroutine wait for them.

It also should not happen by the function call itself, but by some keyword over function call's return value.

SMF spam blocked by CleanTalk