Echo Hollow

Gender-Bending Interactive Stories! :D

User Tools

Site Tools


libecho:classes:persistence

This is an old revision of the document!


LibEcho.Persistence

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, LibEcho 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.

All persistent objects must ultimately derive from the PersistentObject class.

Each PersistentObject has a readonly “id” field (hereafter referred to as the “Persistence ID”), a simple alphanumeric-underscore string (whitespace, punctuation, and special symbols are not allowed).

A PersistentObject may be the “child” of another PersistentObject. This is tracked in the id field via a dot notation. For example, each Person object contains a GeneralInventory object that tracks what the Person is carrying. If the game contains a Person with the id of “joe”, then the id of Joe's GeneralInventory object might be “joe.inventory”. Likewise, the actual contents of that GeneralInventory might be stored as “joe.inventory.contents”, an array called “contents” within the joe.inventory object.

You won't generally need to worry too much about this id hierarchy, though. LibEcho takes care of most of it for you. The vast majority of the time, you'll only need to know the ids of toplevel objects that you have explicitly created: “joe”, “cocktail_dress”, etc.

How It Works

The implementation of PersistentObject isn't very straightforward to read. Here's a high level overview of how it works.

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.

Unless you are actually extending LibEcho 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.

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.

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 by returning a Proxy object from the PersistentObject constructor. The Proxy's handler intercepts all property accesses to the PersistentObject and routes them through our own code that handles the layered data levels.

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 circling back on 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.

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.

LibEcho.Persistence.PersistentObject

Inherits from: Nothing

The PersistentObject is the class from which all of the more complex persistent data types are derived. People, Apparel, Transformable attributes, Inventories of various sorts, and many other classes of objects, are all are ultimately derived from PersistentObject.

ALERT! MAKE NOTE! ALERT!

You should never instantiate a PersistentObject directly! Instead, you must define it with PersistentObject.define(), and then either store the result somewhere or re-fetch it with PersistentObject.fetch() when you want to use it.

If you try to instantiate a PersistentObject directly, rather than wrapping it in a call to fetch(), the Proxy object will get confused about what to do with properties that are set in subclass constructors, and you'll have an unhappy day. fetch() wraps things up with some hackery-magic and makes things behave as you'd expect for the most part.

Static Functions

Two static utility functions are defined on PersistentObject. These are called through the global class name rather than through specific instances of objects.

PersistentObject.define( id, objectClass=PersistentObject, defaults={}, allowRedefine=false )

Defines the default data for a PersistentObject (or subclass thereof) within your game. PersistentObject.define() must only be called at initialization time, from story javascript or initialization passages. The results of trying to use this function after the game has started are undefined.

  • id: the Persistence ID of the object being defined.
  • objectClass: the class of the object being defined. PersistentObject or subclass thereof.
  • defaults: a mapping of property names to data for the object's default data.
  • allowRedefine: if true, the object may be redefined. This is used internally and you probably shouldn't mess with it unless you are sure of what you are doing.
  • Returns: An instantiated instance of the object that was defined.
  • Throws: Error if the object is being redefined (and allowRedefine=false) or if the previously defined object does not have a valid class associated with it.

All PersistentObjects (and subclasses) must be defined using this method before they can be used within the game. See the Tutorial for a better explanation of all of this.

Example:

LibEcho.PersistentObject.define( "joe", LibEcho.Person.Man, {
	"name":               "Joe",
	"lastName":           "Blow",
	"inventory.contents": [ "revolver" ],
	"apparel.contents":   [ "boxers", "jeans", "tshirt", "socks", "tennisshoes" ]
});

PersistentObject.fetch( id, forceClass=undefined )

Given a persistence ID, returns the appropriate instantiated object (a PersistentObject or subclass).

  • id: the Persistence ID of the object being fetched.
  • forceClass: If set, overrides the class associated with the object ID and uses the given class instead. This is used internally and you probably shouldn't mess with it unless you are sure of what you are doing.
  • Returns: An instantiated instance of the object defined with the given id.
  • Throws: Error if the object does not have a valid class associated with it.

Properties

.id

This object's Persistence ID, as defined with PersistentObject.define().

  • Type: String
  • Automatic
  • ReadOnly

.parent

This object's parent. For example, the parent of the object with the id “joe.inventory” would be the object with the id “joe”.

  • Type: PersistentObject or undefined (getter)
  • Automatic
  • ReadOnly

.name

The display name of the object, without any article. For example: “Joe”, “ancient relic”, “pair of gloves”. Will be undefined if the object has no name.

  • Type: String
  • Required

.nameIsProper

Set this field to True to suppress the printing of an article in the .aName, .theName, and generic versions of these fields. Useful for proper names, book titles, etc. See the Tutorial for more information.

  • Type: Boolean
  • Optional

.nameIrregularArticle

Set this field to override the default article choice in .aNameand generic versions of that field. Setting it back to undefined or null will return things to normal.

  • Type: String
  • Optional

.genericName

This is the generic non-detailed name of the object, without any article. This name is used when the item can be detected, but its exact details are unavailable. For example, the genericName is used when an article of apparel is “showing an outline through” an article of thin but non-transparent apparel on a more outer layer. It might also be used when interacting with objects in the dark, where you can feel what they are but cannot see specific details.

  • Type: String
  • Required

.genericNameIsProper

Same as .nameIsProper, but operates on the genericName instead.

  • Type: Boolean
  • Optional

.genericNameIrregularArticle

Same as .nameIrregularArticle, but operates on the genericName instead.

  • Type: String
  • Optional

.aName

The display name of the object, with the correct article prepended. For example: “Joe”, “an ancient relic”, “a pair of gloves”. Will be undefined if the object has no name.

If the first character of the .name field is capitalized, the name will be treated as proper, and no article will be prepended. Ie. “Joe”. Otherwise, if the first character of the .name field is a vowel, the “an” article will be prepended. Ie. “an ancient relic”. Otherwise, the “a” article will be prepended. Ie. “a pair of gloves”.

These rules do not always work. For example, “an honorable man” or “a European swallow”. In these cases, the .nameIsProper and .nameIrregularArticle fields may be used to manually specify the behavior of this field.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.AName

Same as .aName, but with the first character capitalized.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.aGenericName

Same as .aName, but operates on the genericName instead.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.AGenericName

Same as .AName, but operates on the genericName instead.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.theName

The display name of the object, with the “the” prepended unless the object is proper. For example: “Joe”, “the ancient relic”, “the pair of gloves”.

These rules do not always work. For example, “the European swallow”. In this case, the .nameIsProper field may be used to manually specify the behavior of this field.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.TheName

Identical to .theName, but with the first character capitalized.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.theGenericName

Same as .theName, but operates on the genericName instead.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.TheGenericName

Same as .TheName, but operates on the genericName instead.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Automatic

.image

A detail image name for the object (used in object details displays, and in inventory thumbnails if no thumbnail is defined).

FIXME Make this work with external images instead of just image passages.

  • Type: String
  • Required (if you use the Inventory and Apparel UIs)

.thumbnail

A thumbnail image name for the object (used in inventory displays). If a thumbnail image is not defined, code should use the value of .image instead.

FIXME Make this work with external images instead of just image passages.

  • Type: String (getter) FIXME change to function so it can be overridden in story code more easily.
  • Optional

.description

FIXME unimplemented

The name of a passage to display with the detail description of the object. Links within can be used to manipulate the object.

  • Type: String
  • Optional

.inventoryCategory

The inventory category of this object. Used for inventory sorting. Case-sensitive. If falsy, then category is assumed to be “Other”.

  • Type: String
  • Required (if the item can be picked up or worn by the player)

.equals( o )

Overridden to compare object id fields rather than references.

.toString()

Overridden to return this.aName.

.clone()

Overridden to grab an instantiated copy of the object via PersistentObject.fetch().

.toJSON()

Overridden to provide a reviveWrapper that grabs an instantiated copy of the object via PersistentObject.fetch().

.someProperty

FIXME

  • Type: type
  • Optional Required

.someProperty

FIXME

  • Type: type
  • Optional Required

.someProperty

FIXME

  • Type: type
  • Optional Required

.someProperty

FIXME

  • Type: type
  • Optional Required

.someProperty

FIXME

  • Type: type
  • Optional Required

.someProperty

FIXME

  • Type: type
  • Optional Required

.someProperty

FIXME

  • Type: type
  • Optional Required
libecho/classes/persistence.1653096656.txt.gz · Last modified: 2022/05/20 18:30 by lee