It would have been a lot harder if user-created variables weren’t already packaged into a single object. Since they are, it was just a matter of serializing it all as JavaScript and then saving to a cookie.
I have quite a few variables in my game. But one thing I’m doing is erasing the values of some of the bigger ones I don’t need to save, before saving. For instance, there’s a situation where I create a pretty big block of text stored in a variable. But it’s something I just do for formatting purposes, and it doesn’t need to be saved. So set it to blank like this, before calling the “save” macro:
<<set $whatever="">>
You could add a javascript alert in the “save” routine that displays the size of the resulting string. That’s how I figured out how big mine were. Uncomment the alert line in the code to do that. 4k is 4096 bytes. I’ve read that a cookie has something like 50 bytes of overhead too, so you’d probably want to start worrying if your cookies are more than 4000 characters long.
Somewhere at the beginning of the story/game, before giving players an option to save, you’d need to create a few variables that the save macros use:
<<set $tempFlag = false>>
<<set $tempText = “”>>
<<set $saveTime = “”>>
You can name your saves whatever you want, but I called mine “SG1” and “SG2” and so forth. I’m limiting people to 3 saves plus an autosave (which they can turn on or off) since there is also a limit on the number of cookies you can have per domain.
One thing I found out is that you can’t store cookies in Chrome locally. If you have a local web server (localhost or whatever) then sure, but if you just open the file in Chrome from a folder on your hard drive, the cookies “succeed” but don’t actually save. It does work online though, and it works locally in Internet Explorer and FireFox.
The macros are used like this:
<<deletegame “SG1”>>
<<testgame “SG1”>>
Afterwards, $tempText will contain either “Empty” or the date the save was made and the room name. For the room name, you might want to remove it from the code or change it to some other piece of information from your particular game, since this was specific to mine.
<<savegame “SG1”>>
Afterwards, $tempFlag will contain true if successful or false if not.
<<loadgame “SG1”>>
Afterwards, $tempFlag will contain true if successful or false if not. I use “testgame” first, just to make sure the cookie exists before attempting to load it.
Some limitations –
You need to keep the cookie sizes less than 4k, and no more than 20 cookies (saves) per domain. If you’ve got several games on the same domain, all allowing mutliple saves, that could become a problem. I don’t know if adding a “path” to the cookie would help with that, but I don’t think so. So there could be issues if a bunch of Twine games with big saves end up on the same website. In addition, if everybody uses the “SGx” naming I did, the cookies could potentially overwrite each other between games (so use some other unique name).
One work-around for the 4k limit would be to split a save in half and store it in two separate cookies, then load both and concat back together when loading. In theory, you could do multiple splits and allow a pretty huge set of variables for a game.
The cookie content is larger due to the Base64 encoding. You could actually remove the code for the Base64 object (which I found online) and the calls to “encode” and “decode” and end up with cookies that are something like 30% smaller, but then all your variables are human-readable in the cookie.
This completely replaces the current game’s user-created object list with the one loaded. So if you create a game, allow people to save, then release an update that adds some important new variables, you would need to do something to gracefully handle loading the prior save. For instance, checking for the existence of a “version” variable in the loaded data, and then adding the new stuff back in aftewards. Ex:
<><<if $version eq “” || $version < 2>>
<<set $newVar1 = “Something”>>
<<set $newVar2 = “Something Else”>>
Etc.
<><>
[spoiler][code]macros[‘deletegame’] =
{
handler: function (place, name, params)
{
try
{
var cookieName = params[0];
var exdate=new Date();
exdate.setDate(exdate.getDate() - 365);
var delCookie = [cookieName, ‘=; expires=’, exdate.toUTCString(), ‘;’].join(’’);
document.cookie = delCookie;
state.history[0].variables[“tempFlag”] = true;
}
catch(e)
{
state.history[0].variables[“tempFlag”] = false;
}
}
};
macros[‘savegame’] =
{
handler: function (place, name, params)
{
try
{
var cookieName = params[0];
var exdate=new Date();
exdate.setDate(exdate.getDate() + 365);
state.history[0].variables[“saveTime”] = GetTimeString();
var value = JSON.stringify(state.history[0].variables);
var encoded = escape(Base64.encode(value));
var cookie = [cookieName, ‘=’, encoded, ‘; expires=’, exdate.toUTCString(), ‘;’].join(’’);
// window.alert(cookie.length);
document.cookie = cookie;
state.history[0].variables[“tempFlag”] = true;
}
catch(e)
{
state.history[0].variables[“tempFlag”] = false;
}
}
};
macros[‘loadgame’] =
{
handler: function (place, name, params)
{
var cookieName = params[0];
var result = LoadGame(cookieName);
if (result == null)
{
state.history[0].variables[“tempFlag”] = false;
} else {
state.history[0].variables = result;
state.history[0].variables[“tempFlag”] = true;
}
}
};
macros[‘testgame’] =
{
handler: function (place, name, params)
{
var cookieName = params[0];
var result = LoadGame(cookieName);
state.history[0].variables[“tempText”] =
(result == null ? “Empty” : result[“saveTime”] + " - " + result[“roomName”]);
}
}
function LoadGame(cookieName)
{
try
{
var result = document.cookie.match(new RegExp(cookieName + ‘=([^;]+)’));
var decoded = Base64.decode(unescape(result[1]));
var valueObject = JSON.parse(decoded);
return (valueObject);
}
catch(e)
{
return null;
}
}
function GetTimeString()
{
var d = new Date();
var newDate =
(“00” + (d.getMonth() + 1)).slice(-2) + “/” +
(“00” + d.getDate()).slice(-2) + “/” +
d.getFullYear() + " " +
(“00” + d.getHours()).slice(-2) + “:” +
(“00” + d.getMinutes()).slice(-2); // + “:” +
//(“00” + d.getSeconds()).slice(-2);
return newDate;
}
// --------------------------------------------------------------------------------
/**
*
**/
var Base64 = {
// private property
_keyStr : “ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=”,
// public method for encoding
encode : function (input) {
var output = “”;
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = Base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
},
// public method for decoding
decode : function (input) {
var output = “”;
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = Base64._utf8_decode(output);
return output;
},
// private method for UTF-8 encoding
_utf8_encode : function (string) {
string = string.replace(/\r\n/g,"\n");
var utftext = “”;
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
},
// private method for UTF-8 decoding
_utf8_decode : function (utftext) {
var string = “”;
var i = 0;
var c = c1 = c2 = 0;
while ( i < utftext.length ) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
}
else if((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i+1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = utftext.charCodeAt(i+1);
c3 = utftext.charCodeAt(i+2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
} }[/code][/spoiler]