From ee7a9cd3e209f9def3270f34f4a9f779274a7575 Mon Sep 17 00:00:00 2001 From: shenlei Date: Thu, 24 Sep 2026 09:53:02 +0900 Subject: [PATCH] WIP port 4-d quest dialogue modules --- audit/history.jsonl | 1 + audit/port-map/EterLib/parser.cpp.json | 44 + .../UserInterface/PythonEventManager.cpp.json | 128 ++ .../PythonEventManagerMoudle.cpp.json | 80 ++ .../PythonGameEventManagerModule.cpp.json | 14 + .../UserInterface/PythonQuest.cpp.json | 77 ++ docs/PORT-PLAN.md | 3 +- .../ScriptLib/GameplayModuleConstants.cpp | 20 - .../platform/ScriptLib/GameplayModules.cpp | 88 -- .../src/platform/ScriptLib/PythonBoot.cpp | 3 + .../UserInterface/PythonEventManager.cpp | 28 - .../pending/UserInterface/PythonQuest.cpp | 68 - extension/src/port/EterLib/parser.cpp | 443 ++++++ .../port/UserInterface/PythonEventManager.cpp | 1211 +++++++++++++++++ .../PythonEventManagerMoudle.cpp | 315 +++++ .../PythonGameEventManagerModule.cpp | 30 + .../src/port/UserInterface/PythonQuest.cpp | 270 ++++ extension/src/port/UserInterface/StdAfx.h | 3 + extension/src/port/common/Win32Crt.cpp | 31 + extension/src/port/common/Win32Crt.h | 6 + extension/tests/port_login_flow_server.cpp | 51 + extension/tests/port_login_flow_server.h | 4 + 22 files changed, 2713 insertions(+), 205 deletions(-) create mode 100644 audit/port-map/EterLib/parser.cpp.json create mode 100644 audit/port-map/UserInterface/PythonEventManager.cpp.json create mode 100644 audit/port-map/UserInterface/PythonEventManagerMoudle.cpp.json create mode 100644 audit/port-map/UserInterface/PythonGameEventManagerModule.cpp.json create mode 100644 audit/port-map/UserInterface/PythonQuest.cpp.json delete mode 100644 extension/src/platform/pending/UserInterface/PythonEventManager.cpp delete mode 100644 extension/src/platform/pending/UserInterface/PythonQuest.cpp create mode 100644 extension/src/port/EterLib/parser.cpp create mode 100644 extension/src/port/UserInterface/PythonEventManager.cpp create mode 100644 extension/src/port/UserInterface/PythonEventManagerMoudle.cpp create mode 100644 extension/src/port/UserInterface/PythonGameEventManagerModule.cpp create mode 100644 extension/src/port/UserInterface/PythonQuest.cpp diff --git a/audit/history.jsonl b/audit/history.jsonl index 7046286f..61a3037a 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -543,3 +543,4 @@ {"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"]} +{"time": "2026-09-24T22:00:00Z", "event": "wip", "unit": "batch 4-d NPC quest dialogue", "detail": "Added event manager, quest state/module, event manager module, parser and DBCS helpers; fake server now has an Oldster NPC and preliminary GC_SCRIPT / CG_SCRIPT_ANSWER exchange. This is explicitly unfinished: login-flow UI round trip and rendering interaction are not wired/verified; all five source-unit port-map entries remain TODO.", "tests": []} diff --git a/audit/port-map/EterLib/parser.cpp.json b/audit/port-map/EterLib/parser.cpp.json new file mode 100644 index 00000000..6f976fb3 --- /dev/null +++ b/audit/port-map/EterLib/parser.cpp.json @@ -0,0 +1,44 @@ +{ + "reference": "EterLib/parser.cpp", + "reference_sha256": "1b81a802b14e1a5958aaf981ca81248e389ad5c6f572858fd31e4afb7674ba86", + "priority": "P4", + "contracts": [], + "functions": { + "LocaleString_FindChar": { + "status": "TODO" + }, + "LocaleString_RightTrim": { + "status": "TODO" + }, + "OLD_rtrim": { + "status": "TODO" + }, + "LocaleString_Skip": { + "status": "TODO" + }, + "Group::GetArg": { + "status": "TODO" + }, + "Group::Create": { + "status": "TODO" + }, + "Group::GetCmd": { + "status": "TODO" + }, + "Group::ReadCmd": { + "status": "TODO" + }, + "Group::GetError": { + "status": "TODO" + }, + "Group::SetError": { + "status": "TODO" + }, + "Group::Group": { + "status": "TODO" + }, + "Group::~Group": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/UserInterface/PythonEventManager.cpp.json b/audit/port-map/UserInterface/PythonEventManager.cpp.json new file mode 100644 index 00000000..dae8f08d --- /dev/null +++ b/audit/port-map/UserInterface/PythonEventManager.cpp.json @@ -0,0 +1,128 @@ +{ + "reference": "UserInterface/PythonEventManager.cpp", + "reference_sha256": "db306daefe5948ad7b3d533266bea7ef04826b22311c72d421c4f23c647de8e2", + "priority": "P4", + "contracts": [], + "functions": { + "ShowArgument": { + "status": "TODO" + }, + "GetArgumentString": { + "status": "TODO" + }, + "GetArgument": { + "status": "TODO" + }, + "GetCameraSettingFromArgList": { + "status": "TODO" + }, + "CPythonEventManager::__InitEventSet": { + "status": "TODO" + }, + "CPythonEventManager::RegisterEventSet": { + "status": "TODO" + }, + "CPythonEventManager::RegisterEventSetFromString": { + "status": "TODO" + }, + "CPythonEventManager::ClearEventSeti": { + "status": "TODO" + }, + "CPythonEventManager::__ClearEventSetp": { + "status": "TODO" + }, + "CPythonEventManager::GetEmptyEventSetSlot": { + "status": "TODO" + }, + "CPythonEventManager::SetRestrictedCount": { + "status": "TODO" + }, + "CPythonEventManager::SetEventHandler": { + "status": "TODO" + }, + "CPythonEventManager::GetEventSetLocalYPosition": { + "status": "TODO" + }, + "CPythonEventManager::AddEventSetLocalYPosition": { + "status": "TODO" + }, + "CPythonEventManager::InsertText": { + "status": "TODO" + }, + "CPythonEventManager::UpdateEventSet": { + "status": "TODO" + }, + "CPythonEventManager::SetEventSetWidth": { + "status": "TODO" + }, + "CPythonEventManager::ProcessEventSet": { + "status": "TODO" + }, + "CPythonEventManager::RenderEventSet": { + "status": "TODO" + }, + "CPythonEventManager::Skip": { + "status": "TODO" + }, + "CPythonEventManager::IsWait": { + "status": "TODO" + }, + "CPythonEventManager::EndEventProcess": { + "status": "TODO" + }, + "CPythonEventManager::MakeNextButton": { + "status": "TODO" + }, + "CPythonEventManager::MakeQuestion": { + "status": "TODO" + }, + "CPythonEventManager::SelectAnswer": { + "status": "TODO" + }, + "CPythonEventManager::SetVisibleStartLine": { + "status": "TODO" + }, + "CPythonEventManager::SetVisibleLineCount": { + "status": "TODO" + }, + "CPythonEventManager::GetVisibleStartLine": { + "status": "TODO" + }, + "CPythonEventManager::GetLineCount": { + "status": "TODO" + }, + "CPythonEventManager::ClearLine": { + "status": "TODO" + }, + "CPythonEventManager::__InsertLine": { + "status": "TODO" + }, + "CPythonEventManager::RefreshLinePosition": { + "status": "TODO" + }, + "CPythonEventManager::__AddSpace": { + "status": "TODO" + }, + "CPythonEventManager::GetScriptEventIndex": { + "status": "TODO" + }, + "CPythonEventManager::CheckEventSetIndex": { + "status": "TODO" + }, + "CPythonEventManager::Destroy": { + "status": "TODO" + }, + "CPythonEventManager::SetInterfaceWindow": { + "status": "TODO" + }, + "CPythonEventManager::SetLeftTimeString": { + "status": "TODO" + }, + "CPythonEventManager::CPythonEventManager": { + "status": "TODO" + }, + "CPythonEventManager::~CPythonEventManager": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/UserInterface/PythonEventManagerMoudle.cpp.json b/audit/port-map/UserInterface/PythonEventManagerMoudle.cpp.json new file mode 100644 index 00000000..a64fddf8 --- /dev/null +++ b/audit/port-map/UserInterface/PythonEventManagerMoudle.cpp.json @@ -0,0 +1,80 @@ +{ + "reference": "UserInterface/PythonEventManagerMoudle.cpp", + "reference_sha256": "104c2b16cef91b4d41844caa10851ff82e8e2766c264aaaeca3b7f246a17e4ae", + "priority": "P4", + "contracts": [], + "functions": { + "eventRegisterEventSet": { + "status": "TODO" + }, + "eventRegisterEventSetFromString": { + "status": "TODO" + }, + "eventClearEventSet": { + "status": "TODO" + }, + "eventSetRestrictedCount": { + "status": "TODO" + }, + "eventGetEventSetLocalYPosition": { + "status": "TODO" + }, + "eventAddEventSetLocalYPosition": { + "status": "TODO" + }, + "eventInsertText": { + "status": "TODO" + }, + "eventInsertTextInline": { + "status": "TODO" + }, + "eventUpdateEventSet": { + "status": "TODO" + }, + "eventRenderEventSet": { + "status": "TODO" + }, + "eventSetEventSetWidth": { + "status": "TODO" + }, + "eventSkip": { + "status": "TODO" + }, + "eventIsWait": { + "status": "TODO" + }, + "eventEndEventProcess": { + "status": "TODO" + }, + "eventSetEventHandler": { + "status": "TODO" + }, + "eventSelectAnswer": { + "status": "TODO" + }, + "eventGetLineCount": { + "status": "TODO" + }, + "eventSetVisibleStartLine": { + "status": "TODO" + }, + "eventGetVisibleStartLine": { + "status": "TODO" + }, + "eventQuestButtonClick": { + "status": "TODO" + }, + "eventSetInterfaceWindow": { + "status": "TODO" + }, + "eventSetLeftTimeString": { + "status": "TODO" + }, + "eventDestroy": { + "status": "TODO" + }, + "initEvent": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/UserInterface/PythonGameEventManagerModule.cpp.json b/audit/port-map/UserInterface/PythonGameEventManagerModule.cpp.json new file mode 100644 index 00000000..7e0c06e7 --- /dev/null +++ b/audit/port-map/UserInterface/PythonGameEventManagerModule.cpp.json @@ -0,0 +1,14 @@ +{ + "reference": "UserInterface/PythonGameEventManagerModule.cpp", + "reference_sha256": "233eb8e6439422e3262240f8e22e8df6a4049122fe110a218b5c952207414e2e", + "priority": "P4", + "contracts": [], + "functions": { + "eventMgrUpdate": { + "status": "TODO" + }, + "initeventmgr": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/UserInterface/PythonQuest.cpp.json b/audit/port-map/UserInterface/PythonQuest.cpp.json new file mode 100644 index 00000000..a4fa2044 --- /dev/null +++ b/audit/port-map/UserInterface/PythonQuest.cpp.json @@ -0,0 +1,77 @@ +{ + "reference": "UserInterface/PythonQuest.cpp", + "reference_sha256": "2e48148f3da65c4e286843d1b225aaa2021f2731f9f024fe989d1489c6e5f546", + "priority": "P3", + "contracts": [], + "functions": { + "CPythonQuest::RegisterQuestInstance": { + "status": "TODO" + }, + "CPythonQuest::DeleteQuestInstance": { + "status": "TODO" + }, + "CPythonQuest::IsQuest": { + "status": "TODO" + }, + "CPythonQuest::MakeQuest": { + "status": "TODO" + }, + "CPythonQuest::SetQuestTitle": { + "status": "TODO" + }, + "CPythonQuest::SetQuestClockName": { + "status": "TODO" + }, + "CPythonQuest::SetQuestCounterName": { + "status": "TODO" + }, + "CPythonQuest::SetQuestClockValue": { + "status": "TODO" + }, + "CPythonQuest::SetQuestCounterValue": { + "status": "TODO" + }, + "CPythonQuest::SetQuestIconFileName": { + "status": "TODO" + }, + "CPythonQuest::GetQuestCount": { + "status": "TODO" + }, + "CPythonQuest::GetQuestInstancePtr": { + "status": "TODO" + }, + "CPythonQuest::__GetQuestInstancePtr": { + "status": "TODO" + }, + "CPythonQuest::__Initialize": { + "status": "TODO" + }, + "CPythonQuest::Clear": { + "status": "TODO" + }, + "CPythonQuest::CPythonQuest": { + "status": "TODO" + }, + "CPythonQuest::~CPythonQuest": { + "status": "TODO" + }, + "questGetQuestCount": { + "status": "TODO" + }, + "questGetQuestData": { + "status": "TODO" + }, + "questGetQuestIndex": { + "status": "TODO" + }, + "questGetQuestLastTime": { + "status": "TODO" + }, + "questClear": { + "status": "TODO" + }, + "initquest": { + "status": "TODO" + } + } +} diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index 4d520bcc..e9b9a22c 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -450,11 +450,12 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 批次 3-b EffectLib(2026-09-24):`EffectLib/` 18 个 .cpp 原样移植(EffectManager、EffectData/Instance、EffectMesh、粒子系统、SimpleLight 等),删除 `platform/EffectLib` 生成的桩骨架;唯一改动是 `Type.h` 里 `BILLBOARD_TYPE_2FACE` 行尾注释的 `\` 被 clang 当作续行吞掉下一行,去掉反斜杠。`platform/EterBase/Utils.cpp` 剩下的 21 个桩按 §3 照抄 40250 函数体(`GetOnlyPathName`/`IsGlobalFileName` 缺失会让 .msf 以相对名挂的 .mse 丢目录,106 个 RegisterEffect 报错),Win32 调用换成 POSIX 的记 ADAPTED;平台 `CPythonResource` 补上 40250 的 `mde`(CEffectMesh)和 `mdatr`(CAttributeData)资源类型。`port.utils` 覆盖路径/分词/命令行辅助函数;`port.login_flow`:在主角脚下 `CreateEffect(level_up.mse)`,RenderGame 的 `CEffectManager::Render` 录出 levelup_1 贴图的 alpha 混合粒子绘制(dust、dropitem 同样出现)。40250 声明但未定义 `GetRenderingEffectCount`,测试不用。 批次 3-c CMapOutdoor(2026-09-24):`GameLib/` 的 Area、AreaTerrain、DungeonBlock、MapBase、MapManager、MapOutdoor(含 IndexBuffer/Load/Quadtree/Render/Update)、MapType、MonsterAreaInfo、Property/PropertyLoader/PropertyManager、SnowEnvironment/SnowParticle、TerrainDecal/Patch/Quadtree,`PRTerrainLib/` Terrain、TextureSet,`UserInterface/PythonBackground(+Module)`,`EterLib/ColorTransitionHelper` 原样移植,删除 pending 替身与 background 模块桩(含 GameplayModuleConstants 里的常量桩);`EterBase/FileDir`、`TempFile` 在 platform 下照抄 40250 函数体(Win32 查找/临时文件走 common 垫片,记 ADAPTED)。`TEMP_CAreaLoaderThread` 在 40250 已全部注释掉,.cpp 不编(N_A)。手改两处:`CMapManager::BeginEnvironment` 的 `ApplyLight` 标签指针经 `uintptr_t` 转 DWORD;`CTerrainImpl::LoadWaterMap` 按 32 位读 water.wtr 的 `long` 再逐个扩宽(LP64 下 `long` 是 8 字节,否则报 FILE DATA SIZE 16392 != 16400)。平台文件:HTP/STP 地形绘制与 `RenderWater` 为空(地形仍由 Godot 适配层画,§3),STP 的 Initialize/Create/Destroy、水面贴图载入、角色阴影贴图创建照抄;`BeginRenderCharacterShadowToTexture` 返回 false(记 DIVERGENT)。录制设备补齐 `CreateTexture/VertexBuffer/IndexBuffer/DepthStencilSurface`、`Get/SetRenderTarget`、`LightEnable`、`Clear`(CPU 表面/贴图),离屏渲染目标的绘制丢弃,light0 只在启用且为方向光时记录。小地图:`CPythonMiniMap` 在正交投影下画 minimap.dds 瓦片并以 stage 1 相机空间坐标 × `D3DTS_TEXTURE1` 采样 `minimap_image_filter.dds` 做圆形遮罩——录制设备把「正交投影 + 未变换顶点」的绘制按四边形转成 UI 命令(带 mask 贴图与四角 mask uv),`UIRenderAddImage` 按 D3DTS_WORLD 平移、按 TFACTOR 调色(标记点),`python_ui_surface.gd` 用按序的子 canvas item 画 UI 流,带遮罩的图各用一个 shader 材质。`port.login_flow`:Warp 后 `IsMapOutdoor()`、`miniMap.GetAtlasSize()` 已载入、`background.GetCurrentMapName()` 非空;`python_game_render_test` 新增「minimap tiles drawn through the filter mask」(9 块)。截图里主角旁地上的剑是假服 entergame 时按 `GC_ITEM_GROUND_ADD` 放的地面物品(vnum 19,P3-b 拾取测试用),不是武器挂点问题;假服角色没装备武器。 默认路由切换(2026-09-24):`client_main` 不设 `MT_PORT_PATH` 时走 port 路由(`python_ui_surface.gd` + `python_3d_surface.gd` 跑 40250 system.py),`MT_PORT_PATH=legacy` 回到 GDScript `AppFlow`,规则见 §2「迁移方式」。已验证:实例化 `client_main.tscn`,默认/`port` 起 PythonUISurface + Python3DSurface 并产出 UI 命令,`legacy` 起 AppFlow + NetTrace。下一步:port 路由稳定后删除 legacy 宿主与开关。 - 批次 4(2026-09-24 起):补齐 port 路由的玩法,以便删除 legacy。顺序:4-a 文字输入 → 4-b 战斗 → 4-c 商店 → 4-d 任务/NPC 对话 → 4-e 仓库 → 4-f 好友/公会/公会标记 → 4-g systemSetting → 4-h 声音。 + 批次 4(2026-09-24 起):补齐 port 路由的玩法,以便删除 legacy。顺序:4-a 文字输入(完成) → 4-b 战斗(完成) → 4-c 商店(完成) → 4-d 任务/NPC 对话(进行中,未完成) → 4-e 仓库(未开始) → 4-f 好友/公会/公会标记(未开始) → 4-g systemSetting(未开始) → 4-h 声音(未开始)。 批次 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` 字节、背包格和金币。 + 批次 4-d 任务/NPC 对话(进行中,未完成,2026-09-24):已加入 `PythonEventManager`、`PythonQuest`、`GameEventManager` 逻辑及 Python 模块,并开始扩展假服对话 NPC 和 `GC_SCRIPT` / `CG_SCRIPT_ANSWER` 应答;`parser.cpp` 和 DBCS 字符串步进也作为事件脚本解析依赖加入。当前还没有在 `port_login_flow` 中接通并验证完整的点击 NPC → 显示对话 → 选择答案 → NEXT/DONE 往返流程,也未做门禁/回归确认。相关逐函数审计仍为 `TODO`;本批不能记为完成。 每个切片都必须同时跑 legacy/port 双路回归;默认路由已切到 `port`(2026-09-24);port 路由稳定后删除总开关和剩余 legacy 宿主。 diff --git a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp index 045a9838..ac10cac0 100644 --- a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp +++ b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp @@ -36,21 +36,7 @@ // 40250 UserInterface/PythonBackgroundModule.cpp // 40250 UserInterface/PythonEventManagerMoudle.cpp -void AddStubConstants_event(PyObject* poModule) -{ - (void) poModule; - PyModule_AddIntConstant(poModule, "BOX_VISIBLE_LINE_COUNT", CPythonEventManager::BOX_VISIBLE_LINE_COUNT); - PyModule_AddIntConstant(poModule, "BUTTON_TYPE_NEXT", CPythonEventManager::BUTTON_TYPE_NEXT); - PyModule_AddIntConstant(poModule, "BUTTON_TYPE_DONE", CPythonEventManager::BUTTON_TYPE_DONE); - PyModule_AddIntConstant(poModule, "BUTTON_TYPE_CANCEL", CPythonEventManager::BUTTON_TYPE_CANCEL); -} - // 40250 UserInterface/PythonGameEventManagerModule.cpp -void AddStubConstants_eventMgr(PyObject* poModule) -{ - (void) poModule; -} - // 40250 UserInterface/PythonGuild.cpp void AddStubConstants_guild(PyObject* poModule) { @@ -76,12 +62,6 @@ void AddStubConstants_profiler(PyObject* poModule) } // 40250 UserInterface/PythonQuest.cpp -void AddStubConstants_quest(PyObject* poModule) -{ - (void) poModule; - PyModule_AddIntConstant(poModule, "QUEST_MAX_NUM", 5); -} - // 40250 UserInterface/PythonSafeBox.cpp void AddStubConstants_safebox(PyObject* poModule) { diff --git a/extension/src/platform/ScriptLib/GameplayModules.cpp b/extension/src/platform/ScriptLib/GameplayModules.cpp index ad1c4392..40643517 100644 --- a/extension/src/platform/ScriptLib/GameplayModules.cpp +++ b/extension/src/platform/ScriptLib/GameplayModules.cpp @@ -5,12 +5,9 @@ #include "../EterLib/UIRenderCommands.h" // GameplayModuleConstants.cpp: each module's 40250 constants. -void AddStubConstants_event(PyObject* poModule); -void AddStubConstants_eventMgr(PyObject* poModule); void AddStubConstants_guild(PyObject* poModule); void AddStubConstants_messenger(PyObject* poModule); void AddStubConstants_profiler(PyObject* poModule); -void AddStubConstants_quest(PyObject* poModule); void AddStubConstants_safebox(PyObject* poModule); void AddStubConstants_snd(PyObject* poModule); void AddStubConstants_systemSetting(PyObject* poModule); @@ -24,62 +21,6 @@ PyObject* stub_call_named(const char* name, PyObject* result = nullptr) { return result ? result : Py_BuildValue("i", 0); } -// 40250 UserInterface/PythonEventManagerMoudle.cpp: event export names. -PyObject* stub_event_RegisterEventSet(PyObject*, PyObject*) { return stub_call_named("event.RegisterEventSet", Py_BuildValue("i", 0)); } -PyObject* stub_event_RegisterEventSetFromString(PyObject*, PyObject*) { return stub_call_named("event.RegisterEventSetFromString", Py_BuildValue("i", 0)); } -PyObject* stub_event_ClearEventSet(PyObject*, PyObject*) { return stub_call_named("event.ClearEventSet", Py_BuildNone()); } -PyObject* stub_event_SetRestrictedCount(PyObject*, PyObject*) { return stub_call_named("event.SetRestrictedCount", Py_BuildNone()); } -PyObject* stub_event_GetEventSetLocalYPosition(PyObject*, PyObject*) { return stub_call_named("event.GetEventSetLocalYPosition", Py_BuildValue("i", 0)); } -PyObject* stub_event_AddEventSetLocalYPosition(PyObject*, PyObject*) { return stub_call_named("event.AddEventSetLocalYPosition", Py_BuildNone()); } -PyObject* stub_event_InsertText(PyObject*, PyObject*) { return stub_call_named("event.InsertText", Py_BuildNone()); } -PyObject* stub_event_InsertTextInline(PyObject*, PyObject*) { return stub_call_named("event.InsertTextInline", Py_BuildNone()); } -PyObject* stub_event_UpdateEventSet(PyObject*, PyObject*) { return stub_call_named("event.UpdateEventSet", Py_BuildNone()); } -PyObject* stub_event_RenderEventSet(PyObject*, PyObject*) { return stub_call_named("event.RenderEventSet", Py_BuildNone()); } -PyObject* stub_event_SetEventSetWidth(PyObject*, PyObject*) { return stub_call_named("event.SetEventSetWidth", Py_BuildNone()); } -PyObject* stub_event_Skip(PyObject*, PyObject*) { return stub_call_named("event.Skip", Py_BuildNone()); } -PyObject* stub_event_IsWait(PyObject*, PyObject*) { return stub_call_named("event.IsWait", Py_BuildValue("i", 0)); } -PyObject* stub_event_EndEventProcess(PyObject*, PyObject*) { return stub_call_named("event.EndEventProcess", Py_BuildNone()); } -PyObject* stub_event_SelectAnswer(PyObject*, PyObject*) { return stub_call_named("event.SelectAnswer", Py_BuildNone()); } -PyObject* stub_event_GetLineCount(PyObject*, PyObject*) { return stub_call_named("event.GetLineCount", Py_BuildValue("i", 0)); } -PyObject* stub_event_SetVisibleStartLine(PyObject*, PyObject*) { return stub_call_named("event.SetVisibleStartLine", Py_BuildNone()); } -PyObject* stub_event_GetVisibleStartLine(PyObject*, PyObject*) { return stub_call_named("event.GetVisibleStartLine", Py_BuildValue("i", 0)); } -PyObject* stub_event_SetEventHandler(PyObject*, PyObject*) { return stub_call_named("event.SetEventHandler", Py_BuildNone()); } -PyObject* stub_event_SetInterfaceWindow(PyObject*, PyObject*) { return stub_call_named("event.SetInterfaceWindow", Py_BuildNone()); } -PyObject* stub_event_SetLeftTimeString(PyObject*, PyObject*) { return stub_call_named("event.SetLeftTimeString", Py_BuildNone()); } -PyObject* stub_event_QuestButtonClick(PyObject*, PyObject*) { return stub_call_named("event.QuestButtonClick", Py_BuildNone()); } -PyObject* stub_event_Destroy(PyObject*, PyObject*) { return stub_call_named("event.Destroy", Py_BuildNone()); } -PyMethodDef methods_event[] = { - { "RegisterEventSet", stub_event_RegisterEventSet, METH_VARARGS }, - { "RegisterEventSetFromString", stub_event_RegisterEventSetFromString, METH_VARARGS }, - { "ClearEventSet", stub_event_ClearEventSet, METH_VARARGS }, - { "SetRestrictedCount", stub_event_SetRestrictedCount, METH_VARARGS }, - { "GetEventSetLocalYPosition", stub_event_GetEventSetLocalYPosition, METH_VARARGS }, - { "AddEventSetLocalYPosition", stub_event_AddEventSetLocalYPosition, METH_VARARGS }, - { "InsertText", stub_event_InsertText, METH_VARARGS }, - { "InsertTextInline", stub_event_InsertTextInline, METH_VARARGS }, - { "UpdateEventSet", stub_event_UpdateEventSet, METH_VARARGS }, - { "RenderEventSet", stub_event_RenderEventSet, METH_VARARGS }, - { "SetEventSetWidth", stub_event_SetEventSetWidth, METH_VARARGS }, - { "Skip", stub_event_Skip, METH_VARARGS }, - { "IsWait", stub_event_IsWait, METH_VARARGS }, - { "EndEventProcess", stub_event_EndEventProcess, METH_VARARGS }, - { "SelectAnswer", stub_event_SelectAnswer, METH_VARARGS }, - { "GetLineCount", stub_event_GetLineCount, METH_VARARGS }, - { "SetVisibleStartLine", stub_event_SetVisibleStartLine, METH_VARARGS }, - { "GetVisibleStartLine", stub_event_GetVisibleStartLine, METH_VARARGS }, - { "SetEventHandler", stub_event_SetEventHandler, METH_VARARGS }, - { "SetInterfaceWindow", stub_event_SetInterfaceWindow, METH_VARARGS }, - { "SetLeftTimeString", stub_event_SetLeftTimeString, METH_VARARGS }, - { "QuestButtonClick", stub_event_QuestButtonClick, METH_VARARGS }, - { "Destroy", stub_event_Destroy, METH_VARARGS }, - { NULL, NULL } -}; -// 40250 UserInterface/PythonGameEventManagerModule.cpp: eventMgr export names. -PyObject* stub_eventMgr_Update(PyObject*, PyObject*) { return stub_call_named("eventMgr.Update", Py_BuildNone()); } -PyMethodDef methods_eventMgr[] = { - { "Update", stub_eventMgr_Update, METH_VARARGS }, - { NULL, NULL } -}; // 40250 UserInterface/PythonGuild.cpp: guild export names. PyObject* stub_guild_IsGuildEnable(PyObject*, PyObject*) { return stub_call_named("guild.IsGuildEnable", Py_BuildValue("i", 0)); } PyObject* stub_guild_GuildIDToMarkID(PyObject*, PyObject*) { return stub_call_named("guild.GuildIDToMarkID", Py_BuildValue("i", 0)); } @@ -174,20 +115,6 @@ PyMethodDef methods_profiler[] = { { "Pop", stub_profiler_Pop, METH_VARARGS }, { NULL, NULL } }; -// 40250 UserInterface/PythonQuest.cpp: quest export names. -PyObject* stub_quest_GetQuestCount(PyObject*, PyObject*) { return stub_call_named("quest.GetQuestCount", Py_BuildValue("i", 0)); } -PyObject* stub_quest_GetQuestData(PyObject*, PyObject*) { return stub_call_named("quest.GetQuestData", Py_BuildValue("sisi", "", 0, "", 0)); } -PyObject* stub_quest_GetQuestIndex(PyObject*, PyObject*) { return stub_call_named("quest.GetQuestIndex", Py_BuildValue("i", 0)); } -PyObject* stub_quest_GetQuestLastTime(PyObject*, PyObject*) { return stub_call_named("quest.GetQuestLastTime", Py_BuildValue("si", "", 0)); } -PyObject* stub_quest_Clear(PyObject*, PyObject*) { return stub_call_named("quest.Clear", Py_BuildNone()); } -PyMethodDef methods_quest[] = { - { "GetQuestCount", stub_quest_GetQuestCount, METH_VARARGS }, - { "GetQuestData", stub_quest_GetQuestData, METH_VARARGS }, - { "GetQuestIndex", stub_quest_GetQuestIndex, METH_VARARGS }, - { "GetQuestLastTime", stub_quest_GetQuestLastTime, METH_VARARGS }, - { "Clear", stub_quest_Clear, METH_VARARGS }, - { NULL, NULL } -}; // 40250 UserInterface/PythonSafeBox.cpp: safebox export names. PyObject* stub_safebox_GetCurrentSafeboxSize(PyObject*, PyObject*) { return stub_call_named("safebox.GetCurrentSafeboxSize", Py_BuildValue("i", 0)); } PyObject* stub_safebox_GetItemID(PyObject*, PyObject*) { return stub_call_named("safebox.GetItemID", Py_BuildValue("i", 0)); } @@ -344,16 +271,6 @@ PyMethodDef methods_udp[] = { } // namespace void init_2v0_gameplay_stubs() { - { - PyObject* module = Py_InitModule("event", methods_event); - PyModule_AddIntConstant(module, "__port_stub__", 1); - AddStubConstants_event(module); - } - { - PyObject* module = Py_InitModule("eventMgr", methods_eventMgr); - PyModule_AddIntConstant(module, "__port_stub__", 1); - AddStubConstants_eventMgr(module); - } { PyObject* module = Py_InitModule("guild", methods_guild); PyModule_AddIntConstant(module, "__port_stub__", 1); @@ -369,11 +286,6 @@ void init_2v0_gameplay_stubs() { PyModule_AddIntConstant(module, "__port_stub__", 1); AddStubConstants_profiler(module); } - { - PyObject* module = Py_InitModule("quest", methods_quest); - PyModule_AddIntConstant(module, "__port_stub__", 1); - AddStubConstants_quest(module); - } { PyObject* module = Py_InitModule("safebox", methods_safebox); PyModule_AddIntConstant(module, "__port_stub__", 1); diff --git a/extension/src/platform/ScriptLib/PythonBoot.cpp b/extension/src/platform/ScriptLib/PythonBoot.cpp index afa93aeb..81f2c769 100644 --- a/extension/src/platform/ScriptLib/PythonBoot.cpp +++ b/extension/src/platform/ScriptLib/PythonBoot.cpp @@ -400,10 +400,13 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error) initTextTail(); initnet(); initMiniMap(); + initEvent(); initeffect(); initfly(); + initeventmgr(); initshop(); initskill(); + initquest(); initBackground(); initServerStateChecker(); init_2v0_gameplay_stubs(); diff --git a/extension/src/platform/pending/UserInterface/PythonEventManager.cpp b/extension/src/platform/pending/UserInterface/PythonEventManager.cpp deleted file mode 100644 index 758f8c9e..00000000 --- a/extension/src/platform/pending/UserInterface/PythonEventManager.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// Stand-in for the 40250 logic unit UserInterface/PythonEventManager.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/PythonEventManager.h" - -#include "../../PlatformStub.h" - -CPythonEventManager::CPythonEventManager() -{ - MT_PLATFORM_STUB(); -} - -CPythonEventManager::~CPythonEventManager() -{ - MT_PLATFORM_STUB(); -} - -auto CPythonEventManager::RegisterEventSetFromString(const std::string &) -> int -{ - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); -} - -auto CPythonEventManager::SetVisibleLineCount(int, int) -> void -{ - MT_PLATFORM_STUB(); -} diff --git a/extension/src/platform/pending/UserInterface/PythonQuest.cpp b/extension/src/platform/pending/UserInterface/PythonQuest.cpp deleted file mode 100644 index b8071644..00000000 --- a/extension/src/platform/pending/UserInterface/PythonQuest.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// Stand-in for the 40250 logic unit UserInterface/PythonQuest.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/PythonQuest.h" - -#include "../../PlatformStub.h" - -CPythonQuest::CPythonQuest() -{ - MT_PLATFORM_STUB(); -} - -CPythonQuest::~CPythonQuest() -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::RegisterQuestInstance(const SQuestInstance &) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::DeleteQuestInstance(DWORD) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::IsQuest(DWORD) -> bool -{ - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); -} - -auto CPythonQuest::MakeQuest(DWORD) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::SetQuestTitle(DWORD, const char *) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::SetQuestClockName(DWORD, const char *) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::SetQuestCounterName(DWORD, const char *) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::SetQuestClockValue(DWORD, int) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::SetQuestCounterValue(DWORD, int) -> void -{ - MT_PLATFORM_STUB(); -} - -auto CPythonQuest::SetQuestIconFileName(DWORD, const char *) -> void -{ - MT_PLATFORM_STUB(); -} diff --git a/extension/src/port/EterLib/parser.cpp b/extension/src/port/EterLib/parser.cpp new file mode 100644 index 00000000..a3d5abc7 --- /dev/null +++ b/extension/src/port/EterLib/parser.cpp @@ -0,0 +1,443 @@ +#include "StdAfx.h" +#include "parser.h" + +using namespace script; + +#define ishan(ch) (((ch) & 0xE0) > 0x90) +#define isnhspace(ch) (!ishan(ch) && isspace(ch)) + + +extern DWORD GetDefaultCodePage(); + +const char* LocaleString_FindChar(const char* base, int len, char test) +{ + if (!base) + return NULL; + + DWORD codePage = GetDefaultCodePage(); + + int pos = 0; + while (pos < len) + { + const char* cur = base + pos; + const char* next = CharNextExA(codePage, cur, 0); + int cur_len = next - cur; + if (cur_len > 1) + { + pos += cur_len; + } + else if (1 == cur_len) + { + if (*cur == test) + return cur; + + ++pos; + } + else + { + break; + } + } + return NULL; +} + +int LocaleString_RightTrim(char* base, int len) +{ + DWORD codePage = GetDefaultCodePage(); + + int pos = len; + + while (pos > 0) + { + char* cur = base + pos; + char* prev = CharPrevExA(codePage, base, cur , 0); + + int prev_len = cur - prev; + if (prev_len != 1) + break; + + if (!isspace((unsigned char) *prev) && *prev != '\n' && *prev != '\r') + break; + + *prev = '\0'; + + pos -= prev_len; + } + + if (pos > 0) + return pos; + + return 0; +} + +void LocaleString_RightTrim(char* base) +{ + LocaleString_RightTrim(base, strlen(base)); +} + +void OLD_rtrim(char* base) +{ + if (!base) + return; + + DWORD codePage = GetDefaultCodePage(); + + if (949 == codePage || 936 == codePage) + { + char* end = base + strlen(base) - 1; + + while (end != base) + { + if (!isnhspace((unsigned char) *end) && *end != '\n' && *end != '\r' || (end!=base && *((unsigned char*)end-1)>0xa0)) + break; + + *end = '\0'; + + end = CharPrevExA(codePage, base, end, 0); + } + } + else + { + char* end = base + strlen(base); + + while (end != base) + { + char* prev = CharPrevExA(codePage, base, end, 0); + + int prev_len = end - prev; + if (prev_len != 1) + break; + + if (!isspace((unsigned char) *prev) && *prev != '\n' && *prev != '\r') + break; + + *prev = '\0'; + + end = prev; + } + } +} + +const char* LocaleString_Skip(DWORD codePage, const char* cur) +{ + int loopCount = 0; + + while (*cur) + { + if (++loopCount > 9216) + { + TraceError("Infinite loop in LocaleString_Skip [%s]", cur); + break; + } + + const char* next = CharNextExA(codePage, cur, 0); + int cur_len = next - cur; + if (cur_len > 1) + { + cur = next; + } + else if (1 == cur_len) + { + if (!isspace((unsigned char) *cur) && *cur != '\n' && *cur != '\r') + return cur; + } + else + { + break; + } + } + return cur; +} + +bool Group::GetArg(const char *c_arg_base, int arg_len, TArgList & argList) +{ + char szName[32 + 1]; + char szValue[64 + 1]; + + int iNameLen = 0; + int iValueLen = 0; + int iCharLen = 0; + + int pos = 0; + + bool isValue = false; + + DWORD codePage = GetDefaultCodePage(); + + while (pos < arg_len) + { + const char* cur = c_arg_base + pos; + const char* next = CharNextExA(codePage, cur, 0); + iCharLen = next - cur; + + if (iCharLen > 1) + { + if (isValue) + { + if (iValueLen >= 64) + { + TraceError("argument value overflow: must be shorter than 64 letters"); + return false; + } + + memcpy(szValue+iValueLen, cur, iCharLen); + iValueLen += iCharLen; + szValue[iValueLen] = '\0'; + } + else + { + if (iNameLen >= 32) + { + TraceError("argument name overflow: must be shorter than 32 letters"); + return false; + } + memcpy(szName+iNameLen, cur, iCharLen); + iNameLen += iCharLen; + szName[iNameLen] = '\0'; + } + } + else if (iCharLen == 1) + { + const char c = *cur; + if (c == '|') + { + if (iNameLen == 0) + { + TraceError("no argument name"); + return false; + } + + isValue = false; + + iNameLen = LocaleString_RightTrim(szName, iNameLen); + iValueLen = LocaleString_RightTrim(szValue, iValueLen); + argList.push_back(TArg(szName, szValue)); + + iNameLen = 0; + iValueLen = 0; + } + else if (c == ';') + { + isValue = true; + } + // 값이 아니고, 이름이 시작되지 않았을 경우 빈칸은 건너 뛴다. + else if (!isValue && iNameLen == 0 && isspace((unsigned char) c)) + { + } + // 엔터는 건너 뛴다 + else if (c == '\r' || c == '\n') + { + } + else + { + if (isValue) + { + if (iValueLen >= 64) + { + TraceError("argument value overflow: must be shorter than 64 letters"); + return false; + } + + memcpy(szValue+iValueLen, cur, iCharLen); + iValueLen += iCharLen; + szValue[iValueLen] = '\0'; + } + else + { + if (iNameLen >= 32) + { + TraceError("argument name overflow: must be shorter than 32 letters"); + return false; + } + memcpy(szName+iNameLen, cur, iCharLen); + iNameLen += iCharLen; + szName[iNameLen] = '\0'; + } + } + } + else + { + break; + } + + pos += iCharLen; + } + + if (iNameLen != 0 && iValueLen != 0) + { + iNameLen = LocaleString_RightTrim(szName, iNameLen); + iValueLen = LocaleString_RightTrim(szValue, iValueLen); + argList.push_back(TArg(szName, szValue)); + } + + return true; +} + + +bool Group::Create(const std::string & stSource) +{ + m_cmdList.clear(); + + if (stSource.empty()) + return false; + + const char *str_base = stSource.c_str(); + if (!str_base || !*str_base) + { + TraceError("Source file has no content"); + return false; + } + int str_len = stSource.length(); + int str_pos = 0; + + DWORD codePage = GetDefaultCodePage(); + + char box_data[1024 + 1]; + + static std::string stLetter; + + while (str_pos < str_len) + { + TCmd cmd; + + const char* word = str_base + str_pos; + const char* word_next = CharNextExA(codePage, word, 0); + + int word_len = word_next - word; + + if (word_len > 1) + { + str_pos += word_len; + + { + stLetter.assign(word, word_next); + + cmd.name.assign("LETTER"); + cmd.argList.push_back(TArg("value", stLetter)); + + m_cmdList.push_back(cmd); + } + + } + else if (word_len == 1) + { + const char cur = *word; + + if ('[' == cur) + { + ++str_pos; + + const char* box_begin = str_base + str_pos; + const char* box_end = LocaleString_FindChar(box_begin, str_len - str_pos, ']'); + if (!box_end) + { + TraceError(" !! PARSING ERROR - Syntax Error : %s\n", box_begin); + return false; + } + str_pos += box_end - box_begin + 1; + + + int data_len = 0; + { + const char* data_begin = LocaleString_Skip(codePage, box_begin); + const char* data_end = box_end; + data_len = data_end - data_begin; + if (data_len >= 1024) + { + TraceError(" !! PARSING ERROR - Buffer Overflow : %d, %s\n", data_len, str_base); + return false; + } + memcpy(box_data, data_begin, data_len); + box_data[data_len] = '\0'; + + data_len = LocaleString_RightTrim(box_data, data_len); // 오른쪽 빈칸 자르기 + } + + { + const char* space = LocaleString_FindChar(box_data, data_len, ' '); + if (space) // 인자가 있음 + { + int name_len = space - box_data; + cmd.name.assign(box_data, name_len); + + const char* space_next = CharNextExA(codePage, space, 0); + const char* arg = LocaleString_Skip(codePage, space_next); + + int arg_len = data_len - (arg - box_data); + + if (!GetArg(arg, arg_len, cmd.argList)) + { + TraceError(" !! PARSING ERROR - Unknown Arguments : %d, %s\n", arg_len, arg); + return false; + } + } + else // 인자가 없으므로 모든 스트링이 명령어다. + { + cmd.name.assign(box_data); + cmd.argList.clear(); + } + + m_cmdList.push_back(cmd); + } + } + else if (cur == '\r' || cur == '\n') + { + ++str_pos; + } + else + { + ++str_pos; + + { + stLetter.assign(1, cur); + cmd.name.assign("LETTER"); + cmd.argList.push_back(TArg("value", stLetter)); + m_cmdList.push_back(cmd); + } + } + } + else + { + break; + } + } + + return true; +} + +bool Group::GetCmd(TCmd & cmd) +{ + if (m_cmdList.empty()) + return false; + + cmd = m_cmdList.front(); + m_cmdList.pop_front(); + return true; +} + +bool Group::ReadCmd(TCmd & cmd) +{ + if (m_cmdList.empty()) + return false; + + cmd = m_cmdList.front(); + return true; +} + +std::string & Group::GetError() +{ + return m_stError; +} + +void Group::SetError(const char * c_pszError) +{ + m_stError.assign(c_pszError); +} + +Group::Group() +{ +} + +Group::~Group() +{ +} diff --git a/extension/src/port/UserInterface/PythonEventManager.cpp b/extension/src/port/UserInterface/PythonEventManager.cpp new file mode 100644 index 00000000..42086da3 --- /dev/null +++ b/extension/src/port/UserInterface/PythonEventManager.cpp @@ -0,0 +1,1211 @@ +#include "StdAfx.h" +#include "PythonEventManager.h" +#include "PythonNetworkStream.h" +#include "PythonNonPlayer.h" + +#include "AbstractApplication.h" + +#include "../gamelib/ItemData.h" +#include "../gamelib/ItemManager.h" + +#include "../eterPack/EterPackManager.h" + +#include "PythonMiniMap.h" + +const long c_lNormal_Waiting_Time = 10; +const int c_fLine_Temp = 16; + +void ShowArgument(script::TArgList & rArgumentList) +{ + for (script::TArgList::iterator itor=rArgumentList.begin(); itor!=rArgumentList.end(); ++itor) + { + const std::string & rName = (*itor).strName; + + Tracen(rName.c_str()); + } + +} + +const std::string& GetArgumentString(const char * c_szName, script::TArgList & rArgumentList) +{ + for (script::TArgList::iterator itor = rArgumentList.begin(); itor != rArgumentList.end(); ++itor) + { + const std::string & rName = (*itor).strName; + + if (0 == rName.compare(c_szName)) + return (*itor).strValue; + } + + static std::string strEmptyValue = ""; + return strEmptyValue; +} + +const char * GetArgument(const char * c_szName, script::TArgList & rArgumentList) +{ + return GetArgumentString(c_szName, rArgumentList).c_str(); +} + +void GetCameraSettingFromArgList(script::TArgList & rArgList, IAbstractApplication::SCameraSetting * pCameraSetting) +{ + int ix = atoi(GetArgument("x", rArgList)); + int iy = atoi(GetArgument("y", rArgList)); + int iz = atoi(GetArgument("z", rArgList)); + int iUpDir = atoi(GetArgument("up", rArgList)); + int iViewDir = atoi(GetArgument("view", rArgList)); + int iCrossDir = atoi(GetArgument("cross", rArgList)); + int iDistance = atoi(GetArgument("distance", rArgList)); + int iRot = atoi(GetArgument("rot", rArgList)); + int iPitch = atoi(GetArgument("pitch", rArgList)); + + ZeroMemory(pCameraSetting, sizeof(IAbstractApplication::SCameraSetting)); + pCameraSetting->v3CenterPosition.x = float(ix); + pCameraSetting->v3CenterPosition.y = float(iy); + pCameraSetting->v3CenterPosition.z = float(iz); + pCameraSetting->kCmrPos.m_fUpDir = float(iUpDir); + pCameraSetting->kCmrPos.m_fViewDir = float(iViewDir); + pCameraSetting->kCmrPos.m_fCrossDir = float(iCrossDir); + pCameraSetting->fZoom = float(iDistance); + pCameraSetting->fRotation = float(iRot); + pCameraSetting->fPitch = float(iPitch); +} + +void CPythonEventManager::__InitEventSet(TEventSet& rEventSet) +{ + rEventSet.ix = 0; + rEventSet.iy = 0; + rEventSet.iWidth = 0; + rEventSet.iyLocal = 0; + + rEventSet.isLock = false; + rEventSet.lLastDelayTime = 0; + rEventSet.iCurrentLetter = 0; + rEventSet.CurrentColor = D3DXCOLOR(1, 1, 1, 1); + rEventSet.strCurrentLine = ""; + + rEventSet.pCurrentTextLine = NULL; + rEventSet.ScriptTextLineList.clear(); + + rEventSet.isConfirmWait = FALSE; + rEventSet.pConfirmTimeTextLine = NULL; + rEventSet.iConfirmEndTime = 0; + + rEventSet.DiffuseColor = D3DXCOLOR(1, 1, 1, 1); + rEventSet.lWaitingTime = c_lNormal_Waiting_Time; + rEventSet.iRestrictedCharacterCount = 30; + + rEventSet.iVisibleStartLine = 0; + rEventSet.iVisibleLineCount = BOX_VISIBLE_LINE_COUNT; + + rEventSet.iAdjustLine = 0; + + rEventSet.isTextCenterMode = false; + rEventSet.isWaitFlag = false; + + __InsertLine(rEventSet); +} + +int CPythonEventManager::RegisterEventSet(const char * c_szFileName) +{ + CMappedFile File; + LPCVOID pMap; + + if (!CEterPackManager::Instance().Get(File, c_szFileName, &pMap)) + return -1; + + std::string strEventString; + strEventString.resize(File.Size()+1); + + File.Read(&strEventString[0], File.Size()); + + TEventSet * pEventSet = m_EventSetPool.Alloc(); + if (!pEventSet) + return -1; + + if (!pEventSet->ScriptGroup.Create(strEventString)) + { + __ClearEventSetp(pEventSet); + return -1; + } + + strncpy(pEventSet->szFileName, c_szFileName, 32); + + pEventSet->pCurrentTextLine = NULL; + pEventSet->poEventHandler = NULL; + + __InitEventSet(*pEventSet); + + int iEmptySlotIndex = GetEmptyEventSetSlot(); + m_EventSetVector[iEmptySlotIndex] = pEventSet; + return iEmptySlotIndex; +} + +int CPythonEventManager::RegisterEventSetFromString(const std::string& strScript) +{ + TEventSet* pEventSet = m_EventSetPool.Alloc(); + if (!pEventSet) + return -1; + + // SCRIPT_PARSING_FAILURE_CLEAR_BUG 스크립트 파싱 실패시 __ClearEventSetp 에서 에러 발생 + pEventSet->pCurrentTextLine = NULL; + // END_OF_SCRIPT_PARSING_FAILURE_CLEAR_BUG + + if (!pEventSet->ScriptGroup.Create(strScript)) + { + __ClearEventSetp(pEventSet); + return -1; + } + + pEventSet->szFileName[0] = 0; + pEventSet->poEventHandler = NULL; + __InitEventSet(*pEventSet); + + // NOTE : 만약 단순한 스크립트 이벤트 실행 커맨드라면 다시 만든다. + script::TCmd ScriptCommand; + int pEventPosition; + int iEventType; + if (pEventSet->ScriptGroup.ReadCmd(ScriptCommand)) + { + if (GetScriptEventIndex(ScriptCommand.name.c_str(), &pEventPosition, &iEventType)) + { + if (EVENT_TYPE_RUN_CINEMA == iEventType) + { + std::string strFileName = GetArgument("value", ScriptCommand.argList); + + __ClearEventSetp(pEventSet); + + return RegisterEventSet(strFileName.c_str()); + } + } + } + + int iEmptySlotIndex = GetEmptyEventSetSlot(); + m_EventSetVector[iEmptySlotIndex] = pEventSet; + return iEmptySlotIndex; +} + +void CPythonEventManager::ClearEventSeti(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + __ClearEventSetp(m_EventSetVector[iIndex]); + m_EventSetVector[iIndex] = NULL; +} + +void CPythonEventManager::__ClearEventSetp(TEventSet * pEventSet) +{ + if (!pEventSet) + return; + + for (TScriptTextLineList::iterator itor = pEventSet->ScriptTextLineList.begin(); itor != pEventSet->ScriptTextLineList.end(); ++itor) + { + TTextLine & rkLine = *itor; + rkLine.pInstance->Destroy(); + m_ScriptTextLinePool.Free(rkLine.pInstance); + } + pEventSet->ScriptTextLineList.clear(); + + if (pEventSet->pCurrentTextLine) + { + pEventSet->pCurrentTextLine->Destroy(); + m_ScriptTextLinePool.Free(pEventSet->pCurrentTextLine); + } + pEventSet->pCurrentTextLine = NULL; + pEventSet->strCurrentLine = ""; + pEventSet->iCurrentLetter = 0; + + m_EventSetPool.Free(pEventSet); +} + +DWORD CPythonEventManager::GetEmptyEventSetSlot() +{ + for (DWORD i = 0; i < m_EventSetVector.size(); ++i) + { + if (NULL == m_EventSetVector[i]) + { + return i; + } + } + + m_EventSetVector.push_back(NULL); + return m_EventSetVector.size()-1; +} + +void CPythonEventManager::SetRestrictedCount(int iIndex, int iCount) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + { + TraceError("CPythonEventManager::SetRestrictedCount m_EventSetVector[iIndex=%d]==NULL", iIndex); + return; + } + + pEventSet->iRestrictedCharacterCount = iCount; +} + +void CPythonEventManager::SetEventHandler(int iIndex, PyObject * poEventHandler) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + { + TraceError("CPythonEventManager::SetEventHandler m_EventSetVector[iIndex=%d]==NULL", iIndex); + return; + } + + pEventSet->poEventHandler = poEventHandler; +} + +int CPythonEventManager::GetEventSetLocalYPosition(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return 0; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + return 0; + + return pEventSet->iyLocal; +} + +void CPythonEventManager::AddEventSetLocalYPosition(int iIndex, int iAddValue) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + return; + + pEventSet->iyLocal += iAddValue; +} + +void CPythonEventManager::InsertText(int iIndex, const char * c_szText,int iX_pos) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + return; + + pEventSet->strCurrentLine = c_szText; + if (pEventSet->pCurrentTextLine) + { + pEventSet->pCurrentTextLine->SetValue(c_szText); + if (iX_pos == 0) + { + pEventSet->pCurrentTextLine->SetHorizonalAlign(CGraphicTextInstance::HORIZONTAL_ALIGN_CENTER); + } + } + + pEventSet->pCurrentTextLine->Update(); + __InsertLine(*pEventSet, FALSE, iX_pos); +} + + +void CPythonEventManager::UpdateEventSet(int iIndex, int ix, int iy) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + return; + + pEventSet->ix = ix; + pEventSet->iy = iy; + + RefreshLinePosition(pEventSet); + + if (pEventSet->isConfirmWait) + { + int iLeftTime = max(0, pEventSet->iConfirmEndTime - timeGetTime()/1000); + pEventSet->pConfirmTimeTextLine->SetValue(_getf(m_strLeftTimeString.c_str(), iLeftTime)); + } + + if (pEventSet->isWaitFlag) + return; + + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Process EventSet + long lElapsedTime = CTimer::Instance().GetElapsedMilliecond(); + + pEventSet->lLastDelayTime = max(0, pEventSet->lLastDelayTime - lElapsedTime); + + while (lElapsedTime > 0) + { + pEventSet->lLastDelayTime -= lElapsedTime; + + if (pEventSet->lLastDelayTime <= 0) + { + lElapsedTime = -pEventSet->lLastDelayTime; + if (lElapsedTime <= 0) + break; + + ProcessEventSet(pEventSet); + + if (pEventSet->lLastDelayTime < 0) + { + pEventSet->lLastDelayTime = 0; + break; + } + } + else + break; + } +} + +void CPythonEventManager::SetEventSetWidth(int iIndex, int iWidth) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + return; + + pEventSet->iWidth = iWidth; +} + +void CPythonEventManager::ProcessEventSet(TEventSet * pEventSet) +{ + if (pEventSet->isLock) + return; + + script::TCmd ScriptCommand; + + IAbstractApplication& rApp=IAbstractApplication::GetSingleton(); + + if (!pEventSet->ScriptGroup.GetCmd(ScriptCommand)) + { + pEventSet->isLock = true; + return; + } + + int pEventPosition; + int iEventType; + if (!GetScriptEventIndex(ScriptCommand.name.c_str(), &pEventPosition, &iEventType)) + return; + + switch (iEventType) + { + case EVENT_TYPE_LETTER: + { + const std::string& c_rstValue = GetArgumentString("value", ScriptCommand.argList); + pEventSet->strCurrentLine.append(c_rstValue); + pEventSet->pCurrentTextLine->SetValueString(pEventSet->strCurrentLine); + pEventSet->pCurrentTextLine->SetColor(pEventSet->CurrentColor.r,pEventSet->CurrentColor.g,pEventSet->CurrentColor.b); + pEventSet->iCurrentLetter+=c_rstValue.length(); + + if (pEventSet->iCurrentLetter >= pEventSet->iRestrictedCharacterCount) + { + __InsertLine(*pEventSet); + } + + pEventSet->lLastDelayTime = pEventSet->lWaitingTime; + break; + } + + case EVENT_TYPE_DELAY: + { + if (EVENT_POSITION_START == pEventPosition) + pEventSet->lWaitingTime = atoi(GetArgument("value", ScriptCommand.argList)); + else + pEventSet->lWaitingTime = c_lNormal_Waiting_Time; + break; + } + + case EVENT_TYPE_COLOR: + { + if (EVENT_POSITION_START == pEventPosition) + { + pEventSet->CurrentColor.r = (float)atof(GetArgument("r", ScriptCommand.argList)); + pEventSet->CurrentColor.g = (float)atof(GetArgument("g", ScriptCommand.argList)); + pEventSet->CurrentColor.b = (float)atof(GetArgument("b", ScriptCommand.argList)); + pEventSet->CurrentColor.a = 1.0f; + } + else + { + pEventSet->CurrentColor.r = 1.0f; + pEventSet->CurrentColor.g = 1.0f; + pEventSet->CurrentColor.a = 1.0f; + pEventSet->CurrentColor.b = 1.0f; + } + break; + } + + case EVENT_TYPE_COLOR256: + { + if (EVENT_POSITION_START == pEventPosition) + { + pEventSet->CurrentColor.r = float(atof(GetArgument("r", ScriptCommand.argList)) / 255.0f); + pEventSet->CurrentColor.g = float(atof(GetArgument("g", ScriptCommand.argList)) / 255.0f); + pEventSet->CurrentColor.b = float(atof(GetArgument("b", ScriptCommand.argList)) / 255.0f); + pEventSet->CurrentColor.a = 1.0f; + } + else + { + pEventSet->CurrentColor.r = 1.0f; + pEventSet->CurrentColor.g = 1.0f; + pEventSet->CurrentColor.a = 1.0f; + pEventSet->CurrentColor.b = 1.0f; + } + break; + } + + case EVENT_TYPE_ENTER: + { + __InsertLine(*pEventSet); + break; + } + + case EVENT_TYPE_WAIT: + { + //캐틱터 만들기 제국 설명등에서 설명 밑으로 쳐지는 버그 수정/EVENT_TYPE_WAIT 관련해서 테스트 필요. + pEventSet->iyLocal = 0; + pEventSet->isLock = true; + break; + } + + case EVENT_TYPE_NEXT: + { + MakeNextButton(pEventSet, BUTTON_TYPE_NEXT); + pEventSet->iAdjustLine += 2; + break; + } + + case EVENT_TYPE_DONE: + { + MakeNextButton(pEventSet, BUTTON_TYPE_DONE); + PyCallClassMemberFunc(pEventSet->poEventHandler, "DoneEvent", Py_BuildValue("()")); + pEventSet->iAdjustLine += 2; + break; + } + + case EVENT_TYPE_CLEAR: + { + ClearLine(pEventSet); + break; + } + + case EVENT_TYPE_QUESTION: + { + MakeQuestion(pEventSet, ScriptCommand.argList); + break; + } + + case EVENT_TYPE_LEFT_IMAGE: + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnLeftImage", Py_BuildValue("(s)", GetArgument("src", ScriptCommand.argList))); + break; + } + + case EVENT_TYPE_TOP_IMAGE: + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnTopImage", Py_BuildValue("(s)", GetArgument("src", ScriptCommand.argList))); + break; + } + + case EVENT_TYPE_BACKGROUND_IMAGE: + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnBackgroundImage", Py_BuildValue("(s)",GetArgument("src", ScriptCommand.argList))); + break; + } + + case EVENT_TYPE_IMAGE: + { + int x = atoi(GetArgument("x", ScriptCommand.argList)); + int y = atoi(GetArgument("y", ScriptCommand.argList)); + const char * src = GetArgument("src", ScriptCommand.argList); + + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnImage", Py_BuildValue("(iis)", x, y, src)); + break; + } + + case EVENT_TYPE_INSERT_IMAGE: + { + const std::string & imageFile = GetArgumentString("image_name", ScriptCommand.argList); + const char * title = GetArgument("title", ScriptCommand.argList); + const char * desc = GetArgument("desc", ScriptCommand.argList); + int index = atoi(GetArgument("index", ScriptCommand.argList)); + int total = atoi(GetArgument("total", ScriptCommand.argList)); + + if (imageFile.empty()) + { + const char * imageType = GetArgument("image_type", ScriptCommand.argList); + int iItemIndex = atoi(GetArgument("idx", ScriptCommand.argList)); + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnInsertItemIcon", Py_BuildValue("(sissii)", imageType, iItemIndex, title, desc, index, total)); + } + else + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnInsertImage", Py_BuildValue("(ssssii)", imageFile.c_str(), title, title, desc, index, total)); + } + pEventSet->iAdjustLine += 2; + break; + } + + case EVENT_TYPE_ADD_MAP_SIGNAL: + { + float x, y; + x = (float)atof(GetArgument("x",ScriptCommand.argList)); + y = (float)atof(GetArgument("y",ScriptCommand.argList)); + CPythonMiniMap::Instance().AddSignalPoint(x,y); + CPythonMiniMap::Instance().OpenAtlasWindow(); + break; + } + + case EVENT_TYPE_CLEAR_MAP_SIGNAL: + { + CPythonMiniMap::Instance().ClearAllSignalPoint(); + break; + } + + // HIDE_QUEST_LETTER + case EVENT_TYPE_QUEST_BUTTON_CLOSE: + { + PyCallClassMemberFunc(m_poInterface, "BINARY_ClearQuest", + Py_BuildValue("(i)", atoi(GetArgument("idx", ScriptCommand.argList)))); + break; + } + // END_OF_HIDE_QUEST_LETTER + + case EVENT_TYPE_QUEST_BUTTON: + { + const std::string& c_rstType = GetArgumentString("icon_type", ScriptCommand.argList); + const std::string& c_rstFile = GetArgumentString("icon_name", ScriptCommand.argList); + + int idx = atoi(GetArgument("idx", ScriptCommand.argList)); + const char * name = GetArgument("name", ScriptCommand.argList); + + // 퀘스트 UI 리뉴얼이 되면 해결 되므로 일단 용혼석만 땜빵 by chrislee + if (!strcmp(name, "조각난 용혼석?....")) { + PyCallClassMemberFunc(m_poInterface, "BINARY_RecvQuest", Py_BuildValue("(isss)", idx, name, "highlight", "")); + } + else { + if (c_rstFile.empty()) + { + PyCallClassMemberFunc(m_poInterface, "RecvQuest", Py_BuildValue("(is)", idx, name)); + } + else + { + PyCallClassMemberFunc(m_poInterface, "BINARY_RecvQuest", + Py_BuildValue("(isss)", idx, name, c_rstType.c_str(), c_rstFile.c_str())); + } + } + break; + } + case EVENT_TYPE_SET_MESSAGE_POSITION: + { + break; + } + case EVENT_TYPE_ADJUST_MESSAGE_POSITION: + { + break; + } + case EVENT_TYPE_SET_CENTER_MAP_POSITION: + { + CPythonMiniMap::Instance().SetAtlasCenterPosition(atoi(GetArgument("x", ScriptCommand.argList)),atoi(GetArgument("y", ScriptCommand.argList))); + break; + } + case EVENT_TYPE_SLEEP: + pEventSet->lLastDelayTime = atoi(GetArgument("value", ScriptCommand.argList)); + break; + case EVENT_TYPE_SET_CAMERA: + { + IAbstractApplication::SCameraSetting CameraSetting; + GetCameraSettingFromArgList(ScriptCommand.argList, &CameraSetting); + rApp.SetEventCamera(CameraSetting); + break; + } + case EVENT_TYPE_BLEND_CAMERA: + { + IAbstractApplication::SCameraSetting CameraSetting; + GetCameraSettingFromArgList(ScriptCommand.argList, &CameraSetting); + + float fBlendTime = atoi(GetArgument("blendtime", ScriptCommand.argList)); + + rApp.BlendEventCamera(CameraSetting, fBlendTime); + break; + } + case EVENT_TYPE_RESTORE_CAMERA: + { + rApp.SetDefaultCamera(); + break; + } + case EVENT_TYPE_FADE_OUT: + { + float fSpeed = (float)atof(GetArgument("speed", ScriptCommand.argList)); + PyCallClassMemberFunc(pEventSet->poEventHandler, "FadeOut", Py_BuildValue("(f)", fSpeed)); + pEventSet->isWaitFlag = true; + break; + } + case EVENT_TYPE_FADE_IN: + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "FadeIn", Py_BuildValue("(f)", atof(GetArgument("speed", ScriptCommand.argList)))); + pEventSet->isWaitFlag = true; + break; + } + case EVENT_TYPE_WHITE_OUT: + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "WhiteOut", Py_BuildValue("(f)", atof(GetArgument("speed", ScriptCommand.argList)))); + pEventSet->isWaitFlag = true; + break; + } + case EVENT_TYPE_WHITE_IN: + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "WhiteIn", Py_BuildValue("(f)", atof(GetArgument("speed", ScriptCommand.argList)))); + pEventSet->isWaitFlag = true; + break; + } + case EVENT_TYPE_CLEAR_TEXT: + { + ClearLine(pEventSet); + break; + } + case EVENT_TYPE_TEXT_HORIZONTAL_ALIGN_CENTER: + { + pEventSet->isTextCenterMode = true; + if (pEventSet->pCurrentTextLine) + pEventSet->pCurrentTextLine->SetHorizonalAlign(CGraphicTextInstance::HORIZONTAL_ALIGN_CENTER); + break; + } + case EVENT_TYPE_TITLE_IMAGE: + { + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnTitleImage", Py_BuildValue("(s)", GetArgument("src", ScriptCommand.argList))); + break; + } + case EVENT_TYPE_DUNGEON_RESULT: + { + int killstone_count = atoi(GetArgument("killstone_count", ScriptCommand.argList)); + int killmob_count = atoi(GetArgument("killmob_count", ScriptCommand.argList)); + int find_hidden = atoi(GetArgument("find_hidden", ScriptCommand.argList)); + int hidden_total = atoi(GetArgument("hidden_total", ScriptCommand.argList)); + int use_potion = atoi(GetArgument("use_potion", ScriptCommand.argList)); + int is_revived = atoi(GetArgument("is_revived", ScriptCommand.argList)); + int killallmob = atoi(GetArgument("killallmob", ScriptCommand.argList)); + int total_time = atoi(GetArgument("total_time", ScriptCommand.argList)); + int bonus_exp = atoi(GetArgument("bonus_exp", ScriptCommand.argList)); + + PyCallClassMemberFunc(m_poInterface, "ShowDungeonResult", + Py_BuildValue("(iiiiiiiii)", + killstone_count, + killmob_count, + find_hidden, + hidden_total, + use_potion, + is_revived, + killallmob, + total_time, + bonus_exp)); + break; + } + case EVENT_TYPE_ITEM_NAME: + { + int iIndex = atoi(GetArgument("value", ScriptCommand.argList)); + CItemData * pItemData; + if (CItemManager::Instance().GetItemDataPointer(iIndex, &pItemData)) + { + pEventSet->strCurrentLine.append(pItemData->GetName()); + pEventSet->pCurrentTextLine->SetValue(pEventSet->strCurrentLine.c_str()); + pEventSet->pCurrentTextLine->SetColor(1.0f, 0.2f, 0.2f); + pEventSet->iCurrentLetter+= strlen(pItemData->GetName()); + + if (pEventSet->iCurrentLetter >= pEventSet->iRestrictedCharacterCount) + __InsertLine(*pEventSet); + + pEventSet->lLastDelayTime = pEventSet->lWaitingTime; + } + + break; + } + case EVENT_TYPE_MONSTER_NAME: + { + int iIndex = atoi(GetArgument("value", ScriptCommand.argList)); + const char * c_szName; + + CPythonNonPlayer& rkNonPlayer=CPythonNonPlayer::Instance(); + if (rkNonPlayer.GetName(iIndex, &c_szName)) + { + pEventSet->strCurrentLine.append(c_szName); + pEventSet->pCurrentTextLine->SetValue(pEventSet->strCurrentLine.c_str()); + pEventSet->iCurrentLetter+= strlen(c_szName); + + if (pEventSet->iCurrentLetter >= pEventSet->iRestrictedCharacterCount) + __InsertLine(*pEventSet); + + pEventSet->lLastDelayTime = pEventSet->lWaitingTime; + } + + break; + } + case EVENT_TYPE_WINDOW_SIZE: + { + int iWidth = atoi(GetArgument("width", ScriptCommand.argList)); + int iHeight = atoi(GetArgument("height", ScriptCommand.argList)); + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnSize", Py_BuildValue("(ii)", iWidth, iHeight)); + break; + } + case EVENT_TYPE_INPUT: + { + __InsertLine(*pEventSet); + PyCallClassMemberFunc(pEventSet->poEventHandler, "OnInput", Py_BuildValue("()")); + break; + } + case EVENT_TYPE_CONFIRM_WAIT: + { + int iTimeOut = atoi(GetArgument("timeout", ScriptCommand.argList)); + pEventSet->isConfirmWait = TRUE; + pEventSet->pConfirmTimeTextLine = pEventSet->pCurrentTextLine; + pEventSet->iConfirmEndTime = timeGetTime()/1000 + iTimeOut; + __InsertLine(*pEventSet, TRUE); + MakeNextButton(pEventSet, BUTTON_TYPE_CANCEL); + break; + } + case EVENT_TYPE_END_CONFIRM_WAIT: + { + for (DWORD i = 0; i < m_EventSetVector.size(); ++i) + { + if (NULL == m_EventSetVector[i]) + continue; + + TEventSet * pSet = m_EventSetVector[i]; + if (!pSet->isConfirmWait) + continue; + + pSet->isConfirmWait = FALSE; + pSet->pConfirmTimeTextLine = NULL; + pSet->iConfirmEndTime = 0; + + PyCallClassMemberFunc(pSet->poEventHandler, "CloseSelf", Py_BuildValue("()")); + } + break; + } + case EVENT_TYPE_SELECT_ITEM: + { + PyCallClassMemberFunc(m_poInterface, "BINARY_OpenSelectItemWindow", Py_BuildValue("()")); + break; + } + } +} + +void CPythonEventManager::RenderEventSet(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + return; + + int iCount = 0; + + for (TScriptTextLineList::iterator itor = pEventSet->ScriptTextLineList.begin(); itor != pEventSet->ScriptTextLineList.end(); ++itor, ++iCount) + { + if (iCount < pEventSet->iVisibleStartLine) + continue; + + if (iCount >= pEventSet->iVisibleStartLine + pEventSet->iVisibleLineCount) + continue; + + TTextLine & rkLine = *itor; + CGraphicTextInstance * pInstance = rkLine.pInstance; + + pInstance->Update(); + pInstance->Render(); + } + + if (iCount >= pEventSet->iVisibleStartLine) + if (iCount < pEventSet->iVisibleStartLine + pEventSet->iVisibleLineCount) + { + pEventSet->pCurrentTextLine->Update(); + pEventSet->pCurrentTextLine->Render(); + } +} + +void CPythonEventManager::Skip(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + + if (true == pEventSet->isLock) + { + pEventSet->lLastDelayTime = 0; + pEventSet->isLock = false; + } + else + { + pEventSet->lLastDelayTime = -1000; + } +} + +bool CPythonEventManager::IsWait(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return false; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + { + return false; + } + + return pEventSet->isLock; +} + +void CPythonEventManager::EndEventProcess(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + if (!pEventSet) + { + return; + } + + pEventSet->isWaitFlag = false; +} + +void CPythonEventManager::MakeNextButton(TEventSet * pEventSet, int iButtonType) +{ + __AddSpace(*pEventSet, c_fLine_Temp+5); + PyCallClassMemberFunc(pEventSet->poEventHandler, "MakeNextButton", Py_BuildValue("(i)", iButtonType)); +} + +void CPythonEventManager::MakeQuestion(TEventSet * pEventSet, script::TArgList & rArgumentList) +{ + if (rArgumentList.empty()) + return; + + PyCallClassMemberFunc(pEventSet->poEventHandler, "MakeQuestion", Py_BuildValue("(i)", rArgumentList.size())); + pEventSet->nAnswer = rArgumentList.size(); + + int iIndex = 0; + for (script::TArgList::iterator itor=rArgumentList.begin(); itor!=rArgumentList.end(); ++itor) + { + script::TArg & rArgument = *itor; + PyCallClassMemberFunc(pEventSet->poEventHandler, "AppendQuestion", Py_BuildValue("(si)", rArgument.strValue.c_str(), iIndex)); + ++iIndex; + } +} + + +void CPythonEventManager::SelectAnswer(int iIndex, int iAnswer) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + //TEventSet * pEventSet = m_EventSetVector[iIndex]; + + CPythonNetworkStream::Instance().SendScriptAnswerPacket(iAnswer); +} + +void CPythonEventManager::SetVisibleStartLine(int iIndex, int iStartLine) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + + if (DWORD(iStartLine) > pEventSet->ScriptTextLineList.size()) + return; + + pEventSet->iVisibleStartLine = iStartLine; +} + +void CPythonEventManager::SetVisibleLineCount(int iIndex, int iLineCount) +{ + if (!CheckEventSetIndex(iIndex)) + return; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + + pEventSet->iVisibleLineCount = iLineCount; +} + +int CPythonEventManager::GetVisibleStartLine(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return 0; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + return pEventSet->iVisibleStartLine; +} + +int CPythonEventManager::GetLineCount(int iIndex) +{ + if (!CheckEventSetIndex(iIndex)) + return 0; + + TEventSet * pEventSet = m_EventSetVector[iIndex]; + return pEventSet->ScriptTextLineList.size()+pEventSet->iAdjustLine; +} + +void CPythonEventManager::ClearLine(TEventSet * pEventSet) +{ + if (!pEventSet) + return; + + for (TScriptTextLineList::iterator itor = pEventSet->ScriptTextLineList.begin(); itor != pEventSet->ScriptTextLineList.end(); ++itor) + { + TTextLine & rkLine = *itor; + CGraphicTextInstance * pInstance = rkLine.pInstance; + pInstance->Destroy(); + pInstance->Update(); + } + + pEventSet->pCurrentTextLine->Destroy(); + pEventSet->pCurrentTextLine->Update(); + + // clear + pEventSet->pCurrentTextLine = NULL; + pEventSet->ScriptTextLineList.clear(); + + __InsertLine(*pEventSet); +} + +void CPythonEventManager::__InsertLine(TEventSet& rEventSet, BOOL isCenter, int iX_pos) +{ + if (rEventSet.pCurrentTextLine) + { + TTextLine kLine; + if (CGraphicTextInstance::HORIZONTAL_ALIGN_CENTER == rEventSet.pCurrentTextLine->GetHorizontalAlign()) + { + kLine.ixLocal = rEventSet.iWidth/2; + kLine.iyLocal = rEventSet.iyLocal; + + } + else + { + int textWidth; + int textHeight; + rEventSet.pCurrentTextLine->GetTextSize(&textWidth,&textHeight); + if (GetDefaultCodePage() == CP_1256) + { + kLine.ixLocal = rEventSet.iWidth; + if (iX_pos != 0) + { + kLine.ixLocal -= iX_pos - 20; + kLine.ixLocal += textWidth / 2; + } + } + else + { + kLine.ixLocal = 0; + if (iX_pos != 0) + { + kLine.ixLocal += (iX_pos - 20); + kLine.ixLocal -= textWidth / 2; + } + } + + kLine.iyLocal = rEventSet.iyLocal; + } + kLine.pInstance = rEventSet.pCurrentTextLine; + rEventSet.ScriptTextLineList.push_back(kLine); + __AddSpace(rEventSet, c_fLine_Temp); + } + + // DEFAULT_FONT + //CGraphicText * pText = (CGraphicText*)CResourceManager::Instance().GetTypeResourcePointer(g_strDefaultFontName.c_str()); + CGraphicText* pkDefaultFont = static_cast(DefaultFont_GetResource()); + if (!pkDefaultFont) + { + TraceError("CPythonEventManager::InsertLine - CANNOT_FIND_DEFAULT_FONT"); + return; + } + + rEventSet.pCurrentTextLine = m_ScriptTextLinePool.Alloc(); + if (!rEventSet.pCurrentTextLine) + { + TraceError("CPythonEventManager::InsertLine - OUT_OF_TEXT_LINE"); + return; + } + + rEventSet.pCurrentTextLine->SetTextPointer(pkDefaultFont); + // END_OF_DEFAULT_FONT + + rEventSet.pCurrentTextLine->SetColor(1.0f, 1.0f, 1.0f); + rEventSet.pCurrentTextLine->SetValue(""); + + if (rEventSet.isTextCenterMode || isCenter) + { + rEventSet.pCurrentTextLine->SetHorizonalAlign(CGraphicTextInstance::HORIZONTAL_ALIGN_CENTER); + rEventSet.pCurrentTextLine->SetPosition(rEventSet.ix+rEventSet.iWidth/2, rEventSet.iy + rEventSet.iyLocal); + } + else + { + if (GetDefaultCodePage() == CP_1256) + { + rEventSet.pCurrentTextLine->SetHorizonalAlign(CGraphicTextInstance::HORIZONTAL_ALIGN_LEFT); + rEventSet.pCurrentTextLine->SetPosition(rEventSet.ix + rEventSet.iWidth, rEventSet.iy + rEventSet.iyLocal); + } + else + { + rEventSet.pCurrentTextLine->SetHorizonalAlign(CGraphicTextInstance::HORIZONTAL_ALIGN_LEFT); + rEventSet.pCurrentTextLine->SetPosition(rEventSet.ix, rEventSet.iy + rEventSet.iyLocal); + } + } + + rEventSet.iCurrentLetter = 0; + rEventSet.strCurrentLine = ""; +} + +void CPythonEventManager::RefreshLinePosition(TEventSet * pEventSet) +{ + //int iCount = 0; + for (TScriptTextLineList::iterator itor = pEventSet->ScriptTextLineList.begin(); itor != pEventSet->ScriptTextLineList.end(); ++itor) + { + TTextLine & rkLine = *itor; + CGraphicTextInstance * pInstance = rkLine.pInstance; + pInstance->SetPosition(pEventSet->ix + rkLine.ixLocal, pEventSet->iy + rkLine.iyLocal); + } + + int ixTextPos; + if (CGraphicTextInstance::HORIZONTAL_ALIGN_CENTER == pEventSet->pCurrentTextLine->GetHorizontalAlign()) + { + ixTextPos = pEventSet->ix+pEventSet->iWidth/2; + } + else + { + if (GetDefaultCodePage() == CP_1256) + ixTextPos = pEventSet->ix+pEventSet->iWidth; + else + ixTextPos = pEventSet->ix; + } + pEventSet->pCurrentTextLine->SetPosition(ixTextPos, pEventSet->iy + pEventSet->iyLocal); +} + +void CPythonEventManager::__AddSpace(TEventSet& rEventSet, int iSpace) +{ + rEventSet.iyLocal += iSpace; +} + +bool CPythonEventManager::GetScriptEventIndex(const char * c_szName, int * pEventPosition, int * pEventType) +{ + const char * c_szEventName; + + if ('/' == c_szName[0]) + { + *pEventPosition = EVENT_POSITION_END; + c_szEventName = &c_szName[1]; + } + else + { + *pEventPosition = EVENT_POSITION_START; + c_szEventName = &c_szName[0]; + } + + std::map::iterator it = EventTypeMap.find(c_szEventName); + if (it == EventTypeMap.end()) + { + Tracef(" !! PARSING ERROR - Strange Command : %s\n", c_szEventName); + return false; + } + + *pEventType = it->second; + + return true; +} + +bool CPythonEventManager::CheckEventSetIndex(int iIndex) +{ + if (iIndex < 0) + return false; + + if ((DWORD) iIndex >= m_EventSetVector.size()) + return false; + + return true; +} + +void CPythonEventManager::Destroy() +{ + m_EventSetVector.clear(); + m_EventSetPool.Clear(); + m_ScriptTextLinePool.Clear(); +} + +void CPythonEventManager::SetInterfaceWindow(PyObject * poInterface) +{ + m_poInterface = poInterface; +} + +void CPythonEventManager::SetLeftTimeString(const char * c_szString) +{ + m_strLeftTimeString = c_szString; +} + +CPythonEventManager::CPythonEventManager() + : m_poInterface(0), m_strLeftTimeString("남은 시간 : %d초") +{ + EventTypeMap["LETTER"]=EVENT_TYPE_LETTER; + EventTypeMap["COLOR"]=EVENT_TYPE_COLOR; + EventTypeMap["DELAY"]=EVENT_TYPE_DELAY; + EventTypeMap["ENTER"]=EVENT_TYPE_ENTER; + EventTypeMap["WAIT"]=EVENT_TYPE_WAIT; + EventTypeMap["CLEAR"]=EVENT_TYPE_CLEAR; + EventTypeMap["QUESTION"]=EVENT_TYPE_QUESTION; + EventTypeMap["NEXT"]=EVENT_TYPE_NEXT; + EventTypeMap["DONE"]=EVENT_TYPE_DONE; + + EventTypeMap["LEFTIMAGE"]=EVENT_TYPE_LEFT_IMAGE; + EventTypeMap["TOPIMAGE"]=EVENT_TYPE_TOP_IMAGE; + EventTypeMap["BGIMAGE"]=EVENT_TYPE_BACKGROUND_IMAGE; + EventTypeMap["IMAGE"]=EVENT_TYPE_IMAGE; + + EventTypeMap["ADDMAPSIGNAL"]=EVENT_TYPE_ADD_MAP_SIGNAL; + EventTypeMap["CLEARMAPSIGNAL"]=EVENT_TYPE_CLEAR_MAP_SIGNAL; + + EventTypeMap["SETMSGPOS"]=EVENT_TYPE_SET_MESSAGE_POSITION; + EventTypeMap["ADJMSGPOS"]=EVENT_TYPE_ADJUST_MESSAGE_POSITION; + EventTypeMap["SETCMAPPOS"]=EVENT_TYPE_SET_CENTER_MAP_POSITION; + + EventTypeMap["QUESTBUTTON"]=EVENT_TYPE_QUEST_BUTTON; + + // HIDE_QUEST_LETTER + EventTypeMap["QUESTBUTTON_CLOSE"]=EVENT_TYPE_QUEST_BUTTON_CLOSE; + // END_OF_HIDE_QUEST_LETTER + + EventTypeMap["SLEEP"]=EVENT_TYPE_SLEEP; + EventTypeMap["SET_CAMERA"]=EVENT_TYPE_SET_CAMERA; + EventTypeMap["BLEND_CAMERA"]=EVENT_TYPE_BLEND_CAMERA; + EventTypeMap["RESTORE_CAMERA"]=EVENT_TYPE_RESTORE_CAMERA; + EventTypeMap["FADE_OUT"]=EVENT_TYPE_FADE_OUT; + EventTypeMap["FADE_IN"]=EVENT_TYPE_FADE_IN; + EventTypeMap["WHITE_OUT"]=EVENT_TYPE_WHITE_OUT; + EventTypeMap["WHITE_IN"]=EVENT_TYPE_WHITE_IN; + EventTypeMap["CLEAR_TEXT"]=EVENT_TYPE_CLEAR_TEXT; + EventTypeMap["TEXT_HORIZONTAL_ALIGN_CENTER"]=EVENT_TYPE_TEXT_HORIZONTAL_ALIGN_CENTER; + EventTypeMap["TITLE_IMAGE"]=EVENT_TYPE_TITLE_IMAGE; + + EventTypeMap["RUN_CINEMA"]=EVENT_TYPE_RUN_CINEMA; + EventTypeMap["DUNGEON_RESULT"]=EVENT_TYPE_DUNGEON_RESULT; + + EventTypeMap["ITEM"]=EVENT_TYPE_ITEM_NAME; + EventTypeMap["MOB"]=EVENT_TYPE_MONSTER_NAME; + + EventTypeMap["COLOR256"]=EVENT_TYPE_COLOR256; + EventTypeMap["WINDOW_SIZE"]=EVENT_TYPE_WINDOW_SIZE; + + EventTypeMap["INPUT"]=EVENT_TYPE_INPUT; + EventTypeMap["CONFIRM_WAIT"]=EVENT_TYPE_CONFIRM_WAIT; + EventTypeMap["END_CONFIRM_WAIT"]=EVENT_TYPE_END_CONFIRM_WAIT; + + EventTypeMap["INSERT_IMAGE"]=EVENT_TYPE_INSERT_IMAGE; + + EventTypeMap["SELECT_ITEM"]=EVENT_TYPE_SELECT_ITEM; +} + +CPythonEventManager::~CPythonEventManager() +{ +} diff --git a/extension/src/port/UserInterface/PythonEventManagerMoudle.cpp b/extension/src/port/UserInterface/PythonEventManagerMoudle.cpp new file mode 100644 index 00000000..1551152d --- /dev/null +++ b/extension/src/port/UserInterface/PythonEventManagerMoudle.cpp @@ -0,0 +1,315 @@ +#include "StdAfx.h" +#include "PythonEventManager.h" +#include "PythonNetworkStream.h" + +PyObject * eventRegisterEventSet(PyObject * poSelf, PyObject * poArgs) +{ + char * szFileName; + if (!PyTuple_GetString(poArgs, 0, &szFileName)) + return Py_BuildException(); + + int iEventIndex = CPythonEventManager::Instance().RegisterEventSet(szFileName); + return Py_BuildValue("i", iEventIndex); +} + +PyObject * eventRegisterEventSetFromString(PyObject * poSelf, PyObject * poArgs) +{ + char * szEventString; + if (!PyTuple_GetString(poArgs, 0, &szEventString)) + return Py_BuildException(); + + int iEventIndex = CPythonEventManager::Instance().RegisterEventSetFromString(szEventString); + return Py_BuildValue("i", iEventIndex); +} + +PyObject * eventClearEventSet(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + CPythonEventManager::Instance().ClearEventSeti(iIndex); + return Py_BuildNone(); +} + +PyObject * eventSetRestrictedCount(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + int iCount; + if (!PyTuple_GetInteger(poArgs, 1, &iCount)) + return Py_BuildException(); + + CPythonEventManager::Instance().SetRestrictedCount(iIndex, iCount); + return Py_BuildNone(); +} + +PyObject * eventGetEventSetLocalYPosition(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + return Py_BuildValue("i", CPythonEventManager::Instance().GetEventSetLocalYPosition(iIndex)); +} + +PyObject * eventAddEventSetLocalYPosition(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + int iPos; + if (!PyTuple_GetInteger(poArgs, 1, &iPos)) + return Py_BuildException(); + + CPythonEventManager::Instance().AddEventSetLocalYPosition(iIndex, iPos); + return Py_BuildNone(); +} + +PyObject * eventInsertText(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + char * szText; + if (!PyTuple_GetString(poArgs, 1, &szText)) + return Py_BuildException(); + + CPythonEventManager::Instance().InsertText(iIndex, szText); + return Py_BuildNone(); +} + +PyObject * eventInsertTextInline(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + char * szText; + if (!PyTuple_GetString(poArgs, 1, &szText)) + return Py_BuildException(); + int iXIndex; + if (!PyTuple_GetInteger(poArgs, 2, &iXIndex)) + return Py_BuildException(); + + CPythonEventManager::Instance().InsertText(iIndex, szText,iXIndex); + return Py_BuildNone(); +} + +PyObject * eventUpdateEventSet(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + int ix; + if (!PyTuple_GetInteger(poArgs, 1, &ix)) + return Py_BuildException(); + + int iy; + if (!PyTuple_GetInteger(poArgs, 2, &iy)) + return Py_BuildException(); + + CPythonEventManager::Instance().UpdateEventSet(iIndex, ix, -iy); + return Py_BuildNone(); +} + +PyObject * eventRenderEventSet(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + CPythonEventManager::Instance().RenderEventSet(iIndex); + return Py_BuildNone(); +} + +PyObject * eventSetEventSetWidth(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + int iWidth; + if (!PyTuple_GetInteger(poArgs, 1, &iWidth)) + return Py_BuildException(); + + CPythonEventManager::Instance().SetEventSetWidth(iIndex, iWidth); + return Py_BuildNone(); +} + +PyObject * eventSkip(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + CPythonEventManager::Instance().Skip(iIndex); + return Py_BuildNone(); +} + +PyObject * eventIsWait(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + return Py_BuildValue("i", CPythonEventManager::Instance().IsWait(iIndex) == true ? 1 : 0); +} + +PyObject * eventEndEventProcess(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + CPythonEventManager::Instance().EndEventProcess(iIndex); + + return Py_BuildNone(); +} + +PyObject * eventSetEventHandler(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + PyObject * poEventHandler; + if (!PyTuple_GetObject(poArgs, 1, &poEventHandler)) + return Py_BuildException(); + + CPythonEventManager::Instance().SetEventHandler(iIndex, poEventHandler); + return Py_BuildNone(); +} + +PyObject * eventSelectAnswer(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + int iAnswer; + if (!PyTuple_GetInteger(poArgs, 1, &iAnswer)) + return Py_BuildException(); + + CPythonEventManager::Instance().SelectAnswer(iIndex, iAnswer); + return Py_BuildNone(); +} + +PyObject * eventGetLineCount(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + int iLineCount = CPythonEventManager::Instance().GetLineCount(iIndex); + return Py_BuildValue("i", iLineCount); +} + +PyObject * eventSetVisibleStartLine(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + int iStartLine; + if (!PyTuple_GetInteger(poArgs, 1, &iStartLine)) + return Py_BuildException(); + + CPythonEventManager::Instance().SetVisibleStartLine(iIndex, iStartLine); + return Py_BuildNone(); +} + +PyObject * eventGetVisibleStartLine(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + return Py_BuildValue("i", CPythonEventManager::Instance().GetVisibleStartLine(iIndex)); +} + +PyObject * eventQuestButtonClick(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BuildException(); + + CPythonNetworkStream::Instance().SendScriptButtonPacket(iIndex); + + return Py_BuildNone(); +} + +PyObject * eventSetInterfaceWindow(PyObject* poSelf, PyObject* poArgs) +{ + PyObject * pyHandle; + if (!PyTuple_GetObject(poArgs, 0, &pyHandle)) + return Py_BadArgument(); + + CPythonEventManager & rpem = CPythonEventManager::Instance(); + rpem.SetInterfaceWindow(pyHandle); + return Py_BuildNone(); +} + +PyObject * eventSetLeftTimeString(PyObject* poSelf, PyObject* poArgs) +{ + char * szText; + if (!PyTuple_GetString(poArgs, 0, &szText)) + return Py_BuildException(); + + CPythonEventManager & rpem = CPythonEventManager::Instance(); + rpem.SetLeftTimeString(szText); + return Py_BuildNone(); +} + +PyObject * eventDestroy(PyObject* poSelf, PyObject* poArgs) +{ + CPythonEventManager & rpem = CPythonEventManager::Instance(); + rpem.Destroy(); + return Py_BuildNone(); +} + +void initEvent() +{ + static PyMethodDef s_methods[] = + { + { "RegisterEventSet", eventRegisterEventSet, METH_VARARGS }, + { "RegisterEventSetFromString", eventRegisterEventSetFromString, METH_VARARGS }, + { "ClearEventSet", eventClearEventSet, METH_VARARGS }, + + { "SetRestrictedCount", eventSetRestrictedCount, METH_VARARGS }, + + { "GetEventSetLocalYPosition", eventGetEventSetLocalYPosition, METH_VARARGS }, + { "AddEventSetLocalYPosition", eventAddEventSetLocalYPosition, METH_VARARGS }, + { "InsertText", eventInsertText, METH_VARARGS }, + { "InsertTextInline", eventInsertTextInline, METH_VARARGS }, + + { "UpdateEventSet", eventUpdateEventSet, METH_VARARGS }, + { "RenderEventSet", eventRenderEventSet, METH_VARARGS }, + { "SetEventSetWidth", eventSetEventSetWidth, METH_VARARGS }, + + { "Skip", eventSkip, METH_VARARGS }, + { "IsWait", eventIsWait, METH_VARARGS }, + { "EndEventProcess", eventEndEventProcess, METH_VARARGS }, + + { "SelectAnswer", eventSelectAnswer, METH_VARARGS }, + { "GetLineCount", eventGetLineCount, METH_VARARGS }, + { "SetVisibleStartLine", eventSetVisibleStartLine, METH_VARARGS }, + { "GetVisibleStartLine", eventGetVisibleStartLine, METH_VARARGS }, + + { "SetEventHandler", eventSetEventHandler, METH_VARARGS }, + { "SetInterfaceWindow", eventSetInterfaceWindow, METH_VARARGS }, + { "SetLeftTimeString", eventSetLeftTimeString, METH_VARARGS }, + + { "QuestButtonClick", eventQuestButtonClick, METH_VARARGS }, + { "Destroy", eventDestroy, METH_VARARGS }, + { NULL, NULL, NULL }, + }; + + PyObject * poModule = Py_InitModule("event", s_methods); + + PyModule_AddIntConstant(poModule, "BOX_VISIBLE_LINE_COUNT", CPythonEventManager::BOX_VISIBLE_LINE_COUNT); + PyModule_AddIntConstant(poModule, "BUTTON_TYPE_NEXT", CPythonEventManager::BUTTON_TYPE_NEXT); + PyModule_AddIntConstant(poModule, "BUTTON_TYPE_DONE", CPythonEventManager::BUTTON_TYPE_DONE); + PyModule_AddIntConstant(poModule, "BUTTON_TYPE_CANCEL", CPythonEventManager::BUTTON_TYPE_CANCEL); +} \ No newline at end of file diff --git a/extension/src/port/UserInterface/PythonGameEventManagerModule.cpp b/extension/src/port/UserInterface/PythonGameEventManagerModule.cpp new file mode 100644 index 00000000..6b273adc --- /dev/null +++ b/extension/src/port/UserInterface/PythonGameEventManagerModule.cpp @@ -0,0 +1,30 @@ +#include "StdAfx.h" +#include "../gamelib/GameEventManager.h" + +PyObject * eventMgrUpdate(PyObject * poSelf, PyObject * poArgs) +{ + float fx; + if (!PyTuple_GetFloat(poArgs, 0, &fx)) + return Py_BuildException(); + float fy; + if (!PyTuple_GetFloat(poArgs, 1, &fy)) + return Py_BuildException(); + float fz; + if (!PyTuple_GetFloat(poArgs, 2, &fz)) + return Py_BuildException(); + + CGameEventManager::Instance().SetCenterPosition(fx, fy, fz); + CGameEventManager::Instance().Update(); + return Py_BuildNone(); +} + +void initeventmgr() +{ + static PyMethodDef s_methods[] = + { + { "Update", eventMgrUpdate, METH_VARARGS }, + { NULL, NULL, NULL }, + }; + + Py_InitModule("eventMgr", s_methods); +} \ No newline at end of file diff --git a/extension/src/port/UserInterface/PythonQuest.cpp b/extension/src/port/UserInterface/PythonQuest.cpp new file mode 100644 index 00000000..ce6f76fa --- /dev/null +++ b/extension/src/port/UserInterface/PythonQuest.cpp @@ -0,0 +1,270 @@ +#include "StdAfx.h" +#include "PythonQuest.h" + +void CPythonQuest::RegisterQuestInstance(const SQuestInstance & c_rQuestInstance) +{ + DeleteQuestInstance(c_rQuestInstance.dwIndex); + m_QuestInstanceContainer.push_back(c_rQuestInstance); + + ///// + + SQuestInstance & rQuestInstance = *m_QuestInstanceContainer.rbegin(); + rQuestInstance.iStartTime = int(CTimer::Instance().GetCurrentSecond()); +} + +struct FQuestInstanceCompare +{ + DWORD dwSearchIndex; + FQuestInstanceCompare(DWORD dwIndex) : dwSearchIndex(dwIndex) {} + bool operator () (const CPythonQuest::SQuestInstance & rQuestInstance) + { + return dwSearchIndex == rQuestInstance.dwIndex; + } +}; + +void CPythonQuest::DeleteQuestInstance(DWORD dwIndex) +{ + TQuestInstanceContainer::iterator itor = std::find_if(m_QuestInstanceContainer.begin(), m_QuestInstanceContainer.end(), FQuestInstanceCompare(dwIndex)); + if (itor == m_QuestInstanceContainer.end()) + return; + + m_QuestInstanceContainer.erase(itor); +} + +bool CPythonQuest::IsQuest(DWORD dwIndex) +{ + TQuestInstanceContainer::iterator itor = std::find_if(m_QuestInstanceContainer.begin(), m_QuestInstanceContainer.end(), FQuestInstanceCompare(dwIndex)); + return itor != m_QuestInstanceContainer.end(); +} + +void CPythonQuest::MakeQuest(DWORD dwIndex) +{ + DeleteQuestInstance(dwIndex); + m_QuestInstanceContainer.push_back(SQuestInstance()); + + ///// + + SQuestInstance & rQuestInstance = *m_QuestInstanceContainer.rbegin(); + rQuestInstance.dwIndex = dwIndex; + rQuestInstance.iStartTime = int(CTimer::Instance().GetCurrentSecond()); +} + +void CPythonQuest::SetQuestTitle(DWORD dwIndex, const char * c_szTitle) +{ + SQuestInstance * pQuestInstance; + if (!__GetQuestInstancePtr(dwIndex, &pQuestInstance)) + return; + + pQuestInstance->strTitle = c_szTitle; +} + +void CPythonQuest::SetQuestClockName(DWORD dwIndex, const char * c_szClockName) +{ + SQuestInstance * pQuestInstance; + if (!__GetQuestInstancePtr(dwIndex, &pQuestInstance)) + return; + + pQuestInstance->strClockName = c_szClockName; +} + +void CPythonQuest::SetQuestCounterName(DWORD dwIndex, const char * c_szCounterName) +{ + SQuestInstance * pQuestInstance; + if (!__GetQuestInstancePtr(dwIndex, &pQuestInstance)) + return; + + pQuestInstance->strCounterName = c_szCounterName; +} + +void CPythonQuest::SetQuestClockValue(DWORD dwIndex, int iClockValue) +{ + SQuestInstance * pQuestInstance; + if (!__GetQuestInstancePtr(dwIndex, &pQuestInstance)) + return; + + pQuestInstance->iClockValue = iClockValue; + pQuestInstance->iStartTime = int(CTimer::Instance().GetCurrentSecond()); +} + +void CPythonQuest::SetQuestCounterValue(DWORD dwIndex, int iCounterValue) +{ + SQuestInstance * pQuestInstance; + if (!__GetQuestInstancePtr(dwIndex, &pQuestInstance)) + return; + + pQuestInstance->iCounterValue = iCounterValue; +} + +void CPythonQuest::SetQuestIconFileName(DWORD dwIndex, const char * c_szIconFileName) +{ + SQuestInstance * pQuestInstance; + if (!__GetQuestInstancePtr(dwIndex, &pQuestInstance)) + return; + + pQuestInstance->strIconFileName = c_szIconFileName; +} + +int CPythonQuest::GetQuestCount() +{ + return m_QuestInstanceContainer.size(); +} + +bool CPythonQuest::GetQuestInstancePtr(DWORD dwArrayIndex, SQuestInstance ** ppQuestInstance) +{ + if (dwArrayIndex >= m_QuestInstanceContainer.size()) + return false; + + *ppQuestInstance = &m_QuestInstanceContainer[dwArrayIndex]; + + return true; +} + +bool CPythonQuest::__GetQuestInstancePtr(DWORD dwQuestIndex, SQuestInstance ** ppQuestInstance) +{ + TQuestInstanceContainer::iterator itor = std::find_if(m_QuestInstanceContainer.begin(), m_QuestInstanceContainer.end(), FQuestInstanceCompare(dwQuestIndex)); + if (itor == m_QuestInstanceContainer.end()) + return false; + + *ppQuestInstance = &(*itor); + + return true; +} + +void CPythonQuest::__Initialize() +{ +/* +#ifdef _DEBUG + for (int i = 0; i < 7; ++i) + { + SQuestInstance test; + test.dwIndex = i; + test.strIconFileName = ""; + test.strTitle = _getf("test%d", i); + test.strClockName = "남은 시간"; + test.strCounterName = "남은 마리수"; + test.iClockValue = 1000; + test.iCounterValue = 1000; + test.iStartTime = 0; + RegisterQuestInstance(test); + } +#endif +*/ +} + +void CPythonQuest::Clear() +{ + m_QuestInstanceContainer.clear(); +} + +CPythonQuest::CPythonQuest() +{ + __Initialize(); +} + +CPythonQuest::~CPythonQuest() +{ + Clear(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +PyObject * questGetQuestCount(PyObject * poSelf, PyObject * poArgs) +{ + return Py_BuildValue("i", CPythonQuest::Instance().GetQuestCount()); +} + +PyObject * questGetQuestData(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BadArgument(); + + CPythonQuest::SQuestInstance * pQuestInstance; + if (!CPythonQuest::Instance().GetQuestInstancePtr(iIndex, &pQuestInstance)) + return Py_BuildException("Failed to find quest by index %d", iIndex); + + CGraphicImage * pImage = NULL; + if (!pQuestInstance->strIconFileName.empty()) + { + std::string strIconFileName; + strIconFileName = "d:/ymir work/ui/game/quest/questicon/"; + strIconFileName += pQuestInstance->strIconFileName.c_str(); + pImage = (CGraphicImage *)CResourceManager::Instance().GetResourcePointer(strIconFileName.c_str()); + } + else + { + { + // 비어있을 경우 디폴트 이미지를 넣는다. + std::string strIconFileName = "season1/icon/scroll_open.tga"; + pImage = (CGraphicImage *)CResourceManager::Instance().GetResourcePointer(strIconFileName.c_str()); + } + } + + return Py_BuildValue("sisi", pQuestInstance->strTitle.c_str(), + pImage, + pQuestInstance->strCounterName.c_str(), + pQuestInstance->iCounterValue); +} + +PyObject * questGetQuestIndex(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BadArgument(); + + CPythonQuest::SQuestInstance * pQuestInstance; + if (!CPythonQuest::Instance().GetQuestInstancePtr(iIndex, &pQuestInstance)) + return Py_BuildException("Failed to find quest by index %d", iIndex); + + return Py_BuildValue("i", pQuestInstance->dwIndex); +} + +PyObject * questGetQuestLastTime(PyObject * poSelf, PyObject * poArgs) +{ + int iIndex; + if (!PyTuple_GetInteger(poArgs, 0, &iIndex)) + return Py_BadArgument(); + + CPythonQuest::SQuestInstance * pQuestInstance; + if (!CPythonQuest::Instance().GetQuestInstancePtr(iIndex, &pQuestInstance)) + return Py_BuildException("Failed to find quest by index %d", iIndex); + + int iLastTime = 0; + + if (pQuestInstance->iClockValue >= 0) + { + iLastTime = (pQuestInstance->iStartTime + pQuestInstance->iClockValue) - int(CTimer::Instance().GetCurrentSecond()); + } + + // 시간 증가 처리 코드 +// else +// { +// iLastTime = int(CTimer::Instance().GetCurrentSecond()) - pQuestInstance->iStartTime; +// } + + return Py_BuildValue("si", pQuestInstance->strClockName.c_str(), iLastTime); +} + +PyObject * questClear(PyObject * poSelf, PyObject * poArgs) +{ + CPythonQuest::Instance().Clear(); + return Py_BuildNone(); +} + +void initquest() +{ + static PyMethodDef s_methods[] = + { + { "GetQuestCount", questGetQuestCount, METH_VARARGS }, + { "GetQuestData", questGetQuestData, METH_VARARGS }, + { "GetQuestIndex", questGetQuestIndex, METH_VARARGS }, + { "GetQuestLastTime", questGetQuestLastTime, METH_VARARGS }, + { "Clear", questClear, METH_VARARGS }, + { NULL, NULL, NULL }, + }; + + PyObject * poModule = Py_InitModule("quest", s_methods); + PyModule_AddIntConstant(poModule, "QUEST_MAX_NUM", 5); +} diff --git a/extension/src/port/UserInterface/StdAfx.h b/extension/src/port/UserInterface/StdAfx.h index 49bc269c..e45a0b83 100644 --- a/extension/src/port/UserInterface/StdAfx.h +++ b/extension/src/port/UserInterface/StdAfx.h @@ -46,6 +46,9 @@ enum void initpack(); void initime(); void initshop(); +void initquest(); +void initEvent(); +void initeventmgr(); void initapp(); void initchr(); void initchrmgr(); diff --git a/extension/src/port/common/Win32Crt.cpp b/extension/src/port/common/Win32Crt.cpp index f708cbb9..8c99b6f8 100644 --- a/extension/src/port/common/Win32Crt.cpp +++ b/extension/src/port/common/Win32Crt.cpp @@ -391,4 +391,35 @@ int WideCharToMultiByte(UINT CodePage, DWORD, LPCWSTR lpWideCharStr, int cchWide return int(bytes.size()); } +BOOL IsDBCSLeadByteEx(UINT CodePage, BYTE TestChar) +{ + switch (CodePage) { + case 932: return (TestChar >= 0x81 && TestChar <= 0x9F) || (TestChar >= 0xE0 && TestChar <= 0xFC); + case 936: + case 949: + case 950: return TestChar >= 0x81 && TestChar <= 0xFE; + default: return FALSE; + } +} + +LPSTR CharNextExA(WORD CodePage, LPCSTR lpCurrentChar, DWORD) +{ + if (!*lpCurrentChar) return const_cast(lpCurrentChar); + if (IsDBCSLeadByteEx(CodePage, BYTE(*lpCurrentChar)) && lpCurrentChar[1]) return const_cast(lpCurrentChar + 2); + return const_cast(lpCurrentChar + 1); +} + +LPSTR CharPrevExA(WORD CodePage, LPCSTR lpStart, LPCSTR lpCurrentChar, DWORD dwFlags) +{ + // Walk forward from lpStart so a trail byte is never taken for a lead byte. + LPCSTR prev = lpStart; + for (LPCSTR p = lpStart; p < lpCurrentChar;) { + prev = p; + LPCSTR next = CharNextExA(CodePage, p, dwFlags); + if (next == p) break; + p = next; + } + return const_cast(prev); +} + #endif // !_WIN32 diff --git a/extension/src/port/common/Win32Crt.h b/extension/src/port/common/Win32Crt.h index 2b3df282..6aa3d093 100644 --- a/extension/src/port/common/Win32Crt.h +++ b/extension/src/port/common/Win32Crt.h @@ -101,4 +101,10 @@ int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, LPCWSTR lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar); +// winnls.h / winuser.h DBCS stepping. Lead bytes follow the double-byte code pages 932, 936, 949 and 950; +// every other code page (1252, UTF-8, ...) steps one byte at a time, as Windows does for single-byte pages. +BOOL IsDBCSLeadByteEx(UINT CodePage, BYTE TestChar); +LPSTR CharNextExA(WORD CodePage, LPCSTR lpCurrentChar, DWORD dwFlags); +LPSTR CharPrevExA(WORD CodePage, LPCSTR lpStart, LPCSTR lpCurrentChar, DWORD dwFlags); + #endif // !_WIN32 diff --git a/extension/tests/port_login_flow_server.cpp b/extension/tests/port_login_flow_server.cpp index f0a4ef54..187e61e7 100644 --- a/extension/tests/port_login_flow_server.cpp +++ b/extension/tests/port_login_flow_server.cpp @@ -603,6 +603,19 @@ bool FakeLoginServer::ServeGame(Connection& c, int index) keeper_update.attack_speed = 100; if (!c.Send(keeper) || !c.Send(keeper_info) || !c.Send(keeper_update)) return Fail(name + ": send shopkeeper"); + GCCharacterAdd elder = keeper; + elder.vid = kQuestVID; + elder.x = kQuestX; + elder.y = kQuestY; + elder.race = kQuestRace; + GCCharAddInfo elder_info = zeroed(); + elder_info.header = HDR_GC_CHAR_ADDITIONAL_INFO; + elder_info.vid = kQuestVID; + std::strncpy(elder_info.name, "Oldster", sizeof(elder_info.name) - 1); + GCCharacterUpdate elder_update = keeper_update; + elder_update.vid = kQuestVID; + if (!c.Send(elder) || !c.Send(elder_info) || !c.Send(elder_update)) + return Fail(name + ": send quest NPC"); Event(name + ":burst_sent"); // A ping after the burst proves that GamePhase consumed every packet above and can reply. @@ -619,6 +632,20 @@ bool FakeLoginServer::ServeGame(Connection& c, int index) 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) + int quest_page = 0; // the quest dialogue's last page sent (1 question, 2 next, 3 done) + // CQuestManager::SendScript: packet_script {header, size, skin, src_size} and the script text. + auto send_script = [&](const std::string& script) { + struct + { + DynHead head; + std::uint8_t skin; + std::uint16_t src_size; + } __attribute__((packed)) head = {{HDR_GC_SCRIPT, static_cast(sizeof(head) + script.size())}, 1 /* QUEST_SKIN_NORMAL */, + static_cast(script.size())}; + std::string bytes(reinterpret_cast(&head), sizeof(head)); + bytes += script; + return c.Send(bytes.data(), bytes.size()); + }; for (;;) { std::uint8_t packet[1024] = {}; @@ -759,6 +786,11 @@ bool FakeLoginServer::ServeGame(Connection& c, int index) CGOnClick click; std::memcpy(&click, packet, sizeof(click)); Event(name + ":click " + std::to_string(click.vid)); + // CHARACTER::OnClick → CQuestManager::Click: say(...) then select("Yes", "No") → GotoSelectState. + if (click.vid == kQuestVID && !send_script("Greetings, young warrior.[ENTER]Shall I tell you a story?[ENTER][QUESTION 1;Yes|2;No]")) + return Fail(name + ": send GC_SCRIPT question"); + if (click.vid == kQuestVID) + quest_page = 1; if (click.vid == kShopVID && !shopping) { shopping = true; @@ -780,6 +812,25 @@ bool FakeLoginServer::ServeGame(Connection& c, int index) return Fail(name + ": send GC_SHOP START"); } } + // CInputMain::ScriptAnswer: an answer above 250 is the [NEXT] button (CQuestManager::Resume), the rest pick + // a [QUESTION] choice (CQuestManager::Select, 0-based). + if (header == HDR_CG_SCRIPT_ANSWER) + { + const std::uint8_t answer = packet[1]; + Event(name + ":answer " + std::to_string(answer)); + if (quest_page == 1 && answer == 0) + { + quest_page = 2; + if (!send_script("Long ago a dragon slept under this village.[ENTER][NEXT]")) + return Fail(name + ": send GC_SCRIPT next"); + } + else if (quest_page == 2 && answer > 250) + { + quest_page = 3; + if (!send_script("It sleeps there still.[ENTER][DONE]")) + return Fail(name + ": send GC_SCRIPT done"); + } + } // 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)). diff --git a/extension/tests/port_login_flow_server.h b/extension/tests/port_login_flow_server.h index a0bb095a..2a16b59e 100644 --- a/extension/tests/port_login_flow_server.h +++ b/extension/tests/port_login_flow_server.h @@ -37,6 +37,10 @@ public: 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; + // An old man (npclist 20009) 3 m east of the store whose click runs a quest dialogue the way CQuestManager + // sends it: say + select ([QUESTION]), then a [NEXT] page, then a [DONE] page. + static constexpr std::uint32_t kQuestVID = 0x00045678, kQuestRace = 20009; + static constexpr std::int32_t kQuestX = kShopX + 300, kQuestY = kShopY; ~FakeLoginServer();