• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

[TFS 1.x] How to create new callbacks

Lordfire

Intermediate OT User
TFS Developer
Joined
Jul 16, 2008
Messages
248
Solutions
3
Reaction score
138
GitHub
ranisalt
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:
  • 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 Player:eek:nLook (when player looks at an item), Party:eek:nJoin and Creature:eek:nChangeOutfit. 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)
 
The lines that we are interested are the last ones:
Code:
  m_scriptInterface->pushFunction(m_scriptId);
  LuaScriptInterface::pushUserdata<Creature>(L, creature);
  LuaScriptInterface::setCreatureMetatable(L, -1, creature);
  lua_pushnumber(L, interval);

  return m_scriptInterface->callFunction(2);

The very first one sets pushes the function we want to call to the stack, and this is pretty much the same for all scripts. Then it pushes the parameters to the callback. For onThink, the first one is the creature "thinking", and the second one is the interval of time since the last time it has executed, currently hardcoded to 1000ms.

Code:
  LuaScriptInterface::pushUserdata<Creature>(L, creature);
  LuaScriptInterface::setCreatureMetatable(L, -1, creature);

When you push a more advanced structure, such as a creature or an item, you will also need to push its metadata - if you are a Lua scripter, you know what I mean. That means you also need to tell the script environment what methods that object has.

For that example, when you set the metatable to be of Creature type, you can use creature functions such as getName() and getHealth() on that parameter. You can set several types of metatables. You have some special helper functions for common metatables:

Code:
  static void pushThing(lua_State* L, Thing* thing);
  static void pushVariant(lua_State* L, const LuaVariant& var);
  static void pushString(lua_State* L, const std::string& value);
  static void pushCallback(lua_State* L, int32_t callback);

  static void setItemMetatable(lua_State* L, int32_t index, const Item* item);
  static void setCreatureMetatable(lua_State* L, int32_t index, const Creature* creature);
(extracted from luascript.h)

The four top ones push the object and set it's metatable directly, and you do not have to issue 2 lines. Creature is not one of those and you have to both push the object and set the metatable on separate lines.

The next one is the interval, that is a simple number. For numbers, you can use directly the Lua API

Code:
  lua_pushnumber(L, interval);

Just always remember to use the correct parameters in order. Callbacks can have any arbitrary number of parameters, for example, the onDeath callback has SIX (and is a pretty good one):

Code:
  LuaScriptInterface::pushUserdata<Creature>(L, creature);
  LuaScriptInterface::setCreatureMetatable(L, -1, creature);

  LuaScriptInterface::pushThing(L, corpse);

  if (killer) {
    LuaScriptInterface::pushUserdata<Creature>(L, killer);
    LuaScriptInterface::setCreatureMetatable(L, -1, killer);
  } else {
    lua_pushnil(L);
  }

  if (mostDamageKiller) {
    LuaScriptInterface::pushUserdata<Creature>(L, mostDamageKiller);
    LuaScriptInterface::setCreatureMetatable(L, -1, mostDamageKiller);
  } else {
    lua_pushnil(L);
  }

  LuaScriptInterface::pushBoolean(L, lastHitUnjustified);
  LuaScriptInterface::pushBoolean(L, mostDamageUnjustified);

  return m_scriptInterface->callFunction(6);

And, as you probably have already figured out, the last line calls the function, and has as parameter the number of parameters the Lua function has (pretty confusing, huh?). As onThink pushes player and interval, it has two parameters:

Code:
return m_scriptInterface->callFunction(6);

When you use callFunction, you expect that the Lua function returns true or false. Generally speaking, when a Lua function returns true, it should mean that the function run fine and the action can continue; when it returns false, there was an error and the action should be halted.

You can also call functions where you don't expect any return value (e.g. onModalWindow). For that, you'll use callVoidFunction:

Code:
m_scriptInterface->callVoidFunction(4);

The same rule applies and you pass the number of parameters the Lua function takes.

Let's go back to our onSay callback. We have two parameters, one of them is the current player, and the other is the text spoken. I also want to halt the player speech when something goes wrong. So, wrap it up, should be like this:

Code:
  bool executeOnSay(Player* player, const std::string& text);

And implementation:

Code:
bool CreatureEvent::executeOnSay(Player* player, const std::string& text)
{
  //onSay(player, text)
  if (!m_scriptInterface->reserveScriptEnv()) {
    std::cout << "[Error - CreatureEvent::executeOnSay] 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<Player>(L, player);
  LuaScriptInterface::setCreatureMetatable(L, -1, player);
  LuaScriptInterface::pushString(L, text);

  return m_scriptInterface->callFunction(2);
}

Easy one, this is simple.

Now, there's one thing missing: where does the Lua scripting interface figures out what function name is, in other words, how does it know it should look for the "onSay" function on our future script file?

In two places, actually. First, on the "configureEvent" function, it has a big if-else structure to look for all callback types:

Code:
  if (tmpStr == "login") {
    m_type = CREATURE_EVENT_LOGIN;
  } else if (tmpStr == "logout") {
    m_type = CREATURE_EVENT_LOGOUT;
  } else if (tmpStr == "think") {
    m_type = CREATURE_EVENT_THINK;
  } else if (tmpStr == "preparedeath") {
    m_type = CREATURE_EVENT_PREPAREDEATH;
  } else if (tmpStr == "death") {
    m_type = CREATURE_EVENT_DEATH;
  } else if (tmpStr == "kill") {
    m_type = CREATURE_EVENT_KILL;
  } else if (tmpStr == "advance") {
    m_type = CREATURE_EVENT_ADVANCE;
  } else if (tmpStr == "modalwindow") {
    m_type = CREATURE_EVENT_MODALWINDOW;
  } else if (tmpStr == "textedit") {
    m_type = CREATURE_EVENT_TEXTEDIT;
  } else if (tmpStr == "healthchange") {
    m_type = CREATURE_EVENT_HEALTHCHANGE;
  } else if (tmpStr == "manachange") {
    m_type = CREATURE_EVENT_MANACHANGE;
  } else if (tmpStr == "extendedopcode") {
    m_type = CREATURE_EVENT_EXTENDED_OPCODE;
  } else {
    std::cout << "[Error - CreatureEvent::configureEvent] Invalid type for creature event: " << m_eventName << std::endl;
    return false;
  }

This code is the one that finds the function type registered on the XML file. For example, if our XML configuration (e.g. data/creaturescripts/creaturescripts.xml) has the following entry:

Code:
  <event type="death" name="PlayerDeath" script="playerdeath.lua" />

Then it will be assigned the CREATURE_EVENT_DEATH type. There's the pattern here too. Just stick our one right there, of course before the last else. I put mine between the "think" and the "preparedeath" event types:

Code:
  } else if (tmpStr == "think") {
    m_type = CREATURE_EVENT_THINK;
  } else if (tmpStr == "say") {
    m_type = CREATURE_EVENT_SAY;
  } else if (tmpStr == "preparedeath") {

Then, on the very next function, called getScriptEventName, which translates the previously assigned function type to the name of the function to be executed:

Code:
  switch (m_type) {
    case CREATURE_EVENT_LOGIN:
      return "onLogin";

    case CREATURE_EVENT_LOGOUT:
      return "onLogout";

    case CREATURE_EVENT_THINK:
      return "onThink";

    case CREATURE_EVENT_PREPAREDEATH:
      return "onPrepareDeath";

    case CREATURE_EVENT_DEATH:
      return "onDeath";

    case CREATURE_EVENT_KILL:
      return "onKill";

    case CREATURE_EVENT_ADVANCE:
      return "onAdvance";

    case CREATURE_EVENT_MODALWINDOW:
      return "onModalWindow";

    case CREATURE_EVENT_TEXTEDIT:
      return "onTextEdit";

    case CREATURE_EVENT_HEALTHCHANGE:
      return "onHealthChange";

    case CREATURE_EVENT_MANACHANGE:
      return "onManaChange";

    case CREATURE_EVENT_EXTENDED_OPCODE:
      return "onExtendedOpcode";

    case CREATURE_EVENT_NONE:
    default:
      return std::string();
  }

Again, just stick the new callback there, I will put mine again between think and preparedeath:

Code:
  case CREATURE_EVENT_THINK:
    return "onThink";

  case CREATURE_EVENT_SAY:
    return "onSay";

  case CREATURE_EVENT_PREPAREDEATH:
    return "onPrepareDeath";

Done. Our function is registered, the server will know that if we add an event with "say" type at creaturescripts.xml it will know it should look for an "onSay" function on the script file, but... when will our callback really be called?

This is the part where you will need to think. When should your callback be called? onSay is a good one to think about, since player speech already has two callbacks: talkactions and spells. I opted to put the callback after all of those.

The player say code is on the game.cpp file, on the playerSay function. It has the following code:

Code:
void Game::playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, const std::string& receiver, const std::string& text)
{
  Player* player = getPlayerByID(playerId);
  if (!player) {
    return;
  }

  player->resetIdleTime();

  uint32_t muteTime = player->isMuted();
  if (muteTime > 0) {
    std::ostringstream ss;
    ss << "You are still muted for " << muteTime << " seconds.";
    player->sendTextMessage(MESSAGE_STATUS_SMALL, ss.str());
    return;
  }

  if (playerSayCommand(player, text)) {
    return;
  }

  if (playerSaySpell(player, type, text)) {
    return;
  }

As you can see, it first checks if the player is not muted, then checks if it is a command (special type of talkactions), then if it is a spell (talkactions are also spells), and now I want to check and execute onSay callbacks, if there are any.

(continues on the next post)
 
If you look back, you will see that our CreatureEvents' playerSay function calls an onSay scripts, returns false if any of the Lua functions return false, or returns true if all of them succeeded (or if there aren't any "say" events).

So I add the following right after the above piece of code:

Code:
  if (!g_creatureEvents->playerSay(player, text)) {
    return;
  }

Where g_creatureEvents is the current singleton instance of CreatureEvents. We return if the return value is false because I don't want the player speech to appear on the screen if it failed to execute the functions. You can, for instance, return false if you want to filter certain keywords.

Now the tense part: compile the server. If it works, you are probably good to go. Write a simple function to test if your callback calls correctly, for example, I have created this:

Code:
function onSay(player, text)
  print(player:getName() .. " has said " .. text .. ".")
  return true
end

Run the server and be happy!

skHJ1XD.png


If you need any help, don't hesitate to comment here or open a topic on the support, I'll be happy to help you!

You can check the entire code I added to the server at this link, a GitHub commit.

Liked it? Don't forget to drop a star by my Forgotten fork, it has more interesting stuff :D
 
Why are you such a god? nice work!
 
I like a lot of the work you've done on your Fork's branches. It'd be nice to see some of it in the official release.

Red
 
Back
Top