port: PythonItem + item module (batch 2 P3-b)

Verbatim 40250 CPythonItem and the item Python module replace the
pending stand-in and the module stub; CPythonApplication::Create calls
m_pyItem.Create() in 40250 order. itemGetIconImage returns its handle
through Py_BuildPointer (the ILP32 int truncated it and wndMgr.SetSlot
crashed on the live inventory). CMapManager::GetNormal evaluates
CTerrain::CalculateNormal on the native height tile; the item text-tail
members stay stubs, Pick answering -1 like an empty tail map.

The fake server drops vnum 19 next to the actor at entergame and answers
CG_ITEM_PICKUP with GC_ITEM_GROUND_DEL; login_flow picks it up through
player.PickCloseItem and checks item.SelectItem(19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-24 00:50:16 +09:00
co-authored by Claude Opus 5.5
parent f0564a7f12
commit 9298500df9
19 changed files with 2178 additions and 454 deletions
@@ -550,6 +550,19 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + sizeof(greet)), 2 /* CHAT_TYPE_NOTICE */, 0, 1};
if (!c.Send(chat) || !c.Send(greet, sizeof(greet)))
return Fail(name + ": send greet notice");
// CItem::EncodeInsertPacket for an owned item in view: GC_ITEM_GROUND_ADD, then GC_ITEM_OWNERSHIP.
GCItemGroundAdd ground = zeroed<GCItemGroundAdd>();
ground.header = HDR_GC_ITEM_GROUND_ADD;
ground.x = kGroundItemX;
ground.y = kGroundItemY;
ground.vid = kGroundItemVID;
ground.vnum = kGroundItemVnum;
GCItemOwnership ownership = zeroed<GCItemOwnership>();
ownership.header = HDR_GC_ITEM_OWNERSHIP;
ownership.vid = kGroundItemVID;
std::strncpy(ownership.owner, kCharacterName, sizeof(ownership.owner) - 1);
if (!c.Send(ground) || !c.Send(ownership))
return Fail(name + ": send ground item");
Event(name + ":burst_sent");
// A ping after the burst proves that GamePhase consumed every packet above and can reply.
@@ -593,6 +606,16 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
return Fail(name + ": SEQUENCE mismatch on CG header " + std::to_string(header));
c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE;
}
// CInputMain::ItemPickup → CHARACTER::PickupItem → CItem::RemoveFromGround → EncodeRemovePacket.
if (header == HDR_CG_ITEM_PICKUP)
{
CGItemPickup pickup;
std::memcpy(&pickup, packet, sizeof(pickup));
Event(name + ":pickup " + std::to_string(pickup.vid));
GCItemGroundDel del = {HDR_GC_ITEM_GROUND_DEL, pickup.vid};
if (pickup.vid == kGroundItemVID && !c.Send(del))
return Fail(name + ": send GC_ITEM_GROUND_DEL");
}
if (header == HDR_CG_MOVE)
{
CGMove move;
+4
View File
@@ -21,6 +21,10 @@ public:
static constexpr std::uint32_t kLoginKey = 0x13572468;
static constexpr std::uint32_t kMainVID = 0x00012345;
static constexpr std::int32_t kX = 469300, kY = 964200;
// A ground item in view at entergame (CItem::EncodeInsertPacket): vnum 19, owned by the character.
static constexpr std::uint32_t kGroundItemVID = 0x00054321;
static constexpr std::uint32_t kGroundItemVnum = 19;
static constexpr std::int32_t kGroundItemX = kX + 150, kGroundItemY = kY;
~FakeLoginServer();
+30 -2
View File
@@ -48,6 +48,7 @@
#include "GameLib/FlyingData.h"
#include "GameLib/FlyingObjectManager.h"
#include "GameLib/ItemManager.h"
#include "UserInterface/PythonItem.h"
#include "UserInterface/StdAfx.h"
#include "UserInterface/PythonCharacterManager.h"
#include "UserInterface/InstanceBase.h"
@@ -491,7 +492,33 @@ int main(int argc, char** argv)
CHECK(!CFlyingManager::Instance().RegisterFlyingData(msf));
}
// 8. (批次 2V3) Click-to-move: a left click on the ground below the actor reaches game.GameWindow's
// item module (PythonItemModule.cpp) over the loaded item_proto: SelectItem + the selected item's fields.
CHECK(py("import item\n"
"item.SelectItem(19)\n"
"assert item.GetItemName() == 'Sword+9', item.GetItemName()\n"
"assert item.GetItemType() == item.ITEM_TYPE_WEAPON and item.GetItemSubType() == item.WEAPON_SWORD\n"
"assert item.GetValue(3) > 0, 'min attack'"));
// Ground items (CPythonItem): the fake server put vnum 19 next to the actor at entergame (GC_ITEM_GROUND_ADD
// + GC_ITEM_OWNERSHIP). player.PickCloseItem finds it within __GetPickableDistance, sends CG_ITEM_PICKUP,
// and the server's GC_ITEM_GROUND_DEL removes it.
if (server)
{
CPythonItem& ground = CPythonItem::Instance();
TPixelPosition item_pos = {};
const char* owner = nullptr;
CHECK(ground.GetGroundItemPosition(FakeLoginServer::kGroundItemVID, &item_pos));
CHECK(ground.GetOwnership(FakeLoginServer::kGroundItemVID, &owner) && owner &&
std::string(owner) == FakeLoginServer::kCharacterName);
CHECK(ground.GetVirtualNumberOfGroundItem(FakeLoginServer::kGroundItemVID) == FakeLoginServer::kGroundItemVnum);
std::printf("port_login_flow_test: ground item at %.0f %.0f\n", item_pos.x, item_pos.y);
CHECK(py("import player\nplayer.PickCloseItem()"));
const std::string pickup = "game2:pickup " + std::to_string(FakeLoginServer::kGroundItemVID);
CHECK(pump_until(2, [&] { return server->Has(pickup); }));
CHECK(pump_until(2, [&] { return !ground.GetGroundItemPosition(FakeLoginServer::kGroundItemVID, &item_pos); }));
}
// 8. (批次 2V3) Click-to-move: a left click on the ground below the actor reaches game.GameWindow's
// OnMouseLeftButtonDown → player.SetMouseState(MBT_LEFT, MBS_PRESS); CPythonPlayer picks the ground
// through the cursor ray (CScreen::SetCursorPosition → CPythonBackground::GetPickingPoint) and the
// actor walks to it, sending CG_MOVE FUNC_MOVE, then FUNC_WAIT on arrival.
@@ -537,7 +564,8 @@ int main(int argc, char** argv)
if (e.rfind(prefix, 0) == 0)
want.push_back(e);
for (const auto& e : events)
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos)
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos &&
e.find(":pickup ") == std::string::npos)
got.push_back(e);
CHECK(want == got);
}