+1 vote
by (400 points)
edited by

Hey there!

I'm using SugarCube 2.3.5 and Twine 2.

I'd like to know how one implements Objects that have prototype properties in a way that SugarCube's save and load system can handle without a hassle.

As you guys may know when your objects define their member function as a prototype. JSON doesn't know how to serialize the object properly and thus it fails to restore all member functions that were defined in the prototype.This is of course a JavaScript issue and has nothing to do with SugarCube.

An article on JSON serialization of prototype methods in a rogue like game:

http://nullprogram.com/blog/2013/03/11/

I'm not asking for finished code, just a clear explanation with an example.

I'd like to see an example where an object is defined, which also has a prototype member function. I want to know what tasks prototype.clone() and prototype.toJSON() have to carry out in order to serialize and deserialize these objects.

What are the most complicated objects to serialize? How does one approach a problem like that?

The game that I'm working on requires me to delve into JavaScript deeper than I ever expected.

I had the same issue with an item system a couple of months ago:

http://twinery.org/questions/3252/how-to-load-a-saved-game-and-restore-its-state-in-sugarcube2

Which was solved, thanks to Thomas! (Creator of SugarCube)

My next challange will be to port an FSM system namely this one:

https://github.com/jakesgordon/javascript-state-machine

to work with SugarCube and be compatible with its save and load system. I'm planning on using it to implement FSM AI for my NPCs.

Any help is welcome!

 

2 Answers

+1 vote
by (68.6k points)
edited by
 
Best answer

EDIT (2018-12-15): Fixed an issue with the answer.

 

> I'd like to see an example where an object is defined through a prototype member function […]

I'm not entirely sure what you're asking for—probably because I'm lacking the context.

 

> […] what tasks prototype.clone() and prototype.toJSON() have to carry out in order to serialize and deserialize these objects.

A custom object instance's clone() method simply needs to return a clone of the instance, whatever that happens to mean for the custom object in question.

A custom object instance's toJSON() method needs to return code string that when evaluated will return a clone of the instance.

In both cases, since the end goal is roughly the same, this means creating a new instance of the base class/object and populating it with copies/clones of the relevant data.  There is no one size fits all example for either of these methods because its the properties of the instance which determine what you need to do.

That said, you already have one example from the item Q&A you linked.  Here's another simple one whose constructor takes a config/option object:

window.ContactInfo = function (contactInfoObj) {
	// Set up our own data properties with some defaults.
	this.name    = '';
	this.address = '';
	this.phone   = '';
	this.email   = '';

	// Copy details from the given contact info object.
	Object.assign(this, contactInfoObj);
};

ContactInfo.prototype.clone = function () {
	// Return a new instance containing our current data.
	return new ContactInfo(this);
};

ContactInfo.prototype.toJSON = function () {
	// Return a code string that will create a new instance
	// containing our current data.
	//
	// NOTE: Do not use `this` directly or you'll trigger
	// out of control recursion.  Make a copy of the own
	// properties instead.
	var data = Object.assign({}, this);
	return JSON.reviveWrapper('new ContactInfo($ReviveData$)', data);
};

The usage would be something like:

<<set $Joe to new ContactInfo({
	name  : 'Joe Blow',
	phone : '1 555 555 1212',
	email : 'joe@blow.com'
})>>

 

Here's the same basic example but whose constructor takes discrete parameters instead:

window.ContactInfo = function (name, addr, phone, email) {
	// Set up our own data properties with some defaults.
	this.name    = name || '';
	this.address = addr || '';
	this.phone   = phone || '';
	this.email   = email || '';
};

ContactInfo.prototype.clone = function () {
	// Return a new instance containing our current data.
	return new ContactInfo(
		this.name,
		this.address,
		this.phone,
		this.email
	);
};

ContactInfo.prototype.toJSON = function () {
	// Return a code string that will create a new instance
	// containing our current data.
	return JSON.reviveWrapper(String.format(
		'new ContactInfo({0},{1},{2},{3})',
		JSON.stringify(this.name),
		JSON.stringify(this.address),
		JSON.stringify(this.phone),
		JSON.stringify(this.email)
	));
};

The usage would be something like:

<<set $Joe to new ContactInfo(
	'Joe Blow',
	'',
	'1 555 555 1212',
	'joe@blow.com'
)>>

 

> What are the most complicated objects to serialize? How does one approach a problem like that?

In general, Purple or Green makes no difference.  It's less about difficulty and more about the amount of work you have to do.

That said, I'd say that the most complicated custom classes/objects are:

  • Those whose constructors take many discrete arguments, rather than a config/option object, since you won't be able to use data parameter to JSON.reviveWrapper().  Having to encode them all in the code string can be a pain.
  • Any you didn't write yourself, for two reasons.  The first being that you'll have to learn what data needs to be copied.  The second being writing the two methods and injecting them into its prototype—not that the latter part is hard is most cases.

 

EDIT (2018-12-15): Fixed an issue with the answer.

by (400 points)
edited by

Thank you for giving a clear answer!
Sorry about that confused sentence.
I meant this:

I'd like to see an example where an object is defined, which also has a prototype member function. I want to know what tasks prototype.clone() and prototype.toJSON() have to carry out in order to serialize and deserialize these objects.

So let's say I added:

ContactInfo.prototype.setName = function(name)
{
    this.name = name;
}

This doesn't change the solution that you proposed, since it works as intended. 

Thank you for that!

I'm trying to work out how I could serialize this FSM example from github, but it's a real challenge so far because of the cyclic objects that it uses (there are two properties that point back to the StateMachine object)  - > notice the [Circular] tokens in the stringified object below:

"{"state":"B","_fsm":{"context":"[Circular]","config":{"options":{"init":"A","transitions":[{"name":"step","from":"A","to":"B"}],"methods":{}},"defaults":{"wildcard":"*","init":{"name":"init","from":"none"}},"states":["none","A","B"],"transitions":["init","step"],"map":{"none":{"init":{"name":"init","from":"none","to":"A","active":true}},"A":{"step":{"name":"step","from":"A","to":"B"}},"*":{},"B":{}},"lifecycle":{"onBefore":{"transition":"onBeforeTransition","init":"onBeforeInit","step":"onBeforeStep"},"onAfter":{"transition":"onAfterTransition","init":"onAfterInit","step":"onAfterStep"},"onEnter":{"state":"onEnterState","none":"onEnterNone","A":"onEnterA","B":"onEnterB"},"onLeave":{"state":"onLeaveState","none":"onLeaveNone","A":"onLeaveA","B":"onLeaveB"},"on":{"transition":"onTransition","none":"onNone","A":"onA","init":"onInit","B":"onB","step":"onStep"}},"init":{"name":"init","from":"none","to":"A","active":true},"methods":{},"plugins":[]},"state":"B","observers":["[Circular]"],"pending":false}}"

and there are these events , config, helper objects to build the FSM that I really don't need in my Story and I'd have no user for them anyway. I think I'll have to write my own FSM that has similar features, but also easier to serialize.

+1 vote
by (63.1k points)
edited by

I'm not a great or professional programmer, but here's an inventory system I built that uses custom object prototypes and cooperates with SugarCube's State: 

https://github.com/ChapelR/custom-macros-for-sugarcube-2/blob/master/scripts/simple-inventory.js

The specific code on the prototype looks like this: 

setup.simpleInv.inventory.prototype = {

//...

    toJSON : function () { // the custom revive wrapper for SugarCube's state tracking
        return JSON.reviveWrapper('new setup.simpleInv.inventory(' + JSON.stringify(this.inv) + ')');
    },
    
    clone : function () { return new setup.simpleInv.inventory(this.inv); }
};

You can also use the special $reviveData$ variable instead of stringifying and concatenating all the stuff on the spot if you want to. Relevant docs

Edit: on checking the links you provided, I see you were already aware of this code and it didn't help you then, so my apologies for posting it again. I'll leave it up in case it helps someone else out and try to dig into the other link soon to help out more if no one gets to it first. 

by (400 points)
edited by
It actually does kind of help! Since, now I have more examples to look at.

Yep, ot seems cyclic objects are going to be an issue.

There are thankfully JSON.stringify() functions, which can detect and replace cyclic references. Now, I just need to figure out how, to revive these when the JSON gets deserialized.

I'll post the code that I have once I refactored it.
...