[SOLVED] Saving game before changing room

Started by Laura Hunt, Thu 28/05/2020 18:22:07

Previous topic - Next topic

Laura Hunt

Hey all,

My situation is the following:

- My game has its Main Menu in Room 1.

- I have a Pause Menu with, among others, a "Back to Main Menu" button.

- When you click on this button, you get a warning saying "Save progress?" and two buttons for Yes and No. Clicking on any of these buttons takes you to Room 1.

I have no idea, however, how to save the game before changing to room 1. Since SaveGameSlot is always executed at the end of the script, the room change will take place before that, and the Save command will never get executed.

Any hints? Is this even possible? ???




Snarky

#1
https://www.youtube.com/watch?v=C9JgZ45ENl4

Put the room change on a timer.


Laura Hunt

I got it! Here's the code for posterity :) (and in case anything can be improved)

Clicking on the "Yes, I wish to save my progress thank you very much" button:

Code: ags
function bYesSave_OnClick(GUIControl *control, MouseButton button)
{
  gPauseMenu.Visible = false;
  SetTimer(20, 3);  // maybe just 1 cycle is enough? Just being cautious here
  SaveGameSlot(23, "saved game");
}


In repeatedly_execute:

Code: ags
if (IsTimerExpired(20)) {
    Game.StopAudio();
    cInvisibleCharacter.ChangeRoom(1);
    cInvisibleCharacter.SetAsPlayer();
}


This causes a little problem, because as soon as you restore your saved game, the timer gets checked again, and you get sent to room 1 again and again. So we need one more check:

Code: ags
function on_event(EventType event, int data)
{
  if (event == eEventRestoreGame && data == 23) {
    SetTimer(20, 0);  // disarm timer
    gPauseMenu.Visible = false; // because there are other points in which you can save with the GUI still open, so just to be sure
  }
}


And everything works perfectly! Thank you so much, Snarky :)

Crimson Wizard

Quote from: Snarky on Thu 28/05/2020 18:26:56
Put the room change on a timer.

Depending on circumstances this may require to script a "disabled" state for your UI, during which player cannot press or click anything and no guis open no matter what.

Laura Hunt

Quote from: Crimson Wizard on Thu 28/05/2020 19:04:59
Quote from: Snarky on Thu 28/05/2020 18:26:56
Put the room change on a timer.

Depending on circumstances this may require to script a "disabled" state for your UI, during which player cannot press or click anything and no guis open no matter what.

What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

Crimson Wizard

Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

That is also an option that might work in most cases.

But personally I'd probably get paranoid that AGS can call on_mouse_click or key_pressed anyway before checking rep-exec, and implement disabled controls mode :).

Laura Hunt

Quote from: Crimson Wizard on Thu 28/05/2020 20:02:37
Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

That is also an option that might work in most cases.

But personally I'd probably get paranoid that AGS can call on_mouse_click or key_pressed anyway before checking rep-exec, and implement disabled controls mode :).

I can send you the game if you want and you can try clicking really fast and see if you manage to break it :D

Laura Hunt

An additional note (just in case this is useful for somebody in the future): I quickly realized that what I want to do is not simply go back to the main menu, because if a player chooses "New Game" from there, all the game states, variables, etc will be completely out of whack. What I actually need is to restart the game in order to revert everything to its initial state.

However, this would make the intro play again instead of going straight to the main menu, so what I've done is:

- Replaced the whole cInvisibleCharacter.ChangeRoom(1); shebang with RestartGame();
- Created a global bool, skipintro, and set it to false.
- Added the following condition to my on_event function:

Code: ags
  if (event == eEventRestoreGame && data == 999) {
    skipintro = true;
    game_start();
  }


- Wrapped my intro in the conditional if (!skipintro)

Et voilà! Now the intro only plays when you launch the game, but not when you restart.

Now my next challenge is figuring out how to store and apply the player's current volume level on restart. It would be very unpleasant for somebody who has set the volume slider really low to suddenly get blasted in the face with the main menu music at its default game_start value, so it would be good to figure out if there's a way to pull this off *puts thinking hat on but also accepts suggestions*

Crimson Wizard

Quote from: Laura Hunt on Thu 28/05/2020 23:14:43
Now my next challenge is figuring out how to store and apply the player's current volume level on restart. It would be very unpleasant for somebody who has set the volume slider really low to suddenly get blasted in the face with the main menu music at its default game_start value, so it would be good to figure out if there's a way to pull this off *puts thinking hat on but also accepts suggestions*

That should be easy with custom file in savegame directory. Files are natural solution for storing state independent of what happens in program.

Laura Hunt

Quote from: Crimson Wizard on Thu 28/05/2020 23:17:04
Quote from: Laura Hunt on Thu 28/05/2020 23:14:43
Now my next challenge is figuring out how to store and apply the player's current volume level on restart. It would be very unpleasant for somebody who has set the volume slider really low to suddenly get blasted in the face with the main menu music at its default game_start value, so it would be good to figure out if there's a way to pull this off *puts thinking hat on but also accepts suggestions*

That should be easy with custom file in savegame directory. Files are natural solution for storing state independent of what happens in program.

Amazing! I'll look into it and see what I can do with this. Thanks! ;-D

Laura Hunt

Code: ags
function bNoSave_OnClick(GUIControl *control, MouseButton button)
{
  gPauseMenu.Visible = false;
  
  File *customvolume = File.Open("$SAVEGAMEDIR$/customdata.dat", eFileWrite);
  if (customvolume == null) return;
  else {
    customvolume.WriteInt(System.Volume);
    customvolume.Close();
  }
  
  RestartGame();
  
}



Code: ags
function game_start() 
{
  File *customvolume = File.Open("$SAVEGAMEDIR$/customdata.dat", eFileRead);
  if (customvolume == null) System.Volume = 70;
  else {
    System.Volume = customvolume.ReadInt();
    sAudioVolume.Value = System.Volume;
    customvolume.Close();
  }
...
...
...
}



Works beautifully! And as a bonus, this also allows me to keep my previous volume level even when I close and re-launch the game. Thanks again for the tip, CW ;-D

fernewelten

#12
Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

The mechanism that AGS provides for that is CallRoomScript().

The button would call "SaveGameSlot(99);" and then  "CallRoomScript(RESTART_GAME);". Each room would have "function on_call(int script) { if (RESTART_GAME == script) do_your_thing();". Then the function do_your_thing() would be implemented in GlobalScript so that all the rooms have identical handling code. This handling code would run after "SaveGameSlot();" AFAIK. I hope.

CallRoomScript() is my go-to method whenever I need to start some code after the current script has finished. What a pity that this approach entails putting a room event handler into _every_ room, which is easy to forget and error prone.

Now if AGS had a similar "CallGlobalScript()" function that would trigger a "on_global_call(int script)" function in GlobalScript.asc … but alas.

Khris

That would be a huge pity, but you can just call the function directly as long as it's above the current one in the global script?

Crimson Wizard

Perhaps for this case, it would be more useful to have something like "after save game" event that you could get in on_event callback, similar to eEventRestoreGame?

But AGS events and callbacks would benefit from thoughtful redesign anyway.

Laura Hunt

#15
Quote from: fernewelten on Fri 29/05/2020 00:14:23
Quote from: Laura Hunt on Thu 28/05/2020 19:47:08
What would you use instead of a timer? A boolean variable maybe? (set to true when you save, check if it is true in repeatedly_execute_always, change room and set to false if it is, set to false also on on_event?)

The mechanism that AGS provides for that is CallRoomScript().

The button would call "SaveGameSlot(99);" and then  "CallRoomScript(RESTART_GAME);". Each room would have "function on_call(int script) { if (RESTART_GAME == script) do_your_thing();". Then the function do_your_thing() would be implemented in GlobalScript so that all the rooms have identical handling code. This handling code would run after "SaveGameSlot();" AFAIK. I hope.

CallRoomScript() is my go-to method whenever I need to start some code after the current script has finished. What a pity that this approach entails putting a room event handler into _every_ room, which is easy to forget and error prone.

Now if AGS had a similar "CallGlobalScript()" function that would trigger a "on_global_call(int script)" function in GlobalScript.asc … but alas.

I did not understand a single word of that :)

Is "RESTART_GAME" an... int? Where do you get/set the value of "script"? What should do_your_thing() do? This is all a bit too much for me, and the manual isn't helping (can't find on_call on there, for example). Would you be so kind as to give me a breakdown of what exactly is happening here to see if I can grasp it?

Snarky

#16
Check out this: https://www.adventuregamestudio.co.uk/manual/ags53.htm#CallRoomScript

Yes, RESTART_GAME is an int (preferably a constant; you could also use an enum).

Laura Hunt

Quote from: Snarky on Fri 29/05/2020 08:20:05
Check out this: https://www.adventuregamestudio.co.uk/manual/ags53.htm#CallRoomScript

Yes, RESTART_GAME is an int (preferably a constant; you could also use an enum).

I read that yesterday trying to grasp what fernewelten was talking about, but I still can't make heads or tails of it. The first thing that this entry says is "Calls the on_call function in the current room script" but there seems to be no entry or explanation in the manual for the "on_call" function and how it works.

If RESTART_GAME is an int, where do you define it? What value do you assign to it? 0, 1, 5, 23675? What exactly are you comparing when you do if (RESTART_GAME == script)? I seriously feel like I'm missing something super basic here that is keeping me from getting the big picture because right now this is like trying to read Japanese to me... Maybe a super super basic example would help me out?

Laura Hunt

#18
ok waitwaitwait I think I got it.

I assume You use RESTART_GAME instead of simply passing an int value directly, in order to make things more readable and not have to memorize what custom event each value refers to. If you want to have different stuff happening, you can create an enum assigning int values to names that make it easier to remember what you're doing (e.g., instead of having to remember that 0 in this context means "restart game", I assign the value 0 to RESTART_GAME). (Edit: I was confusing enums with arrays here; it's all clear now.)

Another thing that was confusing me a bunch was if (RESTART_GAME == script), but this is just the same as if (script == RESTART_GAME), right? You're just comparing both sides of an equation. It felt super counterintuitive to see it written "the other way around".

And then do_your_thing() would actually be RestartGame();

For this method to work though, this has to be true:

Quote from: fernewelten on Fri 29/05/2020 00:14:23
This handling code would run after "SaveGameSlot();" AFAIK. I hope.

Is this the case, then? Can anybody confirm that the handler would indeed be called after SaveGameSlot?

Let me know if I got everything right!

Khris

#19
Here's a minimal example:

Code: ags
// global script header

enum eRoomScriptParam { RESTART_GAME };

import function do_your_thing();

// global script
function do_your_thing() {
  RestartGame();
}

function btnRestart_OnClick(GUIControl* control, MouseButton button) {
  CallRoomScript(RESTART_GAME);
}


And this in *every* room script:
Code: ags
function on_call(eRoomScriptParam p) {
  if (p == RESTART_GAME) do_your_thing();
}


The manual says:
QuoteThe function doesn't get called immediately; instead, the engine will run it in due course, probably during the next game loop, so you can't use any values set by it immediately.

However, to reiterate: the same thing can be achieved by simply using SetTimer() with a delay of 1.

CallRoomScript() is not a mechanism to schedule code for the next engine loop, it's a mechanism to run room code from the global context.
Also: definitely seconding that  if (RESTART_GAME == script)  is counterintuitive and weird to read; the constant is supposed to be on the right of the operator. You don't say "if 5 equals x", you say "if x equals 5".

Laura Hunt

Quote from: Khris on Fri 29/05/2020 09:10:30
Here's a minimal example:

Beautiful. Thanks, Khris. It all clicks now.

Quote from: Khris on Fri 29/05/2020 09:10:30
The manual says:
QuoteThe function doesn't get called immediately; instead, the engine will run it in due course, probably during the next game loop, so you can't use any values set by it immediately.

However, to reiterate: the same thing can be achieved by simply using SetTimer() with a delay of 1.

Yeah the part where it says "probably during the next game loop" is hilarious. Probably. Maybe. Maybe not. Who knows, lol. But yeah, if it runs in the next game loop, then there's no advantage to this over setting a timer, as CW would still be paranoid that something could be pressed in that one cycle :-D

Thanks a lot again. This thread is teaching me SO much.

Snarky

Seems like you got it while I was writing, but since I already wrote it…

Quote from: Laura Hunt on Fri 29/05/2020 09:08:07
ok waitwaitwait I think I got it.

I assume You use RESTART_GAME instead of simply passing an int value directly, in order to make things more readable and not have to memorize what custom event each value refers to.

Correct.

Quote from: Laura Hunt on Fri 29/05/2020 09:08:07If you want to have different stuff happening, you can create an enum assigning int values to names that make it easier to remember what you're doing (e.g., instead of having to remember that 0 in this context means "restart game", I assign the value 0 to RESTART_GAME).

Yes, and I would use an enum rather than constants to ensure each value is unique. (I might also skip 0 just because that's often a "default" value that could conceivably be called by mistakeâ€"though with an enum that isn't a concern since AGS enums start from 1.)

Quote from: Laura Hunt on Fri 29/05/2020 09:08:07Another thing that was confusing me a bunch was if (RESTART_GAME == script), but this is just the same as if (script == RESTART_GAME), right? You're just comparing both sides of an equation. It felt super counterintuitive to see it written "the other way around".

I agree that it's counterintuitive, but this (putting the known value first) is a somewhat common style. For object-type comparisons in languages where the == operator can be overridden, it ensures that you don't try to run null.Equals(), which would crash.

Quote from: Laura Hunt on Fri 29/05/2020 09:08:07And then do_your_thing() would actually be RestartGame();

Right.

Original answer:

Quote from: Laura Hunt on Fri 29/05/2020 08:52:28
I read that yesterday trying to grasp what fernewelten was talking about, but I still can't make heads or tails of it. The first thing that this entry says is "Calls the on_call function in the current room script" but there seems to be no entry or explanation in the manual for the "on_call" function and how it works.

The explanation for on_call() is that entry: you add a function like this to your room script:

Code: ags
function on_call(int value)
{
  // ...
}


And then CallRoomScript(x); means that this function in the current room will be called with the value x as parameter. So if call CallRoomScript(RESTART_GAME); that means that in on_call(), the value of value is set to RESTART_GAME.

QuoteIf RESTART_GAME is an int, where do you define it? What value do you assign to it? 0, 1, 5, 23675? What exactly are you comparing when you do if (RESTART_GAME == script)?

Well, first, script is just what fernewelten has decided to name the function argument instead of value like in the manual.

You can define the constant in several places, but probably the easiest is to do so in the Globalscript header. You could do it like so:

Code: ags
#define RESTART_GAME 1


The number doesn't really matter, the only thing that matters is that if you use CallRoomScript/on_call for different things, you need to use different numbers, so that you can distinguish it in on_call().

Laura Hunt

Quote from: Snarky on Fri 29/05/2020 09:22:11
You can define the constant in several places, but probably the easiest is to do so in the Globalscript header. You could do it like so:

Code: ags
#define RESTART_GAME 1


The number doesn't really matter, the only thing that matters is that if you use CallRoomScript/on_call for different things, you need to use different numbers, so that you can distinguish it in on_call().

Yup, all clear! God, I love this "a-ha!" feeling when everything just clicks. It's like taking mushrooms and suddenly understanding everything, but cheaper.

Crimson Wizard

#23
on_call is missing a proper entry in the manual, it's only mentioned in CallRoomScript entry afaik.

Also, there's a bunch of obscure things in AGS that I am not aware about... for instance searching for on_call in the manual, I found game.roomscript_finished variable, which means that "The on_call function has completed executing." ...

Cassiebsg

Uhm...
I normally just save my restore point after the intro... never had to do all that before.  (roll)
There are those who believe that life here began out there...

Crimson Wizard

#25
Quote from: Cassiebsg on Fri 29/05/2020 21:21:51
I normally just save my restore point after the intro... never had to do all that before.  (roll)

The problem was that if you restore that point, all in-game option changes made by player during this game session will be lost. It does not matter where the restore point is.

Cassiebsg

Yes, that I know.
I was just referring to this post "https://www.adventuregamestudio.co.uk/forums/index.php?topic=58129.msg636621247#msg636621247" ...
QuoteWhat I actually need is to restart the game in order to revert everything to its initial state.

However, this would make the intro play again instead of going straight to the main menu, so what I've done is:

Saving the settings is a 3rd problem that come after.  ;)
There are those who believe that life here began out there...

Crimson Wizard

Quote from: Cassiebsg on Fri 29/05/2020 22:19:01
Yes, that I know.
I was just referring to this post "https://www.adventuregamestudio.co.uk/forums/index.php?topic=58129.msg636621247#msg636621247" ...
QuoteWhat I actually need is to restart the game in order to revert everything to its initial state.

However, this would make the intro play again instead of going straight to the main menu, so what I've done is:

Saving the settings is a 3rd problem that come after.  ;)

Now I understand. But that post is like 15 or 20 comments above in the middle of the previous page, and no one was discussing it since, so I won't be able to know without explanation or a quote

Laura Hunt

Quote from: Cassiebsg on Fri 29/05/2020 21:21:51
Uhm...
I normally just save my restore point after the intro... never had to do all that before.  (roll)

Right. The issue is that I might (not 100% sure yet) want to start the main menu music before the intro so that it starts playing while the intro is running, so putting the restore point after that wouldn't work for me :)

SMF spam blocked by CleanTalk