Source: Persistence/PersistentObject.js

/*****************************************************************************
 * Copyright (c) 2019 Echo Hollow / L. Adamson. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 *  modification, are permitted provided that the following conditions are met:
 * 
 *     1. Redistributions of source code must retain the above copyright 
 *        notice, this list of conditions and the following disclaimer.
 *     2. Redistributions in binary form must reproduce the above copyright 
 *        notice, this list of conditions and the following disclaimer in the 
 *        documentation and/or other materials provided with the distribution.
 *     3. All advertising materials mentioning features or use of this software 
 *        must display the following acknowledgement:
 *          "This product includes software developed by Echo Hollow."
 *     4. Neither the name of the copyright holder nor the names of its 
 *        contributors may be used to endorse or promote products derived from 
 *        this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR 
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 
 * EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

const logName = 'PersistentObject';

const SugarCubeDatastore = 
{
	getTable: function()
	{
		return (variables().LibEcho_Persistence = variables().LibEcho_Persistence || {});
	},
	has: function( id )
	{
		return id in SugarCubeDatastore.getTable();
	},
	get: function( id )
	{
		return SugarCubeDatastore.getTable()[id];
	},
	set: function( id, value )
	{
		SugarCubeDatastore.getTable()[id] = value;
	},
	del: function( id )
	{
		delete SugarCubeDatastore.getTable()[id];
	}
}

const DefaultsStore = 
{
	getDefaultsTable: function()
	{
		window.LibEcho = window.LibEcho || {};
		return (LibEcho.PersistenceDefaults = LibEcho.PersistenceDefaults || {});
	},
	getClassesTable: function()
	{
		window.LibEcho = window.LibEcho || {};
		return (LibEcho.PersistenceClasses = LibEcho.PersistenceClasses || {});
	},
	has: function( id )
	{
		var defaultsTable = DefaultsStore.getDefaultsTable();
		var path = id.split('.');
		var v = defaultsTable[path.shift()];
		if( !v )
			return false;
		for( var e of path )
		{
			if( !(e in v) )
				return false;
			v = v[e];
		}
		return true;
	},
	get: function( id )
	{
		var defaultsTable = DefaultsStore.getDefaultsTable();
		var path = id.split('.');
		var v = defaultsTable[path.shift()];
		if( !v )
			return undefined;
		for( var e of path )
		{
			if( !(e in v) )
				return undefined;
			v = v[e];
		}
		if( typeof v.clone == 'function' )
			return v.clone();
		if( Array.isArray(v) )
			return v.slice(0);
		return v;
	},
	set: function( id, value )
	{
		if( DefaultsStore.has(id) )
			throw new Error( "Attempt to redefine defaults entry for " + id + "!" );
		var defaultsTable = DefaultsStore.getDefaultsTable();
		var path = id.split('.');
		var tail = path.pop();
		if( path.length > 0 )
		{
			var v = defaultsTable[path.shift()];
			for( var e of path )
			{
				if( !v[e] )
					v[e] = {};
				v = v[e];
			}
			v[tail] = value;
		}
		else
		{
			defaultsTable[tail] = value;
		}
	},
}

const idKey = '╰⋃╯'; // Heyyy, you dirty-minded boi!  That's not what you think it is!  :3
// For reals, though.  Because of the way SugarCube's serialization works, we have to store
// object ids in a mutable property.  A hacky solution to this is to store it in a property
// whose name is composed of weird unicode characters that nobody will ever type, but is
// still technically a valid identifier in JS.  Surprisingly, this actually works! ಠ_ಠ

// Unfortunately, a WeakMap won't work here.  The serializer still gets confused.
// We *have* to have a mutable property exposed for objects to get deserialized
// correctly.  :(

const proxyHandler =
{
		get: function( target, prop )
		{
			if( typeof prop != 'string' )
			{
				// This happens sometimes.  Something to do with the internal
				// toString() implementation.  Nothing to worry about.
				return target[prop];
			}
			if( prop == idKey )
				return target[idKey];
			if( prop in target )
				return target[prop];
			if( target.internalData.has(prop) )
				return target.internalData.get( prop );
			return target[prop];
		},
		set: function( target, prop, value )
		{
			if( prop == idKey )
				target[idKey] = value;
			else
				target.internalData.set( prop, value );
			return true;
		},
		has: function( target, prop )
		{
			return target.internalData.has(prop) || prop in target;
		},
		deleteProperty: function( target, prop )
		{
			// This doesn't work exactly according to spec.
			// It deletes the property from the Sugarcube Datastore, but leaves the defaults and target object property alone.
			// This has the effect of "resetting" the property to the default value.
			// If you want to truly delete it, period, just set it to 'undefined' instead.
			// FIXME Document this for the end user.
			if( prop == idKey )
				delete target[idKey];
			else
				target.internalData.reset( prop );
			return true;
		},
}

const aName = function( o, nameProp, nameIsProperProp, nameIrregularArticleProp )
{
	var name = o.internalData.get( nameProp );
	var nameIsProper = o.internalData.get( nameIsProperProp );
	var nameIrregularArticle = o.internalData.get( nameIrregularArticleProp );
	if( !name )
		return undefined;
	var firstLetter = name.charAt(0);
	if( nameIsProper || ( /^[A-Z]$/i.test(firstLetter) && firstLetter == firstLetter.toUpperCase() ) )
		return name;
	if( nameIrregularArticle == "" )
		return name;
	if( nameIrregularArticle )
		return nameIrregularArticle+" "+name;
	switch( name.charAt(0) )
	{
		case 'a':
		case 'e':
		case 'i':
		case 'o':
		case 'u':
			return "an " + name;
		default:
			return "a " + name;
	}
}

const theName = function( o, nameProp, nameIsProperProp )
{
	var name = o.internalData.get( nameProp );
	var nameIsProper = o.internalData.get( nameIsProperProp );
	if( nameIsProper || name.charAt(0) == name.charAt(0).toUpperCase() )
		return name;
	return "the " + name;
}

/**
 * The {@link LibEcho.Persistence.PersistentObject|PersistentObject} is the class from which all of the more complex persistent data types are derived. {@link LibEcho.People|People}, {@link LibEcho.Apparel|Apparel}, {@link LibEcho.Transformable|Transformable} attributes, {@link LibEcho.Inventory|Inventories} of various sorts, and many other classes of objects, are all are ultimately derived from {@link LibEcho.Persistence.PersistentObject|PersistentObject}.
 * 
 * Deriving types and classes in this way allows us to present a more straightforward API to the end-user of the library, saving them the headache of dealing directly with the {@link LibEcho.Persistence|Persistence} API.  The {@link LibEcho.Persistence.PersistentObject|PersistentObject} constructor returns a Proxy Object that traps all <i>external</i> field accesses and redirects them through the {@link LibEcho.Persistence|Persistence} API.  This allows the end-user of the library to (mostly) treat {@link LibEcho.Persistence.PersistentObject|PersistentObjects} the same as plain JavaScript objects, extending them with arbitrary fields, etc.
 * 
 * <i>(Note however:  Due to apparent limitations of Proxy Objects, if you subclass one of the built-in persistent classes, the proxy field accesses don't work from methods defined in the subclass itself.  In this case, you must use the low-level persistence API instead.  But it isn't difficult, and is detailed below.</i>
 * 
 * @class
 * @memberof LibEcho.Persistence
 */
class PersistentObject
{
	/**
	 * Defines the default data for a {@link LibEcho.Persistence.PersistentObject|PersistentObject} (or subclass thereof) within your game. {@link LibEcho.Persistence.define|Persistence.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.
	 * 
	 * All {@link LibEcho.Persistence.PersistentObject|PersistentObjects} (and subclassed objects thereof) must be defined using this method before they can be used within the game. 
	 * @example
	 * LibEcho.Persistence.define( "joe", LibEcho.Person.Man, {
	 * 	"name" : "Joe",
	 * 	"lastName" : "Blow",
	 * 	"inventory" : {
	 * 		"contents" : [
	 * 			"revolver",
	 * 		]
	 * 	},
	 * 	"apparel" : {
	 * 		"contents" : [
	 * 			"boxers", 
	 * 			"jeans", 
	 * 			"tshirt", 
	 * 			"socks",
	 * 			"tennisshoes",
	 * 		]
	 * 	}
	 * } );
	 * 
	 * @param {string} id - a toplevel {@link LibEcho.Persistence|Persistence} id which will be used to fetch the object later. All toplevel ids must be unique.
	 * @param {class} objectClass - an ES6 class, ultimately derived from {@link LibEcho.Persistence.PersistentObject|PersistentObject}, which defines "what" the object is (ie. {@link LibEcho.Person|Person}, {@link LibEcho.Apparel|Apparel}, etc etc).
	 * @param {object} defaults - a JS object that defines the {@link LibEcho.Persistence.PersistentObject|PersistentObject's} default values. Available defaults are documented in the sections pertaining to the built-in persistent object classes.
	 * @returns Nothing.
	 * @throws Error if id contains invalid characters, objectClass is not ultimately derived from {@link LibEcho.Persistence.PersistentObject|PersistentObject}, or defaults isn't a valid Javascript object.
	 */
	static define( id, objectClass=PersistentObject, defaults={} )
	{
		DefaultsStore.set( id, defaults );
		return PersistentObject.fetch( id, objectClass );
	}
	
	/**
	 * Instantiates a working copy of an object (whose class was ultimately derived from {@link LibEcho.Persistence.PersistentObject|PersistentObject}), so that its data may be accessed and manipulated.
	 * 
	 * <i>DO NOT</i> store these objects themselves in the story format's {@link LibEcho.Persistence|Persistence} engine!!! Doing so will needlessly clog up your available savegame space. Simply instantiate an object using this method when you need to use it, and then just let the garbage collector free it when you are done.
	 * 
	 * Since this method is used so much, you may use the window global {@link LibEcho.Persistence.obj|obj()} function as a shorthand for {@link LibEcho.Persistence.obj|LibEcho.Persistence.obj()}. 
	 * @example
	 * var joe = LibEcho.Persistence.instantiate( "joe" );
	 * alert( "Joe's last name is " + joe.lastName() + "!" );
	 * @example
	 * alert( "Joe's last name is " + tf("joe").lastName() + "!" );
	 * 
	 * @param {string} id - a {@link LibEcho.Persistence|Persistence} id denoting an object (whose class is ultimately derived from {@link LibEcho.Persistence.PersistentObject|PersistentObject}) to instantiate.
	 * @param {class} forceClass - an optional ES6 class object, ultimately derived from {@link LibEcho.Persistence.PersistentObject|PersistentObject}, to instantiate the object as. This argument is really only used internally, and (unless you are doing something very weird) you'll probably never need to use it.
	 * @returns {LibEcho.Persistence.PersistentObject} (or subclass) an instantiated object, previously defined with {@link LibEcho.Persistence.define|Persistence.define()}.
	 * @throws Error if id contains invalid characters, forceClass is not ultimately derived from {@link LibEcho.Persistence.PersistentObject|PersistentObject}, or there has been no object defined with {@link LibEcho.Persistence.define|Persistence.define()} that matches the given id.
	 */
	static fetch( id, forceClass=undefined )
	{
		var classesTable = DefaultsStore.getClassesTable();
		if( !forceClass )
			forceClass = classesTable[id];
		if( !forceClass )
			throw new Error( "PersistentObject " + id + " has no Class!" );
		if( !classesTable[id] )
			classesTable[id] = forceClass;
		return new (forceClass)(id);
	}
	
    /**
     * <i>You should never instantiate a {@link LibEcho.Persistence.PersistentObject|PersistentObject} directly!  Instead, you must define it with {@link LibEcho.PersistentObject.define|PersistentObject.define()}, and then instantiate it with the {@link LibEcho.PersistentObject.instantiate|obj()} function when you want to use it.  <i><b>You must also never store {@link LibEcho.Persistence.PersistentObject|PersistentObjects} in Sugarcube variables!</b></i>  Just store their ids, instantiate them with {@link LibEcho.Persistence.obj|obj()} as needed, and then let them be garbage-collected.</i>
     * 
     * @param {string} id - The id of the {@link LibEcho.Persistence|Persistence} defaults entry to be associated with this instance.
     */
	constructor( id )
	{
		if( typeof id != 'string' )
			throw new Error( "Invalid id!" );
		this[idKey] = id;
	    return new Proxy( this, proxyHandler );
	}
	
	/**
	 * The {@link LibEcho.Persistence|Persistence} id of the object.
	 * 
	 * <i>Most of the time you'll only be interacting with the {@link LibEcho.Persistence|Persistence} id when you define your object at initialization time in {@link LibEcho.PersistentObject.define|PersistentObject.define()}, and when you fetch it with {@link LibEcho.PersistentObject.instantiate|obj()}. This field is generally only used internally by derived classes. You probably won't need to use this field unless you are actually extending the library itself.</i>
	 * 
	 * @member {string} id
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get id()
	{
		return this[idKey];
	}
	
	/**
	 * The fully instantiated parent of this object, or <i>undefined</i> if it is a toplevel object (has no parent).
	 * 
	 * <i>This field is generally only used internally by derived classes. You probably won't need to use this field unless you are actually extending the library itself.</i>
	 * 
	 * @member {LibEcho.Persistence.PersistentObject} parent
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get parent()
	{
		var path = this.id.split('.');
		if( path.length <= 1 )
			return undefined;
		path.pop();
		return fetchPersistentObject( path.join(".") );
	}
	
	get internalData()
	{
		const target = this;
		return {
			get: function( prop )
			{
				// We have to rewrite 'toString', because of how DefaultsStore
				// and SugarCubeDatastore work.  Otherwise it returns the default 
				// toString.  So to define a new toString function in a
				// PersistentObject's defaults, use toStringOverride instead.
				if( prop == 'toString' )
					prop = 'toStringOverride';
				const childId = target.id + '.' + prop;
				// First, try Sugarcube's persistence store.
				if( SugarCubeDatastore.has(childId) )
					return SugarCubeDatastore.get( childId );
				// Next, try the defaults.
				if( DefaultsStore.has(childId) )
					return DefaultsStore.get( childId );
				return undefined;
			},
			set: function( prop, value )
			{
				// We have to rewrite 'toString', because of how DefaultsStore
				// and SugarCubeDatastore work.  Otherwise it returns the default 
				// toString.  So to define a new toString function in a
				// PersistentObject's defaults, use toStringOverride instead.
				if( prop == 'toString' )
					prop = 'toStringOverride';
				const childId = target.id + '.' + prop;
				// First we'll set it in the SugarCube datastore.
				SugarCubeDatastore.set( childId, value );
				// Next, we check to see if the value in Sugarcube's datastore is the same as the default value (or if there is no default value, than a value on the object itself).
				// If it is, we clear the value in Sugarcube's datastore.
				// FIXME: This does not work on arrays (or any collection), and will leave data in Sugarcube (which doesn't break anything, just uses some memory unnecessarily).
				if( DefaultsStore.has(childId) )
				{
					if( SugarCubeDatastore.get(childId) == DefaultsStore.get(childId) )
						SugarCubeDatastore.del(childId)
				}
				else if( prop in target )
				{
					if( SugarCubeDatastore.get(childId) == target[prop] )
						SugarCubeDatastore.del(childId)
				}
			},
			has: function( prop )
			{
				// We have to rewrite 'toString', because of how DefaultsStore
				// and SugarCubeDatastore work.  Otherwise it returns the default 
				// toString.  So to define a new toString function in a
				// PersistentObject's defaults, use toStringOverride instead.
				if( prop == 'toString' )
					prop = 'toStringOverride';
				const childId = target.id + '.' + prop;
				return SugarCubeDatastore.has(childId) || DefaultsStore.has(childId);
			},
			reset: function( prop )
			{
				// We have to rewrite 'toString', because of how DefaultsStore
				// and SugarCubeDatastore work.  Otherwise it returns the default 
				// toString.  So to define a new toString function in a
				// PersistentObject's defaults, use toStringOverride instead.
				if( prop == 'toString' )
					prop = 'toStringOverride';
				const childId = target.id + '.' + prop;
				SugarCubeDatastore.del( childId );
			}
		};
	}
	
	/**
	 * 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.
	 * @example
	 * var someDude = obj("joe");
	 * alert( "someDude's name is " + someDude.name + "." );
	 * 
	 * @member {string} name
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	// name
	
	/**
	 * 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 is an error to leave this undefined on concrete apparel classes.  If you define() defaults for an article of clothing of any of the generic types (GenericApparel, GenericMenswear, GenericWomenswear), you MUST include a genericName string in the object's defaults block.
	 */
	// genericName
	
	/**
	 * 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.
	 * 
	 * @example
	 * alert( "You are carrying " + obj("some_item").aName + "." );
	 * 
	 * @member {string} aName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get aName()
	{
		return aName( this, 'name', 'nameIsProper', 'nameIrregularArticle' );
	}

	/**
	 * Identical to <i>.aName</i>, but with the first character capitalized.
	 * 
	 * @member {string} AName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get AName()
	{
		var foo = aName( this, 'name', 'nameIsProper', 'nameIrregularArticle' );
		if( foo )
			return foo.capitalize();
		return foo;
	}
    
	/**
	 * Same as <i>.aName</i>, but operates on the genericName instead.
	 * 
	 * @member {string} aGenericName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get aGenericName()
	{
		return aName( this, 'genericName', 'genericNameIsProper', 'genericNameIrregularArticle' );
	}

	/**
	 * Same as <i>.AName</i>, but operates on the genericName instead.
	 * 
	 * @member {string} AGenericName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get AGenericName()
	{
		var foo = aName( this, 'genericName', 'genericNameIsProper', 'genericNameIrregularArticle' );
		if( foo )
			return foo.capitalize();
		return foo;
	}
    
	/**
	 * 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".  Will be undefined if the object has no name.
	 * 
	 * 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.
	 * 
	 * @example
	 * alert( "You place " + obj("some_item").theName + " in the large box." );
	 * 
	 * @member {string} theName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get theName()
	{
		return theName( this, 'name', 'nameIsProper' );
	}
    
	/**
	 * Identical to <i>.theName</i>, but with the first character capitalized.
	 * 
	 * @member {string} TheName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get TheName()
	{
		var foo = theName( this, 'name', 'nameIsProper' );
		if( foo )
			return foo.capitalize();
		return foo;
	}
    
	/**
	 * Same as <i>.theName</i>, but operates on the genericName instead.
	 * 
	 * @member {string} theGenericName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get theGenericName()
	{
		return theName( this, 'genericName', 'genericNameIsProper' );
	}
    
	/**
	 * Same as <i>.TheName</i>, but operates on the genericName instead.
	 * 
	 * @member {string} TheGenericName
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 * @readonly
	 */
	get TheGenericName()
	{
		var foo = theName( this, 'genericName', 'genericNameIsProper' );
		if( foo )
			return foo.capitalize();
		return foo;
	}
    
	/**
	 * Set this field to True to suppress the printing of an article in the .aName and .theName fields. Useful for proper names, book titles, etc.
	 * 
	 * <i>You'll generally want to set this field with {@link LibEcho.Persistence.define|Persistence.define()}, and then never touch it again. The only time you may need to change this field at runtime is if you change something's name to something that doesn't work with the usual rules.</i>
	 * @example
	 * var book = obj("atlas_shrugged");
	 * // book.name is "\"Atlas Shrugged\" (a novel by Ayn Rand)"
	 * // With the " being the first character, it won't be recognized as proper.
	 * // So .aName will return: a "Atlas Shrugged" (a novel by Ayn Rand).
	 * book.nameIsProper = true;
	 * // Now .aName will properly return just: "Atlas Shrugged" (a novel by Ayn Rand).
	 * 
	 * @member {boolean} nameIsProper
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	// nameIsProper
	
	/**
	 * Set this field to override the default article choice in .aName. Setting it back to null will return things back to normal.
	 * 
	 * <i>You'll generally want to set this field with {@link LibEcho.Persistence.define|Persistence.define()}, and then never touch it again. The only time you may need to change this field at runtime is if you change something's name to something that doesn't work with the usual rules.</i>
	 * @example
	 * obj("honorable_man").nameIrregularArticle = "an";
	 * obj("european_swallow").nameIrregularArticle = "a";
	 * 
	 * @member {string} nameIrregularArticle
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	// nameIrregularArticle
    
	/**
	 * Same as <i>.nameIsProper</i>, but operates on the genericName instead.
	 * 
	 * @member {boolean} genericNameIsProper
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	// genericNameIsProper
	
	/**
	 * Same as <i>.nameIrregularArticle</i>, but operates on the genericName instead.
	 * 
	 * @member {string} genericNameIrregularArticle
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	// genericNameIrregularArticle
    
	/**
	 * A thumbnail image name for the object (used in inventory displays).  If a thumbnail image is not defined, it returns the value of <i>.image</i> instead.
	 * 
	 * FIXME Make this work with external images instead of just image passages.
	 * 
	 * @example
	 * FIXME Add an example.
	 * 
	 * @member {string} thumbnail
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	get thumbnail()
	{
    	if( this.internalData.has('thumbnail') )
    		return this.internalData.get( 'thumbnail' );
    	return this.internalData.get( 'image' );
	}
    
	/**
	 * A detail image name for the object (used in object details displays).
	 * 
	 * FIXME Make this work with external images instead of just image passages.
	 * 
	 * @example
	 * FIXME Add an example.
	 * 
	 * @member {string} image
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	// image
	
	/**
	 * FIXME
	 */
    // description
	
	/**
	 * The inventory category of this object.  Used for inventory sorting.  Case-sensitive.  If falsy, then category is assumed to be "Other".
	 * 
	 * @example
	 * FIXME Add an example.
	 * 
	 * @member {string} inventoryCategory
	 * @memberof LibEcho.Persistence.PersistentObject
	 * @instance
	 */
	get inventoryCategory()
	{
		var cat = this.internalData.get( 'inventoryCategory' );
		if( !cat )
			return "Other";
		return cat;
	}
	
	equals( o )
	{
		return 'id' in o && this.id == o.id;
	}
    
	/**
	 * Overridden.
	 * 
	 * @returns {string} the value of this.aName.
	 */
	toString()
    {
   		return this.aName;
    }
	
	clone()
	{
		return fetchPersistentObject( this.id );
	}
	
	toJSON()
	{
		const reviveString = "fetchPersistentObject('" + this.id + "')";
		return JSON.reviveWrapper( reviveString );
	}
}

module.exports = PersistentObject;