From a12e7fea42fcecd7ecb89b13d7fc583994629bb7 Mon Sep 17 00:00:00 2001 From: shenlei Date: Thu, 24 Sep 2026 09:30:47 +0900 Subject: [PATCH] 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 --- audit/history.jsonl | 1 + .../UserInterface/PythonApplication.cpp.json | 7 +- .../PythonApplicationCursor.cpp.json | 72 +++ .../UserInterface/PythonShop.cpp.json | 236 ++++++++++ docs/PORT-PLAN.md | 2 + .../ScriptLib/GameplayModuleConstants.cpp | 8 - .../platform/ScriptLib/GameplayModules.cpp | 46 -- .../src/platform/ScriptLib/PythonBoot.cpp | 1 + .../UserInterface/PythonApplication.cpp | 116 ++++- .../pending/UserInterface/PythonShop.cpp | 48 -- .../port/UserInterface/PythonApplication.h | 3 +- .../src/port/UserInterface/PythonShop.cpp | 434 ++++++++++++++++++ extension/src/port/UserInterface/StdAfx.h | 1 + extension/tests/port_login_flow_server.cpp | 110 +++++ extension/tests/port_login_flow_server.h | 6 + extension/tests/port_login_flow_test.cpp | 84 +++- 16 files changed, 1060 insertions(+), 115 deletions(-) create mode 100644 audit/port-map/UserInterface/PythonApplicationCursor.cpp.json create mode 100644 audit/port-map/UserInterface/PythonShop.cpp.json delete mode 100644 extension/src/platform/pending/UserInterface/PythonShop.cpp create mode 100644 extension/src/port/UserInterface/PythonShop.cpp diff --git a/audit/history.jsonl b/audit/history.jsonl index c6b41f02..7046286f 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -542,3 +542,4 @@ {"time": "2026-09-24T18:00:00Z", "event": "correction", "unit": "batch 3-c todo", "detail": "The sword next to the actor in python_game_render_test is the fake server ground item (GC_ITEM_GROUND_ADD vnum 19, P3-b), not a weapon-attach bug; the fake character has no weapon, AttachWeapon is never called. Todo withdrawn."} {"time": "2026-09-24T19:00:00Z", "event": "port", "unit": "batch 4-a text input: EterLib/IME.cpp (ADAPTED), UserInterface/PythonIME.cpp, PythonIMEModule.cpp", "detail": "ime module ported; IMM/TSF-free CIME in platform/; Godot key events -> ui_ime_key/ui_char -> CWindowManager RunIME*; login typed through the IME; in-game chat CG_CHAT round-trip", "tests": ["port.login_flow (typed id/password with edits, chat send/echo/close)", "port.login_live", "python_game_render_test (Godot key events)"]} {"time": "2026-09-24T20:00:00Z", "event": "verify", "unit": "batch 4-b combat: InstanceBaseBattle / ActorInstanceCollisionDetection / PythonPlayerInput (already ported)", "detail": "fake server spawns a stray dog outside the BANPK safe disc, answers CG_TARGET/CG_ATTACK with GC_TARGET/GC_DAMAGE_INFO/GC_DEAD; held left button walks up and combo-attacks until death; damage numbers wait on CPythonSystem (4-g)", "tests": ["port.login_flow (step 9 held-button kill)", "port.login_live", "python_game_render_test (Godot mouse events, target HP 100/66/33/0)"]} +{"time": "2026-09-24T21:00:00Z", "event": "port", "unit": "batch 4-c shop: UserInterface/PythonShop.cpp (+ PythonApplicationCursor.cpp, SetMouseHandler adapted)", "detail": "shop module ported verbatim; app cursor state moved into the platform CPythonApplication (uishop reads app.GetCursor, which dereferenced the missing application object); fake server general store answers CG_ON_CLICK/CG_SHOP BUY/SELL2/END like CShop::AddGuest/Buy and CShopManager::Sell", "tests": ["port.login_flow (step 10 buy/sell/ESC)", "port.login_live", "python_game_render_test"]} diff --git a/audit/port-map/UserInterface/PythonApplication.cpp.json b/audit/port-map/UserInterface/PythonApplication.cpp.json index 8d18c6ca..c747dc6c 100644 --- a/audit/port-map/UserInterface/PythonApplication.cpp.json +++ b/audit/port-map/UserInterface/PythonApplication.cpp.json @@ -47,7 +47,12 @@ "status": "TODO" }, "CPythonApplication::SetMouseHandler": { - "status": "TODO" + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:SetMouseHandler" + ], + "note": "Body verbatim on a file-static m_poMouseHandler (no application object yet); SetCursorNum calls its ChangeCursor.", + "test": "extension/tests/port_login_flow_test.cpp (step 10)" }, "CPythonApplication::CheckDeviceState": { "status": "TODO" diff --git a/audit/port-map/UserInterface/PythonApplicationCursor.cpp.json b/audit/port-map/UserInterface/PythonApplicationCursor.cpp.json new file mode 100644 index 00000000..ca645e43 --- /dev/null +++ b/audit/port-map/UserInterface/PythonApplicationCursor.cpp.json @@ -0,0 +1,72 @@ +{ + "reference": "UserInterface/PythonApplicationCursor.cpp", + "reference_sha256": "acd34facc55665509839d75c04feb0af074d6db40f39195c01af492a40b31e93", + "priority": "P4", + "contracts": [], + "functions": { + "CPythonApplication::CreateCursors": { + "status": "N_A", + "note": "Win32 LoadImage/DestroyCursor of the IDC_CURSOR_* resources; the host draws no hardware cursor, so SetCursorNum treats every shape in [0, CURSOR_COUNT) as loaded." + }, + "CPythonApplication::DestroyCursors": { + "status": "N_A", + "note": "Win32 LoadImage/DestroyCursor of the IDC_CURSOR_* resources; the host draws no hardware cursor, so SetCursorNum treats every shape in [0, CURSOR_COUNT) as loaded." + }, + "CPythonApplication::SetCursorVisible": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:SetCursorVisible" + ], + "note": "Body verbatim in the platform CPythonApplication stand-in on file-static members (there is no application object yet); ShowCursor/SetCursor calls dropped.", + "test": "extension/tests/port_login_flow_test.cpp (step 10: uishop reads app.GetCursor)" + }, + "CPythonApplication::GetCursorVisible": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:GetCursorVisible" + ], + "note": "Body verbatim in the platform CPythonApplication stand-in on file-static members (there is no application object yet); ShowCursor/SetCursor calls dropped.", + "test": "extension/tests/port_login_flow_test.cpp (step 10: uishop reads app.GetCursor)" + }, + "CPythonApplication::GetLiarCursorOn": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:GetLiarCursorOn" + ], + "note": "Body verbatim in the platform CPythonApplication stand-in on file-static members (there is no application object yet); ShowCursor/SetCursor calls dropped.", + "test": "extension/tests/port_login_flow_test.cpp (step 10: uishop reads app.GetCursor)" + }, + "CPythonApplication::GetCursorMode": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:GetCursorMode" + ], + "note": "Body verbatim in the platform CPythonApplication stand-in on file-static members (there is no application object yet); ShowCursor/SetCursor calls dropped.", + "test": "extension/tests/port_login_flow_test.cpp (step 10: uishop reads app.GetCursor)" + }, + "CPythonApplication::__IsContinuousChangeTypeCursor": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:__IsContinuousChangeTypeCursor" + ], + "note": "Body verbatim in the platform CPythonApplication stand-in on file-static members (there is no application object yet); ShowCursor/SetCursor calls dropped.", + "test": "extension/tests/port_login_flow_test.cpp (step 10: uishop reads app.GetCursor)" + }, + "CPythonApplication::SetCursorNum": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:SetCursorNum" + ], + "note": "Body verbatim in the platform CPythonApplication stand-in on file-static members (there is no application object yet); ShowCursor/SetCursor calls dropped.", + "test": "extension/tests/port_login_flow_test.cpp (step 10: uishop reads app.GetCursor)" + }, + "CPythonApplication::SetCursorMode": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/UserInterface/PythonApplication.cpp:SetCursorMode" + ], + "note": "Body verbatim in the platform CPythonApplication stand-in on file-static members (there is no application object yet); ShowCursor/SetCursor calls dropped.", + "test": "extension/tests/port_login_flow_test.cpp (step 10: uishop reads app.GetCursor)" + } + } +} diff --git a/audit/port-map/UserInterface/PythonShop.cpp.json b/audit/port-map/UserInterface/PythonShop.cpp.json new file mode 100644 index 00000000..27e7dc27 --- /dev/null +++ b/audit/port-map/UserInterface/PythonShop.cpp.json @@ -0,0 +1,236 @@ +{ + "reference": "UserInterface/PythonShop.cpp", + "reference_sha256": "23c46a5fe95a979c2be6a52fbd4699e6cba6c6a66dca10bc2dc7d720017bb2c5", + "priority": "P3", + "contracts": [], + "functions": { + "CPythonShop::SetTabCoinType": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::SetTabCoinType" + ] + }, + "CPythonShop::GetTabCoinType": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::GetTabCoinType" + ] + }, + "CPythonShop::SetTabName": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::SetTabName" + ] + }, + "CPythonShop::GetTabName": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::GetTabName" + ] + }, + "CPythonShop::SetItemData": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::SetItemData" + ] + }, + "CPythonShop::GetItemData": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::GetItemData" + ] + }, + "CPythonShop::ClearPrivateShopStock": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::ClearPrivateShopStock" + ] + }, + "CPythonShop::AddPrivateShopItemStock": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::AddPrivateShopItemStock" + ] + }, + "CPythonShop::DelPrivateShopItemStock": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::DelPrivateShopItemStock" + ] + }, + "CPythonShop::GetPrivateShopItemPrice": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::GetPrivateShopItemPrice" + ] + }, + "CPythonShop::BuildPrivateShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::BuildPrivateShop" + ] + }, + "CPythonShop::Open": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::Open" + ] + }, + "CPythonShop::Close": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::Close" + ] + }, + "CPythonShop::IsOpen": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::IsOpen" + ] + }, + "CPythonShop::IsPrivateShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::IsPrivateShop" + ] + }, + "CPythonShop::IsMainPlayerPrivateShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::IsMainPlayerPrivateShop" + ] + }, + "CPythonShop::Clear": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::Clear" + ] + }, + "CPythonShop::CPythonShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::CPythonShop" + ] + }, + "CPythonShop::~CPythonShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:CPythonShop::~CPythonShop" + ] + }, + "shopOpen": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopOpen" + ] + }, + "shopClose": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopClose" + ] + }, + "shopIsOpen": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopIsOpen" + ] + }, + "shopIsPrviateShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopIsPrviateShop" + ] + }, + "shopIsMainPlayerPrivateShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopIsMainPlayerPrivateShop" + ] + }, + "shopGetItemID": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetItemID" + ] + }, + "shopGetItemCount": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetItemCount" + ] + }, + "shopGetItemPrice": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetItemPrice" + ] + }, + "shopGetItemMetinSocket": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetItemMetinSocket" + ] + }, + "shopGetItemAttribute": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetItemAttribute" + ] + }, + "shopClearPrivateShopStock": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopClearPrivateShopStock" + ] + }, + "shopAddPrivateShopItemStock": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopAddPrivateShopItemStock" + ] + }, + "shopDelPrivateShopItemStock": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopDelPrivateShopItemStock" + ] + }, + "shopGetPrivateShopItemPrice": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetPrivateShopItemPrice" + ] + }, + "shopBuildPrivateShop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopBuildPrivateShop" + ] + }, + "shopGetTabCount": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetTabCount" + ] + }, + "shopGetTabName": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetTabName" + ] + }, + "shopGetTabCoinType": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:shopGetTabCoinType" + ] + }, + "initshop": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonShop.cpp:initshop" + ] + } + } +} diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index cecc1723..4d520bcc 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -454,6 +454,8 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 批次 4-a 文字输入(2026-09-24):`UserInterface/PythonIME.cpp` 与 `PythonIMEModule.cpp` 原样移植(仅把 MSVC 专有的 `unsigned char(x)` 函数式转换改成 C 式转换),删除 ime 模块桩,`initime()` 按 40250 顺序在 `initdbg()` 之后,`CPythonApplication::Create` 按原序 `CPythonIME::Create` + `SetText("", 0)`。`EterLib/IME.cpp` 在 platform 下:缓冲区、光标、`WMChar`(退格、`|` 转义、按输入代码页 `MultiByteToWideChar`)、候选/读音状态、数字/字符串模式等函数体照抄;IMM/TSF 在 Godot 下没有对应物,组字相关处理器为空、`EnableIME` 恒关,已提交的非单字节文本由 `PythonBoot::UIChar` 模拟 `GCS_RESULTSTR`(`InsertString` + `OnUpdate`),`DelCurPos` 用 memmove 替代重叠的 wcscpy(全部记 ADAPTED)。`GameApplicationAdapter` 的 `RunIME*` 转发到 `CWindowManager`;注意 `RunIMEUpdate/RunIMEKeyDown` 只送达正在渲染的活动窗口(登录框未打开时输入被丢弃)。Godot 侧 `python_ui_surface.gd` 把按键送 `ui_ime_key`(VK)再送 `ui_char`(unicode)。`port.login_flow` 与 `port.login_live` 打开登录框后逐字键入账号(含 LEFT/DELETE/BACK 编辑)、TAB 切到密码、RETURN 登录(凭据只来自环境变量,测试随后删除 Python 局部变量);进游戏后 RETURN 打开聊天、键入 hello、RETURN 发出 `CG_CHAT`(假服回显 `GC_CHAT`),空行 RETURN 关闭聊天(40250 发完后只清空、不关闭)。`python_game_render_test` 用 Godot 按键事件走同一路径登录。 批次 4-b 战斗(2026-09-24):战斗代码此前已原样移植(`InstanceBaseBattle`、`ActorInstanceCollisionDetection`、`PythonPlayerInput*`、`uiTarget`),本批只补验证。假服在出生点北 18 m 放一只野狗(npclist 101,怪物不发 `GC_CHAR_ADDITIONAL_INFO`,名字取自 mob_proto;发了会 syserr),出生点在村庄 `ATTRIBUTE_BANPK` 圈内,`CInstanceBase::IsInSafe()` 会挡掉所有命中。`CG_TARGET` 回 `GC_TARGET`,`CG_ATTACK` 回 `GC_DAMAGE_INFO` + `GC_TARGET`,第三下回 `GC_DEAD`。与 40250 相同:单击怪物(`__OnClickActor`)只走过去,按住左键(MBF_SMART → `__OnPressActor`)才走近并连击。`port.login_flow` 第 9 步用显式透视矩阵投影野狗、按住左键直到死亡,检查 target 面板和至少 3 次 `CG_ATTACK`;`python_game_render_test` 用 Godot 鼠标事件走同一路径,截图 combat_mid/combat.png(野狗位于镜头与战士的连线上,被战士挡住,属正常)。新增宿主/测试辅助 `PythonBoot::Evaluate` / `Metin2PythonHost.evaluate`(40250 无对应物)。`python_3d_surface.gd` 修正:D3D 固定管线中材质 emissive 先加进顶点色,再被纹理 MODULATE,Godot 的 emission 却加在纹理之后,导致黑底加色的点击光圈(click_glow_select copy.jpg)显示成灰色方块,角色也被 0.32 的 emissive 洗灰;现在 emission 乘以同一张纹理(`EMISSION_OP_MULTIPLY`)。战斗步骤先等镜头稳定(上一步的走路可能尚未结束),野狗投影落在小地图或任务栏下时先点地面走近一段。已知缺口:伤害数字依赖 `CPythonSystem::IsShowDamage()`,该单元仍在 pending 桩(4-g),故暂不显示。 + 批次 4-c 商店(2026-09-24):`PythonShop.cpp` 原样移植(`shop` 模块 + `SHOP_SLOT_COUNT`/金币常量,`initshop()` 按 40250 次序接在 `initfly()` 之后;40250 中间的 `initsnd`/`initeventmgr` 尚未移植),删去 pending 桩与模块桩。uishop 在 `OnUpdate` 里读 `app.GetCursor()`,而 `GetCursorNum()` 是头文件内联、读的是不存在的 CPythonApplication 对象的成员,导致空指针崩溃;现把 40250 `PythonApplicationCursor.cpp`(SetCursorNum/Visible/Mode 等)和 `SetMouseHandler` 原样搬进 platform CPythonApplication,成员改为文件静态量,`GetCursorNum` 改为非内联(头文件 PORT 注释);宿主不画硬件光标,`[0, CURSOR_COUNT)` 内的形状都视为已加载。假服在野狗东 4 m 放杂货商(npclist 9003),`CG_ON_CLICK` 回 `GC_SHOP START`(货架 0 为 27001 红药,50 金),`CG_SHOP BUY` 先扣金(`GC_POINT_CHANGE`)再 `GC_ITEM_SET`,`SELL2` 回 `GC_ITEM_DEL` + 加 9 金,`END` 回 `GC_SHOP END`(与 40250 `CShop::Buy`/`CShopManager::Sell` 次序相同;包的附加长度按 `CInputMain::Shop` 读)。`port.login_flow` 第 10 步:左键点商人走过去打开商店窗口和背包,右键货架 0 购买,背包格 0 拾起后放到货架空格出售,ESC 关闭,逐步检查 `CG_SHOP` 字节、背包格和金币。 + 每个切片都必须同时跑 legacy/port 双路回归;默认路由已切到 `port`(2026-09-24);port 路由稳定后删除总开关和剩余 legacy 宿主。 ## 5. 数据源切换:m2dev assets → 40250 diff --git a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp index a8fa32d1..045a9838 100644 --- a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp +++ b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp @@ -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) diff --git a/extension/src/platform/ScriptLib/GameplayModules.cpp b/extension/src/platform/ScriptLib/GameplayModules.cpp index bb4f60eb..ad1c4392 100644 --- a/extension/src/platform/ScriptLib/GameplayModules.cpp +++ b/extension/src/platform/ScriptLib/GameplayModules.cpp @@ -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); diff --git a/extension/src/platform/ScriptLib/PythonBoot.cpp b/extension/src/platform/ScriptLib/PythonBoot.cpp index b349a90e..afa93aeb 100644 --- a/extension/src/platform/ScriptLib/PythonBoot.cpp +++ b/extension/src/platform/ScriptLib/PythonBoot.cpp @@ -402,6 +402,7 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error) initMiniMap(); initeffect(); initfly(); + initshop(); initskill(); initBackground(); initServerStateChecker(); diff --git a/extension/src/platform/UserInterface/PythonApplication.cpp b/extension/src/platform/UserInterface/PythonApplication.cpp index 20f951a0..f0721cd6 100644 --- a/extension/src/platform/UserInterface/PythonApplication.cpp +++ b/extension/src/platform/UserInterface/PythonApplication.cpp @@ -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; } diff --git a/extension/src/platform/pending/UserInterface/PythonShop.cpp b/extension/src/platform/pending/UserInterface/PythonShop.cpp deleted file mode 100644 index 8213862a..00000000 --- a/extension/src/platform/pending/UserInterface/PythonShop.cpp +++ /dev/null @@ -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(); -} diff --git a/extension/src/port/UserInterface/PythonApplication.h b/extension/src/port/UserInterface/PythonApplication.h index b249940a..fadda877 100644 --- a/extension/src/port/UserInterface/PythonApplication.h +++ b/extension/src/port/UserInterface/PythonApplication.h @@ -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); diff --git a/extension/src/port/UserInterface/PythonShop.cpp b/extension/src/port/UserInterface/PythonShop.cpp new file mode 100644 index 00000000..4fd3e293 --- /dev/null +++ b/extension/src/port/UserInterface/PythonShop.cpp @@ -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 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); +} \ No newline at end of file diff --git a/extension/src/port/UserInterface/StdAfx.h b/extension/src/port/UserInterface/StdAfx.h index c031b867..49bc269c 100644 --- a/extension/src/port/UserInterface/StdAfx.h +++ b/extension/src/port/UserInterface/StdAfx.h @@ -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(); diff --git a/extension/tests/port_login_flow_server.cpp b/extension/tests/port_login_flow_server.cpp index e1311b91..f0a4ef54 100644 --- a/extension/tests/port_login_flow_server.cpp +++ b/extension/tests/port_login_flow_server.cpp @@ -467,6 +467,7 @@ bool FakeLoginServer::ServeGame(Connection& c, int index) auto points = zeroed(); points.header = HDR_GC_CHARACTER_POINTS; points.points[1] = 10; // POINT_LEVEL + points.points[11] = kStartGold; // POINT_GOLD auto skill = zeroed(); 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(); + 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(); + 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(); + 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 ":move ". 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(); + static_assert(sizeof(start) == 4 + 4 + 40 * 43); + start.head = {HDR_GC_SHOP, static_cast(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(); + 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(); + set.header = HDR_GC_ITEM_SET; + set.cell = {1, 0}; // INVENTORY, cell 0 + set.vnum = kShopItemVnum; + set.count = 1; + ok = gold_change(-static_cast(kShopItemPrice)) && c.Send(set); + } + else if (sub == SHOP_CG_SELL2 && shopping && packet[2] == 0 && bag0 != 0) + { + bag0 = 0; + GCItemDelDeprecated del = zeroed(); + 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; diff --git a/extension/tests/port_login_flow_server.h b/extension/tests/port_login_flow_server.h index 5f96f069..a0bb095a 100644 --- a/extension/tests/port_login_flow_server.h +++ b/extension/tests/port_login_flow_server.h @@ -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(); diff --git a/extension/tests/port_login_flow_test.cpp b/extension/tests/port_login_flow_test.cpp index 0c21bd18..3f9cc3a2 100644 --- a/extension/tests/port_login_flow_test.cpp +++ b/extension/tests/port_login_flow_test.cpp @@ -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 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 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); }