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:
shenlei
2026-09-24 09:30:47 +09:00
co-authored by Claude Opus 5.5
parent 85e176bdd5
commit a12e7fea42
16 changed files with 1060 additions and 115 deletions
+110
View File
@@ -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;