Adding new LiveScript events and functions is a fairly straightforward process once you’ve built tswow from source. Note that it is not possible to extend the livescripting API from a repack.
The LiveScript API classes can be found in the tswow-core subproject. The Public
folder contains public header files (such as TSPlayer.h
), while the Private
folder contains implementations (such as `TSPlayer.cpp) that contain actual implementations for livescript functions.
For all livescript extensions, it is important that public header files do not refer to internal trinitycore headers, because those headers are by design not available when building livescripts. If you need to refer to internal trinitycore types, they must only be available as forward declarations in your public header files, and only actually imported in the private implementations.
TS* classes refers to classes such as TSPlayer
, TSUnit
, TSMap that wrap internal trinitycore classes such as
Player,
Unit and
Map`.
For example, let’s pretend that we want to add the method TSPlayer::SetLevel
. This method is already implemented, but is used here as a simple example.
TSPlayer
class in TSPlayer.h
: void SetLevel(uint32 level)
TSPlayer.cpp
class: void TSPlayer::SetLevel(uint32 level)
{
// In TSPlayer.h, we can see that TSPlayer has a reference to the internal trinitycore "Player" called "player".
player->SetLevel(level);
}
tswow-core/Public/global.d.ts
. We can find the “TSPlayer” class by searching for “interface TSPlayer” and simply add a line among the other methods declared for it: SetLevel(level: uint32): void
TSPlayerLua.cpp
similar to other functions declared in that file: LUA_FIELD(ts_player, TSPlayer, SetLevel);
_Note: for some classes, such as TSUnit
, functions are actually added in TSUnitLua.h
instead of TSUnitLua.cpp
, just check where all the other methods are registered for your class if you’re unsure where to place them.
We can now compile the core, and if all went well we should have a new livescript method available to our livescripts.
If you want to expose methods that take strings, arrays, maps to lua you need to specify separate convertor functions, because lua doesn’t understand optional arguments or the string/collection classes tswow uses.
Typically, this is done by adding additional private functions to the TS*
class and adding TSLua
as a friend class so it can access them.
For example, let’s pretend that we want to add the made up method TSPlayer::SetName(TSString)
to Lua.
First, we need to add a private method to the TSPlayer
class in TSPlayer.h
called LSetName(std::string const&)
, and implement it in TSPlayer.cpp
:
void TSPlayer::LSetName(std::string const& name)
{
SetName(TSString(name));
}
To register this method in Lua, we need to use a slightly more manual syntax to register our method in TSPlayerLua.cpp
:
player.set_function("SetName", &TSPlayer::LSetName);
You do not need to change anything else in global.d.ts
or the original method declarations.
Arrays and Maps can be handled similarly, and you can find examples for how to convert them and what types to use in TSGlobal.h
, TSGlobal.cpp
and TSGlobalLua.cpp
.
Lua has no concept of C++ optional arguments, so if we want to expose functions with optional arguments, we must create separate lua functions for each possible number of parameters.
For example, let’s pretend we want to add the made up function TSPlayer::CallOptionalArguments(uint32 a = 0,uint32 b = 0)
to Lua.
First, we must add three declarations to the TSPlayer
class in TSPlayer.h
(and just like with the above section, make sure TSLua
is a friend class):
void LCallOptionalArguments0(uint32 a, uint32 b);
void LCallOptionalArguments1(uint32 a);
void LCallOptionalArguments2();
Then, we need to add implementations to these inside TSPlayer.cpp
:
void TSPlayer::LCallOptionalArguments0(uint32 a, uint32 b)
{
CallOptionalArguments(a,b);
}
void TSPlayer::LCallOptionalArguments1(uint32 a)
{
CallOptionalArguments(a);
}
void TSPlayer::LCallOptionalArguments2()
{
CallOptionalArguments();
}
Finally, to register these to Lua we need to add a slightly more manual registration to TSPlayerLua.cpp
:
ts_player.set_function("CallOptionalArguments", sol::override(
&TSPlayer::LCallOptionalArguments0,
&TSPlayer::LCallOptionalArguments1,
&TSPlayer::LCallOptionalArguments2
));
You do not need to change anything else in global.d.ts
or the original method declarations.
Global functions are registered very similar to TS* class methods, with the only difference that in their public declarations it is important to prefix their public header declaration with the TS_GAME_API
(or aliased as TC_GAME_API
):
TS_GAME_API void MyLivescriptFunction();
You can then register the function to lua in one of the XLua.cpp
files, or TSLuaGlobal.cpp
to the sol::state
using the set_function
metod:
state.set_function("MyLivescriptFunction",MyLivescriptFunction)
Handling strings, arrays, maps or optional arguments in Lua works the same for global functions as with class methods.
Let’s pretend that we want to add a new player event “Player.OnLevelChanged”. This event is already implemented in tswow, but is used here as a simple example.
TSEvents
struct in TSEvents.h
. Searching for “PlayerEvents”, we can find the declaration struct PlayerEvents
. Here, we add a new event using the EVENT
macro, where the first argument is the name of the event and all following arguments are the typenames for the parameters the event should have: EVENT(OnLevelChanged, TSPlayer player, uint8 oldLevel)
Note that we avoid adding a separate argument for the new level, since this can already be accessed through the TSPlayer object itself.
Player::GiveLevel
function inside Player.cpp
. To fire this event, we need to make sure the TSEvents.h
header itself is included, as well as any headers for the TS*
classes that it expects. Then, we can fire the event using the FIRE
macro, and make sure to convert all arguments to the TS*
classes that the event expects: // somewhere inside Player::GiveLevel
// First argument is the event category, second is event name, and the rest are the event arguments.
FIRE(Player,OnLevelChanged,TSPlayer(this),oldLevel)
tswow-core/Public/global.d.ts
. Events are found in class declarations inside the _hidden
namespace, and we can find the Player event class by searching for class Player<T>
: // somewhere in Player<T>
OnLevelChanged(callback: (player: TSPlayer, oldLevel: uint8) => void): void
TSEventsLua.cpp
. If we open it, we can find a section intended for player events using the player_events
variable: LUA_HANDLE(player_events, PlayerEvents, OnLevelChanged);
We can now compile the core, and if all went well we should have a new event available to our livescripts.
Some event categories, such as Creature
, have events that can be called both for all creatures in the server, but also for individual creature templates.
To create an id-bound event in an existing category, you simply replace the EVENT
macro inside TSEvents.h
with ID_EVENT
.
When you want to fire an id-bound event, instead of FIRE
you use the FIRE_ID
macro. The argument order is different from the FIRE
macro, and the first argument this macro expects is a special event tag that the individual entity has access to. Where exactly this event is stored differs between entities, but for a TrinityCore Creature
, it can be accessed through Creature::GetCreatureTemplate()->events.id
. The arguments following are the normal arguments you’d give an equivalent FIRE
macro:
FIRE_ID(GetCreatureTemplate()->events.id,Creature,MyCustomCreatureEvent,TSCreature(this),...)
Note that events fired with the FIRE_ID
macro automatically calls the FIRE
macro for the event it receives so that the event fires both bound and unbound listeners. You should not make a separate call to FIRE
if you already call FIRE_ID
, and doing so will cause the event to fire twice for unbound listeners.
In C++, a common way to allow functions or events to manipulate variables (especially numbers, booleans and strings) is to pass the value by reference, e.g:
void MyFunction(uint32 & valueByRef)
{
valueByRef = 20; // changes the value of "valueByRef" not only in the local function
}
Since TypeScript has no concept of passing variables by value or reference, we use a special wrapper class called TSMutable<T>
to pass primitives we want to allow the script to manipulate, which takes a variables address and allows Livescripts to change it via a set
and get
function.
For example, if you have the event Player.MyNutableEvent(TSPlayer, TSMutable<float>)
you would call this event by wrapping some local float variable in a TSMutable class:
// Somewhere in Player.cpp
float someLocalFloatVariable = ...
FIRE(Player,MyMutableEvent, TSPlayer(this), TSMutable<float>(&someLocalFloatVariable));
// If any events manipulated the TSMutable by calling "set", the changes have applied to "someLocalFloatVariable" here.
For strings specifically, we use another special class called TSMutableString
for technical reasons, but it works just the same as other mutables:
// Somewhere in Player.cpp
std::string someLocalStringVariable = "...";
FIRE(Player,MyMutableStringEvent, TSPlayer(this), TSMutableString(&someLocalStringVariable))
TSMutables are declared in global.d.ts just as they are presented, as a TSMutable<T>
or TSMutableString
class wrapping its underlying type:
MyMutableEvent(callback: (player: TSPlayer, value: TSMutable<float>) => void)
MyMutableStringEvent(callback: (player: TSPlayer, value: TSMutableString) => void)
We are generally very welcoming of proposed additions to our livescripting API, but contributions should follow some guidelines for us to accept them into our official repository:
TSLua
as a friend, have the L
prefix and count from 0 and up for overloaded methods)Unit
methods should be added to TSUnit
, not TSPlayer
)