0 votes
by (220 points)

I am trying to implement a container macro similar to button or link, but with additional tag. I had a look at how these macros are implemented in SugarCube but ran into createShadowWrapper function I can't find any information about. What does this function do?

This code fragment is from SugarCube source macrolib.js for button and link macro.

			$link
				.addClass(`macro-${this.name}`)
				.ariaClick({
					namespace : '.macros',
					one       : passage != null // lazy equality for null
				}, this.createShadowWrapper(
					this.payload[0].contents !== ''
						? () => Wikifier.wikifyEval(this.payload[0].contents.trim())
						: null,
					passage != null // lazy equality for null
						? () => Engine.play(passage)
						: null
				))
				.appendTo(this.output);

1 Answer

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

It's a method of the macro execution context API.  It's not documented there, because it's of strictly limited use—and, frankly, I don't trust most developers to use it properly.

It creates a callback wrapper around the given parameters, which are themselves callbacks, that shadows variables, as necessary, for its first callback parameter.  It's what allows the <<widget>> $args array and the variables captured by the <<capture>> macro to work within asynchronous code—e.g. the contents of a <<link>> macro are executed asynchronously, whenever a player clicks on its link.

The method's prototype looks something like the following:

<MacroContext>.createShadowWrapper(
	shadowedCallback,      // Called with variables shadowed, if set up.
	optionalDoneCallback,  // Optional. Called at the end.
	optionalStartCallback  // Optional. Called at the beginning.
)

SEE: <MacroContext>.createShadowWrapper() → src/macros/macrocontext.js (line:165)

by (220 points)

Thank you, it did answer my question and help me get my code working. But I also run into problems with using it together with for loop. For example, the following code works. It very bare-bone and I am setting payload for createShadowWrapper manually by hand.

Macro.add('event', {
	isAsync : true,
	tags: ['condition', 'effect'],
	handler: function () {
		const $link = $(document.createElement('button'));
		let passage;
		for (var i = 0; i < this.payload.length; i++) {
			if (this.payload[i].name === 'event') {
				$link.append(document.createTextNode(this.payload[i].args[0].text));
				passage = this.payload[i].args[0].link;
			}
			if (this.payload[i].name === 'condition') {
				if (!!Scripting.evalJavaScript(this.payload[i].args.full)) {
					
				}
				else {
					$link.addClass('inactive')
				}
			}
			if (this.payload[i].name === 'effect') {
			}
		}
		$link.ariaClick(
			this.createShadowWrapper(
				() => Wikifier.wikifyEval(this.payload[2].contents.trim()),
				() => Engine.play(passage)
		))
		$link
			.addClass('macro-button')
			.appendTo(this.output);
	}
});

But if I move the $link.ariaClick() inside for loop it doesn't work anymore.

Macro.add('event', {
	isAsync : true,
	tags: ['condition', 'effect'],
	handler: function () {
		const $link = $(document.createElement('button'));
		let passage;
		for (var i = 0; i < this.payload.length; i++) {
			if (this.payload[i].name === 'event') {
				$link.append(document.createTextNode(this.payload[i].args[0].text));
				passage = this.payload[i].args[0].link;
			}
			if (this.payload[i].name === 'condition') {
				if (!!Scripting.evalJavaScript(this.payload[i].args.full)) {
					
				}
				else {
					$link.addClass('inactive')
				}
			}
			if (this.payload[i].name === 'effect') {
				$link.ariaClick(
					this.createShadowWrapper(
						() => Wikifier.wikifyEval(this.payload[i].contents.trim()),
						() => Engine.play(passage)
				))
			}
		}
		$link
			.addClass('macro-button')
			.appendTo(this.output);
	}
});

What could be the reason? Is something wrong with my code.

by (220 points)

Ok, nevermind. I couldn't get it to work inside for loop, but I managed to solve differently.

Macro.add('event', {
	isAsync : true,
	tags: ['condition', 'effect'],
	handler: function () {
		const $link = $(document.createElement('button'));
		let passage;
		var tags = {'event': 0};
		for (var i = 1; i < this.payload.length; i++) {
			tags[this.payload[i].name] = i;
		}
		if ('event' in tags) {
			$link.append(document.createTextNode(this.payload[tags.event].args[0].text));
			passage = this.payload[tags.event].args[0].link;
		}
		if ('condition' in tags) {
			if (!!Scripting.evalJavaScript(this.payload[tags.condition].args.full)) {
				
			}
			else {
				$link.addClass('inactive');
			}
		}
		if ('effect' in tags) {
			$link.ariaClick(
				this.createShadowWrapper(
					() => Wikifier.wikifyEval(this.payload[tags.effect].contents.trim()),
					() => Engine.play(passage)
			));
		}
		else {
			$link.ariaClick(
				this.createShadowWrapper(
					null,
					() => Engine.play(passage)
			));
		}
		$link
			.addClass('macro-button')
			.appendTo(this.output);
	}
});

 

by (68.6k points)
edited by

Your chief issue was attempting to use values which had gone out of scope by the time the asynchronous callbacks were invoked.  To resolve that, you needed to wrap the shadow wrapper callback in an IIFE which allows it to capture the values within a localized scope.

You were also doing some other silly things like:

  1. Using a mishmash of ES5 & ES6 features.
  2. Using deprecated features.
  3. Using an empty if statement because you didn't invert the logic for some reason.
  4. Misnaming the base CSS class name.
  5. Not adding CSS classes you probably should be.
  6. Possibly not disabling the button where it seems like you intended to.

Here's a spruced up version:

Macro.add('event', {
	isAsync : true,
	tags    : ['condition', 'effect'],
	handler : function () {
		const $link = $(document.createElement('button'));
		let passage;

		for (var i = 0; i < this.payload.length; ++i) {
			switch (this.payload[i].name) {

			case 'event':
				passage = this.payload[i].args[0].link;
				$link
					.attr('data-passage', passage)
					.addClass(Story.has(passage) ? 'link-internal' : 'link-broken')
					.append(this.payload[i].args[0].text);
				break;

			case 'condition':
				if (!Scripting.evalJavaScript(this.payload[i].args.full)) {
					// I don't know what the class `inactive` does, but based
					// solely on the name it sounds like you should probably
					// be disabling the button here too, so I added that.
					$link
						.prop('disabled', true)
						.addClass('inactive');
				}
				break;

			case 'effect':
				$link.ariaClick(
					(function (passage, content) {
						return this.createShadowWrapper(
							function () { $.wiki(content); },
							function () { Engine.play(passage); }
						);
					}).call(this, passage, this.payload[i].contents.trim())
				);
				break;
			}
		}

		$link
			.addClass('macro-' + this.name)
			.appendTo(this.output);
	}
});

 

...