0 votes
by (870 points)

I've defined an "Inventory" object that is complex and thus requires a unique revival method. I defined that as such:

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

Inventory.prototype.toJSON = function () {
	// Return a code string that will create a new instance
	// containing our current data.
	const data = {};
    Object.keys(this).forEach(pn => data[pn] = clone(this[pn]));
    return JSON.reviveWrapper('new Inventory($ReviveData$)', data);
};

The constructor looks like this:

window.Inventory = class Inventory extends Map {
	constructor(ItemArray){
	if (ItemArray instanceof Array){
		var m = [];
		ItemArray.forEach(function(item){
			m.push([item.name,item]);
		});
		super(m);
	}
	else {
		super(ItemArray);
		Object.keys(ItemArray).forEach(prop => this[prop] = clone(ItemArray[prop]));
	}
	}

However, when I reload a save, the inventory variable turns into this:

Array(2) ["(revive:eval)", Array(2) ["new Inventory($ReviveData$)", Object {}]]

I presume this is because toJSON is, itself, passing an array to the constructor, so the "normal" constructor branch is still being executed.

I'm not entirely sure how I should fix this. Passing an array is the most intuitive way to construct my Inventory object. Is there a way to branch the constructor specifically for a revival?

1 Answer

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

I'm unsure if you can make that work as-is or not, and I don't have time to test right now.

If you plan on adding additional data properties to the class, then you'll probably need to keep the Map on a property of the class, rather than attempting to sub-class/extend Map.  If you just plan to add extra methods to Inventory, then you should be able do it.

Regardless.  Try the following for now:

window.Inventory = class Inventory extends Map {
	constructor(entries) {
		super(entries);
	}

	clone() {
		// Return a new instance containing our current data.
		return new Inventory(Array.from(this.entries()));
	}

	toJSON() {
		// Return a code string that will create a new instance
		// containing our current data.
		return JSON.reviveWrapper('new Inventory($ReviveData$)', Array.from(this.entries()));
	}

	/* Other Inventory class methods here. */
};

EDIT: Fixed passing a non-existent parameter to super() in the constructor.

by (870 points)
edited by

.entries() doesn't work, because the constructor only takes what the values would be (the Item objects). Changing it to this.values(), it works.

Edit: Okay, here's what's happening now:

With this code:

clone () {
		// Return a new instance containing our current data.
		return new Inventory(Array.from(this.values()));
	}
	
	toJSON() {
		// Return a code string that will create a new instance
		// containing our current data.
		let data = Array.from(this.values());
		return JSON.reviveWrapper('new Inventory($ReviveData$)', data);
	}

The Inventory object is meant to, itself, contain complex objects, Items, as its values. Revival turns the Inventory into a Map of the Items' first property, rather than a Map of Items as it's supposed to be.

This seems to be happening even when I comment out the custom toJSON method. Without the custom toJSON method, Inventory revives to a generic object, but its values still change into each Item's "action" property.

The Items' first property is also a complex object, if that's important.

Edit 2: The best I can tell is that this has something to do with Twine's save construction. Console logs tell me that the error is only ocurring when I open the save menu or a save is made.

I've added console logs for every call to clone()toJSON(), and to the constructor. I have also made a log echoing the result of Array.from(this.values()) in toJSON(). Here is what I see on startup when no saves exist:

Initial Inventory construction
Constructing Inventory. ItemArray = 
(20) [Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item]
Inventory clone called
Constructing Inventory. ItemArray = 
(20) [Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item]
(x3)
Inventory toJSON called
(20) [Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item, Item]

But when an autosave exists, this is what I see on startup:

Constructing Inventory. ItemArray = 
(20) [ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction]
(x2)

...followed by the previous logs.

When I open the "Saves" menu with an extant save, this appears in the console:

Constructing Inventory. ItemArray = 
(20) [ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction, ItemAction]
(x5)

When I load the save, a ton of construct calls are made, and this time toJSON() creates an array of ItemActions rather than Items.

Since the strange constructor calls aren't linked to my functions, I assume they're part of the save functionality, which I don't know anything about. So I'll need to know how that works before I can puzzle this out.

by (68.6k points)

.entries() doesn't work, because the constructor only takes what the values would be (the Item objects). Changing it to this.values(), it works.

The Map constructor, thus your Inventory constructor since it's a an extended Map, takes an iterator that yields an array of key/value array pairs.  You cannot pass it only values and expect it to work as it should.

It's also unhelpful to talk about other custom objects and not show your work.  I cannot tell you what you're doing wrong if I don't know what you're doing the in first place.

 

by (870 points)

I know, sorry. It's just a ton of code and I didn't want to drown people in it.

Inventory code is currently:

window.Inventory = class Inventory extends Map {
	constructor(ItemArray){
		console.log("Constructing Inventory. ItemArray = ");
		console.log(ItemArray);
		var m = [];
		ItemArray.forEach(function(item){
			m.push([item.name,item]);
		});
		super(m);
	}
	
	clone () {
		// Return a new instance containing our current data.
		console.log("Inventory clone called");
		return new Inventory(Array.from(this.values()));
	}
	
	toJSON() {
		// Return a code string that will create a new instance
		// containing our current data.
		console.log("Inventory toJSON called");
		let data = Array.from(this.values());
		console.log(data);
		return JSON.reviveWrapper('new Inventory($ReviveData$)', data);
	}
[...]
}

So, the constructor takes a single array and turns it into a key/value pair array. That's why I needed to use values() instead.

Item code is:

window.Item = class Item {
	constructor(name,stock){
		this.name = name;
		if (stock === undefined){
			this.stock = 0;
		} else {
			this.stock = stock;
		}
		this.info = "Info pending.";
		this.desc = "Description pending.";
		
		switch (name) {			
			[database for specific items]
		}

		if (this.usable && !this.action){
			/* Default construction for usable items is an ItemAction with the same name as the item. Can also define actions manually in cases. */
			this.action = new ItemAction(name);
		}
	}

	clone () {
		// Return a new instance containing our current data.
		return new Item(this.name,this.stock);
	}
	
	toJSON () {
		// Return a code string that will create a new instance
		// containing our current data.
		return JSON.reviveWrapper(String.format(
			'new ItemAction({0},{1})',
			JSON.stringify(this.name),
			JSON.stringify(this.stock)
		));
	}
}

The "action" attribute is what's replacing the Item objects. The ItemAction serialization functions are:

clone () {
	// Return a new instance containing our current data.
	return new ItemAction(this.name);
}

toJSON () {
	// Return a code string that will create a new instance
	// containing our current data.
	return JSON.reviveWrapper(String.format(
		'new ItemAction({0})',
		JSON.stringify(this.name)
	));
}

ItemAction is a subclass of Action, which has the following serialization functions:

clone () {
	// Return a new instance containing our current data.
	return new Action(this);
}

toJSON () {
	// Return a code string that will create a new instance
	// containing our current data.
	// Return a code string that will create a new instance
	// containing our current data.
	return JSON.reviveWrapper(String.format(
		'new Action({0})',
		JSON.stringify(this.name)
	));
}

The constructor is formatted the same way as with Items (switch statement matches name to a case and populates data based on case).

by (68.6k points)

♪ One of these things is not like the other. ♪

In the Item class, you're attempting to revive an instance of ItemAction rather than Item.

window.Item = class Item {

	[…]

	toJSON () {
		return JSON.reviveWrapper(String.format(
			'new ItemAction({0},{1})',

	[…]

That seems to be the only gaff in the code I can see.

 

by (870 points)
Yep, it works now. Urgh, it was really that simple. I feel so stupid. >_<

Thanks for helping, sorry I led you on such a run-around.
...