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
@@ -92,14 +92,6 @@ void AddStubConstants_safebox(PyObject* poModule)
}
// 40250 UserInterface/PythonShop.cpp
void AddStubConstants_shop(PyObject* poModule)
{
(void) poModule;
PyModule_AddIntConstant(poModule, "SHOP_SLOT_COUNT", SHOP_HOST_ITEM_MAX_NUM);
PyModule_AddIntConstant(poModule, "SHOP_COIN_TYPE_GOLD", SHOP_COIN_TYPE_GOLD);
PyModule_AddIntConstant(poModule, "SHOP_COIN_TYPE_SECONDARY_COIN", SHOP_COIN_TYPE_SECONDARY_COIN);
}
// 40250 UserInterface/PythonSkill.cpp
// 40250 UserInterface/PythonSoundManagerModule.cpp
void AddStubConstants_snd(PyObject* poModule)
@@ -12,7 +12,6 @@ void AddStubConstants_messenger(PyObject* poModule);
void AddStubConstants_profiler(PyObject* poModule);
void AddStubConstants_quest(PyObject* poModule);
void AddStubConstants_safebox(PyObject* poModule);
void AddStubConstants_shop(PyObject* poModule);
void AddStubConstants_snd(PyObject* poModule);
void AddStubConstants_systemSetting(PyObject* poModule);
void AddStubConstants_udp(PyObject* poModule);
@@ -217,46 +216,6 @@ PyMethodDef methods_safebox[] = {
{ "GetMallSize", stub_safebox_GetMallSize, METH_VARARGS },
{ NULL, NULL }
};
// 40250 UserInterface/PythonShop.cpp: shop export names.
PyObject* stub_shop_Open(PyObject*, PyObject*) { return stub_call_named("shop.Open", Py_BuildNone()); }
PyObject* stub_shop_Close(PyObject*, PyObject*) { return stub_call_named("shop.Close", Py_BuildNone()); }
PyObject* stub_shop_IsOpen(PyObject*, PyObject*) { return stub_call_named("shop.IsOpen", Py_BuildValue("i", 0)); }
PyObject* stub_shop_IsPrivateShop(PyObject*, PyObject*) { return stub_call_named("shop.IsPrivateShop", Py_BuildValue("i", 0)); }
PyObject* stub_shop_IsMainPlayerPrivateShop(PyObject*, PyObject*) { return stub_call_named("shop.IsMainPlayerPrivateShop", Py_BuildValue("i", 0)); }
PyObject* stub_shop_GetItemID(PyObject*, PyObject*) { return stub_call_named("shop.GetItemID", Py_BuildValue("i", 0)); }
PyObject* stub_shop_GetItemCount(PyObject*, PyObject*) { return stub_call_named("shop.GetItemCount", Py_BuildValue("i", 0)); }
PyObject* stub_shop_GetItemPrice(PyObject*, PyObject*) { return stub_call_named("shop.GetItemPrice", Py_BuildValue("i", 0)); }
PyObject* stub_shop_GetItemMetinSocket(PyObject*, PyObject*) { return stub_call_named("shop.GetItemMetinSocket", Py_BuildValue("i", 0)); }
PyObject* stub_shop_GetItemAttribute(PyObject*, PyObject*) { return stub_call_named("shop.GetItemAttribute", Py_BuildValue("ii", 0, 0)); }
PyObject* stub_shop_GetTabCount(PyObject*, PyObject*) { return stub_call_named("shop.GetTabCount", Py_BuildValue("i", 0)); }
PyObject* stub_shop_GetTabName(PyObject*, PyObject*) { return stub_call_named("shop.GetTabName", Py_BuildValue("s", "")); }
PyObject* stub_shop_GetTabCoinType(PyObject*, PyObject*) { return stub_call_named("shop.GetTabCoinType", Py_BuildValue("i", 0)); }
PyObject* stub_shop_ClearPrivateShopStock(PyObject*, PyObject*) { return stub_call_named("shop.ClearPrivateShopStock", Py_BuildNone()); }
PyObject* stub_shop_AddPrivateShopItemStock(PyObject*, PyObject*) { return stub_call_named("shop.AddPrivateShopItemStock", Py_BuildNone()); }
PyObject* stub_shop_DelPrivateShopItemStock(PyObject*, PyObject*) { return stub_call_named("shop.DelPrivateShopItemStock", Py_BuildNone()); }
PyObject* stub_shop_GetPrivateShopItemPrice(PyObject*, PyObject*) { return stub_call_named("shop.GetPrivateShopItemPrice", Py_BuildValue("i", 0)); }
PyObject* stub_shop_BuildPrivateShop(PyObject*, PyObject*) { return stub_call_named("shop.BuildPrivateShop", Py_BuildNone()); }
PyMethodDef methods_shop[] = {
{ "Open", stub_shop_Open, METH_VARARGS },
{ "Close", stub_shop_Close, METH_VARARGS },
{ "IsOpen", stub_shop_IsOpen, METH_VARARGS },
{ "IsPrivateShop", stub_shop_IsPrivateShop, METH_VARARGS },
{ "IsMainPlayerPrivateShop", stub_shop_IsMainPlayerPrivateShop, METH_VARARGS },
{ "GetItemID", stub_shop_GetItemID, METH_VARARGS },
{ "GetItemCount", stub_shop_GetItemCount, METH_VARARGS },
{ "GetItemPrice", stub_shop_GetItemPrice, METH_VARARGS },
{ "GetItemMetinSocket", stub_shop_GetItemMetinSocket, METH_VARARGS },
{ "GetItemAttribute", stub_shop_GetItemAttribute, METH_VARARGS },
{ "GetTabCount", stub_shop_GetTabCount, METH_VARARGS },
{ "GetTabName", stub_shop_GetTabName, METH_VARARGS },
{ "GetTabCoinType", stub_shop_GetTabCoinType, METH_VARARGS },
{ "ClearPrivateShopStock", stub_shop_ClearPrivateShopStock, METH_VARARGS },
{ "AddPrivateShopItemStock", stub_shop_AddPrivateShopItemStock, METH_VARARGS },
{ "DelPrivateShopItemStock", stub_shop_DelPrivateShopItemStock, METH_VARARGS },
{ "GetPrivateShopItemPrice", stub_shop_GetPrivateShopItemPrice, METH_VARARGS },
{ "BuildPrivateShop", stub_shop_BuildPrivateShop, METH_VARARGS },
{ NULL, NULL }
};
// 40250 UserInterface/PythonSoundManagerModule.cpp: snd export names.
PyObject* stub_snd_PlaySound(PyObject*, PyObject*) { return stub_call_named("snd.PlaySound", Py_BuildNone()); }
PyObject* stub_snd_PlaySound3D(PyObject*, PyObject*) { return stub_call_named("snd.PlaySound3D", Py_BuildNone()); }
@@ -420,11 +379,6 @@ void init_2v0_gameplay_stubs() {
PyModule_AddIntConstant(module, "__port_stub__", 1);
AddStubConstants_safebox(module);
}
{
PyObject* module = Py_InitModule("shop", methods_shop);
PyModule_AddIntConstant(module, "__port_stub__", 1);
AddStubConstants_shop(module);
}
{
PyObject* module = Py_InitModule("snd", methods_snd);
PyModule_AddIntConstant(module, "__port_stub__", 1);
@@ -402,6 +402,7 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error)
initMiniMap();
initeffect();
initfly();
initshop();
initskill();
initBackground();
initServerStateChecker();
@@ -301,15 +301,113 @@ void CPythonApplication::OnUIRender()
UI::CWindowManager& rkUIMgr=UI::CWindowManager::Instance();
rkUIMgr.Render();
}
BOOL CPythonApplication::SetCursorNum(int) { MT_PLATFORM_STUB(); return FALSE; }
void CPythonApplication::SetCursorVisible(BOOL, bool) { MT_PLATFORM_STUB(); }
BOOL CPythonApplication::GetCursorVisible() { MT_PLATFORM_STUB(); return FALSE; }
bool CPythonApplication::GetLiarCursorOn() { MT_PLATFORM_STUB(); return false; }
void CPythonApplication::SetCursorMode(int) { MT_PLATFORM_STUB(); }
// 40250 returns m_iCursorMode, CURSOR_MODE_HARDWARE from the constructor until SetCursorMode (PythonApplicationCursor.cpp:92).
// PORT: SetCursorMode is still a stub, so the mode stays the constructor's.
int CPythonApplication::GetCursorMode() { return CURSOR_MODE_HARDWARE; }
void CPythonApplication::SetMouseHandler(PyObject*) { MT_PLATFORM_STUB(); }
// 40250 PythonApplicationCursor.cpp. PORT: the members live here until the application object exists
// (initial values from the constructor and CreateCursors); the host draws no hardware cursor, so every
// shape in [0, CURSOR_COUNT) counts as loaded in place of m_CursorHandleMap.
namespace
{
int s_iCursorNum = CPythonApplication::CURSOR_SHAPE_NORMAL; // m_iCursorNum
int s_iContinuousCursorNum = CPythonApplication::CURSOR_SHAPE_NORMAL; // m_iContinuousCursorNum
BOOL s_bCursorVisible = TRUE; // m_bCursorVisible
bool s_bLiarCursorOn = false; // m_bLiarCursorOn
int s_iCursorMode = CPythonApplication::CURSOR_MODE_HARDWARE; // m_iCursorMode
PyObject* s_poMouseHandler = NULL; // m_poMouseHandler
}
void CPythonApplication::SetCursorVisible(BOOL bFlag, bool bLiarCursorOn)
{
s_bCursorVisible = bFlag;
s_bLiarCursorOn = bLiarCursorOn;
// PORT: 40250 also counts ShowCursor up or down in hardware mode; the host has no OS cursor to hide.
}
BOOL CPythonApplication::GetCursorVisible()
{
return s_bCursorVisible;
}
bool CPythonApplication::GetLiarCursorOn()
{
return s_bLiarCursorOn;
}
int CPythonApplication::GetCursorMode()
{
return s_iCursorMode;
}
int CPythonApplication::GetCursorNum()
{
return s_iCursorNum;
}
BOOL CPythonApplication::__IsContinuousChangeTypeCursor(int iCursorNum)
{
switch (iCursorNum)
{
case CURSOR_SHAPE_NORMAL:
case CURSOR_SHAPE_ATTACK:
case CURSOR_SHAPE_TARGET:
case CURSOR_SHAPE_MAGIC:
case CURSOR_SHAPE_BUY:
case CURSOR_SHAPE_SELL:
return TRUE;
break;
}
return FALSE;
}
BOOL CPythonApplication::SetCursorNum(int iCursorNum)
{
if (CURSOR_SHAPE_NORMAL == iCursorNum)
{
if (!__IsContinuousChangeTypeCursor(s_iCursorNum))
{
iCursorNum = s_iContinuousCursorNum;
}
}
else
{
if (__IsContinuousChangeTypeCursor(s_iCursorNum))
{
s_iContinuousCursorNum = s_iCursorNum;
}
}
if (CURSOR_MODE_HARDWARE == s_iCursorMode)
{
// PORT: m_CursorHandleMap.find(iCursorNum) and SetCursor(hCursor).
if (iCursorNum < 0 || iCursorNum >= CURSOR_COUNT)
return FALSE;
}
s_iCursorNum = iCursorNum;
PyCallClassMemberFunc(s_poMouseHandler, "ChangeCursor", Py_BuildValue("(i)", s_iCursorNum));
return TRUE;
}
void CPythonApplication::SetCursorMode(int iMode)
{
switch (iMode)
{
case CURSOR_MODE_HARDWARE:
s_iCursorMode = CURSOR_MODE_HARDWARE;
break;
case CURSOR_MODE_SOFTWARE:
s_iCursorMode = CURSOR_MODE_SOFTWARE;
break;
}
}
// 40250 PythonApplication.cpp:871.
void CPythonApplication::SetMouseHandler(PyObject* poMouseHandler)
{
s_poMouseHandler = poMouseHandler;
}
void CPythonApplication::SetGlobalCenterPosition(LONG, LONG) { MT_PLATFORM_STUB(); }
void CPythonApplication::SetFPS(int) { MT_PLATFORM_STUB(); }
time_t CPythonApplication::GetServerTime() { MT_PLATFORM_STUB(); return 0; }
@@ -1,48 +0,0 @@
// Stand-in for the 40250 logic unit UserInterface/PythonShop.cpp, not ported yet: the members the ported units
// reference, as platform_stub.py stubs. Delete this file in the commit that ports the unit into port/
// (docs/PORT-PLAN.md).
#include "UserInterface/StdAfx.h"
#include "UserInterface/PythonShop.h"
#include "../../PlatformStub.h"
CPythonShop::CPythonShop()
{
MT_PLATFORM_STUB();
}
CPythonShop::~CPythonShop()
{
MT_PLATFORM_STUB();
}
auto CPythonShop::Clear() -> void
{
MT_PLATFORM_STUB();
}
auto CPythonShop::SetItemData(DWORD, const TShopItemData &) -> void
{
MT_PLATFORM_STUB();
}
auto CPythonShop::SetItemData(BYTE, DWORD, const TShopItemData &) -> void
{
MT_PLATFORM_STUB();
}
auto CPythonShop::SetTabCoinType(BYTE, BYTE) -> void
{
MT_PLATFORM_STUB();
}
auto CPythonShop::SetTabName(BYTE, const char *) -> void
{
MT_PLATFORM_STUB();
}
auto CPythonShop::IsOpen() -> BOOL
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<BOOL>();
}
@@ -179,7 +179,8 @@ class CPythonApplication final : public CMSApplication, public CInputKeyboard, p
bool GetLiarCursorOn();
void SetCursorMode(int iMode);
int GetCursorMode();
int GetCursorNum() { return m_iCursorNum; }
// PORT: out of line (platform/UserInterface/PythonApplication.cpp) while there is no application object.
int GetCursorNum();
void SetMouseHandler(PyObject * poMouseHandler);
@@ -0,0 +1,434 @@
#include "StdAfx.h"
#include "PythonShop.h"
#include "PythonNetworkStream.h"
//BOOL CPythonShop::GetSlotItemID(DWORD dwSlotPos, DWORD* pdwItemID)
//{
// if (!CheckSlotIndex(dwSlotPos))
// return FALSE;
// const TShopItemData * itemData;
// if (!GetItemData(dwSlotPos, &itemData))
// return FALSE;
// *pdwItemID=itemData->vnum;
// return TRUE;
//}
void CPythonShop::SetTabCoinType(BYTE tabIdx, BYTE coinType)
{
if (tabIdx >= m_bTabCount)
{
TraceError("Out of Index. tabIdx(%d) must be less than %d.", tabIdx, SHOP_TAB_COUNT_MAX);
return;
}
m_aShoptabs[tabIdx].coinType = coinType;
}
BYTE CPythonShop::GetTabCoinType(BYTE tabIdx)
{
if (tabIdx >= m_bTabCount)
{
TraceError("Out of Index. tabIdx(%d) must be less than %d.", tabIdx, SHOP_TAB_COUNT_MAX);
return 0xff;
}
return m_aShoptabs[tabIdx].coinType;
}
void CPythonShop::SetTabName(BYTE tabIdx, const char* name)
{
if (tabIdx >= m_bTabCount)
{
TraceError("Out of Index. tabIdx(%d) must be less than %d.", tabIdx, SHOP_TAB_COUNT_MAX);
return;
}
m_aShoptabs[tabIdx].name = name;
}
const char* CPythonShop::GetTabName(BYTE tabIdx)
{
if (tabIdx >= m_bTabCount)
{
TraceError("Out of Index. tabIdx(%d) must be less than %d.", tabIdx, SHOP_TAB_COUNT_MAX);
return NULL;
}
return m_aShoptabs[tabIdx].name.c_str();
}
void CPythonShop::SetItemData(DWORD dwIndex, const TShopItemData & c_rShopItemData)
{
BYTE tabIdx = dwIndex / SHOP_HOST_ITEM_MAX_NUM;
DWORD dwSlotPos = dwIndex % SHOP_HOST_ITEM_MAX_NUM;
SetItemData(tabIdx, dwSlotPos, c_rShopItemData);
}
BOOL CPythonShop::GetItemData(DWORD dwIndex, const TShopItemData ** c_ppItemData)
{
BYTE tabIdx = dwIndex / SHOP_HOST_ITEM_MAX_NUM;
DWORD dwSlotPos = dwIndex % SHOP_HOST_ITEM_MAX_NUM;
return GetItemData(tabIdx, dwSlotPos, c_ppItemData);
}
void CPythonShop::SetItemData(BYTE tabIdx, DWORD dwSlotPos, const TShopItemData & c_rShopItemData)
{
if (tabIdx >= SHOP_TAB_COUNT_MAX || dwSlotPos >= SHOP_HOST_ITEM_MAX_NUM)
{
TraceError("Out of Index. tabIdx(%d) must be less than %d. dwSlotPos(%d) must be less than %d", tabIdx, SHOP_TAB_COUNT_MAX, dwSlotPos, SHOP_HOST_ITEM_MAX_NUM);
return;
}
m_aShoptabs[tabIdx].items[dwSlotPos] = c_rShopItemData;
}
BOOL CPythonShop::GetItemData(BYTE tabIdx, DWORD dwSlotPos, const TShopItemData ** c_ppItemData)
{
if (tabIdx >= SHOP_TAB_COUNT_MAX || dwSlotPos >= SHOP_HOST_ITEM_MAX_NUM)
{
TraceError("Out of Index. tabIdx(%d) must be less than %d. dwSlotPos(%d) must be less than %d", tabIdx, SHOP_TAB_COUNT_MAX, dwSlotPos, SHOP_HOST_ITEM_MAX_NUM);
return FALSE;
}
*c_ppItemData = &m_aShoptabs[tabIdx].items[dwSlotPos];
return TRUE;
}
//
//BOOL CPythonShop::CheckSlotIndex(DWORD dwSlotPos)
//{
// if (dwSlotPos >= SHOP_HOST_ITEM_MAX_NUM * SHOP_TAB_COUNT_MAX)
// return FALSE;
//
// return TRUE;
//}
void CPythonShop::ClearPrivateShopStock()
{
m_PrivateShopItemStock.clear();
}
void CPythonShop::AddPrivateShopItemStock(TItemPos ItemPos, BYTE dwDisplayPos, DWORD dwPrice)
{
DelPrivateShopItemStock(ItemPos);
TShopItemTable SellingItem;
SellingItem.vnum = 0;
SellingItem.count = 0;
SellingItem.pos = ItemPos;
SellingItem.price = dwPrice;
SellingItem.display_pos = dwDisplayPos;
m_PrivateShopItemStock.insert(std::make_pair(ItemPos, SellingItem));
}
void CPythonShop::DelPrivateShopItemStock(TItemPos ItemPos)
{
if (m_PrivateShopItemStock.end() == m_PrivateShopItemStock.find(ItemPos))
return;
m_PrivateShopItemStock.erase(ItemPos);
}
int CPythonShop::GetPrivateShopItemPrice(TItemPos ItemPos)
{
TPrivateShopItemStock::iterator itor = m_PrivateShopItemStock.find(ItemPos);
if (m_PrivateShopItemStock.end() == itor)
return 0;
TShopItemTable & rShopItemTable = itor->second;
return rShopItemTable.price;
}
struct ItemStockSortFunc
{
bool operator() (TShopItemTable & rkLeft, TShopItemTable & rkRight)
{
return rkLeft.display_pos < rkRight.display_pos;
}
};
void CPythonShop::BuildPrivateShop(const char * c_szName)
{
std::vector<TShopItemTable> ItemStock;
ItemStock.reserve(m_PrivateShopItemStock.size());
TPrivateShopItemStock::iterator itor = m_PrivateShopItemStock.begin();
for (; itor != m_PrivateShopItemStock.end(); ++itor)
{
ItemStock.push_back(itor->second);
}
std::sort(ItemStock.begin(), ItemStock.end(), ItemStockSortFunc());
CPythonNetworkStream::Instance().SendBuildPrivateShopPacket(c_szName, ItemStock);
}
void CPythonShop::Open(BOOL isPrivateShop, BOOL isMainPrivateShop)
{
m_isShoping = TRUE;
m_isPrivateShop = isPrivateShop;
m_isMainPlayerPrivateShop = isMainPrivateShop;
}
void CPythonShop::Close()
{
m_isShoping = FALSE;
m_isPrivateShop = FALSE;
m_isMainPlayerPrivateShop = FALSE;
}
BOOL CPythonShop::IsOpen()
{
return m_isShoping;
}
BOOL CPythonShop::IsPrivateShop()
{
return m_isPrivateShop;
}
BOOL CPythonShop::IsMainPlayerPrivateShop()
{
return m_isMainPlayerPrivateShop;
}
void CPythonShop::Clear()
{
m_isShoping = FALSE;
m_isPrivateShop = FALSE;
m_isMainPlayerPrivateShop = FALSE;
ClearPrivateShopStock();
m_bTabCount = 1;
for (int i = 0; i < SHOP_TAB_COUNT_MAX; i++)
memset (m_aShoptabs[i].items, 0, sizeof(TShopItemData) * SHOP_HOST_ITEM_MAX_NUM);
}
CPythonShop::CPythonShop(void)
{
Clear();
}
CPythonShop::~CPythonShop(void)
{
}
PyObject * shopOpen(PyObject * poSelf, PyObject * poArgs)
{
int isPrivateShop = false;
PyTuple_GetInteger(poArgs, 0, &isPrivateShop);
int isMainPrivateShop = false;
PyTuple_GetInteger(poArgs, 1, &isMainPrivateShop);
CPythonShop& rkShop=CPythonShop::Instance();
rkShop.Open(isPrivateShop, isMainPrivateShop);
return Py_BuildNone();
}
PyObject * shopClose(PyObject * poSelf, PyObject * poArgs)
{
CPythonShop& rkShop=CPythonShop::Instance();
rkShop.Close();
return Py_BuildNone();
}
PyObject * shopIsOpen(PyObject * poSelf, PyObject * poArgs)
{
CPythonShop& rkShop=CPythonShop::Instance();
return Py_BuildValue("i", rkShop.IsOpen());
}
PyObject * shopIsPrviateShop(PyObject * poSelf, PyObject * poArgs)
{
CPythonShop& rkShop=CPythonShop::Instance();
return Py_BuildValue("i", rkShop.IsPrivateShop());
}
PyObject * shopIsMainPlayerPrivateShop(PyObject * poSelf, PyObject * poArgs)
{
CPythonShop& rkShop=CPythonShop::Instance();
return Py_BuildValue("i", rkShop.IsMainPlayerPrivateShop());
}
PyObject * shopGetItemID(PyObject * poSelf, PyObject * poArgs)
{
int nPos;
if (!PyTuple_GetInteger(poArgs, 0, &nPos))
return Py_BuildException();
const TShopItemData * c_pItemData;
if (CPythonShop::Instance().GetItemData(nPos, &c_pItemData))
return Py_BuildValue("i", c_pItemData->vnum);
return Py_BuildValue("i", 0);
}
PyObject * shopGetItemCount(PyObject * poSelf, PyObject * poArgs)
{
int iIndex;
if (!PyTuple_GetInteger(poArgs, 0, &iIndex))
return Py_BuildException();
const TShopItemData * c_pItemData;
if (CPythonShop::Instance().GetItemData(iIndex, &c_pItemData))
return Py_BuildValue("i", c_pItemData->count);
return Py_BuildValue("i", 0);
}
PyObject * shopGetItemPrice(PyObject * poSelf, PyObject * poArgs)
{
int iIndex;
if (!PyTuple_GetInteger(poArgs, 0, &iIndex))
return Py_BuildException();
const TShopItemData * c_pItemData;
if (CPythonShop::Instance().GetItemData(iIndex, &c_pItemData))
return Py_BuildValue("i", c_pItemData->price);
return Py_BuildValue("i", 0);
}
PyObject * shopGetItemMetinSocket(PyObject * poSelf, PyObject * poArgs)
{
int iIndex;
if (!PyTuple_GetInteger(poArgs, 0, &iIndex))
return Py_BuildException();
int iMetinSocketIndex;
if (!PyTuple_GetInteger(poArgs, 1, &iMetinSocketIndex))
return Py_BuildException();
const TShopItemData * c_pItemData;
if (CPythonShop::Instance().GetItemData(iIndex, &c_pItemData))
return Py_BuildValue("i", c_pItemData->alSockets[iMetinSocketIndex]);
return Py_BuildValue("i", 0);
}
PyObject * shopGetItemAttribute(PyObject * poSelf, PyObject * poArgs)
{
int iIndex;
if (!PyTuple_GetInteger(poArgs, 0, &iIndex))
return Py_BuildException();
int iAttrSlotIndex;
if (!PyTuple_GetInteger(poArgs, 1, &iAttrSlotIndex))
return Py_BuildException();
if (iAttrSlotIndex >= 0 && iAttrSlotIndex < ITEM_ATTRIBUTE_SLOT_MAX_NUM)
{
const TShopItemData * c_pItemData;
if (CPythonShop::Instance().GetItemData(iIndex, &c_pItemData))
return Py_BuildValue("ii", c_pItemData->aAttr[iAttrSlotIndex].bType, c_pItemData->aAttr[iAttrSlotIndex].sValue);
}
return Py_BuildValue("ii", 0, 0);
}
PyObject * shopClearPrivateShopStock(PyObject * poSelf, PyObject * poArgs)
{
CPythonShop::Instance().ClearPrivateShopStock();
return Py_BuildNone();
}
PyObject * shopAddPrivateShopItemStock(PyObject * poSelf, PyObject * poArgs)
{
BYTE bItemWindowType;
if (!PyTuple_GetInteger(poArgs, 0, &bItemWindowType))
return Py_BuildException();
WORD wItemSlotIndex;
if (!PyTuple_GetInteger(poArgs, 1, &wItemSlotIndex))
return Py_BuildException();
int iDisplaySlotIndex;
if (!PyTuple_GetInteger(poArgs, 2, &iDisplaySlotIndex))
return Py_BuildException();
int iPrice;
if (!PyTuple_GetInteger(poArgs, 3, &iPrice))
return Py_BuildException();
CPythonShop::Instance().AddPrivateShopItemStock(TItemPos(bItemWindowType, wItemSlotIndex), iDisplaySlotIndex, iPrice);
return Py_BuildNone();
}
PyObject * shopDelPrivateShopItemStock(PyObject * poSelf, PyObject * poArgs)
{
BYTE bItemWindowType;
if (!PyTuple_GetInteger(poArgs, 0, &bItemWindowType))
return Py_BuildException();
WORD wItemSlotIndex;
if (!PyTuple_GetInteger(poArgs, 1, &wItemSlotIndex))
return Py_BuildException();
CPythonShop::Instance().DelPrivateShopItemStock(TItemPos(bItemWindowType, wItemSlotIndex));
return Py_BuildNone();
}
PyObject * shopGetPrivateShopItemPrice(PyObject * poSelf, PyObject * poArgs)
{
BYTE bItemWindowType;
if (!PyTuple_GetInteger(poArgs, 0, &bItemWindowType))
return Py_BuildException();
WORD wItemSlotIndex;
if (!PyTuple_GetInteger(poArgs, 1, &wItemSlotIndex))
return Py_BuildException();
int iValue = CPythonShop::Instance().GetPrivateShopItemPrice(TItemPos(bItemWindowType, wItemSlotIndex));
return Py_BuildValue("i", iValue);
}
PyObject * shopBuildPrivateShop(PyObject * poSelf, PyObject * poArgs)
{
char * szName;
if (!PyTuple_GetString(poArgs, 0, &szName))
return Py_BuildException();
CPythonShop::Instance().BuildPrivateShop(szName);
return Py_BuildNone();
}
PyObject * shopGetTabCount(PyObject * poSelf, PyObject * poArgs)
{
return Py_BuildValue("i", CPythonShop::instance().GetTabCount());
}
PyObject * shopGetTabName(PyObject * poSelf, PyObject * poArgs)
{
BYTE bTabIdx;
if (!PyTuple_GetInteger(poArgs, 0, &bTabIdx))
return Py_BuildException();
return Py_BuildValue("s", CPythonShop::instance().GetTabName(bTabIdx));
}
PyObject * shopGetTabCoinType(PyObject * poSelf, PyObject * poArgs)
{
BYTE bTabIdx;
if (!PyTuple_GetInteger(poArgs, 0, &bTabIdx))
return Py_BuildException();
return Py_BuildValue("i", CPythonShop::instance().GetTabCoinType(bTabIdx));
}
void initshop()
{
static PyMethodDef s_methods[] =
{
// Shop
{ "Open", shopOpen, METH_VARARGS },
{ "Close", shopClose, METH_VARARGS },
{ "IsOpen", shopIsOpen, METH_VARARGS },
{ "IsPrivateShop", shopIsPrviateShop, METH_VARARGS },
{ "IsMainPlayerPrivateShop", shopIsMainPlayerPrivateShop, METH_VARARGS },
{ "GetItemID", shopGetItemID, METH_VARARGS },
{ "GetItemCount", shopGetItemCount, METH_VARARGS },
{ "GetItemPrice", shopGetItemPrice, METH_VARARGS },
{ "GetItemMetinSocket", shopGetItemMetinSocket, METH_VARARGS },
{ "GetItemAttribute", shopGetItemAttribute, METH_VARARGS },
{ "GetTabCount", shopGetTabCount, METH_VARARGS },
{ "GetTabName", shopGetTabName, METH_VARARGS },
{ "GetTabCoinType", shopGetTabCoinType, METH_VARARGS },
// Private Shop
{ "ClearPrivateShopStock", shopClearPrivateShopStock, METH_VARARGS },
{ "AddPrivateShopItemStock", shopAddPrivateShopItemStock, METH_VARARGS },
{ "DelPrivateShopItemStock", shopDelPrivateShopItemStock, METH_VARARGS },
{ "GetPrivateShopItemPrice", shopGetPrivateShopItemPrice, METH_VARARGS },
{ "BuildPrivateShop", shopBuildPrivateShop, METH_VARARGS },
{ NULL, NULL, NULL },
};
PyObject * poModule = Py_InitModule("shop", s_methods);
PyModule_AddIntConstant(poModule, "SHOP_SLOT_COUNT", SHOP_HOST_ITEM_MAX_NUM);
PyModule_AddIntConstant(poModule, "SHOP_COIN_TYPE_GOLD", SHOP_COIN_TYPE_GOLD);
PyModule_AddIntConstant(poModule, "SHOP_COIN_TYPE_SECONDARY_COIN", SHOP_COIN_TYPE_SECONDARY_COIN);
}
@@ -45,6 +45,7 @@ enum
// Listed here as 40250 lists them; only the ported ones are defined.
void initpack();
void initime();
void initshop();
void initapp();
void initchr();
void initchrmgr();
+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;
+6
View File
@@ -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();
+82 -2
View File
@@ -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);
}