port: complete 100% logic layer, eliminate pending dir, and unify client_main on port route

- Completed batch 4-f (Messenger, Guild, GuildMark), 4-g (PythonSystem, systemSetting), 4-h (PythonSoundManagerModule, MilesLib CSoundManager)
- Ported UserInterface/Locale.cpp, EterLocale/Japanese.cpp, EterLib/PathStack, EterLib/NetDatagram* UDP suite
- Modularized PythonApplication into Camera and Event handlers aligning with 40250 signatures
- Completely removed extension/src/platform/pending/ directory
- Logic layer reached 100.0% completion (3338/3338 functions, 0 TODO, 0 DIVERGENT) with empty port-map queue
- Removed legacy AppFlow route and fallback from project/client_main.gd, switching main loop unconditionally to native port route
- Verified 15/15 port.login_flow integration steps and 19/19 port_gate suites passing
This commit is contained in:
shen
2026-09-23 22:17:59 -07:00
parent ee7a9cd3e2
commit 837c830182
85 changed files with 10228 additions and 1545 deletions
+152 -5
View File
@@ -616,6 +616,69 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
elder_update.vid = kQuestVID;
if (!c.Send(elder) || !c.Send(elder_info) || !c.Send(elder_update))
return Fail(name + ": send quest NPC");
GCCharacterAdd storekeeper = keeper;
storekeeper.vid = kSafeboxVID;
storekeeper.x = kSafeboxX;
storekeeper.y = kSafeboxY;
storekeeper.race = kSafeboxRace;
GCCharAddInfo storekeeper_info = zeroed<GCCharAddInfo>();
storekeeper_info.header = HDR_GC_CHAR_ADDITIONAL_INFO;
storekeeper_info.vid = kSafeboxVID;
std::strncpy(storekeeper_info.name, "Storekeeper", sizeof(storekeeper_info.name) - 1);
GCCharacterUpdate storekeeper_update = keeper_update;
storekeeper_update.vid = kSafeboxVID;
if (!c.Send(storekeeper) || !c.Send(storekeeper_info) || !c.Send(storekeeper_update))
return Fail(name + ": send storekeeper NPC");
// Messenger (批次 4-f): Send initial friend list
struct
{
GCMessengerHead head;
MessengerListEntry entry;
char name[10];
} __attribute__((packed)) messenger_pkt = {
{HDR_GC_MESSENGER, static_cast<std::uint16_t>(sizeof(messenger_pkt)), MESSENGER_GC_LIST},
{1 /* MESSENGER_CONNECTED_STATE_ONLINE */, static_cast<std::uint8_t>(std::strlen(kFriendName))},
{}
};
std::memcpy(messenger_pkt.name, kFriendName, sizeof(messenger_pkt.name));
if (!c.Send(messenger_pkt))
return Fail(name + ": send messenger list");
// Guild (批次 4-f): Send guild name
struct
{
GCMessengerHead head;
GuildName16 item;
} __attribute__((packed)) guild_name_pkt = {
{HDR_GC_GUILD, static_cast<std::uint16_t>(sizeof(guild_name_pkt)), 16 /* GUILD_GC_NAME */},
{kGuildID, {}}
};
std::strncpy(guild_name_pkt.item.name, kGuildName, sizeof(guild_name_pkt.item.name));
if (!c.Send(guild_name_pkt))
return Fail(name + ": send guild name");
// Guild (批次 4-f): Send guild info (enables guild for player)
struct
{
GCMessengerHead head;
GuildInfo35 info;
} __attribute__((packed)) guild_info_pkt = {
{HDR_GC_GUILD, static_cast<std::uint16_t>(sizeof(guild_info_pkt)), 8 /* GUILD_GC_INFO */},
zeroed<GuildInfo35>()
};
guild_info_pkt.info.guild_id = kGuildID;
guild_info_pkt.info.master_pid = 1;
guild_info_pkt.info.level = 20;
guild_info_pkt.info.exp = 50000;
guild_info_pkt.info.member_count = 1;
guild_info_pkt.info.max_member_count = 50;
guild_info_pkt.info.gold = 123456;
guild_info_pkt.info.has_land = 1;
std::strncpy(guild_info_pkt.info.name, kGuildName, sizeof(guild_info_pkt.info.name) - 1);
if (!c.Send(guild_info_pkt))
return Fail(name + ": send guild info");
Event(name + ":burst_sent");
// A ping after the burst proves that GamePhase consumed every packet above and can reply.
@@ -633,6 +696,9 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
std::int32_t gold = kStartGold;
std::uint32_t bag0 = 0; // vnum in inventory cell 0 (the fake character starts with an empty bag)
int quest_page = 0; // the quest dialogue's last page sent (1 question, 2 next, 3 done)
bool safebox_open = false;
std::uint32_t safebox0 = kSafeboxItemVnum;
std::int32_t safebox_gold = kSafeboxMoney;
// CQuestManager::SendScript: packet_script {header, size, skin, src_size} and the script text.
auto send_script = [&](const std::string& script) {
struct
@@ -743,11 +809,44 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
const std::string text(reinterpret_cast<const char*>(packet + sizeof(head)),
strnlen(reinterpret_cast<const char*>(packet + sizeof(head)), text_len));
Event(name + ":chat " + std::to_string(head.type) + " " + text);
const std::string line = std::string(kCharacterName) + " : " + text;
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + line.size() + 1),
0 /* CHAT_TYPE_TALKING */, kMainVID, 1};
if (!c.Send(chat) || !c.Send(line.c_str(), line.size() + 1))
return Fail(name + ": send GC_CHAT");
if (text.rfind("/safebox_password ", 0) == 0)
{
const std::string pass = text.substr(18);
Event(name + ":safebox_password " + pass);
safebox_open = true;
GCSafeboxSize size_pkt = {HDR_GC_SAFEBOX_SIZE, FakeLoginServer::kSafeboxSize};
GCSafeboxMoneyChange money_pkt = {HDR_GC_SAFEBOX_MONEY_CHANGE, safebox_gold};
if (!c.Send(size_pkt) || !c.Send(money_pkt))
return Fail(name + ": send GC_SAFEBOX_SIZE / MONEY");
if (safebox0 != 0)
{
GCItemSet set = zeroed<GCItemSet>();
set.header = HDR_GC_SAFEBOX_SET;
set.cell = {3 /* SAFEBOX */, 0};
set.vnum = safebox0;
set.count = 1;
if (!c.Send(set))
return Fail(name + ": send GC_SAFEBOX_SET");
}
}
else if (text == "/safebox_close")
{
Event(name + ":safebox_close");
safebox_open = false;
const std::string cmd = "CloseSafebox";
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + cmd.size() + 1),
5 /* CHAT_TYPE_COMMAND */, 0, 1};
if (!c.Send(chat) || !c.Send(cmd.c_str(), cmd.size() + 1))
return Fail(name + ": send CloseSafebox");
}
else
{
const std::string line = std::string(kCharacterName) + " : " + text;
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + line.size() + 1),
0 /* CHAT_TYPE_TALKING */, kMainVID, 1};
if (!c.Send(chat) || !c.Send(line.c_str(), line.size() + 1))
return Fail(name + ": send GC_CHAT");
}
}
// CInputMain::Target → CHARACTER::SetTarget → BroadcastTargetPacket: GC_TARGET with the monster's HP percent.
if (header == HDR_CG_TARGET)
@@ -786,6 +885,14 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
CGOnClick click;
std::memcpy(&click, packet, sizeof(click));
Event(name + ":click " + std::to_string(click.vid));
if (click.vid == kSafeboxVID)
{
const std::string cmd = "ShowMeSafeboxPassword";
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + cmd.size() + 1),
5 /* CHAT_TYPE_COMMAND */, 0, 1};
if (!c.Send(chat) || !c.Send(cmd.c_str(), cmd.size() + 1))
return Fail(name + ": send ShowMeSafeboxPassword");
}
// CHARACTER::OnClick → CQuestManager::Click: say(...) then select("Yes", "No") → GotoSelectState.
if (click.vid == kQuestVID && !send_script("Greetings, young warrior.[ENTER]Shall I tell you a story?[ENTER][QUESTION 1;Yes|2;No]"))
return Fail(name + ": send GC_SCRIPT question");
@@ -881,6 +988,46 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
if (!ok)
return Fail(name + ": send shop reply");
}
if (header == HDR_CG_SAFEBOX_CHECKIN)
{
CGSafeboxCheckin in;
std::memcpy(&in, packet, sizeof(in));
Event(name + ":safebox_checkin " + std::to_string(in.item_pos.cell) + " -> " + std::to_string(in.safe_pos));
if (in.item_pos.cell == 0 && in.safe_pos == 0 && bag0 != 0)
{
safebox0 = bag0;
bag0 = 0;
GCItemDelDeprecated del = zeroed<GCItemDelDeprecated>();
del.header = HDR_GC_ITEM_DEL;
del.cell = {1, 0};
GCItemSet set = zeroed<GCItemSet>();
set.header = HDR_GC_SAFEBOX_SET;
set.cell = {3 /* SAFEBOX */, 0};
set.vnum = safebox0;
set.count = 1;
if (!c.Send(del) || !c.Send(set))
return Fail(name + ": send safebox checkin replies");
}
}
if (header == HDR_CG_SAFEBOX_CHECKOUT)
{
CGSafeboxCheckout out;
std::memcpy(&out, packet, sizeof(out));
Event(name + ":safebox_checkout " + std::to_string(out.safe_pos) + " -> " + std::to_string(out.item_pos.cell));
if (out.safe_pos == 0 && out.item_pos.cell == 0 && safebox0 != 0)
{
bag0 = safebox0;
safebox0 = 0;
GCItemDel del = {HDR_GC_SAFEBOX_DEL, 0};
GCItemSet set = zeroed<GCItemSet>();
set.header = HDR_GC_ITEM_SET;
set.cell = {1, 0};
set.vnum = bag0;
set.count = 1;
if (!c.Send(del) || !c.Send(set))
return Fail(name + ": send safebox checkout replies");
}
}
if (header == HDR_CG_MOVE)
{
CGMove move;
+13 -2
View File
@@ -37,10 +37,21 @@ public:
static constexpr std::int32_t kShopX = kMobX + 400, kShopY = kMobY;
static constexpr std::uint32_t kShopItemVnum = 27001, kShopItemPrice = 50;
static constexpr std::int32_t kStartGold = 10000, kShopSellPrice = 9;
// An old man (npclist 20009) 3 m east of the store whose click runs a quest dialogue the way CQuestManager
// An old man (npclist 20009) 0.5 m east of the store whose click runs a quest dialogue the way CQuestManager
// sends it: say + select ([QUESTION]), then a [NEXT] page, then a [DONE] page.
static constexpr std::uint32_t kQuestVID = 0x00045678, kQuestRace = 20009;
static constexpr std::int32_t kQuestX = kShopX + 300, kQuestY = kShopY;
static constexpr std::int32_t kQuestX = kShopX + 50, kQuestY = kShopY;
// A storekeeper (npclist 9005) 1.5 m west of the shop whose click runs ShowMeSafeboxPassword,
// receives /safebox_password, opens safebox, handles checkin/checkout and close.
static constexpr std::uint32_t kSafeboxVID = 0x00056789, kSafeboxRace = 9005;
static constexpr std::int32_t kSafeboxX = kShopX - 150, kSafeboxY = kShopY;
static constexpr std::uint8_t kSafeboxSize = 9;
static constexpr std::uint32_t kSafeboxItemVnum = 10;
static constexpr std::int32_t kSafeboxMoney = 500;
// Messenger & Guild constants (批次 4-f)
static constexpr const char* kFriendName = "TestFriend";
static constexpr std::uint32_t kGuildID = 1;
static constexpr const char* kGuildName = "TestGuild";
~FakeLoginServer();
+359 -2
View File
@@ -57,6 +57,11 @@
#include "UserInterface/PythonCharacterManager.h"
#include "UserInterface/InstanceBase.h"
#include "UserInterface/PythonBackground.h"
#include "UserInterface/PythonMessenger.h"
#include "UserInterface/PythonGuild.h"
#include "UserInterface/MarkManager.h"
#include "UserInterface/PythonSystem.h"
#include "MilesLib/SoundManager.h"
#include "EterGrnLib/StdAfx.h"
#include "EterGrnLib/LODController.h"
#include "EterGrnLib/ModelInstance.h"
@@ -597,7 +602,7 @@ int main(int argc, char** argv)
};
std::printf("port_login_flow_test: %s\n", tail_info().c_str());
CHECK(tail_info().find("ItemTail (Map 1,") != std::string::npos);
CHECK(tail_info().find("ChrTail (Map 3,") != std::string::npos); // the actor, the stray dog and the shopkeeper
CHECK(tail_info().find("ChrTail (Map 5,") != std::string::npos); // the actor, the stray dog, the shopkeeper, the quest elder and the storekeeper
// Holding ALT (game.GameWindow.ShowName) makes OnRender show every tail in range and project it after
// app.RenderGame; the item tail keeps that screen rect, which is what textTail.Pick tests.
PythonBoot::UIKey(DIK_LMENU, true);
@@ -813,6 +818,357 @@ int main(int argc, char** argv)
CHECK(pump_until(2, [] { return py_true("not __import__('shop').IsOpen() and not _stream.curPhaseWindow.interface.dlgShop.IsShow()"); }));
}
}
// Quest dialogue (CPythonEventManager + CPythonQuest + event module): clicking the quest NPC (kQuestVID)
// triggers CHARACTER::OnClick → CQuestManager::Click which sends GC_SCRIPT. The client parses the script
// through CPythonEventManager / parser.cpp and opens uiQuest.QuestDialog.
// 1. Initial page: say + select ([QUESTION 1;Yes|2;No]).
// Clicking "Yes" (answer 0) sends CG_SCRIPT_ANSWER 0.
// 2. Second page: say + [NEXT].
// Clicking Next sends CG_SCRIPT_ANSWER 254 (> 250).
// 3. Final page: say + [DONE].
// Clicking OK closes the dialogue.
if (server)
{
py("if _stream.curPhaseWindow.interface.wndInventory.IsShow(): _stream.curPhaseWindow.interface.wndInventory.Hide()");
CInstanceBase* elder = CPythonCharacterManager::Instance().GetInstancePtr(FakeLoginServer::kQuestVID);
CHECK(elder != nullptr);
if (elder)
{
auto elder_on_screen = [&] {
UI::CWindowManager& wm = UI::CWindowManager::Instance();
D3DXMATRIX proj, identity;
D3DXMatrixPerspectiveFovRH(&proj, D3DXToRadian(30.0f), wm.GetAspect(), 100.0f,
CPythonBackground::Instance().GetFarClip());
D3DXMatrixIdentity(&identity);
D3DVIEWPORT8 viewport = {0, 0, (DWORD) wm.GetScreenWidth(), (DWORD) wm.GetScreenHeight(), 0.0f, 1.0f};
const D3DXVECTOR3 body = elder->GetGraphicThingInstanceRef().GetPosition() + D3DXVECTOR3(0.0f, 0.0f, 100.0f);
D3DXVECTOR3 screen;
D3DXVec3Project(&screen, &body, &viewport, &proj,
&CCameraManager::Instance().GetCurrentCamera()->GetViewMatrix(), &identity);
return std::make_pair((int) screen.x, (int) screen.y);
};
// CPythonPlayer::__SendClickActorPacket enforces a 1000ms TCP rate-limit between NPC clicks
pump_until(1.0, [] { return false; });
auto [ex, ey] = elder_on_screen();
std::printf("port_login_flow_test: quest elder on screen at %d %d\n", ex, ey);
auto click_elder = [&] {
std::tie(ex, ey) = elder_on_screen();
PythonBoot::UIMouseMove(ex, ey);
pump_until(0.05, [] { return false; });
PythonBoot::UIMouseButton(1, true, ex, ey);
pump_until(0.05, [] { return false; });
PythonBoot::UIMouseButton(1, false, ex, ey);
};
click_elder();
CHECK(pump_until(5, [&] {
if (server->Has("game2:click " + std::to_string(FakeLoginServer::kQuestVID)))
return true;
click_elder();
return false;
}));
CHECK(pump_until(2, [] {
return py_true("len(_stream.curPhaseWindow.interface.wndQuestWindow) > 0 and "
"list(_stream.curPhaseWindow.interface.wndQuestWindow.values())[-1].btnAnswer is not None and "
"len(list(_stream.curPhaseWindow.interface.wndQuestWindow.values())[-1].btnAnswer) >= 2");
}));
auto press_enter = [] {
PythonBoot::UIKey(DIK_RETURN, true);
pump_until(0.05, [] { return false; });
PythonBoot::UIKey(DIK_RETURN, false);
pump_until(0.05, [] { return false; });
};
// Page 1: DIK_RETURN triggers focusBtn.CallEvent() for choice 0 ("Yes").
press_enter();
CHECK(pump_until(2, [&] { return server->Has("game2:answer 0"); }));
// Page 2: [NEXT]
CHECK(pump_until(2, [] {
return py_true("len(_stream.curPhaseWindow.interface.wndQuestWindow) > 0 and "
"list(_stream.curPhaseWindow.interface.wndQuestWindow.values())[-1].btnNext is not None");
}));
// Page 2: DIK_RETURN on Next button triggers OnPressEscapeKey() which selects 254 and closes.
press_enter();
CHECK(pump_until(2, [&] { return server->Has("game2:answer 254"); }));
// Page 3: [DONE]
CHECK(pump_until(2, [] {
return py_true("len(_stream.curPhaseWindow.interface.wndQuestWindow) > 0 and "
"list(_stream.curPhaseWindow.interface.wndQuestWindow.values())[-1].btnNext is not None and "
"getattr(list(_stream.curPhaseWindow.interface.wndQuestWindow.values())[-1], 'nextButtonType', None) == 1");
}));
// Page 3: DIK_RETURN on Done button closes the dialogue without further packets.
press_enter();
CHECK(pump_until(2, [] {
return py_true("len(_stream.curPhaseWindow.interface.wndQuestWindow) == 0");
}));
}
}
// Safebox (CPythonSafeBox + safebox module): clicking the storekeeper (kSafeboxVID)
// triggers CHARACTER::OnClick → ShowMeSafeboxPassword server command. The client opens
// PasswordDialog. Typing the password sends /safebox_password <pass>, opening SafeboxWindow.
// We verify:
// 1. Click Storekeeper, receive password dialog.
// 2. Press DIK_RETURN to submit password, server answers GC_SAFEBOX_SIZE (1 page = 45 slots),
// GC_SAFEBOX_MONEY_CHANGE (500) and initial item (kSafeboxItemVnum = 10 in slot 0).
// 3. Verify SafeboxWindow is open, safebox.GetCurrentSafeboxSize() == 45, safebox.GetItemID(0) == 10.
// 4. Checkout: drag safebox slot 0 to inventory slot 0.
// Server answers GC_SAFEBOX_DEL 0 and GC_ITEM_SET (inventory cell 0 = 10).
// Verify safebox slot 0 is empty (0) and player inventory slot 0 has 10.
// 5. Checkin: drag inventory slot 0 to safebox slot 0.
// Server answers GC_ITEM_DEL (inventory cell 0) and GC_SAFEBOX_SET (safebox cell 0 = 10).
// Verify player inventory slot 0 is empty (0) and safebox slot 0 has 10.
// 6. Close safebox via ESC: client sends /safebox_close, server replies CloseSafebox,
// SafeboxWindow closes.
if (server)
{
CInstanceBase* storekeeper = CPythonCharacterManager::Instance().GetInstancePtr(FakeLoginServer::kSafeboxVID);
CHECK(storekeeper != nullptr);
if (storekeeper)
{
auto storekeeper_on_screen = [&] {
UI::CWindowManager& wm = UI::CWindowManager::Instance();
D3DXMATRIX proj, identity;
D3DXMatrixPerspectiveFovRH(&proj, D3DXToRadian(30.0f), wm.GetAspect(), 100.0f,
CPythonBackground::Instance().GetFarClip());
D3DXMatrixIdentity(&identity);
D3DVIEWPORT8 viewport = {0, 0, (DWORD) wm.GetScreenWidth(), (DWORD) wm.GetScreenHeight(), 0.0f, 1.0f};
const D3DXVECTOR3 body = storekeeper->GetGraphicThingInstanceRef().GetPosition() + D3DXVECTOR3(0.0f, 0.0f, 100.0f);
D3DXVECTOR3 screen;
D3DXVec3Project(&screen, &body, &viewport, &proj,
&CCameraManager::Instance().GetCurrentCamera()->GetViewMatrix(), &identity);
return std::make_pair((int) screen.x, (int) screen.y);
};
auto [sx, sy] = storekeeper_on_screen();
std::printf("port_login_flow_test: storekeeper on screen at %d %d\n", sx, sy);
PythonBoot::UIMouseMove(sx, sy);
pump_until(0.1, [] { return false; });
PythonBoot::UIMouseButton(1, true, sx, sy);
pump_until(0.1, [] { return false; });
PythonBoot::UIMouseButton(1, false, sx, sy);
CHECK(pump_until(5, [&] { return server->Has("game2:click " + std::to_string(FakeLoginServer::kSafeboxVID)); }));
CHECK(pump_until(2, [] {
return py_true("_stream.curPhaseWindow.interface.dlgPassword.IsShow()");
}));
// Submit password dialog with VK_RETURN (IME return on focused edit line)
PythonBoot::UIChar(VK_RETURN);
CHECK(pump_until(2, [&] { return server->Has("game2:safebox_password "); }));
CHECK(pump_until(2, [] {
return py_true("_stream.curPhaseWindow.interface.wndSafebox.IsShow()");
}));
CHECK(pump_until(2, [] {
return py_true(("__import__('safebox').GetCurrentSafeboxSize() == 45 and "
"__import__('safebox').GetItemID(0) == " + std::to_string(FakeLoginServer::kSafeboxItemVnum) + " and "
"__import__('safebox').GetMoney() == " + std::to_string(FakeLoginServer::kSafeboxMoney)).c_str());
}));
// Ensure inventory is visible for slot drag-and-drop
py("_stream.curPhaseWindow.interface.wndInventory.Show()");
CHECK(pump_until(2, [] {
return py_true("_stream.curPhaseWindow.interface.wndInventory.IsShow()");
}));
auto slot0 = [](const char* grid) {
std::string xy;
CHECK(PythonBoot::Evaluate((std::string("'%d %d' % _stream.curPhaseWindow.interface.") + grid + ".GetGlobalPosition()").c_str(), &xy, nullptr));
int x = 0, y = 0;
std::sscanf(xy.c_str(), "%d %d", &x, &y);
return std::make_pair(x + 16, y + 16);
};
auto click = [](int btn, std::pair<int, int> pos) {
PythonBoot::UIMouseMove(pos.first, pos.second);
pump_until(0.05, [] { return false; });
PythonBoot::UIMouseButton(btn, true, pos.first, pos.second);
pump_until(0.05, [] { return false; });
PythonBoot::UIMouseButton(btn, false, pos.first, pos.second);
pump_until(0.05, [] { return false; });
};
const auto safe0 = slot0("wndSafebox.wndItem");
const auto bag0 = slot0("wndInventory.wndItem");
std::printf("port_login_flow_test: safebox slot 0 at %d %d, bag slot 0 at %d %d\n", safe0.first, safe0.second, bag0.first, bag0.second);
// Checkout: drag from safebox slot 0 to inventory slot 0
click(1, safe0);
CHECK(py_true("__import__('mouseModule').mouseController.isAttached()"));
click(1, bag0);
CHECK(pump_until(2, [&] { return server->Has("game2:safebox_checkout 0 -> 0"); }));
CHECK(pump_until(2, [&] {
return py_true(("__import__('safebox').GetItemID(0) == 0 and "
"__import__('player').GetItemIndex(0) == " + std::to_string(FakeLoginServer::kSafeboxItemVnum)).c_str());
}));
// Checkin: drag from inventory slot 0 back to safebox slot 0
click(1, bag0);
CHECK(py_true("__import__('mouseModule').mouseController.isAttached()"));
click(1, safe0);
CHECK(pump_until(2, [&] { return server->Has("game2:safebox_checkin 0 -> 0"); }));
CHECK(pump_until(2, [&] {
return py_true(("__import__('player').GetItemIndex(0) == 0 and "
"__import__('safebox').GetItemID(0) == " + std::to_string(FakeLoginServer::kSafeboxItemVnum)).c_str());
}));
// Close Safebox via ESC
PythonBoot::UIKey(DIK_ESCAPE, true);
pump_until(0.05, [] { return false; });
PythonBoot::UIKey(DIK_ESCAPE, false);
pump_until(0.05, [] { return false; });
CHECK(pump_until(2, [&] { return server->Has("game2:safebox_close"); }));
CHECK(pump_until(2, [] {
return py_true("not _stream.curPhaseWindow.interface.wndSafebox.IsShow()");
}));
}
}
// 13. (批次 4-f) Messenger / Guild / GuildMark:
// The fake server sent friend list ("TestFriend") and guild info/name ("TestGuild", ID 1)
// during the entergame burst.
// Verify:
// - CPythonMessenger / messenger module: IsFriendByName, RemoveFriend
// - CPythonGuild / guild module: IsGuildEnable, GetGuildID, GetGuildName, GetGuildMoney, HasGuildLand
// - CGuildMarkManager: AddMarkIDByGuildID, GetMarkID, GuildIDToMarkID, GetMarkIndexByMarkID
if (server)
{
CHECK(py("import messenger, guild"));
// Messenger
CHECK(CPythonMessenger::Instance().IsFriendByName(FakeLoginServer::kFriendName));
CHECK(py_true("messenger.IsFriendByName('TestFriend') == 1"));
CHECK(py_true("messenger.IsFriendByName('NonExistent') == 0"));
// Guild
CHECK(CPythonGuild::Instance().IsGuildEnable());
CHECK(CPythonGuild::Instance().GetGuildID() == FakeLoginServer::kGuildID);
std::string guild_name;
CHECK(CPythonGuild::Instance().GetGuildName(FakeLoginServer::kGuildID, &guild_name) && guild_name == FakeLoginServer::kGuildName);
CHECK(py_true("guild.IsGuildEnable() == 1"));
CHECK(py_true("guild.GetGuildID() == 1"));
CHECK(py_true("guild.GetGuildName() == 'TestGuild'"));
CHECK(py_true("guild.GetGuildName(1) == 'TestGuild'"));
CHECK(py_true("guild.GetGuildMoney() == 123456"));
CHECK(py_true("guild.HasGuildLand() == 1"));
// GuildMark
CGuildMarkManager::Instance().AddMarkIDByGuildID(FakeLoginServer::kGuildID, 42);
CHECK(CGuildMarkManager::Instance().GetMarkID(FakeLoginServer::kGuildID) == 42);
CHECK(py_true("guild.GuildIDToMarkID(1) == 42"));
CHECK(py_true("guild.GetMarkIndexByMarkID(42) == 42"));
// Messenger RemoveFriend
CHECK(py("import messenger\nmessenger.RemoveFriend('TestFriend')"));
CHECK(!CPythonMessenger::Instance().IsFriendByName(FakeLoginServer::kFriendName));
CHECK(py_true("messenger.IsFriendByName('TestFriend') == 0"));
}
// 14. (批次 4-g) System Settings & Combat Damage Floating Text (CPythonSystem / systemSetting):
// Verify:
// - systemSetting module imported cleanly without __port_stub__
// - CPythonSystem resolution enumeration (via CDummyDirect3D8): GetResolutionCount, GetResolution
// - Width, Height, BPP
// - Combat floating damage flag: IsShowDamage, SetShowDamageFlag, CPythonSystem::Instance().IsShowDamage()
// - Other system setting flags: IsViewChat, SetViewChatFlag, IsAlwaysShowName, SetAlwaysShowNameFlag, IsShowSalesText, SetShowSalesTextFlag
// - Music/Sound volume: GetMusicVolume, SetMusicVolume, GetSoundVolume, SetSoundVolumef
// - Config save & load: GetConfig, SaveConfig
{
CHECK(py("import systemSetting"));
CHECK(py_true("not hasattr(systemSetting, '__port_stub__')"));
// Resolution enumeration & screen size
CHECK(CPythonSystem::Instance().GetResolutionCount() > 0);
CHECK(py_true("systemSetting.GetResolutionCount() > 0"));
CHECK(py_true("systemSetting.GetWidth() > 0 and systemSetting.GetHeight() > 0"));
CHECK(py_true("len(systemSetting.GetResolution(0)) == 3"));
// Combat damage text flag
CPythonSystem::Instance().SetShowDamageFlag(true);
CHECK(CPythonSystem::Instance().IsShowDamage());
CHECK(py_true("systemSetting.IsShowDamage() == 1"));
CHECK(py("systemSetting.SetShowDamageFlag(0)"));
CHECK(!CPythonSystem::Instance().IsShowDamage());
CHECK(py_true("systemSetting.IsShowDamage() == 0"));
CHECK(py("systemSetting.SetShowDamageFlag(1)"));
CHECK(CPythonSystem::Instance().IsShowDamage());
CHECK(py_true("systemSetting.IsShowDamage() == 1"));
// Chat view flag
CHECK(py("systemSetting.SetViewChatFlag(0)"));
CHECK(!CPythonSystem::Instance().IsViewChat());
CHECK(py_true("systemSetting.IsViewChat() == 0"));
CHECK(py("systemSetting.SetViewChatFlag(1)"));
CHECK(CPythonSystem::Instance().IsViewChat());
CHECK(py_true("systemSetting.IsViewChat() == 1"));
// Always show name flag
CHECK(py("systemSetting.SetAlwaysShowNameFlag(0)"));
CHECK(!CPythonSystem::Instance().IsAlwaysShowName());
CHECK(py_true("systemSetting.IsAlwaysShowName() == 0"));
CHECK(py("systemSetting.SetAlwaysShowNameFlag(1)"));
CHECK(CPythonSystem::Instance().IsAlwaysShowName());
CHECK(py_true("systemSetting.IsAlwaysShowName() == 1"));
// Sales text flag
CHECK(py("systemSetting.SetShowSalesTextFlag(0)"));
CHECK(!CPythonSystem::Instance().IsShowSalesText());
CHECK(py_true("systemSetting.IsShowSalesText() == 0"));
CHECK(py("systemSetting.SetShowSalesTextFlag(1)"));
CHECK(CPythonSystem::Instance().IsShowSalesText());
CHECK(py_true("systemSetting.IsShowSalesText() == 1"));
// Sound & music volume
CHECK(py("systemSetting.SetMusicVolume(0.5)"));
CHECK(CPythonSystem::Instance().GetMusicVolume() == 0.5f);
CHECK(py_true("abs(systemSetting.GetMusicVolume() - 0.5) < 0.01"));
CHECK(py("systemSetting.SetSoundVolumef(0.8)"));
CHECK(py_true("systemSetting.GetSoundVolume() >= 0"));
// Config tuple: (res_index, freq_index, is_software_cursor, shadow_level, view_chat, always_show_name, show_damage, show_sales_text)
CHECK(py_true("len(systemSetting.GetConfig()) == 8"));
CHECK(py_true("systemSetting.SaveConfig() == 1"));
unlink("metin2.cfg");
}
// 15. Verify audio & sound manager module (PythonSoundManagerModule / snd)
// - Module existence and no stub flag: import snd; not hasattr(snd, '__port_stub__')
// - Music volume controls: SetMusicVolume, SetMusicVolumef
// - Sound volume controls: SetSoundVolume, SetSoundVolumef
// - Scale controls: SetSoundScale, SetAmbienceSoundScale
// - Sound playback calls: PlaySound, PlaySound3D, StopAllSound
// - Music playback and fading: PlayMusic, FadeInMusic, FadeLimitOutMusic, FadeOutMusic, FadeOutAllMusic
// - CSoundManager C++ singleton state reflection
{
CHECK(py("import snd"));
CHECK(py_true("not hasattr(snd, '__port_stub__')"));
// Music volume
CHECK(py("snd.SetMusicVolume(0.7)"));
CHECK(py("snd.SetMusicVolumef(0.7)"));
CHECK(std::abs(CSoundManager::Instance().GetMusicVolume() - 0.7f) < 0.05f);
// Sound volume
CHECK(py("snd.SetSoundVolume(3)"));
CHECK(py("snd.SetSoundVolumef(0.6)"));
CHECK(CSoundManager::Instance().GetSoundVolume() > 0.0f);
// Scales
CHECK(py("snd.SetSoundScale(250.0)"));
CHECK(CSoundManager::Instance().GetSoundScale() == 250.0f);
CHECK(py("snd.SetAmbienceSoundScale(1500.0)"));
// Playback APIs execute cleanly
CHECK(py("snd.PlaySound('sound/ui/click.wav')"));
CHECK(py("snd.PlaySound3D(100.0, 200.0, 0.0, 'sound/ui/click.wav')"));
CHECK(py("snd.PlayMusic('BGM/login_window.mp3')"));
CHECK(py("snd.FadeInMusic('BGM/login_window.mp3')"));
CHECK(py("snd.FadeLimitOutMusic('BGM/login_window.mp3', 0.2)"));
CHECK(py("snd.FadeOutMusic('BGM/login_window.mp3')"));
CHECK(py("snd.FadeOutAllMusic()"));
CHECK(py("snd.StopAllSound()"));
}
if (server)
{
const std::vector<std::string> expected = {
@@ -833,7 +1189,8 @@ int main(int argc, char** argv)
e.find(":pickup ") == std::string::npos &&
e.find(":exchange ") == std::string::npos && e.find(":chat ") == std::string::npos &&
e.find(":target ") == std::string::npos && e.find(":attack ") == std::string::npos &&
e.find(":click ") == std::string::npos && e.find(":shop ") == std::string::npos)
e.find(":click ") == std::string::npos && e.find(":shop ") == std::string::npos &&
e.find(":answer ") == std::string::npos && e.find(":safebox_") == std::string::npos)
got.push_back(e);
CHECK(want == got);
}