| Both sides previous revisionPrevious revisionNext revision | Previous revision |
| smutbook:classes:persistence:start [2023/08/06 20:44] – lee | smutbook:classes:persistence:start [2026/08/25 11:41] (current) – lee |
|---|
| Most web browsers only allow for 10 megs (though some are as low as 5) of on-disk storage per origin. This not only has to contain the current game state, but also all previous game states in the game history. This is a particular problem with very large and/or complicated Twine games. | Most web browsers only allow for 10 megs (though some are as low as 5) of on-disk storage per origin. This not only has to contain the current game state, but also all previous game states in the game history. This is a particular problem with very large and/or complicated Twine games. |
| |
| In an attempt to minimize this issue, SmutBook uses a defaults and delta Persistence system. "Persistent Objects", be they characters within the game and their statistics and inventory, articles of clothing and their state, etc etc, are all defined at initialization time, <i>outside</i> of SugarCube's game history stack. Later, when an object's state is changed, only the differences from the object's initially defined default state are stored within SugarCube's game history stack. This greatly reduces the game's localstorage footprint (versus storing every object's full state within every game history moment), while still allowing the flexibility for almost all objects to be modified and tracked in the story history. | In an attempt to minimize this issue, SmutBook uses a defaults and delta Persistence system. "Persistent Objects", be they characters within the game and their statistics and inventory, articles of clothing and their state, etc etc, are all defined at initialization time, //outside// of SugarCube's game history stack. Later, when an object's state is changed, only the differences from the object's initially defined state are stored within SugarCube's game history stack. This greatly reduces the game's localstorage footprint (versus storing every object's full state within every game history moment), while still allowing the flexibility for almost all objects to be modified and tracked in the story history. |
| |
| All persistent objects must ultimately derive from the PersistentObject class. | All persistent objects must ultimately derive from the PersistentObject class. |
| ===== How It Works ===== | ===== How It Works ===== |
| |
| The implementation of PersistentObject isn't very straightforward to read. Here's a high level overview of how it works. | The implementation of PersistentObject isn't very straightforward to read. Here's a high level overview of how it works. For a more hands-on explanation, please see the [[Tutorial]] game included with the SmutBook library. |
| |
| There are three layers of data associated with a PersistentObject. | There are three layers of data associated with a PersistentObject. |
| |
| The lowest layer is called the "object layer", and comprises properties that are defined in the actual subclass itself, in the constructor, in the usual way. These data are generally default values that will be the same on any object of a given type, although they can be overridden by higher layers of data. | The lowest layer is called the "Object Layer", and comprises properties that are defined in the actual subclass itself, initialized in the constructor, in the usual way. These data are generally default values that will be the same on any object of a given type, although they can be overridden by higher layers of data. |
| |
| Unless you are actually extending SmutBook itself, you will probably never really need to deal with the object layer in your story. | Unless you are actually extending SmutBook itself, you will probably never really need to deal with the object layer in your story. |
| |
| The middle layer is called the "defaults layer". These data are defined by the story author in a JS file, using the static function PersistentObject.define(). This is used to define every specific concrete object in the game, by registering its Persistence ID, PersistentObject subclass, and default properties. These data will override any properties that are set on the object itself in the class constructor. | The middle layer is called the "Defaults Layer". These data are defined by the story author in a JS file, using the API call SmutBook.API.defineObj(). This is used to define every specific concrete object in the game, by registering its Persistence ID, PersistentObject subclass, and default properties. These data will override any properties that are set on the object itself in the class constructor. |
| | |
| | For example, there is a class called Smutbook.Apparel.Unisex.Shirt. Defining an object of this class in your story without providing any defaults will create an extremely generic shirt, called "a shirt", with no graphic image, etc. When you define this shirt in your game, you might pass some default values to instead create "a green polo shirt", possibly also assigning it an appropriate image. In every //mechanical// sense, it functions in the same way as the generic "a shirt", but has been given more detail. In this way, you can easily define many items of Apparel that, while based on and derive their nature from a particular generic Apparel class, have different appearances and descriptions. |
| |
| Finally, the top layer is called the "SugarCube layer". This layer is stored within SugarCube's state history, and overrides data in the layers below it. If you change a property on an object to something different than the value you defined in the defaults layer, the new value will be stored in the SugarCube layer and will override the data in the defaults and object layers. If you delete the property or set it back to the value you defined in the defaults layer, it will be removed from Sugarcube's state history, and the value from the defaults layer will be used once again. | Finally, the top layer is called the "SugarCube layer". This layer is stored within SugarCube's state history, and overrides data in the layers below it. If you change a property on an object to something different than the value you defined in the defaults layer, the new value will be stored in the SugarCube layer and will override the data in the defaults and object layers. If you delete the property or set it back to the value you defined in the defaults layer, it will be removed from Sugarcube's state history, and the value from the defaults layer will be used once again. |
| | |
| | For example, using the "green polo shirt" object from before, let's say that at some point during play, the player is given the option to rip the sleeves off of the shirt for some reason. We could do something like greenPoloShirt.name = "sleeveless green polo shirt", this changing the object's name in play. Since this change is recorded into the Sugarcube datastore layer, using the passage navigation buttons to rewind the story will erase this change when it is rewound past. |
| |
| In this way, only the changes from the default data are stored in Sugarcube's limited-size story history, yet all properties on any PersistentObject can be mutable. | In this way, only the changes from the default data are stored in Sugarcube's limited-size story history, yet all properties on any PersistentObject can be mutable. |
| |
| These three layers are implemented transparently by returning a Proxy object from the PersistentObject constructor. The Proxy's handler intercepts all property accesses to the PersistentObject and correctly routes them through the layered data levels. | These three layers are implemented transparently by returning a Proxy object from both SmutBook.API.defineObj() and SmutBook.API.fetchObj(). The Proxy's handler intercepts all property accesses to the PersistentObject and correctly routes them through the layered data levels. |
| | |
| | However, this means that you **MUST NOT** instantiate an instance of a PersistentObject (or subclass thereof) via the usual 'new' mechanism. If you do this, you'll get the raw object without the Proxy attached, and none of the values from the Defaults or Sugarcube layers of datastore will be applied to it. Instead, you define your new objects with SmutBook.API.defineObj(), and then later fetch them (by persistence id) with SmutBook.API.fetchObj(). |
| |
| This handler also replaces all references to PersistentObjects stored within Sugarcube's state history with a placeholder object that tracks the real PersistentObject's 'id' field, and then reinstantiates the real PersistentObject when it is fetched back from the Sugarcube datastore. This allows for circular graphs of PersistentObjects to be stored within Sugarcube's state history without causing a covfefe. You still can't have circular graphs of non-PersistentObjects, but as long as there is a PersistentObject somewhere in the graph that keeps the circle from linking back to itself, things should work. It's not the most efficient way to do things, but I think it's the best way short of patching SugarCube itself. | This handler also replaces all references to PersistentObjects stored within Sugarcube's state history with a placeholder object that tracks the real PersistentObject's 'id' field, and then reinstantiates the real PersistentObject when any of its fields are accessed. This allows for circular graphs of PersistentObjects to be stored within Sugarcube's abominable state history implementation without causing a meltdown. You still can't have circular graphs of non-PersistentObjects, but as long as there is a PersistentObject somewhere in the graph that keeps the circle from linking back to itself, things should kinda work, maybe. It's not the most efficient way to do things, but I think it's the best way short of patching SugarCube itself. If we are expected to be able to create complex location-based Twine stories with a rich object hierarchy, the ability to implement a circular object graph is mandatory! |
| |
| Please look at the [[Tutorial]] for more details. This implementation works decently well, but there are some gotchas to look out for that are sort of hacked around, particularly in the constructors of subclasses. | Please look at the [[Tutorial]] for more details. This implementation works decently well, but there are some gotchas to look out for that are sort of hacked around, particularly in the constructors of subclasses. |
| |