Live Scripts (C++)

Last updated: October 02, 2023

Main LiveScripts Article

This article contains information specific to writing livescripts targeting the C++ backend, but the guidelines laid out in Language Differences are useful even if you are currently targeting Lua, since C++ is the more restrictive backend, and by following them you leave the door open to use C++ in the future in case you would ever need to.

Language Differences

Note: As TSWoW develops, some caveats listed in this section might disappear.

LiveScripts transpiled to C++ have much more restrictions than when using the Lua backend, since C++ is a much more different from JavaScript, and a more restrictive language. Generally, any LiveScript that works with C++ should also work for Lua, but the inverse is not true at all.

Memory Management

LiveScripts do their best to abstract away the manual memory management in C++ by using smart pointers and stack objects where possible. However, there are a few concerns you will have to consider.

TS* callback arguments (TSPlayer, TSUnit, TSGroup etc.)

The types we receive from event callbacks are special pointer types. These should never be stored anywhere in a live script, store their GUIDs instead (using .GetGUID). These types are owned by the server core, and there is no way to know if the memory they point at remain valid after the event has finished executing.

Circular References

If two objects in TSWoW refer to each others and form a cycle, they will not be automatically cleaned up if all other references to them are lost, since C++ does not have a (built-in) garbage collector. If we use circular data types, we need to make sure that those cycles are broken when the data should be removed.

Syntax differences

Since livescripts written in TypeScript can be transpiled to C++, they have a few caveats, but also a few improvements over normal TypeScript and JavaScript.

Keywords

These are special variable names that you cannot use.

Type Reason
template Refers to templates in C++

Callback Functions

Callback functions are used either to register events to the TSEvents, or as callbacks to map/array higher order functions (filter, reduce, forEach etc.).

Variable Access

Asynchronoius callback functions cannot read or modify scoped/non-global variables, such as event handlers, timers or delayed events. Trying to do this should raise a compiler error.

Synchronous callbacks, such as arrays map/filter/reduce/forEach, can access their enclosing scope, but still not across asynchronous boundaries.

let globalVar: uint32 = 0;

export function Main(events: TSEvents) {
    let localVar: uint32 = 0;
    events.Player.OnSay(()=>{ // <-- asynchronous callback
        localVar = 10; // does not work
        if(localVar > 10) {} // does not work

        globalVar = 10; // works
        if(globalVar > 10) {} // works

        let cbVar: uint32 = 0;
        [].forEach(()=>{ // <-- synchronous callback
            cbVar = 10; // works
            if(cbVar > 10) {} // works

            globalVar = 10; // works
            if(globalVar > 10) {} // works
        })
    }
}

Arrays

Arrays are created by specifying their type and then assigning normally:

const arr = [1,2,3]; // <-- works, but numbers assumed to be floats
const arr : TSArray<uint32> = []; // <-- works
const arr2 : TSArray<uint32> = [1,2,3]; // <-- works
Iteration

Looping with for…in or for…of is currently disabled, but you can still loop by index:

const arr : TSArray<uint32> = [1,2,3]

for(let value of arr) { } // <-- does not work (for .. of unsupported)
for(let value in arr) { } // <-- does not work (for .. in unsupported)

for(let i=0;i<arr.length;++i) { } // <-- works
arr.forEach((value,index)=>{}) // <-- works
Callback functions

A few callback functions have different arguments compared to normal TypeScript, which might show up incorrectly in the autocompletion examples. This section shows correct parameters of each such function.

const arr: TSArray<uint32> = [1,2,3];

arr.forEach((value,index)=>{});

arr.filter((value,index)=>true);

// it is recommended, but not required, to specify the output type for reduce
arr.reduce<uint32>((prev,value,index)=>prev+value,0);

// map **must** specify the output type explicity (<uint32> in this case)
arr.map<uint32>((x,i,arr)=>x+1)

Dictionaries (maps)

Maps, or dictionaries, are collections that uniquely connects one value type to another. In TSWoW, we have decided to call these dictionaries to avoid confusing them with Maps in World of Warcraft.

Dictionaries are created with the special function CreateDictionary:

const myDictionary = { // <-- does not work (No call to CreateDictionary)
    1: "value"
}

const myDictionary : = CreateDictionary<uint64,string>({ // <-- works!
    1: "value1",
    8: "value8"
});

Iteration

Maps can currently only be iterated using callback functions:

const myDict : TSDictionary<uint64,string> = CreateDictionary<uint64,string>({
    1: "value1",
    2: "value2"
});

for(const key in myDict){} // <-- does not work

myDict.reduce((prev,key,value)=>p+key,0); // <-- works
myDict.filter((key,value)=>true); // <-- works
myDict.forEach((key,value)=>{}); // <-- works

Classes

Classes work mostly as usual. If a class does not extend anything else, they should extend TSClass:

class InnerClass extends TSClass {
    innerValue: int;

    constructor(innerValue: int) {
        super();
        this.innerValue = innerValue;
    }

}

class TestClass extends TSClass {
    a: int = 25;

    inner: InnerClass;

    constructor(innerValue: int) {
        super();
        this.inner = new InnerClass(innerValue);
    }
}

function Main(events: TSEventHandlers) {
    const cls = new TestClass(100);
    // We can print a class using its stringify method
    console.log(cls.stringify());
}

Raw C++

LiveScripts can also be written in C++ directly, it is however considered a more advanced feature of TSWoW, and a few special functions (GetObject, AddTimer) have slightly different syntax when written in C++. ORM classes are not supported in C++ at all.

If building scripts for the Lua backend, raw C++ script files are ignored completely and will not be loaded into the server. If you want to use both raw C++ and transpile some code to Lua, you have to use separate modules.

Calling C++ from TypeScript

First, we create a header raw-file.h

void cpp_function();

Then, a corresponding cpp raw-file.cpp

#include <iostream>

void cpp_function()
{
    std::cout << "Hello world from C++!" << "\n";
}

Then, we’ll need a type declaration file raw-file.d.ts

export declare function cpp_function(): void;

Finally, we can call this function from our main function (or any other TypeScript module).

import { cpp_function } from "./raw-file"

export function Main(events: TSEventHandlers) {
    // will print the hello world message when script is reloaded
    cpp_function();
}

Note: The main file of a project must always be a TypeScript file

Calling TypeScript from C++

Create a typescript file ts-file.ts

export function ts_function() {
    console.log("Hello world from TypeScript!");
}

Then, from any c++ file as created above, simply do the following:

#include "ts-file.h"
// (if the file was in a subdirectory, the include should instead be #include "subdir/ts-file.h")

void some_function()
{
    ts_function();
}

Custom CMakeLists.txt

You can create a special CMakeLists.txt placed in your root livescripts directory to link external libraries. You do not need to set up the entire project in this file, it will be included after TSWoW has created a target named after your module (module named module-a has a target module-a ready when the file is loaded). Simply add any libraries or external headers to this file.

Note: Do not place external library files into the livescripts directory, since TSWoW will think it should compile them as part of the script itself.

Note: Remember that livescripts are completely unloaded from memory any time you rebuild them, but it’s your responsibility to handle heap allocations

Passing arguments

Livescript TypeScript uses different conventions depending on what type it is, and may require including special headers. For example, user types are (currently) always wrapped in std::shared_ptr while TS* types are always sent as is. The string type is called TSString in C++.

To figure out how to accept an argument of a specific type, try writing a TypeScript function and look at the C++ the transpiler produces at livescripts/build/cpp/livescripts.