[EOS/CODE] SaveState - Squeezing hundreds of variables into tease storage
Posted: Thu Nov 25, 2021 7:26 am
In some teases with a ton of game state variables, it can be difficult to fit all that data into the limited 1024 bytes (1 KB) that Eos gives us for tease storage.
To help with that, here's SaveState. Some code you can drop into your Init Script to help with variable saving/loading.
First step is to copy/paste these dependencies at the top of your init script:
(Source Code)
The first way is to make it easier to bolt-on storage to existing teases. For example:
The other way is to be more concise, and allow more advanced things:
For a full demonstration on how to add save states to an existing tease, here's diogaoo's Hero Corruption 0.9d with save states bolted on:
https://milovana.com/webteases/showteas ... e00b9f8244
(JSON)
https://milovana.com/webteases/geteossc ... e00b9f8244
And here's a Magpie you can use to see the changes from the original:
https://codepen.io/fapnip/full/GRjwPXP? ... 0e6b72ed8c
The main changes to implement it in Hero Corruption were:
1. Add dependencies and auto load variables into new SaveState similar to the example code above.
2. Add some helper functions to the Init Script for checking/loading the save state. (You should review these functions and the comments in them in detail by loading the JSON from above into a new tease.)
3. Add allow/block lists and expressions to the Init Script to define pages that save state will be saved, skipped and cleared on.
4. Add a page change event listener in the Init Script that will automatically save, clear or skip save whenever the page changes.
5. Add an IF action to the start page that checks if there's a valid save state, and goes to a --restore-game page to ask the player if they want to resume.
6. Add that "--restore-game" page for prompting resume.
7. Add an eval that clears the game state just before the End action in the score calculate page. (We don't want to automatically clear state every time we hit that page -- only if it falls through to a tease end.)
For a list of SaveSate's functions see:
Need even more space? You can use BitN to condense multiple boolean values in addition to SaveState.
To help with that, here's SaveState. Some code you can drop into your Init Script to help with variable saving/loading.
First step is to copy/paste these dependencies at the top of your init script:
Code: Select all
// First we need to install some dependencies:
// crc16 & crc32 (adapted from: https://github.com/emn178/js-crc)
;(function(){var n,t,e,l,o=[{name:"crc32",polynom:3988292384,initValue:-1,bytes:4,method:null,table:null},{name:"crc16",polynom:40961,initValue:0,bytes:2,method:null,table:null}];for(n=0;n<o.length;++n){for((a=o[n]).method=function(n){return function(t){return r(t,n)}}(a),a.table=[],t=0;t<256;++t){for(l=t,e=0;e<8;++e)l=1&l?a.polynom^l>>>1:l>>>1;a.table[t]=l>>>0}}var r=function(n,t){var e,l,o=t.initValue,r=n.length,a=t.table;for(l=0;l<r;++l)o=(e=n.charCodeAt(l))<128?a[255&(o^e)]^o>>>8:e<2048?a[255&((o=a[255&(o^(192|e>>6))]^o>>>8)^(128|63&e))]^o>>>8:e<55296||e>=57344?a[255&((o=a[255&((o=a[255&(o^(224|e>>12))]^o>>>8)^(128|e>>6&63))]^o>>>8)^(128|63&e))]^o>>>8:a[255&((o=a[255&((o=a[255&((o=a[255&(o^(240|(e=65536+((1023&e)<<10|1023&n.charCodeAt(++l)))>>18))]^o>>>8)^(128|e>>12&63))]^o>>>8)^(128|e>>6&63))]^o>>>8)^(128|63&e))]^o>>>8;return o^=t.initValue};for(n=0;n<o.length;++n){var a=o[n];window[a.name]=a.method}})()
// install hookProperty v0.1.1 (ask fapnip for source code if needed)
;(function(){function e(n,t,o,i){n[t]=o,Object.defineProperty(n,t,{enumerable:!0,set:function(o){var f,r=n[t];Object.defineProperty(n,t,{set:undefined,enumerable:!0}),n[t]=o,r!==o&&"function"==typeof i&&(f=i.call(n,t,o,r)),e(n,t,f===undefined?o:f,i)}})}var n="__test__";e(window,n,n,function(){});var t=window[n]===n;delete window[n],window.hookProperty=function(n,o,i,f){t&&n===window?e(n,o,i,f):function(e,n,t,o){var i=t;Object.defineProperty(e,n,{enumerable:!0,get:function(){return i},set:function(t){var f,r=i;i=t,r!==t&&"function"==typeof o&&(f=o.call(e,n,t,r)),i=f===undefined?t:f}})}(n,o,i,f)}})()
// Install JsonComp v0.5.0 (ask fapnip for source code if needed)
;(function(){function r(){}var e,n,t,o=[[32,33],[35,91],[93,126],[161,254]].reduce(function(r,e){for(var n=e[0],t=e[1];n<=t;n++)r.push(n);return r},[]),u={" ":32,"!":33,"#":35,$:36,"%":37,"&":38,"'":39,"(":40,")":41,"*":42,"+":43,",":44,"-":45,".":46,"/":47,0:48,1:49,2:50,3:51,4:52,5:53,6:54,7:55,8:56,9:57,":":58,";":59,"<":60,"=":61,">":62,"?":63,"@":64,A:65,B:66,C:67,D:68,E:69,F:70,G:71,H:72,I:73,J:74,K:75,L:76,M:77,N:78,O:79,P:80,Q:81,R:82,S:83,T:84,U:85,V:86,W:87,X:88,Y:89,Z:90,"[":91,"]":93,"^":94,_:95,"`":96,a:97,b:98,c:99,d:100,e:101,f:102,g:103,h:104,i:105,j:106,k:107,l:108,m:109,n:110,o:111,p:112,q:113,r:114,s:115,t:116,u:117,v:118,w:119,x:120,y:121,z:122,"{":123,"|":124,"}":125,"~":126,ca:161,cu:162,me:163,al:164,to:165,"\\":166,th:167,ed:168,nd:169,'"':171,",null":175,null:176,",false":179,"´":180,",true":181,"·":183,"¸":184,"],[":185,"},{":186,"\\r\\n":188,"\\n":189,'"],["':190,'"},{"':191,'",':192,'":':193,"\\u":194,"\\ufe0f":195,'{"':196,'"}':197,'["':198,'"]':199,'\\"':200,'\\",\\"':201,false:202,true:203,'":true':204,'":false':205,'":null,"':206,in:207,an:208,or:211,ing:212,ea:213,le:214,ce:215,er:216,oo:217,el:218,en:219,ss:220,te:221,ie:222,',"':223,'","':224,'":false,"':226,'":0,"':227,'":true,"':229,'":1,"':230,",0":240,",1":241},c=1,i=253,a=254,f=[251,244,245,246],l={},s=-200,d=[236,237,238,239].reduce(function(r,e){return o.reduce(function(r,n){for(var t='":'+s+',"';u[t];)t='":'+s+',"',s++;return r[t]=[e,n],s++,r},r)},{});s=-200;var h=[172,173,174,232].reduce(function(r,e){return o.reduce(function(r,n){for(var t=","+s;u[t]||s>=0&&s<=9;)t=","+s,s++;return r[t]=[e,n],s++,r},r)},{});s=0;var g=[[0,7],[11],[14,31],[127,159]].reduce(function(r,e){for(var n=e[0],t=e[1];n<=t;n++){var c=String.fromCharCode(n);u[c]||(r[c]=[170,o[s]],s++)}return r},{});function v(){c=1,e=Object.keys(u).reduce(function(r,e){return r[e]=[u[e]],r},{}),e=Object.keys(d).reduce(function(r,e){return r[e]=d[e],r},e),e=Object.keys(h).reduce(function(r,e){return r[e]=h[e],r},e),e=Object.keys(g).reduce(function(r,e){return r[e]=g[e],r},e),e=Object.keys(l).reduce(function(r,e){return r[e]=l[e],r},e),t=Object.keys(e).reduce(function(r,n){return r[n]=e[n].reduce(function(r,e){return r+=String.fromCharCode(e)},""),r},{}),n=Object.keys(e).reduce(function(r,n){if(e[n].length>1){var t=r[e[n][0]]||[];r[e[n][0]]=t,t[e[n][1]]=n}else r[e[n][0]]=n;return c=Math.max(c,(n+"").length),r},[])}v();var y=o,p=y.reduce(function(r,e,n){return r[e]=n,r},[]),m=y.length-1;function C(r){var e,n,t,o;for(n=[],r.length>1?(n.push(String.fromCharCode(i)),n.push(String.fromCharCode(y[r.length-1]))):n.push(String.fromCharCode(a)),t=0,o=r.length;t<o;t++)e=r[t],n.push(e);return n}r.setDictionary=function(r){var n=0,t=0,u=f[n];l={},v(),l=r.reduce(function(r,c){if("string"!=typeof c)return console.warn("Invalid dictionary entry:",c),r;if(c.length<3)return console.warn("Dictionary entry too short:",c),r;if(r[c]||e[c])return console.warn("Duplicate dictionary entry:",c),r;var i=o[t];if(void 0===i){if(t=0,void 0===(u=f[++n]))return console.warn("Too many dictionary entries:",c),r;i=o[t]}return r[c]=[u,i],t++,r},{}),v()},r.compress=function(r){var e,n,o;r=r.replace(/[\u0100-\uFFFF]/g,function(r){return"\\u"+("0000"+r.charCodeAt(0).toString(16)).substr(-4)});for(var u="",i="",a=[],f=0;f<r.length;){for(n=!1,o=Math.min(c,r.length-f);o>0;o--)if(void 0!==(e=t[r.substr(f,o)])){if(u){if(1===o&&e===r.substr(f,o)&&u.length<m){i+=e;break}i.length&&u.length-i.length==1?(a=a.concat(C(u[0]))).push(i):a=a.concat(C(u)),i="",u=""}a.push(e),f+=o,n=!0;break}n||(u+=r[f],f++,u.length>=m&&(i.length&&u.length-i.length==1?(a=a.concat(C(u[0]))).push(i):a=a.concat(C(u)),i="",u=""))}return u&&(a=a.concat(C(u))),a.join("")},r.decompress=function(r){var e,t,o,u,c,f,l;u="";for(var s=(t=function(){for(_results=[],e=0,c=r.length;e<c;e++)_results.push(r.charCodeAt(e));return _results}()).length,d=t[e=0];e<s;){if(d===a){if(e+1>s)throw"Malformed JsonComp";u+=r[e+1],e+=2}else if(d===i){var h=p[t[e+1]]+1;if(e+h+2>=s)throw"Malformed JsonComp";for(o=0;o<h;o++)u+=r[e+2+o];e+=2+h}else void 0===(f=n[d])&&(console.error("No dictionary entry found for code "+d+", falling back to literal"),f=r[e]),Array.isArray(f)&&(void 0===(l=f[t[e+1]])?(console.error("No dictionary entry found for code "+d+"/"+t[e+1]+", falling back to literal"),f=r[e]+r[e+1]):f=l,e++),u+=f,e++;d=t[e]}return u},window.JsonComp=r})()
// Install SaveState v0.2.6 (ask fapnip for source code if needed)
;(function(){var t={};function e(e){var n=arguments[0];if(!n)throw"Missing SaveState options";var s=n.autoLoad,a="string"!=typeof n.stateKey?"~":n.stateKey;if(a.length>1&&console.warn('State Key of "'+a+"\" is greater 1 character. It's best to keep state keys short."),a in t)throw'State Key of "'+a+'" already defined by another SaveState. Specify a different stateKey.';var r=n.store||window;if("object"!=typeof n.states&&(!s||r!==window))throw"You must specify valid state keys to save.";t[a]=this;var i={__KEYCRC:null,__UPDATED:null};this.reqStates=n.states||{},this.store=r,this.stateKey=a,this.opt=n,this.__states=i,this.__UPDATED=null,this.__RESET=!1,s?(r!==window&&console.warn("State autoLoad only works on global object"),this.__autoVars=!0,this.__rootKeys=Object.keys(r).reduce((function(t,e){return t[e]=!0,t}),{})):this.loadStates()}var n="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";function s(t,e,n){var a=(t+="").split(".");if(a.length>1)return s(a[0],e,n)+"."+s(a[1],e,n);var r="-"===t[0];r&&(t=t.replace(/^-/,""));for(var i=e.length,o=n.length,_=0,u=0,c=t.length;u<c;u++)_=_*i+e.indexOf(t.charAt(u));if(_<0)return r?"-0":"0";for(var l=_%o,f=n.charAt(l),h=Math.floor(_/o);h;)l=h%o,h=Math.floor(h/o),f=n.charAt(l)+f;return r?"-"+f:f}function a(t){return s(t,"0123456789",n)}function r(t){return Number(s(t,n,"0123456789"))}function i(t){function e(t,e,n,s){var a=t.__states;Object.keys(a).forEach((function(t){var n,s=a[t];"function"==typeof s?(s.__states&&(n=e[t]||{},e[t]=n),s.__stateInit&&a[t](s,n,e,t)):(void 0===s&&(s=null),e[t]=s)}))}return e.__stateInit=!0,e.__states=t,e}function o(t,e){var n={store:e};i(t.__states)(t,e,n,"store")}function _(t,e,n){state=this;var s=0,r=state.__states;Object.keys(r).forEach((function(i){var o=r[i],u=e[i];if("function"==typeof o)if(o.__states){var c=e[i]||{},f=n[i]||{};n[i]=f;var h=t[s];Array.isArray(h)||(h=[]),t[s]=h,e[i]=c,_(h,c,f),s++}else"__value"in o&&"function"!=typeof o.__value&&("function"==typeof o.__saveValue&&void 0!==u&&(u=o.__saveValue(u)),t[s]=void 0===u?void 0===o.__value?null:o.__value:u,n[i]=t[s],s++);else if(i in l){switch(i){case"__UPDATED":t[s]=a(Date.now());break;default:t[s]=void 0===state[i]?null:state[i]}s++}else t[s]=void 0===u?void 0===o?null:o:u,n[i]=t[s],s++}))}function u(t,e){if(!e&&t.__SOURCE)return t.__SOURCE;var n=e||teaseStorage.getItem(t.stateKey);if(t.__SIZE=0,n)try{var s=JSON.parse(JsonComp.decompress(n));if(s&&Array.isArray(s)){var a=t._calcCrc(s.length);if(void 0!==a&&a===s[0])return t.__SIZE=(n+"").length+t.stateKey.length+5,t.__UPDATED=s[1],t.__SOURCE=s,t.__RAWSOURCE=n,s;t.__RESET=!0,console.error("Saved state key CRC does not match current state key CRC -- some state keys must have changed:",s,t.crcTable,s.length)}else console.error("Decompressed state not a valid state array:",s)}catch(e){console.error("Invalid save state.",e.toString(),t)}else console.log("No save state found")}function c(t){var e=JsonComp.compress(JSON.stringify(t));teaseStorage.setItem(this.stateKey,e),this.__SOURCE=t,this.__RAWSOURCE=e,this.__SIZE=e.length+this.stateKey.length+5}var l={__KEYCRC:!0,__UPDATED:!0};e.prototype={_calcCrc:function(t){var e=this.crcTable,n=e[t-1];if(void 0===n){var s=Object.keys(this.__states).slice(0,t).join(",");n=a(crc16(s)),e[t-1]=n}return n},loadStates:function(){if(this.__didInit)console.error("Can't init save state twice");else{var t=this.store,e=this.__states,n=this.reqStates||{};if(this.__autoVars){var s=this.__rootKeys,r=Object.keys(t).reduce((function(e,n){return s[n]||(e[n]=t[n]),e}),{}),i=Object.keys(r);i.length&&(console.log("Auto loaded state variables:",r),i.forEach((function(t){n[t]=r[t]})))}console.log("Cleaning states for ",this.stateKey),function t(e,n,s){Object.keys(n).forEach((function(a){if(a in l)throw a+" is a reserved state key. Please select another.";var r=n[a];if((a+"").match(/^[0-9]$/))console.warn('Invalid State Key: "'+e+"."+a+'". Keys must not be an integer number.');else if("function"==typeof r)if(r.__stateInit)if(r.__states){var i={};r.__states=i,t(e+"."+a,r.__states,i)}else s[a]=r;else console.warn('Invalid State Value for "'+e+"."+a+'". Only state init functions are allowed.');else s[a]=r}))}(this.stateKey,n,e);for(var _=Object.keys(e),u=_.length,c=function(t){if(t.__SOURCE)return t.__SOURCE;var e=teaseStorage.getItem(t.stateKey);if(e)try{var n=JSON.parse(JsonComp.decompress(e));if(n&&Array.isArray(n))return n}catch(t){}}(this),f=Math.max(Math.min(c?c.length:u,u)-1,0),h=_.slice(0,f).join(","),v={},y=f,d=_.length;y<d;y++){h+=","+_[y],v[y]=a(crc16(h))}if(this.crcTable=v,this.__STATESIZE=u,this.__KEYCRC=v[_.length-1],!window.JsonComp)throw"SaveStates required JsonComp";o(this,t),this.__didInit=!0}},load:function(t,e){o(this,t=t||this.store);var n={},s=u(this,e);return s&&function t(e,n,s,a){var r=0,i=e.__states;Object.keys(i).forEach((function(o){var _=i[o],u=n[r];if("function"==typeof _)if(_.__states){var c=s[o]||{};s[o]=c;var f=a[o]||{};a[o]=f,t(_,Array.isArray(u)?u:[],c,f),r++}else"__value"in _&&"function"!=typeof _.__value&&("function"==typeof _.__loadValue&&void 0!==u&&(u=_.__loadValue(u)),s[o]=void 0===u?void 0===_.__value?null:_.__value:u,s[o]!==_.__value&&(a[o]=s[o]),r++);else o in l?("__KEYCRC"!==o&&(e[o]=void 0===u?void 0===_?null:_:u),r++):(s[o]=void 0===u?void 0===_?null:_:u,s[o]!==_&&(a[o]=s[o]),r++)}))}(this,s,t,n),this.__LOADDIFF=n,t},loadDiff:function(){return this.__LOADDIFF},hadReset:function(){return this.__RESET},save:function(t){t=t||this.store;var e=[],n={};_.call(this,e,t,n),c.call(this,e),this.__SOURCE=e,this.__STOREDIFF=n},clear:function(){window.teaseStorage.removeItem(this.stateKey),this.__UPDATED=null,this.__SOURCE=null,this.__SIZE=0},storeDiff:function(){return this.__STOREDIFF},hasSave:function(){return!!u(this,null)},getAge:function(){if(u(this,null))return Date.now()-r(this.__UPDATED)},getSaveTime:function(){if(u(this,null))return r(this.__UPDATED)},getSize:function(){return this.__SIZE||0},getPercentUsed:function(){return this.getSize()/1024},clearLoad:function(){state.__SOURCE=null,state.__RAWSOURCE=null},getRaw:function(){return this.__RAWSOURCE},getCurrentStore:function(){var t=this.store;return Object.keys(this.__states).reduce((function(e,n){return n in l||(e[n]=t[n]),e}),{})},setCurrentStore:function(t){var e=this.store;return Object.keys(this.__states).forEach((function(n){n in t&&(e[n]=t[n])}))}},e.clearAll=function(){Object.keys(t).forEach((function(e){t[e].clear()}))},e.getSize=function(){var e=0;return Object.keys(t).forEach((function(n){var s=t[n].getSize();s&&(e&&s++,e+=s)})),e&&(e+=2),e},e.getAvgSize=function(){var e=0,n=0;return Object.keys(t).forEach((function(s){var a=t[s].getSize();a&&(e&&a++,e+=a,n++)})),n?(e&&(e+=2),e/n):0},e.getRemainingBytes=function(){return 1024-e.getSize()},e.getRemainingPercent=function(){return e.getRemainingBytes()/1024},e.fromBase62=r,e.toBase62=a,e.buildHook=function(t,e){function n(n,s,a,r){hookProperty(a,r,t,e)}return n.__stateInit=!0,n.__value=t,n},e.buildObject=i,e.compressedNumber=function(t,e){function n(n,s,a,r){"function"==typeof e?hookProperty(a,r,t,e):a[r]=t}return n.__stateInit=!0,n.__value=t,n.__loadValue=function(t){return a(t)},n.__saveValue=function(t){return r(t)},n},window.SaveState=e})()
- Spoiler: show
The first way is to make it easier to bolt-on storage to existing teases. For example:
Code: Select all
// (all the dependencies from above here)
var myGameState = new SaveState({
store: window, // Track variables in the global (window) object
autoLoad: true, // Start looking for new variables
})
// Note: Don't prefix with "var", else SaveState won't be able to see them.
// If you rename or change the order of these variables, any existing saved states will be lost.
myVariable1 = true;
myVariable2 = false;
anotherVariable = 25;
// and so on ...
myGameState.loadStates() // Now load and track all those variables
// Now, we can do things like:
myGameState.save() // Save all those variables to tease storage
if (myGameState.hasSave()) {
myGameState.load() // Restore what's saved in storage to variables
// Do something else?
}
// Or automatically have the SavesSate save on page change
pages.addEventListener('change', function() {
var currentPageId = pages.getCurrentPageId()
// Don't auto save if we're changing to the "start" page, or a page that begins with "--"
if (currentPageId.match(/(^start$|^--)/)) return
// Else save
myGameState.save()
})Code: Select all
// (all the dependencies from above here)
// If we find we're storing a bunch of text in storage and running out of room, you can optionally add words from that text to the compression dictionary, like:
JsonComp.setDictionary([
"Hello",
"World",
// Words are case sensitive
// Words can be added to the list without breaking existing compressed states
// but renaming or changing the order of existing words will break decompression of previous saves that used them
]}
// Define game state
var myGameState = new SaveState({
stateKey: "$", // Default stateKey is "~". Each state you define needs its own key.
store: window, // Track variables in the global (window) object
states: {
//Note: If you rename or change the order of these variables, any existing saved states will be lost.
myVariable1: true,
myVariable2: false,
anotherVariable : 25,
// and so on ...
// We can also do things like:
myWatchedVariable: SaveState.buildHook(25, function(key, newVal, oldVal){
// This function will be called whenever myWatchedVariable is modified.
// for example if in an eval you did: myWatchedVariable = 50
if (newVal > 25) {
// If we return a value, it will override the new value
return 25 // Don't let this value go above 25
}
}),
// Or nest more compactible data:
myNestedObject: SaveState.buildObject({
// These could be access via myNestedObject.option1, etc.
option1: 23,
option2: 'Hello World',
}),
}
})
// Now, we can do things like:
myGameState.save() // Save all those variables to tease storage
if (myGameState.hasSave()) {
var testLoad = myGameState.load({}) // Load state into a new object instead of its original store
if (!testLoad.myVariable1) {
// To something else?
} else {
myGameState.load() // Restore what's been saved back to original variables
}
}https://milovana.com/webteases/showteas ... e00b9f8244
(JSON)
https://milovana.com/webteases/geteossc ... e00b9f8244
And here's a Magpie you can use to see the changes from the original:
https://codepen.io/fapnip/full/GRjwPXP? ... 0e6b72ed8c
The main changes to implement it in Hero Corruption were:
1. Add dependencies and auto load variables into new SaveState similar to the example code above.
2. Add some helper functions to the Init Script for checking/loading the save state. (You should review these functions and the comments in them in detail by loading the JSON from above into a new tease.)
3. Add allow/block lists and expressions to the Init Script to define pages that save state will be saved, skipped and cleared on.
4. Add a page change event listener in the Init Script that will automatically save, clear or skip save whenever the page changes.
5. Add an IF action to the start page that checks if there's a valid save state, and goes to a --restore-game page to ask the player if they want to resume.
6. Add that "--restore-game" page for prompting resume.
7. Add an eval that clears the game state just before the End action in the score calculate page. (We don't want to automatically clear state every time we hit that page -- only if it falls through to a tease end.)
For a list of SaveSate's functions see:
- Spoiler: show
Need even more space? You can use BitN to condense multiple boolean values in addition to SaveState.