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:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user