Custom savegame stuffsies

Started by Dualnames, Sat 30/03/2019 01:08:03

Previous topic - Next topic

Dualnames

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:

Code: ags

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?
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

eri0o

#1
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

Crimson Wizard

#2
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.

Dualnames

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.
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Crimson Wizard

#4
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.

Dualnames

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
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Crimson Wizard

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.

Dualnames

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?
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Dualnames

Answered my own question

Code: ags


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);
	}
}
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Dualnames

Code: AGS


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.
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Dualnames

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!
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Crimson Wizard

#11
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

        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.

SMF spam blocked by CleanTalk