(This topic addresses to the most common crashes that occur using server_timers in dungeons)
In this short tutorial i'm gonna explain why are the crashes happening and how we can deal with them along with a use case of this.
The difference between the Normal Timers and Server Timers
Normal Timers:
- Directly tied to a character pointer.
- Timer execution halts if the associated character logs out.
- Implicitly dependent on the character's in-game presence.
Server Timers:
- Operate independently of character pointers.
- Continue to execute even if the initiating character exits the game.
- Offers persistent timing functionalities.
You might ponder on the need for server timers, especially when normal timers are present. Here's why:
1. Party Dynamics: In a multiplayer setting, if a party member possessing the timer logs out, gameplay can be disrupted, leading to a halted dungeon instance. Server timers mitigate this risk.
2. Dungeon Persistence: Players often exit and re-enter dungeons. With a normal timer, exiting erases the timer, jeopardizing the dungeon's progression. Server timers ensure continuity.
Why do crashes occur?
Server timers, by virtue of their independence, lack character pointers upon invocation. This absence becomes problematic when:
Distributing or dropping items.
Executing specific chats or commands.
Running functions reliant on character pointers.
The game struggles to reference a non-existent character, which can either lead to functional anomalies or outright crashes.
How can we solve the problem?
My way of solving this issue is simple.
I've created a global function that when called is selecting the character pointer of the specified PID and returns a boolean of whether it selected it or not.
// questlua_global.cpp
int _select_pid(lua_State* L)
{
DWORD dwPID = static_cast<DWORD>(lua_tonumber(L, 1));
quest::PC* pPC = CQuestManager::instance().GetPC(dwPID);
if(pPC)
{
LPCHARACTER lpSelectedChar = CQuestManager::instance().GetCurrentCharacterPtr();
lua_pushboolean(L, (lpSelectedChar ? 1 : 0));
return 1;
}
lua_pushboolean(L, false);
return 1;
}
{ "select_pid", _select_pid },
Now we set the leaderPID as a dungeon flag upon entering the instance for the first time. (if it's not a group, it will set the pid of the player that entered)
when login with <PC_IN_DUNGEON> begin
if (((party.is_party() and party.is_leader()) or not party.is_party()) and d.getf("leaderPID") < 1) then
d.setf("leaderPID", pc.get_player_id())
end
end
Now when we call the server_timer, we first select the leader and check if we succeeded or not.
when give_item.server_timer begin
if (d.select(get_server_timer_arg())) then
if (select_pid(d.getf("leaderPID"))) then
pc.give_item2(19, 1)
else
-- handle what happends if the selection was unsuccessful
end
end
end
That's basically it. You can now call any function in server_timer.
How can this get very useful?
Let's say we want to create an update timer that constantly updates different information on players client.
I've made a function that is pushing each dungeon's member PID. (only if it's in dungeon)
#include <functional>
struct FDungeonPIDCollector
{
std::vector<DWORD> vecPIDs;
void operator () (LPCHARACTER ch)
{
if (ch)
vecPIDs.push_back(ch->GetPlayerID());
}
};
int dungeon_get_member_pids(lua_State* L)
{
LPDUNGEON pDungeon = CQuestManager::instance().GetCurrentDungeon();
if (!pDungeon)
return 0;
FDungeonPIDCollector collector;
pDungeon->ForEachMember(std::ref(collector));
for (const auto& pid : collector.vecPIDs)
{
lua_pushnumber(L, pid);
}
return collector.vecPIDs.size();
}
{ "get_member_pids", dungeon_get_member_pids },
Using this, we can just update each dungeon's member informations:
when dungeon_update_info.server_timer begin
if (d.select(get_server_timer_arg())) then
local dungeonMemberIds = {d.get_member_pids()};
for index, value in ipairs(dungeonMemberIds) do
if (select_pid(value)) then
cmdchat(string.format("UpdateDungeonInformation %d %d", 1, 2))
-- pc.update_dungeon_info()
else
-- handle the negative outcome
end
end
end
end