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

(Leugengroot) #1

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
#2

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
#3

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.

2 Likes
(Leugengroot) #4

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

#5

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.