A step towards an alternative save game system?
The Pixie
06 Jun 2016, 09:26viewtopic.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
08 Jun 2016, 18:49Should we start implementing this one? Or are you working on something more elaborate/is this the first draft?
The Pixie
09 Jun 2016, 07:15s = 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
14 Jun 2016, 21:50Not 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
15 Jun 2016, 16:03dfisher
16 Jun 2016, 13:26XanMag 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
16 Jun 2016, 14:27dfisher 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
16 Jun 2016, 17:37Does that make sense?
The Pixie
16 Jun 2016, 19:47viewtopic.php?f=18&t=5512
Anonynn
22 Jun 2016, 19:36msg ("<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
23 Jun 2016, 00:151. 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
24 Jun 2016, 07:48Not 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
28 Jun 2016, 02:32Not 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
28 Jun 2016, 07:00Anonynn 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
28 Jun 2016, 07:39hat.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
28 Jun 2016, 08:26Anonynn 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
01 Jul 2016, 07:46SaveLoadInit
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
02 Jul 2016, 02:39The Pixie
02 Jul 2016, 15:33Anonynn
03 Jul 2016, 03:59The Pixie
19 Aug 2016, 09:27This 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>
onimike
15 Sept 2016, 22:39Hey 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