+2 votes
by (380 points)

Sorry for the confusing title, here's what I mean: In the first passage, I place an object variable into an array, like this:

<<set $item_test to {}>>
<<set $array to []>>
<<set $array.push($item_test)>>

At this point, in the same passage, $array.includes($item_test) returns true. 

Then, I link directly from this passage to a second passage. In the second passage, $array.includes($item_test) returns false. In both cases the array knows that its length is 1 and if I have it print the first item's parameters they match the original object. But if I ask it for .includes() or .count() to find that original item while it's in the second passage, it can't.

If I'm trying to do something that Sugarcube doesn't want to do, then is there another way to tell how many of an object are inside an array?

2 Answers

+2 votes
by (8.6k points)
selected by
Best answer

SugarCube breaks object references. The comparison (and by extension, includes() and count()) can't work because as soon as you change passages, the objects inside the array are clones of the original object. As are all the objects. As is the array itself.

Another consequence of this is that this simple line of code will break your game.

<<set $A = {}, $B = {}, $A.B = B, $B.A = A>>

If you really want to keep proper object references (and a whole lot more things SugarCube breaks, like Symbols or getters/setters), you'll have to stop using $ and _ variables for them and either pack them all in "setup" variables or some other namespace you define. But this way, you will lose the history handling and will have to write load and save handlers.

The other often-used solution is to reference the objects by IDs.

by (68.6k points)

Another consequence of this is that this simple line of code will break your game.

<<set $A = {}, $B = {}, $A.B = B, $B.A = A>>

That has less to do with SugarCube and more to do with you shouldn't be creating cyclical references unless you have end-to-end control over the system—because even native JavaScript library systems, like JSON.stringify(), will throw if you try that.

Beyond that, SugarCube does not guarantee that every possible object will be compatible with its history—no story formats do—so it's less a matter of "SugarCube breaks stuff" and more of that some things are unsupported.  Generic objects used as, data, property bags and a smattering of native object types are supported, beyond that… there be dragons.

by (8.6k points)

@TheMadExile: This has everything to do with how SugarCube implements passage change. The very same function which breaks object references also chokes on any data structure which contains graphs implemented via object references as soon as they contain even one loop.

This even happens when you effectively disable the history.by setting history.maxStates to 1.

by (68.6k points)
I never said cyclical references weren't an issue within SugarCube's code.  I said that even standard library APIs will puke upon encountering cyclical references--specifically, the JSON encoder.  SugarCube uses the JSON encoder to marshal its history graph for storage.  Thus, even if I rewrote SugarCube to be able to handle cyclical references, the system would still choke on them once the graph was passed off to the JSON encoder--even if Config.history.maxStates has been set to 1.

Unless you have end-to-end control over the system, and thus are able to guarantee that no such issues will crop up, creating cyclical references is a non-starter.
by (8.6k points)

@TheMadExile: The less is said about the deficiencies of the JSON format and JavaScript's implementation thereof the better.

That said, SugarCube doesn't, strictly speaking, need JSON.stringify()/JSON.parse(), does it? Looking through the code, all it needs is some method to turn its state (an object) into an arbitrary string (and it doesn't actually care what's inside of it) and some method to do the reverse. These can be JSON.stringify and JSON.parse per default, but could be user-settable, allowing people to easily make SugarCube deal with whatever object types and data structures they require.

+1 vote
by (63.1k points)
by (380 points)

Interesting, thank you all for the help!

What I was trying to do was figure out whether an object with a given "type" key is inside of an array. Here's a widget that I made that seems to get the job done:

<<widget doesThisContainThat>>
<<set _count to 0>>
<<set _contains to false>>
<<if def $args and def $args[1]>>
	<<set _array to $args[0]>>
	<<set _itemtype to $args[1].type>>
	<<for _i to 0; _i < _array.length; _i++>>
		<<set _values to Object.values(_array[_i])>>
		<<if _values.includes(_itemtype)>>
			<<set _contains to true>>
			<<set _count += 1>>

The first widget argument should be an array you want to check, and the second is the object you're looking for. This returns a boolean variable _contains which tells you whether the object is in the array, and an integer variable _count which tells you how many times it occurs. You'd have to either add a "type" key to the object you're searching for, or change "type" in my example to a key that your object has.

Hope that's useful for someone!

by (8.6k points)

Or you just let the browser's JavaScript engine do the work for you.

<<set _contains =
    (_count = ($args[0] || []).filter(item => item && item.type === ($args[1] || {}).type).length) > 0>>

You can leave out the array and object guards if you feel like passing wrong or missing parameters should be an error which needs to be displayed at once too. This makes the statement somewhat shorter.

<<set _contains =
    (_count = $args[0].filter(item => item.type === $args[1].type).length) > 0>>