Port route 4-c: NPC shop (PythonShop.cpp) + app cursor state
- UserInterface/PythonShop.cpp ported verbatim (shop module, initshop after initfly); pending stub and module stubs removed. - 40250 PythonApplicationCursor.cpp and SetMouseHandler moved into the platform CPythonApplication on file-static members; GetCursorNum out of line, since uishop's app.GetCursor() dereferenced the missing application object. - Fake server: general store (npclist 9003) answers CG_ON_CLICK with GC_SHOP START, CG_SHOP BUY/SELL2/END like CShop::Buy / CShopManager::Sell. - port.login_flow step 10: click the shopkeeper, right-click buy, drag-sell, ESC close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
85e176bdd5
commit
a12e7fea42
@@ -467,6 +467,7 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
auto points = zeroed<GCPoints>();
|
||||
points.header = HDR_GC_CHARACTER_POINTS;
|
||||
points.points[1] = 10; // POINT_LEVEL
|
||||
points.points[11] = kStartGold; // POINT_GOLD
|
||||
auto skill = zeroed<GCSkillLevel>();
|
||||
skill.header = HDR_GC_SKILL_LEVEL;
|
||||
if (!c.Phase(PHASE_LOADING) || !c.Send(main) || !c.Send(points) || !c.Send(skill))
|
||||
@@ -581,6 +582,27 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
mob_update.attack_speed = 100;
|
||||
if (!c.Send(mob) || !c.Send(mob_update))
|
||||
return Fail(name + ": send monster");
|
||||
// An NPC is ADD + ADDITIONAL_INFO + UPDATE like a PC (the name is the mob_proto locale name).
|
||||
GCCharacterAdd keeper = zeroed<GCCharacterAdd>();
|
||||
keeper.header = HDR_GC_CHARACTER_ADD;
|
||||
keeper.vid = kShopVID;
|
||||
keeper.x = kShopX;
|
||||
keeper.y = kShopY;
|
||||
keeper.type = 1; // CHAR_TYPE_NPC
|
||||
keeper.race = kShopRace;
|
||||
keeper.moving_speed = 100;
|
||||
keeper.attack_speed = 100;
|
||||
GCCharAddInfo keeper_info = zeroed<GCCharAddInfo>();
|
||||
keeper_info.header = HDR_GC_CHAR_ADDITIONAL_INFO;
|
||||
keeper_info.vid = kShopVID;
|
||||
std::strncpy(keeper_info.name, "General Store", sizeof(keeper_info.name) - 1);
|
||||
GCCharacterUpdate keeper_update = zeroed<GCCharacterUpdate>();
|
||||
keeper_update.header = HDR_GC_CHARACTER_UPDATE;
|
||||
keeper_update.vid = kShopVID;
|
||||
keeper_update.moving_speed = 100;
|
||||
keeper_update.attack_speed = 100;
|
||||
if (!c.Send(keeper) || !c.Send(keeper_info) || !c.Send(keeper_update))
|
||||
return Fail(name + ": send shopkeeper");
|
||||
Event(name + ":burst_sent");
|
||||
|
||||
// A ping after the burst proves that GamePhase consumed every packet above and can reply.
|
||||
@@ -594,6 +616,9 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
// CInputProcessor::Process frames it (static size or TDynamicSizePacketHeader, then the sequence
|
||||
// byte). A CG_MOVE (input_main.cpp CInputMain::Move) becomes "<name>:move <func> <arg> <rot> <x> <y>".
|
||||
int mob_hp = kMobHits;
|
||||
bool shopping = false;
|
||||
std::int32_t gold = kStartGold;
|
||||
std::uint32_t bag0 = 0; // vnum in inventory cell 0 (the fake character starts with an empty bag)
|
||||
for (;;)
|
||||
{
|
||||
std::uint8_t packet[1024] = {};
|
||||
@@ -616,6 +641,13 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
return Fail(name + ": unknown CG header " + std::to_string(header) + " in the game phase");
|
||||
else if (size > 1 && c.Read(packet + 1, size - 1) != 1)
|
||||
return true;
|
||||
// CInputMain::Shop returns the bytes each subheader carries past TPacketCGShop; the sequence byte follows them.
|
||||
if (header == HDR_CG_SHOP)
|
||||
{
|
||||
const size_t extra = packet[1] == SHOP_CG_BUY || packet[1] == SHOP_CG_SELL2 ? 2 : packet[1] == SHOP_CG_SELL ? 1 : 0;
|
||||
if (extra && c.Read(packet + size, extra) != 1)
|
||||
return true;
|
||||
}
|
||||
if (is_sequence_cg(header))
|
||||
{
|
||||
std::uint8_t sequence = 0;
|
||||
@@ -720,6 +752,84 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
return Fail(name + ": send GC_DEAD");
|
||||
}
|
||||
}
|
||||
// CInputMain::OnClick → CHARACTER::OnClick → CShopManager::StartShopping → CShop::AddGuest: GC_SHOP START with
|
||||
// the owner's vid and the 40 shelf slots.
|
||||
if (header == HDR_CG_ON_CLICK)
|
||||
{
|
||||
CGOnClick click;
|
||||
std::memcpy(&click, packet, sizeof(click));
|
||||
Event(name + ":click " + std::to_string(click.vid));
|
||||
if (click.vid == kShopVID && !shopping)
|
||||
{
|
||||
shopping = true;
|
||||
struct
|
||||
{
|
||||
DynHead head;
|
||||
std::uint8_t subheader;
|
||||
std::uint32_t owner_vid;
|
||||
ShopItem43 items[SHOP_HOST_ITEM_MAX_NUM];
|
||||
} __attribute__((packed)) start = zeroed<decltype(start)>();
|
||||
static_assert(sizeof(start) == 4 + 4 + 40 * 43);
|
||||
start.head = {HDR_GC_SHOP, static_cast<std::uint16_t>(sizeof(start))};
|
||||
start.subheader = SHOP_SUB_START;
|
||||
start.owner_vid = kShopVID;
|
||||
start.items[0].vnum = kShopItemVnum;
|
||||
start.items[0].price = kShopItemPrice;
|
||||
start.items[0].count = 1;
|
||||
if (!c.Send(start))
|
||||
return Fail(name + ": send GC_SHOP START");
|
||||
}
|
||||
}
|
||||
// CInputMain::Shop: END → CShop::RemoveGuest (GC_SHOP END); BUY → CShop::Buy (PointChange(POINT_GOLD), then
|
||||
// CItem::AddToCharacter's GC_ITEM_SET into the first empty cell); SELL2 → CShopManager::Sell (the cell empties
|
||||
// with the 42-byte header-20 packet, then PointChange(POINT_GOLD)).
|
||||
if (header == HDR_CG_SHOP)
|
||||
{
|
||||
const std::uint8_t sub = packet[1];
|
||||
Event(name + ":shop " + std::to_string(sub) +
|
||||
(sub == SHOP_CG_BUY || sub == SHOP_CG_SELL2 ? " " + std::to_string(packet[2]) + " " + std::to_string(packet[3]) : std::string()));
|
||||
auto gold_change = [&](std::int32_t amount) {
|
||||
gold += amount;
|
||||
GCPointChange change = zeroed<GCPointChange>();
|
||||
change.header = HDR_GC_CHARACTER_POINT_CHANGE;
|
||||
change.vid = kMainVID;
|
||||
change.type = 11; // POINT_GOLD
|
||||
change.amount = amount;
|
||||
change.value = gold;
|
||||
return c.Send(change);
|
||||
};
|
||||
bool ok = true;
|
||||
if (sub == SHOP_CG_END && shopping)
|
||||
{
|
||||
shopping = false;
|
||||
struct
|
||||
{
|
||||
DynHead head;
|
||||
std::uint8_t subheader;
|
||||
} __attribute__((packed)) end = {{HDR_GC_SHOP, 4}, SHOP_SUB_END};
|
||||
ok = c.Send(end);
|
||||
}
|
||||
else if (sub == SHOP_CG_BUY && shopping && packet[3] == 0 && bag0 == 0)
|
||||
{
|
||||
bag0 = kShopItemVnum;
|
||||
GCItemSet set = zeroed<GCItemSet>();
|
||||
set.header = HDR_GC_ITEM_SET;
|
||||
set.cell = {1, 0}; // INVENTORY, cell 0
|
||||
set.vnum = kShopItemVnum;
|
||||
set.count = 1;
|
||||
ok = gold_change(-static_cast<std::int32_t>(kShopItemPrice)) && c.Send(set);
|
||||
}
|
||||
else if (sub == SHOP_CG_SELL2 && shopping && packet[2] == 0 && bag0 != 0)
|
||||
{
|
||||
bag0 = 0;
|
||||
GCItemDelDeprecated del = zeroed<GCItemDelDeprecated>();
|
||||
del.header = HDR_GC_ITEM_DEL;
|
||||
del.cell = {1, 0};
|
||||
ok = c.Send(del) && gold_change(kShopSellPrice);
|
||||
}
|
||||
if (!ok)
|
||||
return Fail(name + ": send shop reply");
|
||||
}
|
||||
if (header == HDR_CG_MOVE)
|
||||
{
|
||||
CGMove move;
|
||||
|
||||
@@ -31,6 +31,12 @@ public:
|
||||
static constexpr std::uint32_t kMobVID = 0x00023456, kMobRace = 101;
|
||||
static constexpr std::int32_t kMobX = kX, kMobY = kY - 1800;
|
||||
static constexpr int kMobHits = 3;
|
||||
// A general store (npclist 9003 "goods") 4 m east of the dog, for the shop step. Its one shelf item is a
|
||||
// small red potion; selling it back pays kShopSellPrice (CShopManager::Sell: proto price / 5 minus 3 % tax).
|
||||
static constexpr std::uint32_t kShopVID = 0x00034567, kShopRace = 9003;
|
||||
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;
|
||||
|
||||
~FakeLoginServer();
|
||||
|
||||
|
||||
@@ -597,7 +597,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 2,") != std::string::npos); // the actor and the stray dog
|
||||
CHECK(tail_info().find("ChrTail (Map 3,") != std::string::npos); // the actor, the stray dog and the shopkeeper
|
||||
// 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);
|
||||
@@ -734,6 +734,85 @@ int main(int argc, char** argv)
|
||||
CHECK(attacks >= (size_t) FakeLoginServer::kMobHits);
|
||||
}
|
||||
}
|
||||
// 10. (批次 4-c) NPC shop: a click on the general store walks up to it (__OnClickActor) and sends CG_ON_CLICK;
|
||||
// the fake server's GC_SHOP START fills CPythonShop and game.StartShop opens uishop.ShopDialog next to the
|
||||
// inventory. A right click on shelf slot 0 buys (uishop.UnselectItemSlot → net.SendShopBuyPacket); the
|
||||
// potion lands in inventory cell 0 and the gold drops. Picking it up in the inventory and dropping it on an
|
||||
// empty shelf slot sells it (SelectEmptySlot → SellAttachedItem → SendShopSellPacketNew); ESC closes the
|
||||
// shop (CG_SHOP END → GC_SHOP END).
|
||||
if (server)
|
||||
{
|
||||
CInstanceBase* keeper = CPythonCharacterManager::Instance().GetInstancePtr(FakeLoginServer::kShopVID);
|
||||
CHECK(keeper != nullptr);
|
||||
if (keeper)
|
||||
{
|
||||
auto keeper_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 = keeper->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 [kx, ky] = keeper_on_screen();
|
||||
std::printf("port_login_flow_test: shopkeeper on screen at %d %d\n", kx, ky);
|
||||
PythonBoot::UIMouseMove(kx, ky);
|
||||
pump_until(0.1, [] { return false; });
|
||||
PythonBoot::UIMouseButton(1, true, kx, ky);
|
||||
pump_until(0.1, [] { return false; });
|
||||
PythonBoot::UIMouseButton(1, false, kx, ky);
|
||||
CHECK(pump_until(5, [&] { return server->Has("game2:click " + std::to_string(FakeLoginServer::kShopVID)); }));
|
||||
CHECK(pump_until(2, [] { return py_true("__import__('shop').IsOpen() and _stream.curPhaseWindow.interface.dlgShop.IsShow()"); }));
|
||||
CHECK(py("import shop\n"
|
||||
"assert shop.GetItemID(0) == " + std::to_string(FakeLoginServer::kShopItemVnum) + ", shop.GetItemID(0)\n"
|
||||
"assert shop.GetItemPrice(0) == " + std::to_string(FakeLoginServer::kShopItemPrice) + "\n"
|
||||
"assert shop.IsPrivateShop() == 0\n"
|
||||
"assert _stream.curPhaseWindow.interface.wndInventory.IsShow()"));
|
||||
// Slot 0 is the top-left cell of each grid.
|
||||
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 button, std::pair<int, int> at) {
|
||||
PythonBoot::UIMouseMove(at.first, at.second);
|
||||
pump_until(0.05, [] { return false; });
|
||||
PythonBoot::UIMouseButton(button, true, at.first, at.second);
|
||||
pump_until(0.05, [] { return false; });
|
||||
PythonBoot::UIMouseButton(button, false, at.first, at.second);
|
||||
pump_until(0.05, [] { return false; });
|
||||
};
|
||||
const auto shelf0 = slot0("dlgShop.itemSlotWindow");
|
||||
const auto bag0 = slot0("wndInventory.wndItem");
|
||||
std::printf("port_login_flow_test: shelf slot at %d %d, bag slot at %d %d\n", shelf0.first, shelf0.second, bag0.first, bag0.second);
|
||||
click(2, shelf0);
|
||||
CHECK(pump_until(2, [&] { return server->Has("game2:shop 1 1 0"); }));
|
||||
const std::string bought_gold = std::to_string(FakeLoginServer::kStartGold - (int) FakeLoginServer::kShopItemPrice);
|
||||
CHECK(pump_until(2, [&] {
|
||||
return py_true(("__import__('player').GetItemIndex(0) == " + std::to_string(FakeLoginServer::kShopItemVnum) +
|
||||
" and __import__('player').GetElk() == " + bought_gold).c_str());
|
||||
}));
|
||||
click(1, bag0);
|
||||
CHECK(py_true("__import__('mouseModule').mouseController.isAttached()"));
|
||||
click(1, std::make_pair(shelf0.first + 32, shelf0.second));
|
||||
CHECK(pump_until(2, [&] { return server->Has("game2:shop 3 0 1"); }));
|
||||
const std::string sold_gold = std::to_string(FakeLoginServer::kStartGold - (int) FakeLoginServer::kShopItemPrice + FakeLoginServer::kShopSellPrice);
|
||||
CHECK(pump_until(2, [&] {
|
||||
return py_true(("__import__('player').GetItemIndex(0) == 0 and __import__('player').GetElk() == " + sold_gold).c_str());
|
||||
}));
|
||||
PythonBoot::UIKey(DIK_ESCAPE, true);
|
||||
PythonBoot::UIKey(DIK_ESCAPE, false);
|
||||
CHECK(pump_until(2, [&] { return server->Has("game2:shop 0"); }));
|
||||
CHECK(pump_until(2, [] { return py_true("not __import__('shop').IsOpen() and not _stream.curPhaseWindow.interface.dlgShop.IsShow()"); }));
|
||||
}
|
||||
}
|
||||
if (server)
|
||||
{
|
||||
const std::vector<std::string> expected = {
|
||||
@@ -753,7 +832,8 @@ int main(int argc, char** argv)
|
||||
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos &&
|
||||
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(":target ") == std::string::npos && e.find(":attack ") == std::string::npos &&
|
||||
e.find(":click ") == std::string::npos && e.find(":shop ") == std::string::npos)
|
||||
got.push_back(e);
|
||||
CHECK(want == got);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user