+2 votes
by (140 points)
edited by
Hi All,

I'm a novice twine user and attempting a somewhat daunting task, help would be very appreciated.

I recently found this repository for a multi-user twine story hosted on Heroku: https://bitbucket.org/runhello/spool-heroku/addon/pipelines/home

I've been playing around with it, but I can't figure out what editor the original poster used. I tried downloading Twine 1, loading the Jonah story format, and opening it, but it didn't work. So that's my first question - how can I open either the test.tws file or the test.html file.

Second - the server works with sugacube, but the macros that interact with the server don't. I recently copied the custom macros into my story's javascript area & tried using them in a simple prototype, but get the error "Variable is undefined"

I've included the java below - I'd love some input on what what might be out of date / incorrect for the sugarcube format?


Again - all credit for the script below goes to the phenomenal user Runhello!


After lots of poking around, I think the problem may lie in the use if "wikifier.parse" and "parse.fullargs" these are functions/methods/properties that I can't find anywhere in the sugercube api and I worry they may have been specific to the OP's Jonah story format. Looks like she was using "Jonah 2.1, based on: TiddlyWiki 1.2.39"

(function () {
    // First load jquery
    var s = document.createElement('script');
    s.src = '/support/jquery-1.9.1.min.js';
    s.onload = function f(){

    version.extensions['spool'] = {
        major: 0,
        minor: 1,
        revision: 1
    // Utility
    function onesplit(str, sep) {
        var at = str.indexOf(sep);
        if (at < -1) return null
        return [str.slice(0,at), str.slice(at+1)]
    function varname(str) {
        var splut = str.split(".")
        str = splut[splut.length-1]        
        return str
    // Handler objects. Start with "superclass"
    function Handler() {
    Handler.prototype.macroToSend = function(contents, session) {
        return null
    Handler.prototype.resultsToOutput = function(data, session) {
        return null
    Handler.prototype.handler = function (place, macroName, params, parser) {
        if (jQuery) {
            var tagEnd = parser.source.indexOf('>>', parser.matchStart) + 2;
            var rest = parser.source.slice(tagEnd);
            var session = {}

            var send = this.macroToSend( parser.fullArgs(), session )
            if (session.halt) return
            parser.nextMatch = parser.source.length // Skip to end. Continue only if AJAX returns.
            var outerThis = this
            if (send != null) send = JSON.stringify(send)
            jQuery.post("/data", send, function(data) {
                var output = outerThis.resultsToOutput(data, session)
                if (output) { new Wikifier(place, output); }
                new Wikifier(place, rest);
            }, "json");

        } else {
            new Wikifier(place, ":(");
    // -- Examples of handler functions: Send and receive --
    function SendHandler(action) {
        if (action) this.action = action
        else this.action = "send"
    SendHandler.prototype = new Handler()
    SendHandler.prototype.macroToSend = function (contents, session) {
        var splut = onesplit(contents, "=")
        if (splut) {
            var name = splut[0].trim()
            var val = eval(Wikifier.parse(splut[1]))
            return {
                action: this.action,
                target: name,
                value: val
        session.halt = true
        return null
    function ReceiveHandler(action) {
    ReceiveHandler.prototype = new Handler()
    ReceiveHandler.prototype.macroToSend = function (contents, session) {
        var splut = onesplit(contents, "=")
        if (splut) {
            session.local = Wikifier.parse(splut[0])
            var target = splut[1].trim()
            return {
                action: "receive",
                target: target,
        session.halt = true
        return null
    ReceiveHandler.prototype.resultsToOutput = function(data, session) {
        var splut = varname(session.local)
        state.history[0].variables[splut] = data.value
        return null
    // Turn handlers into macros    
    var receiveHandler = new ReceiveHandler()
    macros['receive'] = {
        handler: function (place, macroName, params, parser) { receiveHandler.handler(place,macroName,params,parser) }
    var sendHandler = new SendHandler()
    macros['send'] = {
        handler: function (place, macroName, params, parser) { sendHandler.handler(place,macroName,params,parser) }

    var addHandler = new SendHandler("accumulate")
    macros['add'] = {
        handler: function (place, macroName, params, parser) { addHandler.handler(place,macroName,params,parser) }

1 Answer

0 votes
by (8.5k points)

I wouldn't use this macro set, or anything like this. The problem is two-fold: First, it blocks the rendering of the passage via some parser tricks until the server answers (and what if it doesn't?), and second it has no way for you to deal with errors.

What I'd suggest you do instead is wrap the AJAX calls into a macro which only renders what's inside after the server answered. This means that whatever is inside the macro will not be available to the remainder of the passage at the moment it is executed, but that's the price you have to pay when you're dealing with a client-server architecture. The macro itself doesn't need to be complicated, something like this works already:

Macro.add("request", {
	isAsync : true,
	skipArgs : true,
	tags : ["done", "fail"],
	handler() {
		const statement = this.args.raw.trim();
		const parts = statement.match(/^((GET|POST)\s+|)(\S+?)(\s+(\S.*?)|)$/i);
		const method = parts[2] || "GET";
		const url = parts[3];
		if(!url) {
			return this.error("no service URL in <<request>>");
		const data = parts[5];
		const output = this.output;
		const alwaysPayload = this.payload.find(p => p.name === "request");
		const donePayload = this.payload.find(p => p.name === "done");
		const failPayload = this.payload.find(p => p.name === "fail");
		if(donePayload.arguments && Wikifier.parseStoryVariable(donePayload.arguments) === null) {
			return this.error("'" + donePayload.arguments + "' in <<done>> is not a valid SugarCube identifier");
		if(failPayload.arguments && Wikifier.parseStoryVariable(failPayload.arguments) === null) {
			return this.error("'" + failPayload.arguments + "' in <<fail>> is not a valid SugarCube identifier");
			url: url,
			method: method,
			data: data
		}).always(function() {
			if(alwaysPayload) {
				new Wikifier(output, alwaysPayload.contents);
		}).done(function(data, textStatus, jqXHR) {
			if(donePayload) {
				if(donePayload.arguments) {
					Wikifier.setValue(donePayload.arguments, data);
				new Wikifier(output, donePayload.contents);
		}).fail(function(jqXHR, textStatus, errorThrown) {
			if(failPayload) {
				if(failPayload.arguments) {
					Wikifier.setValue(failPayload.arguments, jqXHR);
				new Wikifier(output, failPayload.contents);

Syntax is: <<request (GET or POST, defaults to GET) url data>>The part which is always ran when the request finishes<<done $variable>>The part which is additionally ran when the request is successful, setting $variable to the response<<fail $variable>>The part which is additionally ran when the request fails, setting $variable to the jqXHR object with details.<</request>>

The "data" is sent as string; if you want it to be something else (like JSON objects), you'll have to modify the "const data = ..." part of the macro - keep in mind it will be undefined when no data was supplied.. Usage example (using the JSONPlaceholder service):

<div><<request http://jsonplaceholder.typicode.com/posts/1>>
Requesting stuff.
<<done $result>>
Stuff done, got $result.title
<<fail $error>>
Stuff failed: $error

<div><<request http://jsonplaceholder.typicode.com/posts/101>>
Requesting stuff.
<<done $result>>
Stuff done, got <<= $result.title>>
<<fail $error>>
Stuff failed: $error.statusText

Output I get:

Requesting stuff. Stuff done, got sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Requesting stuff. Stuff failed: Not Found


by (140 points)
Hi Akjosch!


Thanks for the indepth response! I really appreciate it. I'm a little unclear on how to use the macro you've created to change a variable on a server. I'm able to request data, but I'm unclear what I can do to setup a server to change according to a GET request.

by (8.5k points)
You'll need to be more precise about how the server expects the data to be delivered and modify the macro according to its expectations and capabilities.

Is it literally the one from the repository, one you are designing yourself, or some other third-party one (and which one specifically then)?
Welcome to Twine Q&A, where you can ask questions and receive answers from other members of the community.

You can also find hints and information on Twine on the official wiki and the old forums archive.

See a spam question? Flag it instead of downvoting. A question flagged enough times will automatically be hidden while moderators review it.