[EOS/CODE] SaveState - Squeezing hundreds of variables into tease storage

All about the past, current and future webteases and the art of webteasing in general.
---
Post Reply
fapnip
Explorer At Heart
Explorer At Heart
Posts: 431
Joined: Mon Apr 06, 2020 1:54 pm

[EOS/CODE] SaveState - Squeezing hundreds of variables into tease storage

Post by fapnip »

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:

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})()
(Source Code)
Spoiler: show
(hookProperty source code)
Spoiler: show

Code: Select all

/**
 * Install hookProperty v0.1.1 method
 *  Use:  hookProperty(object, keyName, defaultValue, onChangeFunction)
 *          onChangeFunction = function called when values of property changed:
 *          example use on global object (window):

hookProperty(window, 'points', 0, function(keyName, newValue, oldValue){
  console.log(keyName, ' changed from ', oldValue, ' to ', newValue)
  if (newValue < 0) {
    return 0; // Override newValue with 0 if less than 0
  }
  if (newValue >= 100) {
    pages.goto('won') // Have enough points to win, goto 'won' page
  }
  // If we return nothing or undefined, newValue will be used.
})

 */
;(function(){
  function uglyHook(obj, prop, defaultVal, onChange) {
    // Ugly work-around for broken JS-Interpreter
    // JS Interpreter currently used on Eos has broken getters on global object
    obj[prop] = defaultVal
    Object.defineProperty(obj, prop, {
      enumerable: true,
      set: function(val) {
        var lastVal = obj[prop], newVal
        Object.defineProperty(obj, prop, {set: undefined, enumerable: true})
        obj[prop] = val
        if(lastVal !== val && typeof onChange === 'function') newVal = onChange.call(obj, prop, val, lastVal)
        uglyHook(obj, prop, newVal === undefined ? val : newVal, onChange)
      }
    })
  }
  function realHook(obj, prop, defaultVal, onChange) {
    // Usable on non-global objects, or global object on non-broken interpreter
    // JS Interpreter used by OpenEOS does not have broken getters on global object
    var value = defaultVal
    Object.defineProperty(obj, prop, {
      enumerable: true,
      get: function() {return value},
      set: function(val) {
        var lastVal = value, newValue
        value = val
        if(lastVal !== val && typeof onChange === 'function') newValue = onChange.call(obj, prop, val, lastVal)
        value = newValue === undefined ? val : newValue        
      }
    })
  }
  // Check if we're using a broken JS Interpreter
  var test = '__test__'
  uglyHook(window, test, test, function(){})
  var needsUgly = window[test] === test
  delete window[test]
  // Export hookProperty method
  window.hookProperty = function(obj, prop, defaultVal, onChange) {
    if (needsUgly && obj === window) {
      uglyHook(obj, prop, defaultVal, onChange)
    } else {
      realHook(obj, prop, defaultVal, onChange)
    }
  }
})()
(JsonComp source code)
Spoiler: show

Code: Select all


/**
 * JsonComp v0.5.0 by fapnip -- JSON string compression.
 * Used to reduce overhead of json encoding, allowing more data to be saved in tease storage's 1KB limit
 * Usage:
 * JsonComp.setDictionary(["word1", "word2", ...]) // Set optional statc dictionary to improve compression
 * var compressedJson = JsonComp.compress(jsonString)
 * var decompressedJson = JsonComp.decompress(compressedJson)
 * 
 * TODO?:
 *  - Clean up baseCodebook character mapping to make it more sensible
 *  - Use better/statistically significant common letter combinations in baseCodebook
 *  - Add 3+ digit integer to base-188 encoding to compress larger integer numbers
 *  - Add CRC-7 for simple decompression integrity checks
 * 
 */

;(function() {

  function JsonComp() {}

  // We only have 188 printable characters that take a single byte once encoded in JSON.
  // (Characters 127 to 159 cause trouble on some backends.)
  var allowedCharCodes = ([[32,33],[35,91],[93,126],[161,254]]).reduce(function(a, r) {
    for (var i = r[0], l = r[1]; i <= l; i++) {
      a.push(i)
    }
    return a
  },[])

  // allowedCharCodes.forEach(function(c){
  //   console.log('allowed:', JSON.stringify(String.fromCharCode(c)), c)
  // })
  // console.log("allowedCharCodes", allowedCharCodes.length, allowedCharCodes)

  // for (var i = 0; i < 256; i++) {
  //   var char = JSON.stringify(String.fromCharCode(i))
  //   if (char.length === 3) console.log('char:', char, i)
  // }
// // teaseStorage.clear()
//   var test1 = teaseStorage.getItem('test')
//   if  (test1) {
//     for (var i = 0, l = test1.length; i < l; i+=2) {
//       console.log('test get:', i, test1[i], test1[i + 1].charCodeAt(0), JSON.stringify(test1[i + 1]))
//     }
//   } else {
//     console.log('No test found')
//   }

//   var testCharCodes = ([[173,174]]).reduce(function(a, r) {
//     for (var i = r[0], l = r[1]; i < l; i++) {
//       a.push(i)
//       a.push(String.fromCharCode(i))
//     }
//     return a
//   },[])
//   console.log('storing:', testCharCodes)
//   // teaseStorage.clear()
//   teaseStorage.setItem('test', testCharCodes)

  // Changing anything in this codebook will stop you from decompressing any
  // previsouly compressed items.
  var baseCodebook = {
    // common charcters (0-9, a-b, A-B, etc.), we'll keep in dictionary
    //  else contantly jumping in and out of verbatim mode will bloat result
    //  -- so only replace character's you'll hardly ever use in your storage
    // 0 - 31 take two characters -- don't use
    " ":32,
    "!":33,
    // "\"":34, // Don't use.  Takes two chars. Remapped to 171
    "#":35,
    "$":36,
    "%":37,
    "&":38,
    "\'":39, // Only takes a single character
    "(":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,
    // "\\":92, // Don't use.  Takes two chars. Remapped to 166
    "]":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,
    // 127 - 159 not usable on some back-ends
    // " ":160, // not usable
    "ca":161, // "¡":161,
    "cu":162, // "¢":162,
    "me":163,  // "£":163,
    "al":164, // "¤":164,
    "to":165, // "¥":165,
    "\\":166, // "¦":166,
    "th":167, // "§":167,
    "ed":168, // "¨":168,
    "nd":169, // "©":169,
    // "ª":170, // Used for strangeCodebook
    "\"":171, // "«":171,
    // "¬":172, // Used for numberCodebookSignals2
    // "­":173, // Used for numberCodebookSignals2
    // "®":174, // Used for numberCodebookSignals2
    ",null":175, // "¯":175,
    "null":176, // "°":176,
    // "±":177,
    // "²":178,
    ",false":179, // "³":179,
    "´":180,
    ",true":181, // "µ":181,
    // "¶":182,
    "·":183,
    "¸":184,
    "],[": 185,// "¹":185,
    "},{":186,// "º":186,
    // "»":187,
    "\\r\\n":188, // "¼":188,
    "\\n":189,// "½":189,
    "\"],[\"":190, // "¾":190,
    "\"},{\"":191, // "¿":191,
    "\",":192, // "À":192,
    "\":":193, // "Á":193,
    "\\u":194, // "Â":194,
    "\\ufe0f":195, // "Ã":195, // Emoji variation selector
    "{\"":196, // "Ä":196,
    "\"}":197, // "Å":197,
    "[\"":198, // "Æ":198,
    "\"]":199, // "Ç":199,
    "\\\"":200, // "È":200,
    "\\\",\\\"":201, // "É":201,
    "false":202, // "Ê":202,
    "true":203, // "Ë":203,
    "\":true":204, // "Ì":204,
    "\":false":205, // "Í":205,
    "\":null,\"":206, // "Î":206,
    "in":207, // "Ï":207,
    "an":208, // "Ð":208,
    // "Ñ":209,
    // "Ò":210,
    "or":211, // "Ó":211,
    "ing":212, // "Ô":212,
    "ea":213,  // "Õ":213,
    "le":214, // "Ö":214,
    "ce":215, // "×":215,
    "er":216,  // "Ø":216,
    "oo":217,  // "Ù":217,
    "el":218,  // "Ú":218,
    "en":219, // "Û":219,
    "ss":220,  // "Ü":220,
    "te":221,  // "Ý":221,
    "ie":222,  // "Þ":222,
    ",\"":223, // "ß":223,
    "\",\"":224, // "à":224,
    
    // "á":225,

    "\":false,\"": 226, // "â":226,
    "\":0,\"":227, // "ã":227,
    // "ä":228,
    "\":true,\"": 229, // "å":229,
    "\":1,\"": 230, // "æ":230,
    // "ç":231,
    // "è":232, // Used for numberCodebookSignals2
    // "é":233,
    // "ê":234,
    // "ë":235,
    // "ì":236, // Used for numberCodebookSignals
    // "í":237, // Used for numberCodebookSignals
    // "î":238, // Used for numberCodebookSignals
    // "ï":239, // Used for numberCodebookSignals
    ",0": 240, // "ð":240,
    ",1": 241, // "ñ":241,
    // "ò":242,
    // "ó":243,
    // "ô":244, // Used for altCodebookSignals
    // "õ":245, // Used for altCodebookSignals
    // "ö":246, // Used for altCodebookSignals

    // "÷":247,
    // "ø":248,
    // "ù":249,
    // "ú":250,
    // "û":251, // Used for altCodebookSignals
    // "ü":252,
    // "ý":253, // Used for verbatimMultiChar
    // "þ":254  // Used for verbatimSingleChar
    }
  
  var maxWordSize = 1
  var verbatimMultiChar = 253
  var verbatimSingleChar = 254
  var strangeCharacterSignal = 170
  var altCodebookSignals = [251, 244, 245, 246]
  var numberCodebookSignals = [236, 237, 238, 239] // for generating ":nnn," codes
  var numberCodebookSignals2 = [172, 173, 174, 232] // for generating ",nnn" codes // TODO:  replace with base 188 encoding
  var codebook, codebookLookup, codebookCodes
  var altCodebook = {}

  // Encodings for ":nnn," (common numbers in object) compression (-200 through around 550)
  var n = -200
  var numberCodebook = numberCodebookSignals.reduce(function(a, s){
    return allowedCharCodes.reduce(function(a, c){
      var k = '":' + n + ',"'
      while (baseCodebook[k]) {
        k = '":' + n + ',"'
        n++
      }
      a[k] = [s, c]
      n++
      return a
    }, a)
  }, {})
  // Encodings for ,nnn (common numbers in array) compression (-200 through around 550)
  n = -200
  var numberCodebook2 = numberCodebookSignals2.reduce(function(a, s){
    return allowedCharCodes.reduce(function(a, c){
      var k = ',' + n
      while (baseCodebook[k] || (n >= 0 && n <= 9)) {
        k = ',' + n
        n++
      }
      a[k] = [s, c]
      n++
      return a
    }, a)
  }, {})
  // Encodings for characters that Milovana's API may not store correctly
  // (some of these characters may cause the remote storage API to error on store)
  n = 0
  var strangeCodebook = [[0,7],[11],[14,31],[127,159]].reduce(function(a, r){
    for (var i = r[0], l = r[1]; i <= l; i++) {
      var k = String.fromCharCode(i)
      if (!baseCodebook[k]) {
        a[k] = [strangeCharacterSignal, allowedCharCodes[n]]
        n++
      }
    }
    return a
  }, {})
  // Build codebook
  function initCodebook() {
    maxWordSize = 1
    codebook = Object.keys(baseCodebook).reduce(function(a, k){
      a[k] = [baseCodebook[k]]
      return a
    }, {})
    codebook = Object.keys(numberCodebook).reduce(function(a, k){
      a[k] = numberCodebook[k]
      return a
    }, codebook)
    codebook = Object.keys(numberCodebook2).reduce(function(a, k){
      a[k] = numberCodebook2[k]
      return a
    }, codebook)
    codebook = Object.keys(strangeCodebook).reduce(function(a, k){
      a[k] = strangeCodebook[k]
      return a
    }, codebook)
    codebook = Object.keys(altCodebook).reduce(function(a, k){
      a[k] = altCodebook[k]
      return a
    }, codebook)
    codebookCodes = Object.keys(codebook).reduce(function(a, k){
      a[k] = codebook[k].reduce(function(w, c){
        w += String.fromCharCode(c)
        return w
      }, '')
      return a
    }, {})
    codebookLookup = Object.keys(codebook).reduce(function(a, k){
      if (codebook[k].length > 1) {
        var r = a[codebook[k][0]] || []
        a[codebook[k][0]] = r
        r[codebook[k][1]] = k
      } else {
        a[codebook[k][0]] = k
      }
      maxWordSize = Math.max(maxWordSize, (k + '').length)
      return a
    }, [])
    // console.log('Codebook:', codebook, strangeCodebook)
  }

  initCodebook()
  var verbatimLenLookup = allowedCharCodes
  var reverseLenLookup = verbatimLenLookup.reduce(function(a, v, i){a[v] = i; return a}, [])
  var maxVerbatimLen = verbatimLenLookup.length - 1
  
  function flushVerbatim(verbatim) {
    var k, output, _i, _len
    output = []
    if (verbatim.length > 1) {
      output.push(String.fromCharCode(verbatimMultiChar))
      output.push(String.fromCharCode(verbatimLenLookup[verbatim.length - 1]))
    } else {
      output.push(String.fromCharCode(verbatimSingleChar))
    }
    for (_i = 0, _len = verbatim.length; _i < _len; _i++) {
      k = verbatim[_i]
      output.push(k)
    }
    return output
  };

  JsonComp.setDictionary = function(dict) {
    var d = 0
    var c = 0
    var signal = altCodebookSignals[d]
    altCodebook = {}
    initCodebook()
    altCodebook = dict.reduce(function(a, word) {
      if (typeof word !== 'string') {
        console.warn('Invalid dictionary entry:', word)
        return a
      }
      if (word.length < 3) {
        console.warn('Dictionary entry too short:', word)
        return a
      }
      if (a[word] || codebook[word]) {
        console.warn('Duplicate dictionary entry:', word)
        return a
      }
      var dictCode = allowedCharCodes[c]
      if (dictCode === undefined) {
        c = 0
        d++
        signal = altCodebookSignals[d]
        if (signal === undefined) {
          console.warn('Too many dictionary entries:', word)
          return a
        }
        dictCode = allowedCharCodes[c]
      }
      a[word] = [signal, dictCode]
      c++
      return a
    }, {})
    initCodebook()
    // altCodebookLookup = Object.keys(altCodebook).reduce(function(a, k, i){
    //   a[altCodebook[k]] = k; 
    //   maxWordSize = Math.max(maxWordSize, (k + '').length)
    //   return a}, [])
  }

  JsonComp.compress = function(input) {
    // First, encode any 16-bit characters that JSON.stringify missed
    // (Many back-ends do not like unencoded 16-bit characters.)
    input = input.replace(/[\u0100-\uFFFF]/g, function(chr) {
        return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4)
    })
    // Now compress the result the best we can
    var code, encoded, j
    var verbatim = ''
    var verbOrCode = ''
    var output = []
    var i = 0
    while (i < input.length) {
      encoded = false
      for (j = Math.min(maxWordSize, input.length - i); j > 0; j--) {
        code = codebookCodes[input.substr(i, j)]
        if (code !== undefined) {
          if (verbatim) {
            if (j === 1 && code === input.substr(i, j) && verbatim.length < maxVerbatimLen) {
              // We're in verbatim.  Append codes that could be verbatim to possible verbatim queue.
              verbOrCode += code
              break
            }
            if (verbOrCode.length && (verbatim.length - verbOrCode.length) === 1) {
              output = output.concat(flushVerbatim(verbatim[0]))
              // Encode what can be coded as codes if what must be verbatim is only a single character
              output.push(verbOrCode)
            } else {
              output = output.concat(flushVerbatim(verbatim))
            }
            verbOrCode = ''
            verbatim = ''
          }
          output.push(code)
          i += j
          encoded = true
          break
        }
      }
      if (!encoded) {
        verbatim += input[i]
        i++
        if (verbatim.length >= maxVerbatimLen) {
          if (verbOrCode.length && (verbatim.length - verbOrCode.length) === 1) {
            // Encode what can be codes as codes if what must be verbatim is only a single character
            output = output.concat(flushVerbatim(verbatim[0]))
            output.push(verbOrCode)
          } else {
            output = output.concat(flushVerbatim(verbatim))
          }
          verbOrCode = ''
          verbatim = ''
        }
      }
    }
    if (verbatim) {
      output = output.concat(flushVerbatim(verbatim))
    }
    var result = output.join('')
    // console.log('Compressed to:', input, input.length, result, result.length)
    return result
  };

  JsonComp.decompress = function(string) {
    var i, input, j, output, l, word, word2
    output = ''
    input = (function() {
      _results = []
      for (i = 0, l = string.length; i < l; i++) {
        _results.push(string.charCodeAt(i))
      }
      return _results
    })()
    var il = input.length
    i = 0
    var inChar = input[i]
    // Inflate
    while (i < il) {
      if (inChar === verbatimSingleChar) {
        if (i + 1 > il) {
          throw 'Malformed JsonComp'
        }
        output += string[i + 1]
        i += 2
      } else if (inChar === verbatimMultiChar) {
        var wordLen = reverseLenLookup[input[i + 1]] + 1
        if (i + wordLen + 2 >= il) {
          throw 'Malformed JsonComp'
        }
        for (j = 0; j < wordLen; j++) {
          output += string[i + 2 + j]
        }
        i += (2 + wordLen)
      } else {
        word = codebookLookup[inChar]
        if (word === undefined) {
          console.error('No dictionary entry found for code ' + inChar +  ', falling back to literal')
          word = string[i]
        }
        if (Array.isArray(word)) {
          word2 = word[input[i + 1]]
          if (word2 === undefined) {
            console.error('No dictionary entry found for code ' + inChar + '/' + input[i + 1] +  ', falling back to literal')
            word = string[i] + string[i + 1]
          } else {
            word = word2
          }
          i++
        }
        output += word
        i++
      }
      inChar = input[i]
    }
    // console.log('Decompressed to:', string, string.length, output, output.length)
    return output
  }

  window.JsonComp = JsonComp

})()
(SaveState source code)
Spoiler: show

Code: Select all

/**
 * SaveState v0.2.6 -- fapnip
 */
;(function(){

  var stateKeys = {}

  function SaveState(opta){
    var opt = arguments[0] // workaround for uglify bug
    if (!opt) throw 'Missing SaveState options'
    var autoLoad = opt.autoLoad
    var stateKey = typeof opt.stateKey !== 'string' ? "~" : opt.stateKey
    if (stateKey.length > 1) {
      console.warn('State Key of "' + stateKey + '" is greater 1 character.  It\'s best to keep state keys short.')
    }
    if (stateKey in stateKeys) {
      throw 'State Key of "' + stateKey + '" already defined by another SaveState.  Specify a different stateKey.'
    }
    var store = opt.store || window
    if (typeof opt.states !== 'object' && (!autoLoad || store !== window)) {
      throw 'You must specify valid state keys to save.'
    }
    stateKeys[stateKey] = this
    var states = {__KEYCRC: null, __UPDATED: null}
    this.reqStates = opt.states || {}
    this.store = store
    this.stateKey = stateKey
    this.opt = opt
    this.__states = states
    this.__UPDATED = null
    this.__RESET = false // sets true if load of existing fails

    if (autoLoad) {
      if (store !== window) {
        console.warn('State autoLoad only works on global object')
      }
      this.__autoVars = true
      this.__rootKeys = Object.keys(store).reduce(function(a, k){a[k] = true; return a}, {})
    } else {
      this.loadStates()
    }
  }

  var BASE10 = "0123456789"
  var BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

  function _baseConvert(src, srctable, desttable) {
    src += ''
    var srcParts = src.split('.')
    if (srcParts.length > 1) {
      return _baseConvert(srcParts[0], srctable, desttable) + '.' + _baseConvert(srcParts[1], srctable, desttable)
    }
    var isNeg = src[0] === '-'
    if (isNeg) {
      src = src.replace(/^-/, '')
    }
    var srclen = srctable.length
    var destlen = desttable.length
    var val = 0
    for (var i = 0, l = src.length; i < l; i++) {
      val = val * srclen + srctable.indexOf(src.charAt(i))
    }
    if (val < 0) return isNeg ? '-0' : '0'
    var r = val % destlen
    var res = desttable.charAt(r)
    var q = Math.floor(val / destlen)
    while (q) {
      r = q % destlen
      q = Math.floor(q / destlen)
      res = desttable.charAt(r) + res
    }
    return isNeg ? '-' + res : res
  }

  // Convert base 10 number to base 62 to save space
  function _toBase62(src) {
    return _baseConvert(src, BASE10, BASE62)
  }
  // Restore base 10 from base 62
  function _fromBase62(src) {
    return Number(_baseConvert(src, BASE62, BASE10))
  }

  function _buildHook(value, onChange) {
    function _init(state, store, parentStore, parentKey) {
      hookProperty(parentStore, parentKey, value, onChange)
    }
    _init.__stateInit = true
    _init.__value = value
    return _init
  }

  function _compressedNumber(value, onChange) {
    function _init(state, store, parentStore, parentKey) {
      if (typeof onChange === 'function') {
        hookProperty(parentStore, parentKey, value, onChange)
      } else {
        parentStore[parentKey] = value
      }
    }
    _init.__stateInit = true
    _init.__value = value
    _init.__loadValue = function(value) {
      return _toBase62(value)
    }
    _init.__saveValue = function(value) {
      return _fromBase62(value)
    }
    return _init
  }
  
  function _buildObject(states) {
    function _init(state, store, parentStore, parentKey) {
      var ss = state.__states
      Object.keys(ss).forEach(function(k){
        var s = ss[k]
        if (typeof s === 'function') {
          var stateStore
          if (s.__states) {
            stateStore = store[k] || {}
            store[k] = stateStore
          }
          if (s.__stateInit) {
            ss[k](s, stateStore, store, k) // Must call from ss[k], not s, else context is not set
          }
        } else {
          if (s === undefined) s = null
          store[k] = s
        }
      })
    }
    _init.__stateInit = true
    _init.__states = states
    return _init
  }

  function _initStore(state, store) {
    var ps = {store: store}
    _buildObject(state.__states)(state, store, ps, 'store')
  }

  function _saveStore(source, store, storeDiff) {
    state = this
    var i = 0
    var states = state.__states
    Object.keys(states).forEach(function(k){
      var s = states[k]
      var v = store[k]
      if (typeof s === 'function') {
        if (s.__states) {
          var sStore = store[k] || {}
          var storeDiff2 = storeDiff[k] || {}
          storeDiff[k] = storeDiff2
          var aSource = source[i]
          if (!Array.isArray(aSource)) aSource = []
          source[i] = aSource
          store[k] = sStore
          _saveStore(aSource, sStore, storeDiff2)
          i++
        } else if ('__value' in s && typeof s.__value !== 'function') {
          if (typeof s.__saveValue === 'function' && v !== undefined) {
            v = s.__saveValue(v)
          }
          source[i] = v === undefined ? s.__value === undefined ? null : s.__value : v
          storeDiff[k] = source[i]
          i++
        }
      } else if (k in reserveKeys) {
        switch (k) {
          case '__UPDATED':
            source[i] = _toBase62(Date.now())
            break
         default:
            source[i] = state[k] === undefined ? null : state[k]
            break          
        }
        i++
      } else {
        source[i] = v === undefined ? s === undefined ? null : s : v
        storeDiff[k] = source[i]
        i++
      }
    })
  }

  function _loadStore(state, source, store, storeDiff) {
    var i = 0
    var states = state.__states
    Object.keys(states).forEach(function(k){
      var s = states[k]
      var v = source[i]
      if (typeof s === 'function') {
        if (s.__states) {
          var sStore = store[k] || {}
          store[k] = sStore
          var storeDiff2 = storeDiff[k] || {}
          storeDiff[k] = storeDiff2
          _loadStore(s, Array.isArray(v) ? v : [], sStore, storeDiff2)
          i++
        } else if ('__value' in s && typeof s.__value !== 'function') {
          if (typeof s.__loadValue === 'function' && v !== undefined) {
            v = s.__loadValue(v)
          }
          store[k] = v === undefined ? s.__value === undefined ? null : s.__value : v
          if (store[k] !== s.__value) storeDiff[k] = store[k]
          i++
        }
      } else if (k in reserveKeys) {
        if (k !== '__KEYCRC') state[k] = v === undefined ? s === undefined ? null : s : v
        i++
      } else {
        store[k] = v === undefined ? s === undefined ? null : s : v
        if (store[k] !== s) storeDiff[k] = store[k]
        i++
      }
    })
  }

  function _testSource(state) {
    if (state.__SOURCE) return state.__SOURCE
    var compressedSource = teaseStorage.getItem(state.stateKey)
    if (compressedSource) {
      try {
        var source = JSON.parse(JsonComp.decompress(compressedSource))
        if (source && Array.isArray(source)) {
          return source
        } else {
          //  console.warn('Non array source test for stateKey: ' + state.stateKey, e.toString())
        }
      } catch (e) {
        // console.warn('Invalid source test for stateKey: ' + state.stateKey, e.toString())
      }
    } else {
      // console.log('No source to test')
    }
  }

  function _clearLoad() {
    state.__SOURCE = null
    state.__RAWSOURCE = null
  }
  
  function _loadSource(state, rawSource) {
    if (!rawSource && state.__SOURCE) return state.__SOURCE
    var compressedSource = rawSource || teaseStorage.getItem(state.stateKey)
    state.__SIZE = 0
    if (compressedSource) {
      try {
        var source = JSON.parse(JsonComp.decompress(compressedSource))
        if (source && Array.isArray(source)) {
          var crcCheck = state._calcCrc(source.length) 
          if (crcCheck !== undefined && crcCheck === source[0]) {
            state.__SIZE = (compressedSource + '').length + state.stateKey.length + 5
            state.__UPDATED = source[1]
            state.__SOURCE = source
            state.__RAWSOURCE = compressedSource
            // console.warn('Loaded state source array:', source, state.crcTable, source.length)
            return source
          }
          state.__RESET = true
          console.error('Saved state key CRC does not match current state key CRC -- some state keys must have changed:', source, state.crcTable, source.length)
        } else {
          console.error('Decompressed state not a valid state array:', source)
        }
      } catch (e) {
        console.error('Invalid save state.', e.toString(), state)
      }
    } else {
      console.log('No save state found')
    }
  }

  function _saveSource(source) {
    var compressedSource = JsonComp.compress(JSON.stringify(source))
    teaseStorage.setItem(this.stateKey, compressedSource)
    this.__SOURCE = source
    this.__RAWSOURCE = compressedSource
    this.__SIZE = compressedSource.length + this.stateKey.length + 5
  }

  var reserveKeys = {
    __KEYCRC: true,
    __UPDATED: true,
  }

  function _cleanStateKeys(path, reqStates, states) {
    Object.keys(reqStates).forEach(function(k){
      if (k in reserveKeys) throw k + ' is a reserved state key.  Please select another.'
      var s = reqStates[k]
      if ((k + '').match(/^[0-9]$/)) {
        console.warn('Invalid State Key: "' + path + '.'  + k + '".  Keys must not be an integer number.')
      } else if (typeof s === 'function') {
        if (!s.__stateInit) {
          console.warn('Invalid State Value for "' + path + '.'  + k + '".  Only state init functions are allowed.')
        } else if (s.__states) {
          var cleanedStates = {}
          s.__states = cleanedStates
          _cleanStateKeys(path + '.' + k, s.__states, cleanedStates)
        } else {
          states[k] = s
        }
        
      } else {
        states[k] = s
      }
    })
  }


  SaveState.prototype = {

    _calcCrc: function(length) {
      var crcTable = this.crcTable
      var crc = crcTable[length-1]
      if (crc === undefined) {
        var stateKeyArray = Object.keys(this.__states)
        var crcMessage = stateKeyArray.slice(0, length).join(',')
        crc = _toBase62(crc16(crcMessage))
        crcTable[length-1]=crc
      }
      return crc
    },

    loadStates: function() {
      if (this.__didInit) {
        console.error("Can't init save state twice")
        return
      }

      var store = this.store
      var states = this.__states
      var reqStates = this.reqStates || {}

      if (this.__autoVars) {
        var rootKeys = this.__rootKeys
        var newVars = Object.keys(store).reduce(function(a, k){if(!rootKeys[k]) a[k] = store[k]; return a}, {})
        var newKeys = Object.keys(newVars)
        if (newKeys.length) {
          console.log('Auto loaded state variables:', newVars)
          newKeys.forEach(function(k){
            reqStates[k] = newVars[k]
          })
        }
      }

      console.log('Cleaning states for ', this.stateKey)
      _cleanStateKeys(this.stateKey, reqStates, states)
      var stateKeyArray = Object.keys(states)
      var stateArraySize = stateKeyArray.length
      var testSource = _testSource(this)
      var tableStart = Math.max(Math.min(testSource ? testSource.length : stateArraySize, stateArraySize) - 1, 0)
      // Build a lookup table for valid state key CRCs
      // This is used to check if key names have been re-ordered or modified since
      var crcMessage = stateKeyArray.slice(0, tableStart).join(',')
      var crcTable = {}
      for (var i = tableStart, l = stateKeyArray.length; i < l; i++) {
        var k = stateKeyArray[i]
        crcMessage += ',' + k
        crcTable[i] = _toBase62(crc16(crcMessage))
      }
      this.crcTable = crcTable
      this.__STATESIZE = stateArraySize
      this.__KEYCRC = crcTable[stateKeyArray.length - 1]
      // console.log('CRC table:', this.crcTable, ' for ', stateKeyArray)
      if (!window.JsonComp) {
        throw 'SaveStates required JsonComp'
      }
      _initStore(this, store)

      this.__didInit = true

    },
    load: function(store, rawSource) {
      store = store || this.store
      _initStore(this, store)
      var storeDiff = {}
      var source = _loadSource(this, rawSource)
      if (source) _loadStore(this, source, store, storeDiff)
      this.__LOADDIFF = storeDiff
      return store
    },

    loadDiff: function() {
      return this.__LOADDIFF
    },

    hadReset: function() {
      return this.__RESET
    },

    save: function(store) {
      store = store || this.store
      var source = []
      var storeDiff = {}
      _saveStore.call(this, source, store, storeDiff)
      _saveSource.call(this, source)
      this.__SOURCE = source
      this.__STOREDIFF = storeDiff
    },

    clear: function() {
      window.teaseStorage.removeItem(this.stateKey)
      this.__UPDATED = null
      this.__SOURCE = null
      this.__SIZE = 0
    },

    storeDiff: function() {
      return this.__STOREDIFF
    },

    hasSave: function() {
      return !!_loadSource(this, null)
    },

    getAge: function() {
      if(!_loadSource(this, null)) return
      return Date.now() - _fromBase62(this.__UPDATED)
    },

    getSaveTime: function() {
      if(!_loadSource(this, null)) return
      return _fromBase62(this.__UPDATED)
    },
    getSize: function() {
      return this.__SIZE || 0
    },

    getPercentUsed: function() {
      return this.getSize() / 1024
    },

    clearLoad: _clearLoad,

    getRaw: function() {
      return this.__RAWSOURCE
    },

    getCurrentStore: function() {
      var store = this.store
      return Object.keys(this.__states).reduce(function(a,k){
        if (!(k in reserveKeys)) a[k] = store[k]
        return a
      },{})
    },

    setCurrentStore: function(obj) {
      var store = this.store
      return Object.keys(this.__states).forEach(function(k){
        if(k in obj) store[k] = obj[k]
      })
    }

  }

  SaveState.clearAll = function() {
    Object.keys(stateKeys).forEach(function(k){
      stateKeys[k].clear()
    })
  }

  SaveState.getSize = function() {
    var result = 0
    Object.keys(stateKeys).forEach(function(k){
      var state = stateKeys[k]
      var stateSize = state.getSize()
      if (stateSize) {
        if (result) stateSize ++ // Add for comma separation
        result += stateSize
      }
    })
    if (result) {
      result += 2 // Add for object curly brackets
    }
    return result
  }

  SaveState.getAvgSize = function() {
    var result = 0
    var count = 0
    Object.keys(stateKeys).forEach(function(k){
      var state = stateKeys[k]
      var stateSize = state.getSize()
      if (stateSize) {
        if (result) stateSize ++ // Add for comma separation
        result += stateSize
        count ++
      }
    })
    if (!count) {
      return 0
    }
    if (result) {
      result += 2 // Add for object curly brackets
    }
    return result / count
  }

  SaveState.getRemainingBytes = function() {
    return 1024 - SaveState.getSize()
  }

  SaveState.getRemainingPercent = function() {
    return SaveState.getRemainingBytes() / 1024
  }

  SaveState.fromBase62 = _fromBase62
  SaveState.toBase62 = _toBase62
  SaveState.buildHook = _buildHook
  SaveState.buildObject = _buildObject
  SaveState.compressedNumber = _compressedNumber

  window.SaveState = SaveState
})()
Next, you 'll need to define your save state and tell it what variables to track. That can be done in two different ways.

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()
})
The other way is to be more concise, and allow more advanced things:

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
  }
}
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:
Spoiler: show

Instance methods:
(in "var mySaveState = new SaveState({...})", "mySaveState" is the instance.)

load(optionalStoreOverrideObject)
Loads values from teaseStorage into a store object.
Example:
mySaveState.load() // Will load save state to store object specified when creating instance
or:
var myOtherObject = mySaveState.load({}) // Load state into new object, "{}", and return to "myOtherObject" variable.

save(optionalStoreOverrideObject)
Saves values from a store object to teaseStorage.
Example:
mySaveState.save() // Save state from the store object specified when creating instance
or:
mySaveState.save(myOtherObject) // Save state from a different object

loadStates()
Loads new global variables that have been set after a SaveState instance was created with the "autoLoad" option set.
Example:
var mySaveState = new SaveState({autoLoad: true})
myStateVar1 = 'hello'
myStateVar2 = 'world'
mySaveState.loadStates()

clear()
Clears the save state from tease storage.
Example:
mySaveState.clear()

getSize()
Returns the estimated number of bytes this state is using in teaseStorage
Example:
var numberOfBytes = mySaveState.getSize()

getSaveTime()
Returns the number of milliseconds since Epoch (January 1, 1970 00:00:00 UTC) that this state was last saved. Will return undefined if there is no saved state.
Example:
var lastTimeStamp = mySaveState.getSaveTime()

getAge()
Returns the number of milliseconds between the last time this state was saved and now. Will return undefined if there is no saved state.
Example:
var ageOfState = mySaveState.getAge()
if (ageOfState/1000/60 > 20) {
// State is over twenty minutes old
pages.goto('you-are-too-old')
}

(There or more instance methods, and will be documented as I have time and based on level of interest.)

Static Methods:

SaveState.clearAll()
Clears all save states.

SaveState.getSize()
Returns number of bytes used by all save states

SaveState.getRemainingBytes()
Returns estimated number of bytes remaining in tease storage. (If teaseStorage has been used outside of SaveSate, this number will not be accurate.)

(There or more static methods, and will be documented as I have time and based on level of interest.)

Need even more space? You can use BitN to condense multiple boolean values in addition to SaveState.
Last edited by fapnip on Thu Nov 03, 2022 2:36 pm, edited 33 times in total.
User avatar
diogaoo
Explorer At Heart
Explorer At Heart
Posts: 333
Joined: Fri Oct 16, 2020 5:26 pm
Contact:

Re: [EOS/CODE/FRAMEWORK] SaveState (AKA: Squeezing a bunch of data into tease storage)

Post by diogaoo »

Thanks for your hard work fapnip, I'm sure many high quality and complex webteases will emerge thanks to this
Download Hero Corruption 2 on the Website
Check out the new HC2 thread
Play Hero Corruption 1
Support my work on Patreon
fapnip
Explorer At Heart
Explorer At Heart
Posts: 431
Joined: Mon Apr 06, 2020 1:54 pm

Re: [EOS/CODE/FRAMEWORK] SaveState (AKA: Squeezing a bunch of data into tease storage)

Post by fapnip »

diogaoo wrote: Thu Nov 25, 2021 10:43 am Thanks for your hard work fapnip, I'm sure many high quality and complex webteases will emerge thanks to this
You're welcome. Hopefully it's of use to some of you.
User avatar
ritewriter
Explorer At Heart
Explorer At Heart
Posts: 454
Joined: Sun Jan 02, 2022 6:51 am
Gender: Male
Sexual Orientation: Open to new ideas!
I am a: Switch

Re: [EOS/CODE] SaveState - Squeezing hundreds of variables into tease storage

Post by ritewriter »

Wow! This worked so well. I wish I'd noticed this thread a few weeks ago before I set about kludging together my own save functions.

Thank you for creating this, fapnip. I hope bumping the thread will let more people know it exists!
User avatar
ritewriter
Explorer At Heart
Explorer At Heart
Posts: 454
Joined: Sun Jan 02, 2022 6:51 am
Gender: Male
Sexual Orientation: Open to new ideas!
I am a: Switch

Re: [EOS/CODE] SaveState - Squeezing hundreds of variables into tease storage

Post by ritewriter »

Bumping this.

This is the code I used to add save states to ESMv1.5 and it's really great.

If you have any questions about implementing it, let me know. I think I have it all figured out. Hopefully.
oneiromantica
Explorer
Explorer
Posts: 12
Joined: Thu Feb 22, 2024 7:16 am
Sexual Orientation: Pansexual
I am a: Switch

Re: [EOS/CODE] SaveState - Squeezing hundreds of variables into tease storage

Post by oneiromantica »

Believe it or not, we ran out of tease storage even with SaveState and JsonComp, so I had to create some more tools. They follow similar principles but achieve far better compression.

The API requires a little more typing, but it's more predictable when you start juggling multiple states (like current game, stuff I'd like to keep independent like global high score etc.). You can find them at https://github.com/oneiromantica/eos-tools. Comments/issues/PRs always welcome.
Post Reply