Adventure Game Studio

AGS Support => Advanced Technical Forum => Topic started by: Dualnames on Sat 30/03/2019 01:08:03

Title: Custom savegame stuffsies
Post by: Dualnames on Sat 30/03/2019 01:08:03
Alright, so as the typical AGS developer I have started on 'creating' my own save game system (the reasons behind this is the data I'm currently saving is way too big, and due to me precaching views and stuff my savegames are bloated to 120mb each. While that is no issue, the issue lies that I wanna make an autosave system and whenever I save due to the amount of data being saved, on autosave this slows downs the game to a significant degree. I'm of course open to ideas. Anyhow, so this is specific to my project so, yeah.

This is my plan.


-save game.doonceonlys states
-save dialog states
-save inventory items
-save player position x y
-save player room
-save states of rooms
-save local room variables
-save global variables
-save script variables

-Functions
-RainFX
-Visions
-Parallax
-BackgroundSpeech
-IconbarHandle
-UiHandling
-Payphone
-TarrotCards
-DialogArray
-DeadNPC
-Globalscript

-save UIs clickable/visible/position



For now I've gathered in all these scripts all the variables that I need to save (so then it will be loaded).
If this is the variables I want saved:


int store_mx=-1;
int store_my=-1;
bool StarfishAnimation=false;
bool StarfisherPrice=false;
int cPrompt=0;
String PromptOrder;
AudioClip*Hold;
struct Hint
{
  bool Given;
  int wasgiven;
  bool Active;
};
Hint Hints[39];



What would be an easy way to set up a system to export and import them safely?
Title: Re: Custom savegame stuffsies
Post by: eri0o on Sat 30/03/2019 02:52:46
Maybe you can use some other language (python?) to generate the ags script functions to read things and write in your variables and vice-versa. (I do that to create classes from json for c++ with quicktype and boost for a completely unrelated project)

Honestly feels like a lack of some dictionary like type will make harder to write this in a pretty way.

Sometime ago I pursued something similar here (I never finished but maybe there is something useful for you in the discussion):

https://www.adventuregamestudio.co.uk/forums/index.php?topic=55398.msg636573540#msg636573540
Title: Re: Custom savegame stuffsies
Post by: Crimson Wizard on Sat 30/03/2019 09:24:28
You can write variable names and some kind of type tags for safety preceding the value.
When reading:
- read name and type.
- does this match something you expect? if yes continue reading.
- if no, then use the type information to skip it (esp efficient for skipping arrays and structs).

If you have an array, esp array of structs, write a table of name/type beforehand, then on reading you will be able to use same table for all elements of array to save disk space (and bit of time).

Decide beforehand if you want to use "safe" functions from AGS File that add their own tags before values for detecting inconsistent reading. You may also write everything as strings too (convert values to strings).
If you have not much variables, then you could also use IniFile module, but that will open your data to everyone, so idk.

Regarding types, there may be ones that are difficult to write and read back. AudioClips, for instance, in current API they don't have IDs (which is in todo for too long). So when writing and readong them you can only make a huge switch and compare pointers, then write a custom name.


On format and reading: I see three approaches :

1) Read 1 data at a time then find a variable for that data. This is convenient for languages with reflection (C#, Lua etc), but sadly not AGS because you will need a big switch. Note that if you remember what struct you are reading right now, then the size of switch may be reduced. Grouping variables in "namespaces" may also reduce the size of switch for global variables (in other words, even if your variables are all global in script you may still write and read some of them as if they were struct).
Regardless this approach has always one benefit: you (mostly) do not need to care about format change, you will be able to read old format and probably even new format (and detect missing/unknown data).

2) Read all data at once into temporary storage, then find values for required variables. You may do this group by group (struct by struct) too.
IMHO this may be most convenient for AGS. Similarily to above it lets you (mostly) detect and resolve changes to format, except when variables are moved to another group. The disadvantage is that you have to script temporary storage, but this may be not a big deal.

3) Strict and dumb method: on every step declare which variable and type you expect next, then read a piece of data. If it does not match expectation, then either discard it or bail out with error.
The benefit of this is that it's the simpliest and fastest to code. You basically just create a list of what you want to write and read.
The obvious disadvantage: you won't be able to support format changes.
Title: Re: Custom savegame stuffsies
Post by: Dualnames on Sat 30/03/2019 11:40:22
Anyhow, it's a really complex thing right now, I'm looking at options. One idea i had which doesn't particularly work, because of how AGS runs interactions, is to store the clicks of the player and what he clicks on, and then reproduce the state of the game, by playing all these clicks. I'm already using my own custom wait function, and I'm also using my own say command, so all i would have to do is replace eblock on animations by setting the last frame instead of playing the animation. Unfortunately that part doesn't work because runinteractions runs the script at the end I believe.
Title: Re: Custom savegame stuffsies
Post by: Crimson Wizard on Sat 30/03/2019 11:45:49
Quote from: Dualnames on Sat 30/03/2019 11:40:22
Anyhow, it's a really complex thing right now, I'm looking at options. One idea i had which doesn't particularly work, because of how AGS runs interactions, is to store the clicks of the player and what he clicks on, and then reproduce the state of the game, by playing all these clicks.

That sounds overly complicated, something that even AGS does not do internally. BTW, you have to keep room states in memory and apply them only after particular room was loaded.

If you are going this way perhaps more sane approach is to devise logical story states, or maybe "states" for each interactable object. Then apply them in every "Room before fade-in" regardless of whether game was loaded or this is normal playthrough, for consistency.

We used this approach in one project I was helping with in the past. It also allowed to jump between different game chapters at ease. At the expense of spending extra time setting things up.

Judging by my own experience, you better have a document where every room is described as it should look in each story state it may be visited.
Title: Re: Custom savegame stuffsies
Post by: Dualnames on Sun 31/03/2019 22:34:32
I've made some decent progress today, really hard to tell if this works yet, is there a way to reset the game.doonceonly's (the tokens)? I'm gonna guess probably not
Title: Re: Custom savegame stuffsies
Post by: Crimson Wizard on Sun 31/03/2019 22:55:18
Quote from: Dualnames on Sun 31/03/2019 22:34:32
I've made some decent progress today, really hard to tell if this works yet, is there a way to reset the game.doonceonly's (the tokens)? I'm gonna guess probably not

No. But perhaps its worth a feature suggestion. After all, this is simply a map of keys for user convenience (instead of global variables). ResetDoOnceOnly(xxx) may be a thing (or ResetAll...).
After all we have ResetRoom command that resets its local script variables too.
Title: Re: Custom savegame stuffsies
Post by: Dualnames on Mon 01/04/2019 00:07:22
Yes, please <3

Also quick question, I want to create a function in a plugin that takes String as an argument and another that returns String as an argument, how would I go about that?
Title: Re: Custom savegame stuffsies
Post by: Dualnames on Mon 01/04/2019 00:16:19
Answered my own question



void SaveVariable(const char*value,int id)
{
GameData[id].value=value;
}

const char* ReadVariable(int id)
{
if (GameData[id].value==NULL)
{
return engine->CreateScriptString("");
}
else
{
return engine->CreateScriptString(GameData[id].value);
}
}
Title: Re: Custom savegame stuffsies
Post by: Dualnames on Fri 05/04/2019 08:44:08
Code (AGS) Select


char*Token[10000];
int TokenUnUsed[10000];

int usedTokens=0;


void SetGDState(char*value,bool setvalue)
{
int i=0;
int id=-1;
while (i <= usedTokens)
{
if (Token[i]!=NULL && strcmp(Token[i], value) == 0)
{
id=i;
TokenUnUsed[i]=setvalue;
i=usedTokens+1;
}
i++;
}
if (id==-1)
{
//it doesn't find it while trying to set its state
//create the thing with said state
id=usedTokens;
TokenUnUsed[id]=setvalue;
free(Token[id]);
Token[id]=strdup(value);
usedTokens++;

}
}

bool GetGDState(char*value)
{
int i=0;
int id=-1;

while (i <= usedTokens)
{
if (Token[i]!=NULL && strcmp(Token[i], value) == 0)
{
id=i;
i=usedTokens+1;
}
i++;
}

if (id==-1)
{
return true;
}
else
{
return TokenUnUsed[id];
}
}


void ResetAllGD()
{
int i=0;
while (i <= usedTokens)
{
if (Token[i]!=NULL)
{
free(Token[i]);
}
Token[i]=NULL;
TokenUnUsed[i]=true;
i++;
}
usedTokens=0;
}

int GameDoOnceOnly(char*value)
{
if (GetGDState(value)==true)
{
//set state to false
SetGDState(value,false);
return true;
}
else
{
return false;
}
}




Is this proper, I mean this works, fully tested, I'm just wondering if I've fucked up something I can't see. This is a 'replacement' of sorts for the Game.DoOnceOnly, which is also incidentally the last step towards me being able to do my own custom savegames, at least, of the steps I could think of.
Title: Re: Custom savegame stuffsies
Post by: Dualnames on Sat 06/04/2019 08:53:14
Regardless, today I've managed to make a very preliminary version of it work, it has to go through testing, but I'm stoked as fuck!
Title: Re: Custom savegame stuffsies
Post by: Crimson Wizard on Sat 06/04/2019 12:22:29
Quote from: Dualnames on Fri 05/04/2019 08:44:08
Is this proper, I mean this works, fully tested, I'm just wondering if I've fucked up something I can't see.

Well, first thing I noticed it uses AGS-style while loops instead of for. And you could use C++ and std::map of std::strings instead of char*Token to make your life much easier and run code faster.

Code (cpp) Select

        if (id==-1)
        {
                //it doesn't find it while trying to set its state
                //create the thing with said state
                id=usedTokens;         
                TokenUnUsed[id]=setvalue;
                free(Token[id]);
                Token[id]=strdup(value);
                usedTokens++;

        }


1) One important thing when using "free" is always checking if the pointer is not NULL, otherwise free will cause error.
2) It looks like you do not test if there are any available slots left at all.