Braxy 623 Posted August 7, 2023 Share Posted August 7, 2023 (edited) (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 Edited August 7, 2023 by Braxy 1 5 1 As long as I'll be a threat for you , i will always be your target :3 Link to comment Share on other sites More sharing options...
Premium Syreldar 1886 Posted March 12 Premium Share Posted March 12 (edited) I stumbled upon a customer that had this issue today. I was not aware that you posted a solution for it almost a year ago. This is the full Lua workaround I used for the guy, posting it here just for the sake of doing it. From my functions.lua: Spoiler --[[ Description: Returns an array of ASCII values representing each character in the 'str' string. Arguments: str (string): The string to be converted into an array of ASCII values. Example: string_to_ascii_array("hello") -- Returns {104, 101, 108, 108, 111} Returns: table: An array of ASCII values. Time complexity: O(n), where n is the length of the string. Space complexity: O(n), where n is the length of the string. ]] string_to_ascii_array = function(str) local array = {}; local str_len = string.len(str); for i = 1, str_len do array[i] = string.byte(str, i); end -- for return array; end -- function --[[ Description: Returns a string converted from an array of ASCII values. Arguments: array (table): The array of ASCII values to be converted into a string. Example: ascii_array_to_string({104, 101, 108, 108, 111}) -- Returns "hello" Returns: string: The string representation of the ASCII values. Time complexity: O(n), where n is the length of the array. Space complexity: O(n), where n is the length of the array. ]] ascii_array_to_string = function(array) local str = ""; local array_len = table_get_count(array); for i = 1, array_len do str = str .. string.char(array[i]); end -- for return str; end -- function New functions: Spoiler set_dungeon_leader_name = function(name) local array = string_to_ascii_array(name); for index, value in ipairs(array) do d.setf(string.format("leader_name%d", index), value); end -- for end -- function get_dungeon_leader_name = function() local table_ex, i = {}, 1; local flag = d.getf(string.format("leader_name%d", i)); while (flag and flag ~= 0) do table_ex[table.getn(table_ex) + 1] = flag; i = i + 1; flag = d.getf(string.format("leader_name%d", i)); end -- while return ascii_array_to_string(table_ex); end -- function Usage example: Spoiler when login with Dungeon.InDungeon() begin pc.set_warp_location(--[[outside]]); -- Initialize the dungeon if (d.getf("initialized") == 0) then d.setf("initialized", 1); d.setf("start_time", get_time()); d.regen_file(string.format("%s/dungeon_regen.txt", --[[regen_path]])); server_loop_timer("dungeon_monster_count", 2, d.get_map_index()); end -- if -- Here we set the name of the leader. if (party.is_leader() or not party.is_party()) then d.setf("is_party_dungeon", party.is_leader() and 1 or 0); set_dungeon_leader_name(pc.get_name()); end -- if notice("<Dungeon> Kill all the monsters to complete the dungeon.") end -- when when dungeon_monster_count.server_timer begin local instance_index = get_server_timer_arg(); if (not d.select(instance_index)) then return; end -- if local data = Dungeon.GetData(); local count_monster = d.count_monster(); if (count_monster > 0) then --d.notice(string.format("Remaining monsters: %d.", count_monster)); return; end -- if clear_server_timer("dungeon_monster_count", instance_index); -- Here we get the name we previously memorized for the instance. local leader_name = get_dungeon_leader_name(); notice_all(string.format("[Dungeon] %s has completed `Dungeon Name`.", d.getf("is_party_dungeon") == 1 and string.format("%s's party", leader_name) or leader_name)) end -- when You could also do this upon d.new_jump* call instead of onLogin. Edited March 12 by Syreldar 5 "Nothing's free in this life. Ignorant people have an obligation to make up for their ignorance by paying those who help them. Either you got the brains or cash, if you lack both you're useless." Syreldar Link to comment Share on other sites More sharing options...
Recommended Posts