SugarCube v2.36.0 & v2.36.1 have been published

SugarCube v2.36.1 has been published.

Downloads & documentation for all Twine/Twee compilers: https://www.motoslave.net/sugarcube/2/

v2.36.1 Changelog highlights:

  • Fixed an issue with the build system that was producing subtly broken builds.

v2.36.0 Changelog highlights:

  • Fixed an issue with the selected keyword in the <<cycle>> and <<listbox>> macros’ <<option>> tags.
  • Fixed instances where using the [img[]] markup as an argument to macros would drop the link-image class.
  • Fixed Config.history.maxStates to disallow unlimited states (value: 0).
  • Added the init special tag that, similar to StoryInit, allows pre-story-start initialization tasks. Intended for add-on/library use.
  • Added a data-init-passage content attribute to StoryInterface that allows content to be updated only once at initialization.
  • Added the State.metadata.entries() and State.metadata.keys() static methods.
  • Added a once keyword to the <<cycle>> macro that ends the cycle upon reaching the final option.
  • Added the <Array>.countWith() method.
  • Added the Save Events API.
  • Added support for template literals within TwineScript.
  • Added various accessibility improvements.
  • Updated the <<done>> macro to better serve when used to wait for DOM updates.
  • <<widget>> macro updates:
    • Added a container keyword that allows non-void/container widgets and an associated _contents special variable.
    • Added a new special arguments variable, _args, and deprecated the old variable, $args.
  • Updated the default value of Config.history.maxStates from 100 to 40.
  • Updated passage objects to maintain the order of passage tags as specified in the data chunk.
  • Deprecated the Config.saves.onLoad and Config.saves.onSave settings in favor of the Save Events API.
  • Updated bundled library: jQuery to v3.6.0.
  • Updates to locale files:
    • Updated the localization template. Translators are asked to updated the locale files as necessary.
    • Added nl.js – Dutch.
  • Various documentation updates.
  • Various internal improvements.
  • Build system updates.
14 Likes

Nice! And some welcome changes.

I’m at work right now (in an online meeting, believe it or not), so I can’t easily check this myself, but are there any breaking changes in this update?

Thanks,
Pete

I try to keep breaking changes out of minor/patch releases. That said, there is possibly one in this release related to this entry:

If you’ve manually set Config.history.maxStates to a value of 0, for unbounded states (not actually unlimited), then that’s no longer allowed and will generate a range error now. It was too big a foot gun cannon nuke for me to ignore any longer, which is why I classified it as a bug.

If anyone feels that they actually need unbound states (you really, really don’t), then they can set it thus:

// Old school.
Config.history.maxStates = Math.pow(2, 32);

// New school.
Config.history.maxStates = 2**32;

2³² (i.e., 4294967296) is the nominal maximum anyway, so it’s the same effect but you have to be completely explicit now.

1 Like
  1. Depreciating $args in favor of _args, means we should rewrite all widgets as soon as possible to reflect this change?
  2. Can you show some examples of how _contents is used? Or what does it mean to be [container]?
  3. Is there any change to `` interpreting/evaluating string inside? I had a situation where `false and true` was still interpreted as boolean, but something more complex like `someTrueFalseFunc() and setup.anotherTrueFalseFunc()` was somehow not boolean (not sure), I think I narrowed it down to this part that’s not working the same as in 2.35.0, where this.args[2] can be something like `someTrueFalseFunc() and setup.anotherTrueFalseFunc()`
let isDisabled = this.args.length > 2 && typeof this.args[2] === "boolean" && !this.args[2] ? true : this.args.length > 1 && typeof this.args[1] === "boolean" && !this.args[1] ? true : false;

so I removed the “boolean” check, and seems to process the rest of the code as expected.

let isDisabled = this.args.length > 2 && !this.args[2] ? true : this.args.length > 1 && !this.args[1] ? true : false;

For context, the macro is called like this (it is a modified link macro):

<<choice_enabled "link text" "targetpassage" `someTrueFalseFunc() and setup.anotherTrueFalseFunc()`>>
<</choice_enabled>>
full macro code after removing the boolean check ```

Macro.add([‘choice_shown’, ‘choice_enabled’], {

isAsync : true,
tags    : null,
handler() {
	if (this.args.length === 0) {
		return this.error(`no ${this.name === 'choice_shown' ? 'choice_shown' : 'choice_enabled'} text specified`);
	}

	const $link = jQuery(document.createElement("a"));
	let passage;

	if (typeof this.args[0] === 'object') {
		if (this.args[0].isImage) {
			/* Argument was in wiki image syntax. */
			const $image = jQuery(document.createElement('img'))
				.attr('src', this.args[0].source)
				.appendTo($link);

			if (this.args[0].hasOwnProperty('passage')) {
				$image.attr('data-passage', this.args[0].passage);
			}

			if (this.args[0].hasOwnProperty('title')) {
				$image.attr('title', this.args[0].title);
			}

			if (this.args[0].hasOwnProperty('align')) {
				$image.attr('align', this.args[0].align);
			}

			passage = this.args[0].link;
		}
		else {
			/* Argument was in wiki link syntax. */
			$link.append(document.createTextNode(this.args[0].text));
				passage = this.args[0].link;
		}
	}
	else {
		/* Argument was simply the link text. */
		$link.wikiWithOptions({ profile : 'core' }, this.args[0]);
		passage = this.args.length > 2 && typeof this.args[2] !== "boolean" ? this.args[2] :  this.args.length > 1 && typeof this.args[1] !== "boolean" ? this.args[1] : undefined;
	}

    let isDisabled = this.args.length > 2 && !this.args[2] ? true : this.args.length > 1 && !this.args[1] ? true : false;

	if (passage != null) {
		$link.attr('data-passage', passage);

		if (Story.has(passage)) {
			$link.addClass('link-internal');

			if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
				$link.addClass('link-visited');
			}
		}
		else { $link.addClass('link-broken'); }
	}
	else { $link.addClass('link-internal'); }

	if (this.name === "choice_enabled") {
		$link
			.addClass(`${this.name}`)
			.ariaClick({
				namespace : '.macros',
				one       : passage != null
			}, this.createShadowWrapper(
					this.payload[0].contents !== ''
					? () => Wikifier.wikifyEval(this.payload[0].contents.trim())
						: null,
					passage != null
						? () => Engine.play(passage)
						: null
			))
			.ariaDisabled(isDisabled)
			.appendTo(this.output);
	}
	else if (!isDisabled) {
		$link
			.addClass(`${this.name}`)
			.ariaClick({
				namespace : '.macros',
				one       : passage != null
			}, this.createShadowWrapper(
				 this.payload[0].contents !== ''
						? () => Wikifier.wikifyEval(this.payload[0].contents.trim())
						: null,
					passage != null
						? () => Engine.play(passage)
						: null
			))
			.appendTo(this.output);
	}
}

});

</details>
1 Like

You can, if you like to be forward looking, but you don’t absolutely need to. Deprecated means that you should no longer use it for new code and eventually replace it. For compatibilities sake, however, it’s not going away anytime soon, so you have plenty of time. Specifically, it should remain available in v2 for its lifetime.

 

A container widget simply has an opening and closing tag—i.e., it can contain things. The _contents special variable is used internally, by container widgets, to store the contents they enclose.

The widget documentation explains it fairly well I think. It also provides a simple example, which I’ll replicate here.

Container Widget Example

Creating a simple dialog box widget:

<<widget "say" container>>
	<div class="say-box">
		<img class="say-image" @src="'images/' + _args[0].toLowerCase() + '.png'">
		<p class="say-text">_contents</p>
	</div>
<</widget>>

Using it:

<<say "Chapel">>Tweego is a pathway to many abilities some consider to be… unnatural.<</say>>

Output:

<div class="say-box">
	<img class="say-image" src="images/chapel.png">
	<p class="say-text">Tweego is a pathway to many abilities some consider to be… unnatural.</p>
</div>

 

There were no changes to how backquote expression arguments work, no. There is an issue in the current release due to the build system turning traitor on me, but you’d probably know if you’d run into it as the likely result is fireworks—it’s been hashed out and I’ll have an updated release out soon.

As to your code. Allowing two optional arguments requires some way to differentiate them. In this case, I’d simply enforce that the expression must yield a boolean value. Your code could also stand a rework.

As a suggestion:

Code Example

Find:

	else {
		/* Argument was simply the link text. */
		$link.wikiWithOptions({ profile : 'core' }, this.args[0]);
		passage = this.args.length > 2 && typeof this.args[2] !== "boolean" ? this.args[2] :  this.args.length > 1 && typeof this.args[1] !== "boolean" ? this.args[1] : undefined;
	}

	let isDisabled = this.args.length > 2 && !this.args[2] ? true : this.args.length > 1 && !this.args[1] ? true : false;

	if (passage != null) {
		$link.attr('data-passage', passage);

		if (Story.has(passage)) {
			$link.addClass('link-internal');

			if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
				$link.addClass('link-visited');
			}
		}
		else { $link.addClass('link-broken'); }
	}
	else { $link.addClass('link-internal'); }

Try replacing it with the following:

	else {
		/* Argument was simply the link text. */
		$link.wikiWithOptions({ profile : 'core' }, this.args[0]);
	}

	let isDisabled = false; // or whatever the default value should be

	if (passage != null) { // lazy equality for null
		if (this.args.length > 1) {
			isDisabled = this.args[1];
		}

		$link.attr('data-passage', passage);

		if (Story.has(passage)) {
			$link.addClass('link-internal');

			if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
				$link.addClass('link-visited');
			}
		}
		else {
			$link.addClass('link-broken');
		}
	}
	else {
		if (this.args.length === 2) {
			if (typeof this.args[1] === 'boolean') {
				isDisabled = this.args[1];
			}
			else {
				passage = this.args[1];
			}
		}
		else if (this.args.length > 2) {
			passage = this.args[1];
			isDisabled = this.args[2];
		}

		$link.addClass('link-internal');
	}
1 Like

SugarCube v2.36.1 has been published.

Changelog highlights:

  • Fixed an issue with the build system that was producing subtly broken builds.
2 Likes