A step towards an alternative save game system?

The Pixie
Sometimes the existing save game mechanism is not what you want; perhaps you want a system that preserves saved game across updates to the game.
viewtopic.php?f=3&t=6386

This is going to be a nightmare! Be clear on this before you start. You will need to save the state of anything that can change in your game, such as player attributes, the position of anything moveable, the state of anything that can change, such as any NPC, the visited state of every room, and perhaps more. You will need to not save anything that will potentially updated (this is the issue with the existing system, it assumes anything can be changed, so saves the lot). You will also need to ensure every update can handle save games from every previous version.

Alternatively, you might want to make a series of linked games, and need a way to transfer player data from one game to another. You can use exactly the same mechanism (but do not have to worry about saving the state of every object and room, unless the player can go back to previous games).

What I am offering here is a basic save mechanism. Quest cannot save files, so the only way to do this is to present a dialog box, and ask the player to paste the code into a text file, and save that.

ETA: Look at the second post on the second page; it supersedes this post, and takes the second step towards an alternative save game system by implementing the two functions for you.

Here is the library file:
<library>

<object name="saveloaddata">
<saveloadtext><![CDATA[
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script>
var secretPassphrase = 'UltraSecret Passphrase';

function loadGame2() {
s = prompt("Copy-and-paste your save game code here", "");
if (s) {
decode(s);
}
}

function decode(s) {
s = CryptoJS.AES.decrypt(s, secretPassphrase);
s = s.toString();
str = '';
for (i = 0; i < s.length; i += 2) {
s2 = s.charAt(i) + s.charAt(i + 1);
n = parseInt(s2, 16);
str += String.fromCharCode(n);
}
ASLEvent("LoadGame", str);
}

var loadDialog = $("#load-dialog").dialog({
autoOpen: false,
width: 600,
height: 500,
buttons: {
Ok: function() {
decode($('textarea#data').val());
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});

var saveDialog = $("#save-dialog").dialog({
autoOpen: false,
width: 600,
height: 500,
buttons: {
Ok: function() {
$(this).dialog("close");
},
}
});

function loadGame() {
loadDialog.dialog("open");
}

function saveGame(s) {
str = CryptoJS.AES.encrypt(s, secretPassphrase);
$('textarea#savedata').val(str);
saveDialog.dialog("open");
}

</script>
<div id="load-dialog">
<p>Please paste your save game here:</p>
<textarea id="data" rows="13" cols="49"></textarea>
</div>
<div id="save-dialog">
<p>Please copy-and-paste your save game into a text file from here:</p>
<textarea id="savedata" rows="13" cols="49"></textarea>
</div>
]]></saveloadtext>
</object>


<command name="savecmd">
<pattern>save</pattern>
<script>
JS.eval ("saveGame('" + SaveGame() + "');")
</script>
</command>


<command name="loadcmd">
<pattern>load</pattern>
<script>
JS.eval ("loadGame();")
</script>
</command>

<function name="SaveLoadInit">
OutputTextNoBr (saveloaddata.saveloadtext)
</function>


</library>

It is full of JavaScript so I cannot upload it to the forum as a file; you will need to copy-and-paste to a text file, and name the text file saveload.aslx. You can then include the library in your game as normal.

To use it, add this to your start script on the game object:
SaveLoadInit

...and create two new functions, LoadGame and SaveGame. What goes in them depends on your game. SaveGame should return a string, and that string will contain all the game state data. LoadGame should take a string parameter, which has all that data, and set the game state from it.

Here is an example that just stores a simple string, and then prints. You can use that to convince yourself the data is saved.
  <function name="SaveGame" type="string">
return ("some text to be remembered")
</function>
<function name="LoadGame" parameters="s">
msg (s)
</function>

ETA: See my second post of 28-Jun-16 to see how to implement these to junctions.

Anonynn
Hmm! This looks really great!

Should we start implementing this one? Or are you working on something more elaborate/is this the first draft? :)

The Pixie
That bit works fine. What you have to do is create a SaveGame function that stores all the data you want saved, and a LoadGame that takes that data and sets attributes from it. It is a start:
s = player.alias
s = s + ";" + player.intelligence
s = s + ";" + player.strength
etc...
return(s)


ary = Split(s, ";")
player.alias = StringListItem(ary, 0)
player.intelligence = ToInt(StringListItem(ary, 1))
player.strength= ToInt(StringListItem(ary, 2))
etc...

dfisher
A quick thought: another approach is to save the original commands entered by the player, and re-run them after the game is updated.

Not recommending this as a general approach to saving, though (since loading would take longer and longer as the transcript grows) - just for handling game updates.

XanMag
The problem with saving all the original commands the player entered and re-running them is that if the player enters a command that works but shouldn't (i.e. picking up an object that might not now be there, exiting through an unlocked exit that should have been locked, any command that triggers a Boolean switch, etc), all other commands after that are moot. It's similar to using a walkthrough to test your games while you make them (all input is saved and rerun so you don't have to type everything again). This would work well for grammar issues resolved in updates but would probably not work for fixing 90% of found bugs/game-breakers. Unless the grammar is terrible, I won't update a game of mine unless I am correcting some bug. When I do that, I will also correct any grammar issues identified up until that point. From personal experience, when I updated when fixing bugs pre-publishing, I either had to go into the walkthrough FIND and adjust specific inputs where I knew the bug started or, in some cases, it was easier to start the walkthrough all over again.

dfisher
XanMag wrote:The problem with saving all the original commands the player entered and re-running them is that if the player enters a command that works but shouldn't [...] all other commands after that are moot.

Good point.

Another possibility is to have checkpoints the player can fast forward to (which would only work for certain games). The author of the Inform 7 game Scroll Thief mentioned doing that in this post on the IF forum.

The Pixie
dfisher wrote:Another possibility is to have checkpoints the player can fast forward to (which would only work for certain games). The author of the Inform 7 game Scroll Thief mentioned doing that in this post on the IF forum.

That is a clever idea, and would be fairly easy to implement. Very useful for debugging big games too if you can jump to where you need to be.

XanMag
How would you go about saving current game states in a walk-through? I might try to set up checkpoints with codes if there is a way to save everything as is at those points. That way an update won't necessarily force the player to restart. It wouldn't be perfect but it would certainly be helpful.

Does that make sense?

The Pixie
Or have an update command that loads a data file from a website, and creates the new objects, rooms and exits from the file. As far as I know you cannot add scripts, so it would be limited to expanding the world, you could not correct bugs in code, or add functions, types or commands.
viewtopic.php?f=18&t=5512

Anonynn
So I'm beginning to implement the save, so I thought I would ask in the beginning of the game about whether or not the player has a saved game or not so that they didn't have to slog through the creation process over and over just to get to a point where they could load their game. So everything works if they say "no" because it goes through the character process like normal. However if they say "yes" I leave a place for them to implement the loaded game, however if they type in something wrong the game ends with a game-over. Is there a way to loop the question or any suggestions about how to go about that? Here's the code I have so far...

msg ("<br/>Would you like to <i>load</i> a previously saved game?<br/><br/><i>Yes</i> or <i>No</i><br/>")
get input {
switch (LCase(result)) {
case ("yes") {
msg ("Please <i>Paste</i> In Your Saved Code. Then Press <i>Enter</i>")
get input {
}
}
default {



Also...I just want to make sure I understand this. On the LoadGame Function...

ary = Split(s, ";")
player.alias = StringListItem(ary, 0)
player.intelligence = ToInt(StringListItem(ary, 1))
player.strength= ToInt(StringListItem(ary, 2))
etc...


What is this part supposed to entail?
(ary, 1))
(ary, 2))

And are "Booleans" that need to be kept track off also considered "StringListItem"? Would they be written like..
player.blahblah = Boolean(StringListItem(ary, 1))

HegemonKhan
to loop:

1. have the scripting in a Function (Functions have/allow-for useful Parameters if they're needed), and then (re)call (loop) the Function:

GUI~Editor: run as script -> add new script -> 'output' category/section I think -> 'call function' Script -> (type in the name of your function into the skinny rectangle text box, and if need, add in any Arguments/Parameters as you want via the 'add Parameters' button of the big box)

<game name="xxx">
<pov type="object">player</pov>
<start type="script">
load_game_function (game.pov)
</start>
</game>

<function name="load_game_function" parameters="character_parameter">
msg ("<br/>Would you like to <i>load</i> a previously saved game?<br/><br/><i>Yes</i> or <i>No</i><br/>")
get input {
ClearScreen
switch (LCase(result)) {
case ("yes") {
msg ("Please <i>Paste</i> In Your Saved Code. Then Press <i>Enter</i>")
get input {
}
}
case ("no") {
character_creation_function (character_parameter)
}
default {
load_game_function (character_parameter)
}
}
}
</function>

<function name="character_creation_function" parameters="character_parameter">
msg ("Name?")
get input {
character_parameter.alias = result
}
// etc etc etc
</function>


2. have the scripting be a Script Attribute of an Object, and (re)call/run (via using 'do/invoke'), the Script Attribute, however, Script Attributes have no Argument/Parameter ability, which makes it very limited:

<game name="xxx">
<pov type="object">player</pov>
<start type="script">
invoke (load_game_object.load_game_script_attribute)
</start>
</game>

<object name="load_game_object">
<attr name="load_game_script_attribute" type="script">
msg ("<br/>Would you like to <i>load</i> a previously saved game?<br/><br/><i>Yes</i> or <i>No</i><br/>")
get input {
ClearScreen
switch (LCase(result)) {
case ("yes") {
msg ("Please <i>Paste</i> In Your Saved Code. Then Press <i>Enter</i>")
get input {
}
}
case ("no") {
character_creation_function (game.pov)
}
default {
ClearScreen
invoke (load_game_object.load_game_script_attribute)
}
</object>

<function name="character_creation_function" parameters="character_parameter">
msg ("Name?")
get input {
character_parameter.alias = result
}
// etc etc etc
</function>


3. Delegates: these are like a hybrid of Script Attributes (using Objects I think) and Functions (having the ability of Parameters/Arguments), but I personally had a bit difficulty in understanding how to use them (when I tried to learn them a long time ago now). Though, I'm sure Pixie/Jay can help you with using them, if you want to use them.

--------------------------------------

anonynn wrote:Also...I just want to make sure I understand this. On the LoadGame Function...

player.intelligence = ToInt(StringListItem(ary, 1))
player.strength= ToInt(StringListItem(ary, 2))


the 'StringListItem()' Function/Script selects and returns an item from a list, Pixie's 'ary' is his/her local temporary Stringlist Variable, and the '0...N' numbers, are the index numbers (which is what item you want to get from the list)

how a list works:

game.my_list = split ("red; blue; yellow")

index number: item (item quantity count / max item quantity)

0: red (1/3)
1: blue (2/3)
2: yellow (3/3)

ListCount (game.my_list): 3

StringListItem (game.my_list, 0): red
StringListItem (game.my_list, 1): blue
StringListItem (game.my_list, 2): yellow
StringListItem (game.my_list, 3): ERROR, there is no 4th item!

game.my_list = split ("yellow; red; blue")

the leftmost item is the first item (index number: 0)
or, in the GUI~Editor, the first added item, is the first item (index number: 0)

index number: ~ item ~ (item quantity count)

0: yellow (1/3)
1: red (2/3)
2: blue (3/3)

ListCount (game.my_list): 3 total items
Last Item (index number) in the List: ListCount (game.my_list) - 1: (3) - 1 = 2: 2 is the last index number, the last item in a 3 item list

StringListItem (game.my_list, 0): yellow
StringListItem (game.my_list, 1): red
StringListItem (game.my_list, 2): blue
StringListItem (game.my_list, 3): ERROR, there is no 4th item!

randomization:

VARIABLE = StringListItem (game.my_list, GetRandomInt (0, ListCount (game.my_list) - 1))
// VARIABLE = (red/blue/yellow)

---------------

anonynn wrote:And are "Booleans" that need to be kept track off also considered "StringListItem"? Would they be written like..
player.blahblah = Boolean(StringListItem(ary, 1)


I believe that you're putting/getting all data from your game code into a long string, and thus using a StringList to split it up, and then to rematch the data over to the new/loaded game's data

so, for putting your Boolean Attributes (data) back into your new/loaded game code, you'd need to do something like this (wait for Pixie though for an accurate answer though, as I'm just guessing here), an example:

(err... this is a bit too complicated for me to do quickly, or if at all, lol)

// pretend that we've got something like this (conceptual pseudo code only below, NOT actual proper code):
ary = split ("player.strength=100;player.endurance=100;player.dexterity=100;orc.dead=false;troll.dead=true;ogre.dead=false", ";")
StringListItem (ary, 0) -> set (player, "strength", 100)
StringListItem (ary, 1) -> set (player, "endurance", 100)
StringListItem (ary, 2) -> set (player, "dexterity", 100)
StringListItem (ary, 3) -> set (orc, "dead", false)
StringListItem (ary, 4) -> set (troll, "dead", true)
StringListItem (ary, 5) -> set (ogre, "dead", false)

The Pixie
Hi Anonynn

Not sure why it happens as you say, but I suggest you try this and see what happens:
msg ("<br/>Would you like to <i>load</i> a previously saved game?<br/><br/><i>Yes</i> or <i>No</i><br/>")
get input {
switch (LCase(result)) {
case ("yes") {
JS.eval ("loadGame();")
}
default {
// do the normal stuff
}
}
}

Also...I just want to make sure I understand this. On the LoadGame Function...

ary = Split(s, ";")
player.alias = StringListItem(ary, 0)
player.intelligence = ToInt(StringListItem(ary, 1))
player.strength= ToInt(StringListItem(ary, 2))
etc...


What is this part supposed to entail?
(ary, 1))
(ary, 2))

And are "Booleans" that need to be kept track off also considered "StringListItem"? Would they be written like..
player.blahblah = Boolean(StringListItem(ary, 1))


The data is save as a string of values separated by semi-colons (which makes me think it will mess up big time if you save anything with a semi-colon...). When you load it splits that into an array of strings, so it is always StringListItem to get the value. You then need to convert it into an integer or string as appropiate.

I have just found there is no built-in function to conver a string to a Boolean, so do this instead:
player.blahblah = (LCase(StringListItem(ary, 1)) = "true")

Anonynn

Not sure why it happens as you say, but I suggest you try this and see what happens:



I think it happens because if the game doesn't go through the character creation process, it doesn't receive any values for the player's health and therefore starts the game and assumes they have died because they have "0" health. That's my theory anyway. That's why I'm not sure what to do if the player goes to input their "game save code" and it fails somehow or isn't the right code, it'll probably go to instant game over.

What is this part supposed to entail?
(ary, 1))
(ary, 2))



I meant more like what are the numbers? Are they a sequence order like ....
(ary, 1))
(ary, 2))
(ary, 3))
(ary, 4))

Or are they supposed to fit the values of integers out of 100? Like, for example...

player.intelligence = ToInt(StringListItem(ary, 100))
player.strength= ToInt(StringListItem(ary, 100))

Also, what would the code be for like recording what the player has in their inventory? Or the values of their inventory, like the entries of a journal item?

The Pixie
Anonynn wrote:I think it happens because if the game doesn't go through the character creation process, it doesn't receive any values for the player's health and therefore starts the game and assumes they have died because they have "0" health. That's my theory anyway. That's why I'm not sure what to do if the player goes to input their "game save code" and it fails somehow or isn't the right code, it'll probably go to instant game over.

Makes sense. That will be tricky to handle, because of the way the JavaScript call back works. I will have a think about it.

I meant more like what are the numbers? Are they a sequence order like ....
(ary, 1))
(ary, 2))
(ary, 3))
(ary, 4))


It is a sequence (that starts at zero!). The data is stored as a string, perhaps this:
Mary;4;1;0;true;6
The load file uses each in turn, so:
player.name = StringListItem(ary, 0)   // Mary
player.intelligence = ToInt(StringListItem(ary, 1)) //4
player.strength= ToInt(StringListItem(ary, 2)) //1
etc.

Also, what would the code be for like recording what the player has in their inventory? Or the values of their inventory, like the entries of a journal item?


The basic strategy is to convert to and from a string.

You would need to record the location of each object (not just those in the inventory). The location is held in the parent attribute, so you need to save the name of the parent object and then find that object when you load. For an object called hat:
// When saving
s = s + ";" + hat.parent.name
// When loading
hat.parent = GetObject(StringListItem(ary, 74))

For a string list, you will need to convert that to a string, and back.
// When saving
s = s + ";" + Join(journal.entries, "|")
// When loading
journal.entries = Split(StringListItem(ary, 104), "|")

If you have an object list, it will be even more complicated...

The Pixie
I am wondering if a better approach would be to have a list of things that need saving, and have Quest iterate through that. Call it saveloadatts and put it on the game object. Each entry would then look like this:
hat.alias.string
player.parent.object
player.strength.int
player.flag.boolean

So there are three parts, separated by dots. The first is the name of the object, the second is the name of the attribute. These must be exactly as you have them in your game. The third part is the type of attribute, which must be one of: object, integer (or int), boolean (or flag), list (and it must be a string list) or string (which is actually the default).

Your LoadGame and SaveGame functions will than look like this:
  <function name="LoadGame" parameters="s">
pos = 0
foreach (val, Split(s, ";")) {
ary = Split(StringListItem(game.saveloadatts, pos), ".")
pos = pos + 1
obj = GetObject(StringListItem (ary, 0))
att = StringListItem (ary, 1)
type = LCase(StringListItem (ary, 2))
msg ("obj=" + obj)
msg ("att=" + att)
msg ("val=" + val)
if (type = "object") {
set (obj, att, GetObject(val))
}
else if (type = "list") {
set (obj, att, Split(val, "|"))
}
else if (type = "int" or type = "integer") {
set (obj, att, ToInt(val))
}
else if (type = "boolean" or type = "flag") {
set (obj, att, LCase(val) = "true")
}
else {
set (obj, att, Replace(val, "@@@semicolon@@@", ";"))
}
}
</function>
<function name="SaveGame" type="string">
data = NewStringList()
foreach (att, game.saveloadatts) {
ary = Split(att, ".")
obj = GetObject(StringListItem (ary, 0))
att = StringListItem (ary, 1)
type = LCase(StringListItem (ary, 2))
val = GetAttribute(obj, att)
msg ("obj=" + obj)
msg ("att=" + att)
msg ("val=" + val)
if (type = "object") {
list add (data, val.name)
}
else if (type = "list") {
list add (data, Join(val, "|"))
}
else if (type = "int" or type = "integer") {
list add (data, "" + val)
}
else if (type = "boolean" or type = "flag") {
list add (data, "" + val)
}
else {
list add (data, Replace(val, ";", "@@@semicolon@@@"))
}
}
return (Join(data, ";"))
</function>

HegemonKhan
Anonynn wrote:I meant more like what are the numbers? Are they a sequence order like ....
(ary, 1))
(ary, 2))
(ary, 3))
(ary, 4))

Or are they supposed to fit the values of integers out of 100? Like, for example...

player.intelligence = ToInt(StringListItem(ary, 100))
player.strength= ToInt(StringListItem(ary, 100))


(this below is basis of what you're doing, but it's extremely much more complicated as your using encoding/decoding with it, which Pixie is helping you through it's coding process/work)

think of List Attributes (Stringlist and Objectlist Attributes) as your grocery shopping list:

player.grocery_shopping_list = split ("milk;eggs;bread", ";")

1. milk
2. eggs
3. bread

except your list starts with 0, NOT 1:

0. milk
1. eggs
2. bread

I need to get item (index number) 0 -> ListItem (player.grocery_shopping_list, 0) -> you get the 'milk' (the name of the item) from the 'grocery store' (your List Attribute), which you can then use that item's name to do actions upon that item, be it the name of a String or an Object)

I need to get item (index number) 1 -> ListItem (player.grocery_shopping_list, 1) -> you get the 'eggs' (the name of the item) from the 'grocery store' (your List Attribute), which you can then use that item's name to do actions upon that item, be it the name of a String or an Object)

I need to get item (index number) 2 -> ListItem (player.grocery_shopping_list, 2) -> you get the 'bread' (the name of the item) from the 'grocery store' (your List Attribute), which you can then use that item's name to do actions upon that item, be it the name of a String or an Object)

I need to get item (index number) 3 -> ERROR! Whoopsy... I/you don't have a 4th item on my/your grocery shopping list!

---------------------

anonynn wrote:Or are they supposed to fit the values of integers out of 100? Like, for example...

player.intelligence = ToInt(StringListItem(ary, 100))
player.strength= ToInt(StringListItem(ary, 100))


whereas, using Dictionary Attributes would/could allow you to include the Values of those Attributes, but this is too difficult for me to explain, especially in conjunction with the encoding/decoding that Pixie is helping you with.

The Pixie
Here is a revised version that incorporates the functions of my previous post. As before, you need to save the code at the end into a text file called saveload.aslx, and save that in your game folder. Load into your game as normal. Then put this in your game "Start" script:
SaveLoadInit

Then you need to tell it what data is to be saved. This is done with a string list attribute called "saveloadatts" on the game object. Each entry would then look like this:
hat.alias.string
player.parent.object
player.strength.int
player.flag.boolean

So there are three parts, separated by dots. The first is the name of the object, the second is the name of the attribute. These must be exactly as you have them in your game. The third part is the type of attribute, which must be one of: object, integer (or int), boolean (or flag), list (and it must be a string list) or string (which is actually the default).

When you release updates to your released game, you can add to this list, but you can never edit or remove the existing entries if you want players to use saves from earlier versions.

Here is the code for the library
<library>

<object name="saveloaddata">
<saveloadtext><![CDATA[
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script>
var secretPassphrase = 'UltraSecret Passphrase';

function loadGame2() {
s = prompt("Copy-and-paste your save game code here", "");
if (s) {
decode(s);
}
}

function decode(s) {
s = CryptoJS.AES.decrypt(s, secretPassphrase);
s = s.toString();
str = '';
for (i = 0; i < s.length; i += 2) {
s2 = s.charAt(i) + s.charAt(i + 1);
n = parseInt(s2, 16);
str += String.fromCharCode(n);
}
ASLEvent("LoadGame", str);
}

var loadDialog = $("#load-dialog").dialog({
autoOpen: false,
width: 600,
height: 500,
buttons: {
Ok: function() {
decode($('textarea#data').val());
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});

var saveDialog = $("#save-dialog").dialog({
autoOpen: false,
width: 600,
height: 500,
buttons: {
Ok: function() {
$(this).dialog("close");
},
}
});

function loadGame() {
loadDialog.dialog("open");
}

function saveGame(s) {
str = CryptoJS.AES.encrypt(s, secretPassphrase);
$('textarea#savedata').val(str);
saveDialog.dialog("open");
}

</script>
<div id="load-dialog">
<p>Please paste your save game here:</p>
<textarea id="data" rows="13" cols="49"></textarea>
</div>
<div id="save-dialog">
<p>Please copy-and-paste your save game into a text file from here:</p>
<textarea id="savedata" rows="13" cols="49"></textarea>
</div>
]]></saveloadtext>
</object>


<command name="savecmd">
<pattern>save</pattern>
<script>
JS.eval ("saveGame('" + SaveGame() + "');")
</script>
</command>


<command name="loadcmd">
<pattern>load</pattern>
<script>
JS.eval ("loadGame();")
</script>
</command>


<function name="SaveLoadInit">
OutputTextNoBr (saveloaddata.saveloadtext)
</function>


<function name="LoadGame" parameters="s">
pos = 0
foreach (val, Split(s, ";")) {
ary = Split(StringListItem(game.saveloadatts, pos), ".")
pos = pos + 1
obj = GetObject(StringListItem (ary, 0))
att = StringListItem (ary, 1)
type = LCase(StringListItem (ary, 2))
if (type = "object") {
set (obj, att, GetObject(val))
}
else if (type = "list") {
set (obj, att, Split(val, "|"))
}
else if (type = "int" or type = "integer") {
set (obj, att, ToInt(val))
}
else if (type = "boolean" or type = "flag") {
set (obj, att, LCase(val) = "true")
}
else {
set (obj, att, Replace(val, "@@@semicolon@@@", ";"))
}
}
</function>


<function name="SaveGame" type="string">
data = NewStringList()
foreach (att, game.saveloadatts) {
ary = Split(att, ".")
obj = GetObject(StringListItem (ary, 0))
att = StringListItem (ary, 1)
type = LCase(StringListItem (ary, 2))
val = GetAttribute(obj, att)
if (type = "object") {
list add (data, val.name)
}
else if (type = "list") {
list add (data, Join(val, "|"))
}
else if (type = "int" or type = "integer") {
list add (data, "" + val)
}
else if (type = "boolean" or type = "flag") {
list add (data, "" + val)
}
else {
list add (data, Replace(val, ";", "@@@semicolon@@@"))
}
}
return (Join(data, ";"))
</function>

</library>

Anonynn
Just curious. On the new version when I uploaded the library, I didn't see the Functions "SaveGame" and "LoadGame". When I tried to add them as Functions the game said they already existed. I deleted them from the old version.

The Pixie
That is right, they are now implemented in the library.

Anonynn
Gotcha. I figured that's what you meant, I'm a dummy though and had to ask just to make sure.

This is an updated version of the library. All the comments in my post of 1st July still apply, so look there for how to use it.

This version is more robust; it will warn you if your attribute values do not make sense. Also, if the load game dialogue gets displayed, but no data is entered, the game will terminate.

Note that this version can handle the "once" text processor command, but you will need to save the data that handles it, so include this entry in your list of attributes:

game.textprocessor_seen.dictionaryoflists

Note that the firsttime and otherwise script commands cannot be handled, so if the player loads a game, Quest will assume it is the first time for them as though the player is playing from the start.

<library>

  <object name="saveloaddata">
    <saveloadtext><![CDATA[
      <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js">
      </script>
      <script>
        var secretPassphrase = 'UltraSecret Passphrase';
       
        function loadGame2() {
          s = prompt("Copy-and-paste your save game code here", "");
          if (s) {
            decode(s);
          }
        }
         
        function decode(s) {
          s = CryptoJS.AES.decrypt(s, secretPassphrase);
          s = s.toString();
          str = '';
          for (i = 0; i < s.length; i += 2) {
            s2 = s.charAt(i) + s.charAt(i + 1);
            n = parseInt(s2, 16);
            str += String.fromCharCode(n);
          }
          ASLEvent("LoadGame", str);
        }
     
        var loadDialog = $("#load-dialog").dialog({
          autoOpen: false,
          width: 600,
          height: 500,
            buttons: {
              Ok: function() {
                  decode($('textarea#data').val());
                  $(this).dialog("close");
              }
          }
        });

        var saveDialog = $("#save-dialog").dialog({
          autoOpen: false,
          width: 600,
          height: 500,
            buttons: {
              Ok: function() {
                  $(this).dialog("close");
              },
          }
        });

        function loadGame() {
          loadDialog.dialog("open");
        }

        function saveGame(s) {
          str = CryptoJS.AES.encrypt(s, secretPassphrase);
          $('textarea#savedata').val(str);
          saveDialog.dialog("open");
        }

      </script>
      <div id="load-dialog">
        <p>Please paste your save game here:</p>
        <textarea id="data" rows="13" cols="49"></textarea>
      </div>
      <div id="save-dialog">
        <p>Please copy-and-paste your save game into a text file from here:</p>
        <textarea id="savedata" rows="13" cols="49"></textarea>
      </div>
    ]]></saveloadtext>
  </object>
 
 
  <command name="savecmd">
    <pattern>save</pattern>
    <script>
      JS.eval ("saveGame('" + SaveGame() + "');")
    </script>
  </command>
 
 
  <command name="loadcmd">
    <pattern>load</pattern>
    <script>
      JS.eval ("loadGame();")
    </script>
  </command>
 
 
  <function name="SaveLoadInit">
    OutputTextNoBr (saveloaddata.saveloadtext)
  </function>
 
 
  <function name="LoadGame" parameters="s">
    if (s = "") {
      msg("No save game data found.")
      finish
    }
    pos = 0
    foreach (val, Split(s, ";")) {
      ary = Split(StringListItem(game.saveloadatts, pos), ".")
      obj = GetObject(StringListItem (ary, 0))
      att = StringListItem (ary, 1)
      type = LCase(StringListItem (ary, 2))
      if (val = "" or TypeOf(val) = "null") {
        // Do nothing, this attribute has not been set
      }
      else if (type = "object") {
        set (obj, att, GetObject(val))
      }
      else if (type = "list") {
        set (obj, att, Split(val, "|"))
      }
      else if (type = "int" or type = "integer") {
        if (IsInt(val)) {
          set (obj, att, ToInt(val))
        }
        else {
          msg("Load error: Cannot convert \"" + val + "\" to an integer for " + StringListItem(game.saveloadatts, pos))
        }
      }
      else if (type = "boolean" or type = "flag") {
        set (obj, att, LCase(val) = "true")
      }
      else if (type = "dictionaryoflists") {
        set (obj, att, DictSplit(val, 0))
      }
      else {
        set (obj, att, Replace(val, "@@@semicolon@@@", ";"))
      }
      pos = pos + 1
    }
    game.loading = false
  </function>
  
  
  

  <function name="SaveGame" type="string">
    data = NewStringList()
    foreach (att, game.saveloadatts) {
      ary = Split(att, ".")
      obj = GetObject(StringListItem (ary, 0))
      if (not ListCount(ary) = 3) {
        msg("Save error: Badly formatted entry: " + att)
      }
      else if (obj = null) {
        msg("Save error: Failed to find an object called: " + StringListItem (ary, 0))
      }
      else {
        att = StringListItem (ary, 1)
        type = LCase(StringListItem (ary, 2))
        val = GetAttribute(obj, att)
        if (TypeOf(val) = "null") {
          list add (data, "")
        }
        else if (type = "object") {
          list add (data, val.name)
        }
        else if (type = "list") {
          list add (data, Join(val, "|"))
        }
        else if (type = "int" or type = "integer") {
          list add (data, "" + val)
        }
        else if (type = "boolean" or type = "flag") {
          list add (data, "" + val)
        }
        else if (type = "dictionaryoflists") {
          list add (data, SuperJoin(val))
        }
        else if (type = "string") {
          list add (data, Replace(val, ";", "@@@semicolon@@@"))
        }
        else {
          msg("Save error: Invalid type for " + StringListItem (ary, 0) + ": " + type)
        }
      }
    }
    return (Join(data, ";"))
  </function>  
 
  <!--
  Will join a diction or list into a string. The dictionary or list can itself contain
  further lists and dictionaries, which in turn can contain even more.  
  -->  
  <function name="SuperJoin" parameters="col" type="string">
    return (_SuperJoin(col, 0))
  </function>
 
  <function name="_SuperJoin" parameters="col, n" type="string">
    if (TypeOf(col) = "dictionary") {
      sublist = NewStringList()
      foreach (key, col) {
        if (not TypeOf(key) = "string") {
          msg("Error; key is a " + TypeOf(key))
          msg(key)
        }
        val = DictionaryItem(col, key)
        s2 = _SuperJoin(val, n + 1)
        s = key + "~" + n + s2
        list add (sublist, s)
      }
      return(Join(sublist, "|" + n))
    }
    else if (TypeOf(col) = "list") {
      sublist = NewStringList()
      foreach (s, col) {
        list add(sublist, _SuperJoin(s, n + 1))
      }
      return(Join(sublist, "|" + n))
    }
    else if (TypeOf(col) = "stringlist") {
      return(Join(col, "|" + n))
    }
    else {
      return ("" + col)
    }
  </function>
 
 
  <function name="DictSplit" parameters="str, n" type="dictionary">
    dict = NewDictionary()
    list = Split(str, "|" + n)
    foreach (s, list) {
      sublist = Split(s, "~" + n)
      key = StringListItem(sublist, 0)
      value = StringListItem(sublist, 1) // here
      if (Instr(value, "|" + (n+1)) = 0) {
        // Not a collection, just use the value
        dictionary add(dict, key, value)
      }
      else if (Instr(s, "~" + (n+1)) = 0) {
        // A list
        dictionary add(dict, key, ListSplit(value, n + 1))
      }
      else {
        // A dict
        dictionary add(dict, key, DictSplit(value, n + 1))
      }
    }
    return(dict)
  </function>


  <function name="ListSplit" parameters="str, n" type="list">
    l = NewList()
    list = Split(str, "|" + n)
    foreach (s, list) {
      if (Instr(s, "|" + (n+1)) = 0) {
        // Not a collection, just use the value
        list add(l, s)
      }
      else if (Instr(s, "~" + (n+1)) = 0) {
        // A list
        list add(l, ListSplit(value, n + 1))
      }
      else {
        // A dict
        list add(l, DictSplit(value, n + 1))
      }
    }
    return(l)
  </function>


</library>

Hey Pixie thanks for the save and load features really awesome, question though. I made the library, added it to game, call on Start game

SaveLoadInit

Then when I save i get a message box telling me to copy and paste a bunch of letters in the saveload library but also get a error

 > save
Error running script: Cannot foreach over '' as it is not a list

Am I doing this right? Did I miss a step? And what function names do i call if I wanted to add a button for save and load?
Also does this save positions on map for objects? And do i make 'saveloadatts' Attribute in every object I want to save? Sorry for so many questions just really want to use this function :)

Thanks Again for everything Pixie really awesome stuff you do for us.

Mike


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums