Jump to content

[HOW-TO] Avoid crashes when calling server_timers in Dungeons (workaround)


Recommended Posts

(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 by Braxy
  • Metin2 Dev 1
  • Good 5
  • Love 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

  • 7 months later...
  • Premium
Posted (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 by Syreldar
  • Love 4

 

"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

Announcements



×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.