From 44f9d92e8ca27535de8506e94250aaab44a850fc Mon Sep 17 00:00:00 2001 From: server Date: Tue, 14 Apr 2026 14:38:21 +0200 Subject: [PATCH] tests: cover quest framing and restart cooldowns --- src/game/char.cpp | 3 +- src/game/cmd_general.cpp | 3 +- src/game/quest_packet.cpp | 80 +++++++++++++++++++++++++++++++++++++ src/game/quest_packet.h | 23 +++++++++++ src/game/questpc.cpp | 69 +++++++------------------------- src/game/request_cooldown.h | 6 +++ tests/CMakeLists.txt | 1 + tests/smoke_auth.cpp | 58 ++++++++++++++++++++++++++- 8 files changed, 185 insertions(+), 58 deletions(-) create mode 100644 src/game/quest_packet.cpp create mode 100644 src/game/quest_packet.h create mode 100644 src/game/request_cooldown.h diff --git a/src/game/char.cpp b/src/game/char.cpp index 95905f3..7969ea1 100644 --- a/src/game/char.cpp +++ b/src/game/char.cpp @@ -40,6 +40,7 @@ #include "xmas_event.h" #include "banword.h" #include "target.h" +#include "request_cooldown.h" #include "wedding.h" #include "mob_manager.h" #include "mining.h" @@ -5700,7 +5701,7 @@ void CHARACTER::ReqSafeboxLoad(const char* pszPassword) int iPulse = thecore_pulse(); const int last_safebox_load_time = GetSafeboxLoadTime(); - if (last_safebox_load_time > 0 && iPulse - last_safebox_load_time < PASSES_PER_SEC(10)) + if (HasRecentRequestCooldown(last_safebox_load_time, iPulse, PASSES_PER_SEC(10))) { ChatPacket(CHAT_TYPE_INFO, LC_TEXT("<창고> 창고를 닫은지 10초 안에는 열 수 없습니다.")); return; diff --git a/src/game/cmd_general.cpp b/src/game/cmd_general.cpp index 51dd4f7..ca67f27 100644 --- a/src/game/cmd_general.cpp +++ b/src/game/cmd_general.cpp @@ -27,6 +27,7 @@ #include "unique_item.h" #include "threeway_war.h" #include "log.h" +#include "request_cooldown.h" #include "common/VnumHelper.h" extern int g_server_id; @@ -946,7 +947,7 @@ ACMD(do_mall_password) return; } - if (last_mall_load_time > 0 && iPulse - last_mall_load_time < passes_per_sec * 10) // 10초에 한번만 요청 가능 + if (HasRecentRequestCooldown(last_mall_load_time, iPulse, passes_per_sec * 10)) // 10초에 한번만 요청 가능 { ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("<창고> 창고를 닫은지 10초 안에는 열 수 없습니다.")); return; diff --git a/src/game/quest_packet.cpp b/src/game/quest_packet.cpp new file mode 100644 index 0000000..bcd1b2d --- /dev/null +++ b/src/game/quest_packet.cpp @@ -0,0 +1,80 @@ +#include "stdafx.h" + +#include "common/tables.h" +#include "packet_structs.h" +#include "quest_packet.h" + +#include +#include +#include + +namespace quest +{ +namespace +{ +constexpr uint8_t QUEST_SEND_ISBEGIN_LOCAL = 1 << 0; +constexpr uint8_t QUEST_SEND_TITLE_LOCAL = 1 << 1; +constexpr uint8_t QUEST_SEND_CLOCK_NAME_LOCAL = 1 << 2; +constexpr uint8_t QUEST_SEND_CLOCK_VALUE_LOCAL = 1 << 3; +constexpr uint8_t QUEST_SEND_COUNTER_NAME_LOCAL = 1 << 4; +constexpr uint8_t QUEST_SEND_COUNTER_VALUE_LOCAL = 1 << 5; +constexpr uint8_t QUEST_SEND_ICON_FILE_LOCAL = 1 << 6; + +void AppendBytes(std::vector& packet, const void* data, size_t size) +{ + const auto* bytes = static_cast(data); + packet.insert(packet.end(), bytes, bytes + size); +} + +template +void AppendFixedString(std::vector& packet, const std::string& value) +{ + std::array field {}; + const size_t copy_size = std::min(value.size(), N - 1); + if (copy_size > 0) + std::memcpy(field.data(), value.data(), copy_size); + AppendBytes(packet, field.data(), field.size()); +} +} + +std::vector BuildQuestInfoPacket(const QuestInfoPacketData& data) +{ + packet_quest_info header {}; + header.header = GC::QUEST_INFO; + header.length = sizeof(header); + header.index = data.quest_index; + header.flag = data.send_flags; + + std::vector packet; + packet.reserve(sizeof(header) + 128); + AppendBytes(packet, &header, sizeof(header)); + + if (data.send_flags & QUEST_SEND_ISBEGIN_LOCAL) + { + const uint8_t is_begin = data.is_begin ? 1 : 0; + AppendBytes(packet, &is_begin, sizeof(is_begin)); + } + + if (data.send_flags & QUEST_SEND_TITLE_LOCAL) + AppendFixedString<30 + 1>(packet, data.title); + + if (data.send_flags & QUEST_SEND_CLOCK_NAME_LOCAL) + AppendFixedString<16 + 1>(packet, data.clock_name); + + if (data.send_flags & QUEST_SEND_CLOCK_VALUE_LOCAL) + AppendBytes(packet, &data.clock_value, sizeof(data.clock_value)); + + if (data.send_flags & QUEST_SEND_COUNTER_NAME_LOCAL) + AppendFixedString<16 + 1>(packet, data.counter_name); + + if (data.send_flags & QUEST_SEND_COUNTER_VALUE_LOCAL) + AppendBytes(packet, &data.counter_value, sizeof(data.counter_value)); + + if (data.send_flags & QUEST_SEND_ICON_FILE_LOCAL) + AppendFixedString<24 + 1>(packet, data.icon_file); + + auto* final_header = reinterpret_cast(packet.data()); + final_header->length = static_cast(packet.size()); + return packet; +} +} diff --git a/src/game/quest_packet.h b/src/game/quest_packet.h new file mode 100644 index 0000000..a579e90 --- /dev/null +++ b/src/game/quest_packet.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace quest +{ +struct QuestInfoPacketData +{ + uint16_t quest_index = 0; + uint8_t send_flags = 0; + bool is_begin = false; + std::string title; + std::string clock_name; + int clock_value = 0; + std::string counter_name; + int counter_value = 0; + std::string icon_file; +}; + +std::vector BuildQuestInfoPacket(const QuestInfoPacketData& data); +} diff --git a/src/game/questpc.cpp b/src/game/questpc.cpp index ae28a3a..5961f12 100644 --- a/src/game/questpc.cpp +++ b/src/game/questpc.cpp @@ -1,8 +1,8 @@ #include "stdafx.h" #include "constants.h" +#include "quest_packet.h" #include "questmanager.h" #include "packet_structs.h" -#include "buffer_manager.h" #include "char.h" #include "desc_client.h" #include "questevent.h" @@ -234,75 +234,34 @@ namespace quest assert(m_iSendToClient); assert(m_RunningQuestState); - packet_quest_info qi; - TEMP_BUFFER payload; - - qi.header = GC::QUEST_INFO; - qi.length = sizeof(struct packet_quest_info); - qi.index = m_RunningQuestState->iIndex; - qi.flag = m_iSendToClient; + QuestInfoPacketData packet_data {}; + packet_data.quest_index = static_cast(m_RunningQuestState->iIndex); + packet_data.send_flags = static_cast(m_iSendToClient); + packet_data.is_begin = m_RunningQuestState->bStart; + packet_data.title = m_RunningQuestState->_title; + packet_data.clock_name = m_RunningQuestState->_clock_name; + packet_data.clock_value = m_RunningQuestState->_clock_value; + packet_data.counter_name = m_RunningQuestState->_counter_name; + packet_data.counter_value = m_RunningQuestState->_counter_value; + packet_data.icon_file = m_RunningQuestState->_icon_file; if (m_iSendToClient & QUEST_SEND_ISBEGIN) - { - BYTE temp = m_RunningQuestState->bStart?1:0; - payload.write(&temp,1); - qi.length+=1; - - sys_log(1, "QUEST BeginFlag %d", (int)temp); - } + sys_log(1, "QUEST BeginFlag %d", static_cast(m_RunningQuestState->bStart ? 1 : 0)); if (m_iSendToClient & QUEST_SEND_TITLE) - { - m_RunningQuestState->_title.reserve(30+1); - payload.write(m_RunningQuestState->_title.c_str(), 30+1); - qi.length+=30+1; - sys_log(1, "QUEST Title %s", m_RunningQuestState->_title.c_str()); - } if (m_iSendToClient & QUEST_SEND_CLOCK_NAME) - { - m_RunningQuestState->_clock_name.reserve(16+1); - payload.write(m_RunningQuestState->_clock_name.c_str(), 16+1); - qi.length+=16+1; - sys_log(1, "QUEST Clock Name %s", m_RunningQuestState->_clock_name.c_str()); - } if (m_iSendToClient & QUEST_SEND_CLOCK_VALUE) - { - payload.write(&m_RunningQuestState->_clock_value, sizeof(int)); - qi.length+=4; - sys_log(1, "QUEST Clock Value %d", m_RunningQuestState->_clock_value); - } if (m_iSendToClient & QUEST_SEND_COUNTER_NAME) - { - m_RunningQuestState->_counter_name.reserve(16+1); - payload.write(m_RunningQuestState->_counter_name.c_str(), 16+1); - qi.length+=16+1; - sys_log(1, "QUEST Counter Name %s", m_RunningQuestState->_counter_name.c_str()); - } if (m_iSendToClient & QUEST_SEND_COUNTER_VALUE) - { - payload.write(&m_RunningQuestState->_counter_value, sizeof(int)); - qi.length+=4; - sys_log(1, "QUEST Counter Value %d", m_RunningQuestState->_counter_value); - } if (m_iSendToClient & QUEST_SEND_ICON_FILE) - { - m_RunningQuestState->_icon_file.reserve(24+1); - payload.write(m_RunningQuestState->_icon_file.c_str(), 24+1); - qi.length+=24+1; - sys_log(1, "QUEST Icon File %s", m_RunningQuestState->_icon_file.c_str()); - } - TEMP_BUFFER buf; - buf.write(&qi, sizeof(qi)); - if (payload.size() > 0) - buf.write(payload.read_peek(), payload.size()); - - CQuestManager::instance().GetCurrentCharacterPtr()->GetDesc()->Packet(buf.read_peek(),buf.size()); + auto packet = BuildQuestInfoPacket(packet_data); + CQuestManager::instance().GetCurrentCharacterPtr()->GetDesc()->Packet(packet.data(), packet.size()); m_iSendToClient = 0; diff --git a/src/game/request_cooldown.h b/src/game/request_cooldown.h new file mode 100644 index 0000000..b345364 --- /dev/null +++ b/src/game/request_cooldown.h @@ -0,0 +1,6 @@ +#pragma once + +inline bool HasRecentRequestCooldown(int last_request_pulse, int current_pulse, int cooldown_pulses) +{ + return last_request_pulse > 0 && current_pulse - last_request_pulse < cooldown_pulses; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 03ab9fe..a3c13bc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ endif() add_executable(metin_smoke_tests smoke_auth.cpp ${CMAKE_SOURCE_DIR}/src/game/SecureCipher.cpp + ${CMAKE_SOURCE_DIR}/src/game/quest_packet.cpp ) add_executable(metin_login_smoke diff --git a/tests/smoke_auth.cpp b/tests/smoke_auth.cpp index 829d2eb..976468c 100644 --- a/tests/smoke_auth.cpp +++ b/tests/smoke_auth.cpp @@ -5,9 +5,14 @@ #include #include #include +#include -#include "common/packet_headers.h" #include "game/stdafx.h" +#include "common/packet_headers.h" +#include "common/tables.h" +#include "game/packet_structs.h" +#include "game/quest_packet.h" +#include "game/request_cooldown.h" #include "game/SecureCipher.h" #include "libthecore/fdwatch.h" #include "libthecore/signal.h" @@ -366,6 +371,55 @@ void TestFdwatchSlotReuseAfterDelete() close(sockets_b[0]); close(sockets_b[1]); } + +void TestQuestInfoPacketFraming() +{ + quest::QuestInfoPacketData data {}; + data.quest_index = 77; + data.send_flags = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6); + data.is_begin = true; + data.title = "Mall reward"; + data.clock_name = "Soon"; + data.clock_value = 15; + data.counter_name = "Kills"; + data.counter_value = 2; + data.icon_file = "d:/icon/test.tga"; + + const auto quest_packet = quest::BuildQuestInfoPacket(data); + Expect(!quest_packet.empty(), "Quest info packet is empty"); + Expect(quest_packet.size() == sizeof(packet_quest_info) + 1 + 31 + 17 + 4 + 17 + 4 + 25, + "Unexpected quest info packet size"); + + const auto* quest_header = reinterpret_cast(quest_packet.data()); + Expect(quest_header->header == GC::QUEST_INFO, "Unexpected quest info header"); + Expect(quest_header->length == quest_packet.size(), "Quest info packet length does not match payload size"); + Expect(quest_packet[sizeof(packet_quest_info)] == 1, "Quest begin flag payload mismatch"); + + TPacketGCItemGet item_get {}; + item_get.header = GC::ITEM_GET; + item_get.length = sizeof(item_get); + item_get.dwItemVnum = 50187; + item_get.bCount = 1; + item_get.bArg = 0; + + std::vector stream = quest_packet; + const auto* item_bytes = reinterpret_cast(&item_get); + stream.insert(stream.end(), item_bytes, item_bytes + sizeof(item_get)); + + const size_t next_frame_offset = quest_header->length; + Expect(stream.size() >= next_frame_offset + sizeof(item_get), "Combined stream truncated after quest packet"); + + const auto* next_frame = reinterpret_cast(stream.data() + next_frame_offset); + Expect(next_frame->header == GC::ITEM_GET, "Quest info packet left trailing bytes before next frame"); + Expect(next_frame->length == sizeof(TPacketGCItemGet), "Item get packet length mismatch after quest packet"); +} + +void TestRequestCooldownGuard() +{ + Expect(!HasRecentRequestCooldown(0, 5, 10), "Initial zero request pulse should not trigger cooldown"); + Expect(HasRecentRequestCooldown(95, 100, 10), "Recent request pulse should still be on cooldown"); + Expect(!HasRecentRequestCooldown(90, 100, 10), "Cooldown boundary should allow request"); +} } int main() @@ -379,6 +433,8 @@ int main() TestCheckpointBackendMetadata(); TestFdwatchReadAndOneshotWrite(); TestFdwatchSlotReuseAfterDelete(); + TestQuestInfoPacketFraming(); + TestRequestCooldownGuard(); std::cout << "metin smoke tests passed\n"; return 0; }