port: P3-c PythonExchange + exchange module (1:1 40250)

Copy PythonExchange.cpp/PythonExchangeModule.cpp verbatim, drop the pending
stand-in and the exchange module stub, register initTrade() after
initNonPlayer(). The fake server answers CG_EXCHANGE START with the partner's
item + gold and CANCEL with END; port.login_flow drives the exchange round-trip
through the Python exchange/net modules and uiexchange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-24 00:59:05 +09:00
co-authored by Claude Opus 5.5
parent 9298500df9
commit 72b19b677d
14 changed files with 853 additions and 162 deletions
@@ -616,6 +616,45 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
if (pickup.vid == kGroundItemVID && !c.Send(del))
return Fail(name + ": send GC_ITEM_GROUND_DEL");
}
// CInputMain::Exchange (input_main.cpp) → CHARACTER::ExchangeStart / CExchange::Cancel: the partner is
// played by the server itself — START names the requested vid, then the partner's item (vnum 19, cell 0)
// and gold arrive the way CExchange::AddItem/AddGold send them with is_me=false; CANCEL answers END.
if (header == HDR_CG_EXCHANGE)
{
CGExchange ex;
std::memcpy(&ex, packet, sizeof(ex));
// arg1 only means something for START (40250 SendExchangeExitPacket leaves it uninitialised).
Event(name + ":exchange " + std::to_string(ex.subheader) +
(ex.subheader == EXCHANGE_CG_START ? " " + std::to_string(ex.arg1) : std::string()));
GCExchange out = zeroed<GCExchange>();
out.header = HDR_GC_EXCHANGE;
out.arg2 = {0xff, 0xffff}; // NPOS
bool ok = true;
if (ex.subheader == EXCHANGE_CG_START)
{
out.subheader = EXCHANGE_GC_START;
out.arg1 = ex.arg1;
ok = c.Send(out);
out.subheader = EXCHANGE_GC_ITEM_ADD;
out.arg1 = kExchangeItemVnum;
out.arg2 = {1, 0}; // INVENTORY, cell 0
out.arg3 = 1;
ok = ok && c.Send(out);
out = zeroed<GCExchange>();
out.header = HDR_GC_EXCHANGE;
out.subheader = EXCHANGE_GC_GOLD_ADD;
out.arg2 = {0xff, 0xffff};
out.arg1 = kExchangeGold;
ok = ok && c.Send(out);
}
else if (ex.subheader == EXCHANGE_CG_CANCEL)
{
out.subheader = EXCHANGE_GC_END;
ok = c.Send(out);
}
if (!ok)
return Fail(name + ": send GC_EXCHANGE");
}
if (header == HDR_CG_MOVE)
{
CGMove move;
+1
View File
@@ -25,6 +25,7 @@ public:
static constexpr std::uint32_t kGroundItemVID = 0x00054321;
static constexpr std::uint32_t kGroundItemVnum = 19;
static constexpr std::int32_t kGroundItemX = kX + 150, kGroundItemY = kY;
static constexpr std::uint32_t kExchangeItemVnum = 19, kExchangeGold = 1000;
~FakeLoginServer();
+23 -1
View File
@@ -50,6 +50,7 @@
#include "GameLib/ItemManager.h"
#include "UserInterface/PythonItem.h"
#include "UserInterface/StdAfx.h"
#include "UserInterface/PythonExchange.h"
#include "UserInterface/PythonCharacterManager.h"
#include "UserInterface/InstanceBase.h"
#include "UserInterface/PythonBackground.h"
@@ -518,6 +519,26 @@ int main(int argc, char** argv)
CHECK(pump_until(2, [&] { return !ground.GetGroundItemPosition(FakeLoginServer::kGroundItemVID, &item_pos); }));
}
// Exchange (CPythonExchange + exchange module): net.SendExchangeStartPacket → the fake partner's GC_EXCHANGE
// START/ITEM_ADD/GOLD_ADD fill the target side and open uiexchange; net.SendExchangeExitPacket → END.
if (server)
{
CHECK(py("import net\nnet.SendExchangeStartPacket(" + std::to_string(FakeLoginServer::kMainVID) + ")"));
CHECK(pump_until(2, [&] { return CPythonExchange::Instance().GetElkFromTarget() == FakeLoginServer::kExchangeGold; }));
CHECK(py("import exchange\n"
"assert exchange.isTrading() == 1\n"
"assert exchange.GetItemVnumFromTarget(0) == " + std::to_string(FakeLoginServer::kExchangeItemVnum) + "\n"
"assert exchange.GetItemCountFromTarget(0) == 1\n"
"assert exchange.GetElkFromSelf() == 0"));
CHECK(py("import net\nnet.SendExchangeExitPacket()"));
CHECK(pump_until(2, [&] { return !CPythonExchange::Instance().isTrading(); }));
CHECK(server->Has("game2:exchange 0 " + std::to_string(FakeLoginServer::kMainVID)));
CHECK(server->Has("game2:exchange 5"));
// uiexchange.CloseDialog armed wndMgr.OnceIgnoreMouseLeftButtonUpEvent (in the client that release belongs
// to the click that closed the dialog); spend it so step 8's click-release is not swallowed.
PythonBoot::UIMouseButton(1, false, 400, 520);
}
// 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
@@ -565,7 +586,8 @@ int main(int argc, char** argv)
want.push_back(e);
for (const auto& e : events)
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos &&
e.find(":pickup ") == std::string::npos)
e.find(":pickup ") == std::string::npos &&
e.find(":exchange ") == std::string::npos)
got.push_back(e);
CHECK(want == got);
}