Hello, my fellows! Today I come with a tutorial on how to write your own useful callbacks for TFS versions 1.0 and up. It is easier than you might think and you can have much more customized behavior of your very own server scripts
This is my first tutorial in English, so sorry if I extend too much or use the wrong words (and correct me!).
First of all, spot which type of script you want to have. You will need some knowledge of C++ to work here, but don't be afraid. An IDE with a good set of shortcuts might also help. A friend of mine has asked me to create an "onSay" callback to check for special keywords on player speech, so I'll use it as an example. We have some types of scripts to choose from:
I could put my script both under creature scripts or events. I prefer the creature scripts as events for a metatable must all be on a single file and can get a tad messy easily. You may read a discussion on when to use creature scripts or events here.
Open the file that relates to the type of callback you want. For creature scripts, look at creatureevent.cpp, else try events.cpp or globalevent.cpp. You're going to try and find patterns.
On creatureevent.cpp and its header, there is a C++ function for each callback on the CreatureEvents class. For the headers:
And there's also the implementation:
Can you spot the pattern right there? All functions are almost exactly equal, except for the type of the event they expect to call. Let's stick our callback right there, and follow the pattern:
The onSay callback function takes two parameters, by the way: the player that is saying and the words he/she said. Note this is different from the talkactions' onSay, that one splits the talkaction words and the params sent, thus taking three parameters.
Fine, but where does that CREATURE_EVENT_SAY should come from? If you have read the header, you will see there's an enumeration of creature event types:
And all of the function refer to types on that enumeration. Just put CREATURE_EVENT_SAY anywhere in that enumeration. You may also notice that, in our new function, following the pattern, we call a new executeOnSay function. This function lies inside another class called CreatureEvent (also the type of it->second).
Maybe you'll get confused about those 2 classes with almost the same name, but I'll clarify: CreatureEvent is a class the represents a single creature script (i.e. one instance of onLogin callback), while the CreatureEvents is a manager class that contains all CreatureEvent instances, and it's this class you call when you want to do like that: "player X has logged in, please someone execute ALL callbacks for onLogin for X!". CreatureEvents is the one.
So up with the tales, let's figure out how to create out event inside the CreatureEvent now. You'll note respective functions for each callback we saw on CreatureEvents too:
And the respective implementations:
You'll also see there's some callbacks not related in CreatureEvents, for those callbacks are called in different ways. You can track their usage to see how broad callbacks can be!
We need to first understand what those functions actually do. I'll take onThink as an example:
First of all, it tries to reserve an script interface to execute the Lua code. If it fails, probably you have run out of memory, that's why you return false (and also print a message at the console).
Then, it sets some variables, such as the script environment (the one that was reserved just before), and sets the current script id - in this case, the onThink function that we want to call, and after all, get the Lua state. I won't explore this subject as this is somewhat advanced and you have to understand the inner workings of Lua scripting.
(continues on the next post)
This is my first tutorial in English, so sorry if I extend too much or use the wrong words (and correct me!).
First of all, spot which type of script you want to have. You will need some knowledge of C++ to work here, but don't be afraid. An IDE with a good set of shortcuts might also help. A friend of mine has asked me to create an "onSay" callback to check for special keywords on player speech, so I'll use it as an example. We have some types of scripts to choose from:
- Creature scripts, lying under data/creaturescripts, for callbacks related to interaction with and between monsters, such as onKill, onDeath and onLogin, but also onModalWindow (for when you click on a modal) and the OTClient related onExtendedOpcode.
- Events, under data/events, generally related to generic callbacks for some metatables, very similar to creature scripts, such as PlayernLook (when player looks at an item), PartynJoin and CreaturenChangeOutfit. Different from the others, events have a flag to enable or disable the callback on the fly.
- Global events, under data/globalevents, is where the scripts that affect the server and world lie. Examples are the onStartup, executed when the server... starts up, the onRecord that executes when there's a new player record, and also scripts to be executed at a certain time or interval, such as server save and timed events.
I could put my script both under creature scripts or events. I prefer the creature scripts as events for a metatable must all be on a single file and can get a tad messy easily. You may read a discussion on when to use creature scripts or events here.
Open the file that relates to the type of callback you want. For creature scripts, look at creatureevent.cpp, else try events.cpp or globalevent.cpp. You're going to try and find patterns.
On creatureevent.cpp and its header, there is a C++ function for each callback on the CreatureEvents class. For the headers:
Code:
bool playerLogin(Player* player) const;
bool playerLogout(Player* player) const;
bool playerAdvance(Player* player, skills_t, uint32_t, uint32_t);
And there's also the implementation:
Code:
bool CreatureEvents::playerLogin(Player* player) const
{
//fire global event if is registered
for (const auto& it : m_creatureEvents) {
if (it.second->getEventType() == CREATURE_EVENT_LOGIN) {
if (!it.second->executeOnLogin(player)) {
return false;
}
}
}
return true;
}
bool CreatureEvents::playerLogout(Player* player) const
{
//fire global event if is registered
for (const auto& it : m_creatureEvents) {
if (it.second->getEventType() == CREATURE_EVENT_LOGOUT) {
if (!it.second->executeOnLogout(player)) {
return false;
}
}
}
return true;
}
bool CreatureEvents::playerAdvance(Player* player, skills_t skill, uint32_t oldLevel, uint32_t newLevel)
{
for (const auto& it : m_creatureEvents) {
if (it.second->getEventType() == CREATURE_EVENT_ADVANCE) {
if (!it.second->executeAdvance(player, skill, oldLevel, newLevel)) {
return false;
}
}
}
return true;
}
Can you spot the pattern right there? All functions are almost exactly equal, except for the type of the event they expect to call. Let's stick our callback right there, and follow the pattern:
Code:
bool playerSay(Player* player, const std::string&) const;
Code:
bool CreatureEvents::playerSay(Player* player, const std::string& text) const
{
//fire global event if is registered
for (const auto& it : m_creatureEvents) {
if (it.second->getEventType() == CREATURE_EVENT_SAY) {
if (!it.second->executeOnSay(player, text)) {
return false;
}
}
}
return true;
}
The onSay callback function takes two parameters, by the way: the player that is saying and the words he/she said. Note this is different from the talkactions' onSay, that one splits the talkaction words and the params sent, thus taking three parameters.
Fine, but where does that CREATURE_EVENT_SAY should come from? If you have read the header, you will see there's an enumeration of creature event types:
Code:
enum CreatureEventType_t {
CREATURE_EVENT_NONE,
CREATURE_EVENT_LOGIN,
CREATURE_EVENT_LOGOUT,
CREATURE_EVENT_THINK,
CREATURE_EVENT_PREPAREDEATH,
CREATURE_EVENT_DEATH,
CREATURE_EVENT_KILL,
CREATURE_EVENT_ADVANCE,
CREATURE_EVENT_MODALWINDOW,
CREATURE_EVENT_TEXTEDIT,
CREATURE_EVENT_HEALTHCHANGE,
CREATURE_EVENT_MANACHANGE,
CREATURE_EVENT_EXTENDED_OPCODE, // otclient additional network opcodes
};
And all of the function refer to types on that enumeration. Just put CREATURE_EVENT_SAY anywhere in that enumeration. You may also notice that, in our new function, following the pattern, we call a new executeOnSay function. This function lies inside another class called CreatureEvent (also the type of it->second).
Maybe you'll get confused about those 2 classes with almost the same name, but I'll clarify: CreatureEvent is a class the represents a single creature script (i.e. one instance of onLogin callback), while the CreatureEvents is a manager class that contains all CreatureEvent instances, and it's this class you call when you want to do like that: "player X has logged in, please someone execute ALL callbacks for onLogin for X!". CreatureEvents is the one.
So up with the tales, let's figure out how to create out event inside the CreatureEvent now. You'll note respective functions for each callback we saw on CreatureEvents too:
Code:
bool executeOnLogin(Player* player);
bool executeOnLogout(Player* player);
bool executeOnThink(Creature* creature, uint32_t interval);
bool executeOnPrepareDeath(Creature* creature, Creature* killer);
bool executeOnDeath(Creature* creature, Item* corpse, Creature* killer, Creature* mostDamageKiller, bool lastHitUnjustified, bool mostDamageUnjustified);
void executeOnKill(Creature* creature, Creature* target);
bool executeAdvance(Player* player, skills_t, uint32_t, uint32_t);
And the respective implementations:
Code:
bool CreatureEvent::executeOnLogin(Player* player)
{
//onLogin(player)
if (!m_scriptInterface->reserveScriptEnv()) {
std::cout << "[Error - CreatureEvent::executeOnLogin] Call stack overflow" << std::endl;
return false;
}
ScriptEnvironment* env = m_scriptInterface->getScriptEnv();
env->setScriptId(m_scriptId, m_scriptInterface);
lua_State* L = m_scriptInterface->getLuaState();
m_scriptInterface->pushFunction(m_scriptId);
LuaScriptInterface::pushUserdata(L, player);
LuaScriptInterface::setMetatable(L, -1, "Player");
return m_scriptInterface->callFunction(1);
}
bool CreatureEvent::executeOnLogout(Player* player)
{
//onLogout(player)
if (!m_scriptInterface->reserveScriptEnv()) {
std::cout << "[Error - CreatureEvent::executeOnLogout] Call stack overflow" << std::endl;
return false;
}
ScriptEnvironment* env = m_scriptInterface->getScriptEnv();
env->setScriptId(m_scriptId, m_scriptInterface);
lua_State* L = m_scriptInterface->getLuaState();
m_scriptInterface->pushFunction(m_scriptId);
LuaScriptInterface::pushUserdata(L, player);
LuaScriptInterface::setMetatable(L, -1, "Player");
return m_scriptInterface->callFunction(1);
}
bool CreatureEvent::executeOnThink(Creature* creature, uint32_t interval)
{
//onThink(creature, interval)
if (!m_scriptInterface->reserveScriptEnv()) {
std::cout << "[Error - CreatureEvent::executeOnThink] Call stack overflow" << std::endl;
return false;
}
ScriptEnvironment* env = m_scriptInterface->getScriptEnv();
env->setScriptId(m_scriptId, m_scriptInterface);
lua_State* L = m_scriptInterface->getLuaState();
m_scriptInterface->pushFunction(m_scriptId);
LuaScriptInterface::pushUserdata<Creature>(L, creature);
LuaScriptInterface::setCreatureMetatable(L, -1, creature);
lua_pushnumber(L, interval);
return m_scriptInterface->callFunction(2);
}
You'll also see there's some callbacks not related in CreatureEvents, for those callbacks are called in different ways. You can track their usage to see how broad callbacks can be!
We need to first understand what those functions actually do. I'll take onThink as an example:
Code:
bool CreatureEvent::executeOnThink(Creature* creature, uint32_t interval)
{
//onThink(creature, interval)
if (!m_scriptInterface->reserveScriptEnv()) {
std::cout << "[Error - CreatureEvent::executeOnThink] Call stack overflow" << std::endl;
return false;
}
ScriptEnvironment* env = m_scriptInterface->getScriptEnv();
env->setScriptId(m_scriptId, m_scriptInterface);
lua_State* L = m_scriptInterface->getLuaState();
m_scriptInterface->pushFunction(m_scriptId);
LuaScriptInterface::pushUserdata<Creature>(L, creature);
LuaScriptInterface::setCreatureMetatable(L, -1, creature);
lua_pushnumber(L, interval);
return m_scriptInterface->callFunction(2);
}
First of all, it tries to reserve an script interface to execute the Lua code. If it fails, probably you have run out of memory, that's why you return false (and also print a message at the console).
Then, it sets some variables, such as the script environment (the one that was reserved just before), and sets the current script id - in this case, the onThink function that we want to call, and after all, get the Lua state. I won't explore this subject as this is somewhat advanced and you have to understand the inner workings of Lua scripting.
(continues on the next post)