Adventure Game Studio

AGS Development => Editor Development => Topic started by: blexx on Tue 20/08/2024 01:21:04

Title: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: blexx on Tue 20/08/2024 01:21:04
Hello, I've been working with AGS for a while now and I have finally decided to register. First of all, I have to say that I have already tried a few engines, but AGS is definitely the most intuitive - it's really fun to work with and very well thought-out!

The only thing I've noticed, both when playing a lot of AGS games and now when creating them, is that one thing in particular is strange and REALLY outdated:

Let's say you define a staircase as an hotspot exit so that you can eventually change the room by using it. Like this:

function stairs_Interact(Hotspot *theHotspot, CursorMode mode)
{
character.Walk(248, 178, eBlock); 
character.ChangeRoom(2,7,75); 
}

Clicking on this hotspot will make the player walk to it, BUT the cursor will be disabled in this process. This prevents you from canceling the walk. I have seen that there is a template that tries to prevent this behavior, but it does not always work reliably (Tumbleweed). I don't understand why the default behavior is not that you can cancel this walk. Pretty much every adventure game does this nowadays. I think it breaks the flow of the game if you're forced to wait as if it is a cutscene. I haven't seen that anywhere else. It would be also nice, to support double click to skip the walking instantly to change the room.

To demonstrate how much better it would be, here's another video:

https://streamable.com/y3bfw4

Especially if you often have to walk long distances and have many screens, this is immediately noticeable in a negative way. That's why i wanted to ask if there is a possibility to add a function that makes it possible to cancel this walk. :)
Title: Re: Feature request: Behavior of hotspot that results in a room change
Post by: eri0o on Tue 20/08/2024 01:26:16
I simply do this differently, I put the change room in a space check (either edge, or region or a check in rep exec if it requires some other condition) and leave the walk as non-blocking. Then you can just click elsewhere before the player arrives.

I don't do this way but you can also do a walk non blocking and then throw a while that just waits for the player to finish walking and check for the cancellation condition and if it arrives the function exits earlier. Never tried but it sounds like it should work.
Title: Re: Feature request: Behavior of hotspot that results in a room change
Post by: blexx on Tue 20/08/2024 01:42:11
But you know where I'm going with this, right? That really stands out. It somehow doesn't fit in with the rest, which is incredibly intuitive and well thought-out.

Can you give me some example codes of how you would approach this issue, eri0o? That would be nice. Then I'll play around with it a bit...
Title: Re: Feature request: Add alternative behavior of hotspot that results in room change
Post by: Crimson Wizard on Tue 20/08/2024 03:18:32
The basic way which lets player to cancel a continuous action in AGS is to call such action with eNoBlock argument.

Such as:
Code (ags) Select
function stairs_Interact(Hotspot *theHotspot, CursorMode mode)
{
    character.Walk(248, 178, eNoBlock);
}

And then detect when the character actually arrived at the location and call ChangeRoom in a separate event, using either "stand on hotspot" event, using a Region, or Room's "crossed edge" event, whichever seems more suitable.

That is, so to say, a foundation of scripting non-blocking sequences of actions in AGS, when some things (like character actions and reaction to player input) are supposed to happen in parallel. You break the chain of actions in 2 or more parts: the start and the end (etc), and separate them in script.

If you need another event to happen after certain previous action has finished, while player retains ability to control the game, but detecting a "position" is not suitable, then this is done by testing current character or game's state in "repeatedly_execute" function. There's a related article in the manual:
https://adventuregamestudio.github.io/ags-manual/RepExec.html

I suspect that certain blocking actions may be cancelled with player input, if you test for mouse button states in "repeatedly_execute_always" and issue a command that cancels the current action, e.g. character.StopMoving(). But that may not be convenient for a number of reasons, so above method is preferrable.



FURTHER EDIT:


I suppose that it could be technically possible to support interrupting blocking Walk by a player's input. After all Say command and few similar ones (Display, etc) are intended to be interrupted by input (mouse key). So why not do the same to Walk (and others)?
The simplest answer is probably: that won't be enough to solve your problem, and may create more inconsistencies in the end.

The problem is, AGS scripting system is made in such way, that it does not allow to have two or more script functions run simultaneously, nor have player input events (mouse clicks, key presses, etc) be handled while the script function is running, or any blocking action within it. The script functions also cannot be interrupted or aborted from elsewhere.

If player "cancels" the Walk command, the script execution will return to the same function and continue running next command (ChangeRoom in your case).
In order to fix that , you would have to check for Walk's result. Like in this pseudo-code:

Code (ags) Select
result = character.Walk(248, 178, eBlock);
if (!result)
    return; // break out of the function
character.ChangeRoom();

Here it looks simple enough, but imagine you have a longer sequence, then your script will have to be littered with these result checks.

Furthermore: since in AGS you cannot handle player's input events in script while a blocking command is running so functions like "on_mouse_click", "on_key_press" and similar - won't work during blocking walk. That will prevent from freely scripting and customizing reaction to input during this action. Because of that, if we have a "cancellable" Walk, then we'll also have to introduce extra input configuration for cancelling, just like there's a configuration for skipping speech now, because each game developer will want their own control style.
And still the list of what is possible to do during a blocking Walk command will be very limited. For example, GUI won't work, so player won't be able to access menu while character is walking, and so forth.

Therefore such function support will solve one tiny issue, but won't raise general restriction, won't make things much easier.

So it is rather essential to learn how non-blocking actions can be scripted in AGS instead.
Title: Re: Feature request: Add alternative behavior of hotspot that results in room change
Post by: Crimson Wizard on Tue 20/08/2024 07:38:08
Right, I missed this question:

Quote from: blexx on Tue 20/08/2024 01:21:04It would be also nice, to support double click to skip the walking instantly to change the room.

AGS provides you with basic functions, but requires that you script any advanced behavior yourself.
It has everything necessary for you to script double click which skips the walking to destination.
There's "on_mouse_click" function that detects clicks, alternatively mouse button state may be tested in "repeatedly_execute" function.
Skipping Character's walk may be done using SkipUntilCharacterStops() function, or simply moving character to the target walking position, referenced by Character.DestinationX/Y properties.

I'm just quickly overviewing this here, maybe there are other things that will have to be addressed in your particular case.
There also may be existing script modules or templates that do what you need.

Overall, I recommend to rather begin with asking "how to do this in AGS?" questions in the "AGS Support" section of the forum, because for the most part there are existing methods to do what you want, even though they may not be obvious at start.
Title: Re: Feature request: Add alternative behavior of hotspot that results in room change
Post by: Danvzare on Tue 20/08/2024 12:23:12
Quote from: blexx on Tue 20/08/2024 01:21:04Clicking on this hotspot will make the player walk to it, BUT the cursor will be disabled in this process. This prevents you from canceling the walk.
I literally copy and paste the same snippet of code between all of my projects, exactly because of this.

I originally stole it from the Maniac Mansion Mania template.
I'm not sure how helpful it'll be for you, but here it is:
function move_player (int x, int y)
{
// Move the player character to x,y coords, waiting until he/she gets there,
// but allowing to cancel the action by pressing a mouse button.
  mouse.ChangeModeGraphic(eModeWait, 5);
  cancelled = 0;
  player.Walk(x, y, eNoBlock, eWalkableAreas);
  player.Walk(x, y, eNoBlock, eWalkableAreas); // Seems to fix a bug where it cancels out early if you're already moving
  // Wait for release of mouse button
  while (player.Moving && (mouse.IsButtonDown(eMouseLeft) || mouse.IsButtonDown(eMouseRight)))
  {
    Wait (1);
    lblActionText.Text = Game.GetLocationName(mouse.x, mouse.y);
  }
  // Abort moving on new mouse down
  while (player.Moving)
  {
    if (mouse.IsButtonDown(eMouseLeft) || mouse.IsButtonDown(eMouseRight))
    {
      player.StopMoving();
      player.ActiveInventory = null;
      cancelled = 1;
    }
    else
    {
      Wait (1);
      lblActionText.Text = Game.GetLocationName(mouse.x, mouse.y);
    }
  }
  // A counter measure in case the function doesn't run correctly.
  if (player.x != x || player.y != y)
  {
      //player.Say("I am currently at X:%d   Y:%d", player.x, player.y);
      //player.Say("And I'm supposed to be at X:%d   Y:%d", x, y);
      cancelled = 1;
  }
  mouse.ChangeModeGraphic(eModeWait, 0);
  lblActionText.Text = "";
  return !cancelled;
}

You just throw that into an If statement, and there you go.
Something like this:
if(move_player(248, 178))
{
  player.ChangeRoom(2,7,75);
}


It would be nice if there was a function that already did this though.
Title: Re: Feature request: Add interrupting blocking walk by a player's input
Post by: blexx on Tue 20/08/2024 12:37:25
Thank you very much for responding to my post in such great detail. I really appreciate it!

Quote from: Crimson Wizard on Tue 20/08/2024 03:18:32The basic way which lets player to cancel a continuous action in AGS is to call such action with eNoBlock argument.

Such as:
Code (ags) Select
function stairs_Interact(Hotspot *theHotspot, CursorMode mode)
{
    character.Walk(248, 178, eNoBlock);
}

And then detect when the character actually arrived at the location and call ChangeRoom in a separate event, using either "stand on hotspot" event, using a Region, or Room's "crossed edge" event, whichever seems more suitable.

That is, so to say, a foundation of scripting non-blocking sequences of actions in AGS, when some things (like character actions and reaction to player input) are supposed to happen in parallel. You break the chain of actions in 2 or more parts: the start and the end (etc), and separate them in script.

If you need another event to happen after certain previous action has finished, while player retains ability to control the game, but detecting a "position" is not suitable, then this is done by testing current character or game's state in "repeatedly_execute" function. There's a related article in the manual:
https://adventuregamestudio.github.io/ags-manual/RepExec.html

I suspect that certain blocking actions may be cancelled with player input, if you test for mouse button states in "repeatedly_execute_always" and issue a command that cancels the current action, e.g. character.StopMoving(). But that may not be convenient for a number of reasons, so above method is preferrable.
The main problem is that as soon as you start function stairs_Interact(Hotspot *theHotspot, CursorMode mode) the mouse cursor  gets invisible no matter what and I want to have this exit as a hotspot. So yes, I could break the chain of actions in more parts, but that wouldn't have any effect on the mouse disappearing if I define it as a hotspot.


Quote from: Crimson Wizard on Tue 20/08/2024 03:18:32I suppose that it could be technically possible to support interrupting blocking Walk by a player's input. After all Say command and few similar ones (Display, etc) are intended to be interrupted by input (mouse key). So why not do the same to Walk (and others)?
The simplest answer is probably: that won't be enough to solve your problem, and may create more inconsistencies in the end.

The problem is, AGS scripting system is made in such way, that it does not allow to have two or more script functions run simultaneously, nor have player input events (mouse clicks, key presses, etc) be handled while the script function is running, or any blocking action within it. The script functions also cannot be interrupted or aborted from elsewhere.
[...]
Because of that, if we have a "cancellable" Walk, then we'll also have to introduce extra input configuration for cancelling, just like there's a configuration for skipping speech now, because each game developer will want their own control style.
And still the list of what is possible to do during a blocking Walk command will be very limited. For example, GUI won't work, so player won't be able to access menu while character is walking, and so forth.

Therefore such function support will solve one tiny issue, but won't raise general restriction, won't make things much easier.

I don't have an extremely deep understanding on the entirety of this project, but I still think that the fact that the default settings of a hotspot is to make the mouse cursor invisible is anything but intuitive. Even if you couldn't do anything until the character stops walking, it would be better than waiting for a (seemingly) little cutscene to start, where you can't do anything - as it is currently the case. It never feels good when you are prevented to interact in a adventure game (at least, when it is something like that). That's just something I've noticed in pretty much all AGS games when I play them. No other engine does it like that these days and I'm always able to interrupt this process.

I also find it odd that there is simply no function for the double-click exit, as it has become the normal standard in adventures. It makes totally sense that you can and should script advanced behavior yourself, but I think these two things are nowadays a "basic function" that should be easy to achieve and AGS should provide a simple function for them. But well... the way you explained it sounds good. I'm sure that can be implemented in a way.


Apart from that, I really have nothing to complain about, those are the only negative things I noticed.

Thanks again for your post!  :smiley:

Quote from: Danvzare on Tue 20/08/2024 12:23:12It would be nice if there was a function that already did this though.
Indeed, but thank you very much for this code! :D I'll take a closer look tonight and give you feedback.
Title: Re: Feature request: Add alternative behavior of hotspot that results in room change
Post by: eri0o on Tue 20/08/2024 12:42:49
The mouse doesn't become invisible, you probably just haven't set a cursor for the wait mode.

This double click interaction exit is not standard too, each person will develop their own way - what could be done here is have some way to detect double clicks in engine script API itself. Because of all things that CW mentioned I don't think this makes sense in the Engine Script API but someone could write something and put in a module. That someone will probably be yourself after you learn more about the engine.

Sometimes I think something like a standard library in script would make sense for these things that are really hard to do in engine code but trivial in script.
Title: Re: Feature request: Add alternative behavior of hotspot that results in room change
Post by: Snarky on Tue 20/08/2024 14:05:48
Quote from: eri0o on Tue 20/08/2024 12:42:49This double click interaction exit is not standard too, each person will develop their own way - what could be done here is have some way to detect double clicks in engine script API itself. Because of all things that CW mentioned I don't think this makes sense in the Engine Script API but someone could write something and put in a module.

https://www.adventuregamestudio.co.uk/forums/modules-plugins-tools/module-doubleclick-keylistener-detect-double-clicks-and-more/

I agree that a "standard library" would be nice. It could even be something as simple as a manual page (maybe an FAQ) with a list of "If you want to do X, we recommend: module A."
Title: Re: Feature request: Add function to interrupt walk by a player's input
Post by: Crimson Wizard on Tue 20/08/2024 18:49:39
1. The game engine is a system of multiple levels of functionality. There's a core level which provides most basic functions, and then there are advanced levels that have more complicated and specialized ones, but any advanced function is ideally built of the basic ones. These levels are usually divided between the engine itself as a program executable, and the game script, written by its users. The game script futher may contain several of such levels. It is possible to write your own "game framework" fully in a script, which is then used as a foundation for your own games, or given out to other people to base their games on.

The thing here is that the lower the level the harder it is to customize the functionality, and the more difficult is to change anything. If a function is inside the engine, it will affect everyone, whether they want it or not, and they won't be able to modify its behavior without hacking and recompiling the engine. If a function is in a game script - then it will affect only people using that script, and these people will be able to modify the behavior easily.

The balance, the distribution of what is implemented inside engine program itself, and what is implemented in the script, is the significant problem of program design. Each game engine may decide this in its own way.

AGS is the engine purposed initially for adventure games, so it already has a big number of specialized things in its core functionality: things like inventory, speech, etc. But at the same time it provides more basic functions which are enough (at least in many cases) to customize, or remake any kind of behavior from scratch.

But it is a very old engine, and its core behavior sort of "solidified" in its state. Changing it now may be difficult for programmers, because it has to be "plunged" into existing system, and also because it will affect every user. Adding complex functions into the engine may also be difficult, because it's not easy to design them universal or customizable enough to suit majority of users.

This is why today we prioritize adding "basic" functions instead: something that will increase user's ability to script things the way they prefer in their own game.

2. AGS is a very old engine, it was created 25 years ago. Back then the standards and habits were different; people mostly wanted to replicate classic adventure of 80-ies and 90-ies. The initial AGS functionality addressed these wishes.

Supposedly, it might be possible to add new functionality that addresses todays "adventure game standards" into the engine. But how wise that would be?

Let's take "double click to exit" example.

First of all, AGS does not have a concept of "exit", at all. There are things like hotspots, characters, objects, but none of them are "exit". The thing serving as an "exit" is defined only by the commands that user types in script. So, if we want such function built-in into the engine, we will have to introduce a complete new concept. Whether that is a new type of object, or a new property of hotspot, etc, something that would let users to tell "this is the exit".

Then, having such exit to trigger when player double click on it, that's a quite a specific behavior. It may be a "standard" today, but what would happen if users will suddenly want a different one? Hold mouse button for 1 second, or make a gesture with the mouse. Should we modify the engine every time a "fashion" changes?
What if users will want to support hotkeys on keyboard too? Or another device, such as touch-screen on mobiles. What about porting the game to consoles that don't have a mouse at all: there would have to be certain combination of buttons and stick moves on gamepad.

If that's done inside the engine, that will be an endless annoyance for both the engine developers, and also users who want something else.
If that's done inside the script, provided engine lets script this easily enough, - each game developer will be able to customize their game as they see fit.

Of course, requiring each individual user to do this from scratch may not be a good thing. But this is where shared script modules come into play. If someone writes a module, or a whole "framework" in script that implements "modern standard" for game controls, then others could use that too.
Engine's responsibility here would be to provide all necessary basic bits, from which such framework could be scripted.
Title: Re: Feature request: Add function to interrupt walk by a player's input
Post by: Crimson Wizard on Tue 20/08/2024 19:22:56
Quote from: blexx on Tue 20/08/2024 12:37:25I still think that the fact that the default settings of a hotspot is to make the mouse cursor invisible is anything but intuitive.

There's no setting of a hotspot that makes cursor invisible.
A number of things must be clarified here.

The only case in which hotspot does something automatically is when:
* you use its "WalkTo Point" property. I don't remember if there's a way to disable this other than not having this "WalkTo Point" set. This is one of the unfortunate quirks of AGS.
* have "automatically walk to hotspot in Look mode" option set in General Settings. That's another ancient quirk, which is there mostly for backwards compatibility.

If you do not have "WalkTo Point" set, nor that option enabled, then AGS is not supposed to do anything on its own. Instead it does what you command it to in script.

In any case, when AGS is running a blocking action, like function that you call with eBlock argument, or Wait() command, it enables a "waiting" mode, where all GUI is disabled, and mouse cursor switches to "Wait" cursor mode.
Each cursor must have a graphic assigned to it. If none is assigned then none is drawn. If you do not have such graphic assigned to Wait cursor, then during waiting mode you will see no cursor.

The rest depends on whether you want a blocking or non-blocking action.
Title: Re: Feature request: Add function to interrupt walk by a player's input
Post by: Crimson Wizard on Tue 20/08/2024 19:39:09
Quote from: Danvzare on Tue 20/08/2024 12:23:12You just throw that into an If statement, and there you go.
Something like this:
if(move_player(248, 178))
{
  player.ChangeRoom(2,7,75);
}


It would be nice if there was a function that already did this though.


Alright, it might be possible to add a modified variant of Walk function, that is interrupted by player input and returns a boolean result.

But this raises a question: how do we customize the interruption?
Right now there are several separate skipping settings already in AGS: for speech, for video, for cutscenes. There's Wait command that accepts a "input type" as an argument, but does not let to e.g. specify exact button or key.
Supposedly we could add similar argument to the Walk function. Which other functions may require that, which will eventually be asked by users?
Character.Walk and Character.Move, Object.Move; what about other blocking functions such as Animate, will it need interruption too?
Is it good to have this as an argument to each function call, or this should rather be a game-wide setting?
This has to be well designed.

If there were this "Actions" feature that eri0o once suggested, where you define "Action" and setup controls for it, then we could utilize that. But without one, I'm afraid this will just add another bit to the pile of mess.

**EDIT:**

Hypothetically, if I had to do this in the engine, then I'd probably introduce a new BlockStyle type, something like eBlockCancellable (can't figure a better name now). This will allow to tag commands which you want to be interruptible per case more easily. Then add a game-wide setting that defines skipping controls for ALL functions that are run with eBlockCancellable.




And I need to clarify again: because this function is blocking, nothing else will work during its run. It may be only "interrupted", but it won't be possible to do things like:
- issue another command while character still walks (you'd have to do 2 clicks: first interrupt current walking, then order something else);
- interact with any GUI
- open a menu
- pause the game
- exit the game
etc.

All the above cannot be fixed without scripting this in a different way.
So, I am in doubts here.
Title: Re: Feature request: Add function to interrupt walk by a player's input
Post by: eri0o on Tue 20/08/2024 22:03:01
The skip things need to support gamepad/joystick too, which I only partially implemented. There's also a question of how it should work with touch - non-mouse emulated. Presumably a person may rest one finger and later put a second finger in multi-touch interface.
Title: Feature request: Add function to interrupt walk by a player's input /double-clicking
Post by: blexx on Wed 21/08/2024 19:30:49
QuoteSupposedly, it might be possible to add new functionality that addresses todays "adventure game standards" into the engine. But how wise that would be?

Let's take "double click to exit" example.

First of all, AGS does not have a concept of "exit", at all. There are things like hotspots, characters, objects, but none of them are "exit". The thing serving as an "exit" is defined only by the commands that user types in script. So, if we want such function built-in into the engine, we will have to introduce a complete new concept. Whether that is a new type of object, or a new property of hotspot, etc, something that would let users to tell "this is the exit".

Then, having such exit to trigger when player double click on it, that's a quite a specific behavior. It may be a "standard" today, but what would happen if users will suddenly want a different one? Hold mouse button for 1 second, or make a gesture with the mouse. Should we modify the engine every time a "fashion" changes?
I get where you're coming from and I totally understand that you don't want to follow every fashion trend, but double-clicking and interrupting a walk by a player's input has been a standard for over 10 years now. Just like the popup inventory bar, it's not going anywhere because it has proven it's worth in the adventure genre. That's why I think they deserve to have at least a basic function that can then be further defined by scripting.

QuoteHypothetically, if I had to do this in the engine, then I'd probably introduce a new BlockStyle type, something like eBlockCancellable (can't figure a better name now). This will allow to tag commands which you want to be interruptible per case more easily. Then add a game-wide setting that defines skipping controls for ALL functions that are run with eBlockCancellable.
Unfortunately, I can't help you how to customize interruption or double-clicking, but reading something so well thought out as your first thought process makes me positive that you will find a proper implementation in the future. I don't know how or if this will be implemented, but I'm glad that this has been discussed and that I'm not alone in wanting this kind of feature.

I'm going to take a closer look at Danvzare's solution now. Sounds promising. ^^
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Wed 21/08/2024 22:18:09
Quote from: blexx on Wed 21/08/2024 19:30:49I get where you're coming from and I totally understand that you don't want to follow every fashion trend, but double-clicking and interrupting a walk by a player's input has been a standard for over 10 years now. Just like the popup inventory bar, it's not going anywhere because it has proven it's worth in the adventure genre. That's why I think they deserve to have at least a basic function that can then be further defined by scripting.

No, "double click to exit" is a way too specific behavior to add it inside this engine.
AGS already has enough basic functions to do "double click to exit" in script rather easily.

We might definitely add a explicit "double click" event for the script, and we might even consider adding an individual "double click" event for hotspots too.
But not more than that. What happens on "double click" is a task for s game script, not the engine.
We like to suggest and encourage users to write game templates that define standard control schemes, and share these.

For the last several years we've been trying to remove some of the overspecialized engine's behavior, or replace it with more basic functions that would let users recreate and customize these in script. You've mentioned a "popup inventory bar". In the past we've been considering to remove the builtin "Popup when mouse is at top of the screen" gui style from the engine completely in the next major version, because it's at the same time a) limited, b) has a number of hardcoded quirks that sometimes contradict user's expectation. If you check out BASS template, for instance, it is not using the built-in "popup at y" gui style, but has one coded right in script. So we're sort of been going into the different direction lately.



As for the "exit", having any interaction leading to "exit" requires to have "exit" as a game element. Something that may be created and configured in the Room Editor. Unless that is implemented, any kind of special interaction regarding "exit" will be overcomplicated to do in the engine.

Maybe it would make sense to implement such type of game element. But this is a engine design question that I cannot discuss right now, it touches bigger subjects, such as, where is the line between what should be in the engine and what should be in script. Like I said, every engine has its own. Without defining that, any feature addition will bring a risk of complicating the engine and increasing inconsistency. I have my own opinion on this, others might have theirs.
If someone would like take control and responsibility over AGS design, they could decide which other game elements AGS needs, and their standard behavior.
I cannot do this, because I already have enough of work, and I am actually at the edge of deciding to leave this project, been working on it for over 12 years and grown very tired of it.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Babar on Thu 22/08/2024 04:14:49
Sorry to interject, but I was just wondering about approaching stuff from the opposite direction as well:
Instead of investigating how to interrupt blocking functions, what about how to incorporate non-blocking functions?

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

And have that all work in that order, in that sequence, and if the walk is interrupted by something else, stop the function?
Because the desired functionality here doesn't appear to be wanting to interupt blocking actions, but rather, wanting to be able to have actions be non-blocking, which currently can't be.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Thu 22/08/2024 04:51:31
Quote from: Babar on Thu 22/08/2024 04:14:49Sorry to interject, but I was just wondering about approaching stuff from the opposite direction as well:
Instead of investigating how to interrupt blocking functions, what about how to incorporate non-blocking functions?

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

And have that all work in that order, in that sequence, and if the walk is interrupted by something else, stop the function?
Because the desired functionality here doesn't appear to be wanting to interupt blocking actions, but rather, wanting to be able to have actions be non-blocking, which currently can't be.


Exactly! I was speaking about this couple of times in my past comments.

While it is technically feasible to let interrupt blocking action both with player input and a script command (from repeatedly_execute_always), - and we already have examples of that: speech, Wait, blocking video playbacks - all these may be interrupted, - the blocking nature of this "interruptible" action prevents doing anything else while its running.

It is only when the action is non-blocking (in AGS terms) - then both player and script will have full control of the game during the action.

Each action in AGS may be run non-blocking (with a very few exceptions). It is possible to have that.
@Babar when you say "which currently can't be", I assume you mean that it's not possible to have the next actions to wait until non-blocking finishes in the same script function?

The thing is that if you run action non-blocking you got to separate the continuation of the sequence. There has to be a trigger that runs next part of this sequence, currently that is something that you have to script yourself. All the newcomers have problem with that, but eventually this is something that you just have to learn.

So the question here may be put like:
How can we improve AGS scripting to let easier scripting of scheduled sequence of actions, some of which may be non-blocking, a sequence that also detects when previous action was cancelled and automatically cancels itself.

Is that it?
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Babar on Thu 22/08/2024 05:38:59
Quote from: Crimson Wizard on Thu 22/08/2024 04:51:31@Babar when you say "which currently can't be", I assume you mean that it's not possible to have the next actions to wait until non-blocking finishes in the same script function?
Yes, that is what I meant. I know it is possible to script around this situation, but it being a generalised "basic" functionality to be able to, as you say, schedule the sequence of nonblocking actions in script, would make things a lot easier
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Snarky on Thu 22/08/2024 09:24:36
If we had anonymous functions and function pointers (lambda expresssions, anonymous functions or delegates), we could do something like:

Code (ags) Select
cEgo.Walk(x, y, eNobBlock,
         (completed) ->
         {
           if(completed)
           {
             cEgo.Say("I will say this when I reach x and y!");
             cEgo.ChangeRoom(5);
           }
         });

(This assumes that Character.Walk accepts an optional delegate, an anonymous function of signature void f(bool), which it calls once the walk completes or is interrupted.)

Personally I don't like the way code using lambdas ends up looking, so if AGS was to introduce something like this I would suggest something more like:

Code (ags) Select
  waitfor(cEgo.Walk(x, y, eNobBlock) => bool completed)
  {
    if(completed)
    {
      cEgo.Say("I will say this when I reach x and y!");
      cEgo.ChangeRoom(5);
    }
  }

Here waitfor (could also be called "schedule", "async" or some other suitable name) is a new keyword similar to if, for, while, etc., which accepts functions that return an asynchronous result as an argument, and schedules the following block to run (mapping the result to a parameter) once it does.

(Under the hood, what probably happens is that Character.Walk returns a pointer to an event listener, and waitfor adds the block as an event handler to that event.)
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Thu 22/08/2024 09:58:38
I 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.

Having nesting with condition is what makes it even worse. In majority of cases this likely will be a sequence with no branching, next commands are only supposed to be executed if previous ones were not interrupted.

This means that the cleaner variant of syntax is the one that assumes that case, and implies the condition check, but does not require user to write one explicitly.

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

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

where "waitfor" wraps waiting for completion, and "waitorcancel" also wraps result check and return statement.

Well, that's just a dumb example, but I'd rather propose go into the direction of this "linear" syntax.


EDIT:
But lambdas or not, I think the first problem is to support functions of type "coroutine". That is - functions that return temporarily and then enter back at the point where they stopped last time.
OR, support engine running multiple function threads at the same time.
Because right now AGS does not process player input while inside any script function.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: eri0o on Thu 22/08/2024 10:20:35
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

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
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Thu 22/08/2024 11:50:47
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Snarky on Thu 22/08/2024 13:02:21
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) Select
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) Select
  // 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) Select
  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) Select
  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) Select
  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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Thu 22/08/2024 14:08:43
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) Select
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) Select
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".
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Snarky on Thu 22/08/2024 17:21:38
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) Select
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) Select
while (condition)
{
  waitFor(action);
  someOtherFunction();
}

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

Code (ags) Select
  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) Select
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: eri0o on Thu 22/08/2024 17:31:25
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
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Fri 23/08/2024 10:49:28
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) Select
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) Select
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.


Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Snarky on Fri 23/08/2024 11:32:43
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?
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Sat 07/09/2024 16:28:50
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) Select
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Snarky on Sat 07/09/2024 17:11:37
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) Select
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.)
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Sat 07/09/2024 17:14:38
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: eri0o on Sat 07/09/2024 18:46:44
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Snarky on Sat 07/09/2024 20:14:30
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) Select
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.)
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Thu 19/09/2024 23:23:30
@blexx, in regards to solving a cancellable interaction in the current version of AGS,
Looking back at my first reply in this thread (https://www.adventuregamestudio.co.uk/forums/index.php?msg=636665112) 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/


Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Danvzare on Fri 20/09/2024 12:44:44
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.  :-\
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Fri 20/09/2024 13:00:32
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Tue 07/01/2025 18:59:22
So, speaking of coroutines discussed above in this thread (among other things),
I've been rewriting the script executor class (https://github.com/adventuregamestudio/ags/pull/2621) 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) Select
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Danvzare on Tue 07/01/2025 19:30:07
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: eri0o on Wed 08/01/2025 01:16:04
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.
Title: Re: Request: Add function to interrupt walk by a player's input and double-clicking
Post by: Crimson Wizard on Wed 08/01/2025 11:14:19
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.