TADS3 - Connection to external (C#/C++)-Libraries?

Hi folks,

I’ve been working for some time on a conversion of a pen’n’paper rpg system in combination with TADS3. So far I handled all the necessary stuff with the possibilities TADS3 provides me.

As things got more complicated I began to think about, that it would be nice if I could write stuff with C# (or C++) in an external library (which I might call from my TADS files) and then return values from the library back to the TADS code.

Is something like this basically possible? I so, has it been done yet?

Jens

1 Like

I wouldn’t know about the TADS side of things, but I think in general it’s easier to use unmanaged libraries from managed code (that is, call a C/C++ DLL from C#) than vice versa.

1 Like

It is possible by adding custom intrinsic functions to the interpreter. This is not a hack or anything; TADS 3 was designed with this kind of extensibility in mind. Some of the functionality in Thaumistry was implemented that way (Steam achievements, the in-game map, some of the savegame handling, etc.)

This requires changing the interpreter source code. Thaumistry uses a modified version of QTads, but you could do the same with any TADS 3 interpreter. On the interpreter side, you need to define your C++ functions as static functions in a class that inherits from CVmBif. This is how it’s done for Thaumistry:

intrinsics.h

#ifndef INTRINSICS_H
#define INTRINSICS_H
#include "vmbif.h"

class QTadsVmBif: public CVmBif
{
public:
    static vm_bif_desc bif_table[];

    static void qtads_update_map(VMG_ uint argc);
    static void qtads_show_map(VMG_ uint argc);
    static void qtads_menu(VMG_ uint argc);
    static void qtads_uses_accessible_mode(VMG_ uint argc);
    static void qtads_scroll_to_bottom(VMG_ uint argc);
    static void qtads_show_credits(VMG_ uint argc);
    static void qtads_unlock_achievement(VMG_ uint argc);
    static void qtads_patches_path(VMG_ uint argc);
    static void qtads_saves_path(VMG_ uint argc);
    static void qtads_platform(VMG_ uint argc);
    static void qtads_platform_str(VMG_ uint argc);
    static void qtads_startup_check(VMG_ uint argc);
    static void qtads_version(VMG_ uint argc);
};

#endif // INTRINSICS_H

/*
 *   Sample function set vector.  Define this only if VMBIF_DEFINE_VECTOR has
 *   been defined.
 *
 *   IMPORTANT - this definition is outside the #ifdef INTRINSICS_H section of
 *   the header file, because we specifically want this part of the file to
 *   be able to be included multiple times.
 *
 *   ALSO IMPORTANT - the ORDER of the definitions here is significant.  You
 *   must use the EXACT SAME ORDER in your "intrinsic" definition in the
 *   header file you create for inclusion in your TADS (.t) source code.
 *
 *   The vector must always be called 'bif_table', and it must be a static
 *   member of the function-set class.  The order of the functions defined
 *   here MUST match the order in the library header file for the function
 *   set, since the compiler generates ordinal references to the functions.
 */
#ifdef VMBIF_DEFINE_VECTOR

vm_bif_desc QTadsVmBif::bif_table[] =
{
    { &QTadsVmBif::qtads_menu, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_uses_accessible_mode, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_scroll_to_bottom, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_show_credits, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_unlock_achievement, 1, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_patches_path, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_saves_path, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_platform, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_platform_str, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_startup_check, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_version, 0, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_update_map, 1, 0, FALSE, { }, { } },
    { &QTadsVmBif::qtads_show_map, 0, 0, FALSE, { }, { } },
};

#endif

The functions need of course to be implemented somewhere. You can use the existing TADS intrinsics as a guide on how to implement your own. You need to use T3VM functions to query and pop the function arguments from the VM stack, and push return values to the VM stack.

You then need to replace tads3/vmbifregx.cpp with your own version, which just adds your own intrinsics at the end. In the case of Thaumistry:

/*
 *   Copyright (c) 1998, 2002 Michael J. Roberts.  All Rights Reserved.
 *
 *   Please see the accompanying license file, LICENSE.TXT, for information
 *   on using and copying this software.
 */
#include "vmbifreg.h"

/* ------------------------------------------------------------------------ */
/*
 *   Include the function set vector definitions.  Define
 *   VMBIF_DEFINE_VECTOR so that the headers all generate vector
 *   definitions.
 */
#define VMBIF_DEFINE_VECTOR

#include "vmbiftad.h"
#include "vmbiftio.h"
#include "vmbift3.h"
#include "vmbiftix.h"
#include "intrinsics.h"

#undef VMBIF_DEFINE_VECTOR

#define MAKE_ENTRY(entry_name, cls) \
    { entry_name, countof(cls::bif_table), cls::bif_table, \
      &cls::attach, &cls::detach }

/* ------------------------------------------------------------------------ */
/*
 *   The function set registration table.  Each entry in the table
 *   provides the definition of one function set, keyed by the function
 *   set's universally unique identifier.
 */
vm_bif_entry_t G_bif_reg_table[] =
{
    /* T3 VM system function set */
    MAKE_ENTRY("t3vm/010006", CVmBifT3),

    /* T3 VM Testing interface */
    MAKE_ENTRY("t3vmTEST/010000", CVmBifT3Test),

    /* TADS generic data manipulation functions */
    MAKE_ENTRY("tads-gen/030008", CVmBifTADS),

    /* TADS input/output functions */
    MAKE_ENTRY("tads-io/030007", CVmBifTIO),

    /* TADS extended input/output functions (if the platform supports them) */
    MAKE_ENTRY("tads-io-ext/030000", CVmBifTIOExt),

    // !!! ADD ANY HOST-SPECIFIC FUNCTION SETS HERE
    MAKE_ENTRY("bodgers/002", QTadsVmBif),

    /* end of table marker */
    { 0, 0, 0, 0, 0 }
};

This is on the interpreter side. On the game’s side, you need to declare the intrinsics in a TADS 3 header:

#charset "CP1252"
#pragma once

/*
 * These are the intrinsic functions of our custom interpreter.
 *
 * The order of declaration is important; do not shift them around.
 */

intrinsic 'bodgers/002'
{
    // Shows the main menu. This function does not block.
    qtads_menu();

    // Returns true if the interpreter is currently in screen reader friendly
    // mode. Otherwise, returns nil.
    qtads_uses_accessible_mode();

    // Scrolls the main window to the bottom.
    qtads_scroll_to_bottom();

    // Shows the credits screen.
    qtads_show_credits();

    // Unlock a Steam achievement.
    qtads_unlock_achievement(id);

    // Returns the path where the patch files can be found.
    // Always ends with a '/'.
    qtads_patches_path();

    // Returns the path to the savegame files.
    qtads_saves_path();

    // Returns the platform of the interpreter.
    qtads_platform();

    // Returns the platform of the interpreter as a string.
    qtads_platform_str();

    // Returns true on first call, nil on every subsequent call unless the
    // interpreter is restarted. Is not affected by restoring a saved game or
    // by UNDO.
    //
    // This is kind of a kludge. It's used to display help messages to the
    // player only once after starting the interpreter, but not after restoring
    // a saved game or UNDO-ing turns.
    qtads_startup_check();

    // Returns the interpreter version.
    qtads_version();

    // Updates the game map. The argument is a string containing a JSON object
    // describing the current state of the rooms. See map.t.
    qtads_update_map(jsonStr);

    // Shows the game map.
    qtads_show_map();
}

You can now call these function from TADS game code just like normal TADS functions.

5 Likes

Ah, very interesting. Didn’t know that Thaumistry is based on the QTads Interpreter …

One more question. Not that I have any plans to release anything, but if I might “modify” QTads for my needs, would I be allowed to release it non-commercially?
Jens

1 Like

You can release it commercially too. You can choose from two licenses:

https://realnc.github.io/qtads/#license

Both allow commercial use.

If you choose the TADS license, you don’t need to publish the modified interpreter sources, but you still need to adhere to the license of the middleware used by the interrpeter (the Qt library), which is licensed under the LGPL. The LGPL allows commercial (and even proprietary closed source) distribution as long as you link against the Qt libraries dynamically instead of statically.

If you choose the GPL, then all you need to do is offer the modified source code of the interpreter somewhere.

Note that none of this affects your game. Only the interpreter.

Edit:
Oops, the “HTML TADS Freeware Source Code License” does not allow commercial distribution and in fact only applies to the source code, not to compiled executables. So right now, you can modify and distribute QTads under the GPL. Which, again, does allow you to do so commercially.

1 Like

Are the functions such as qtads_show_map implemented in C++ in a separate file that needs to be included with a custom build of the QTads source? Does anything graphical need to go through the Qt library, since QTads is drawn by it?

1 Like

In TADS, intrinsic functions are implemented in the interpreter. So in this case, it’s just regular C++ code using the Qt API that creates a QWidget-based map window. The call to that function from the TADS side just tells it which rooms have been visited so it can put clouds in front of location that have not yet been visited by the player.

Of course this means you need to modify QTads to suit your own needs.

It’s completely up to you what those C++ functions do. You can do whatever you want. You could even implement video playback or 3D graphics with intrinsics. Or write an intrinsic API that allows you to control a 3rd party engine (Unreal Engine or whatever) from TADS code.

3 Likes

Okay, wow… that’s a lot of potential! Outside of TADS, I’m pretty wet behind the ears at programming, so I’ll probably have to do a lot of reading and asking to get anywhere, but it’s exciting to start exploring the options!

1 Like

Is the Thaumistry map source code private, or can I see it (or something like it) to get a few bearings on getting started? I’ve never used the Qt library, for one thing, just SFML…

1 Like

It’s private, but mostly because it would require time consuming modifications before publishing it. Also, it’s based on an old version of QTads which I wouldn’t recommend using anymore.

However, I believe you can use SFML to implement your map. There was a tutorial for SFML 1.6 on how to implement a Qt widget that is rendered by SFML:

https://www.sfml-dev.org/tutorials/1.6/graphics-qt.php

I don’t know what changed with SFML 2 though.

2 Likes

Thanks for the info!

1 Like

one must also assess the portability/preservation issue in writing work requiring a customised 'terp…

Best regards from Italy,
dott. Piergiorgio.

3 Likes

This is the only thing keeping me from trying stuff like this. I don’t want to force people to use an interpreter they aren’t comfortable with, nevermind one that was modded too.

I recognize that really powerful things could be leveraged with this, but if someone uses a specific terp for screen readers or dyslexia or high contrast colors, it’d suck if I enforced a terp that either didn’t support that, or forced them to redo their settings in my game specifically.

Hmm, interesting considerations… I wonder if I could release a regular .t3 file in addition to a map-version with a custom terp…

2 Likes

I think that’s what I would do too, yeah.

2 Likes