diff --git a/CMakeLists.txt b/CMakeLists.txt index cb0c71c..76ccca0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ install(DIRECTORY "${VGUI2EXTENSION_GAMEDATA_DIR}/" DESTINATION svencoop/metahoo option(VGUI2EXTENSION_BUILD_TESTS "Build existing language and IME regression tests" OFF) if(VGUI2EXTENSION_BUILD_TESTS) enable_testing() - foreach(test language_registry_tests ime_window_message_test combobox_unicode_tests career_player_name_tests teammenu_hook_tests default_server_name_tests career_popup_order_tests career_profile_storage_tests buildmode_layout_tests) + foreach(test language_registry_tests ime_window_message_test combobox_unicode_tests career_player_name_tests teammenu_hook_tests default_server_name_tests career_popup_order_tests career_profile_storage_tests buildmode_layout_tests career_end_menu_tests) add_executable(${test} "tests/${test}.cpp") target_compile_features(${test} PRIVATE cxx_std_17) target_compile_options(${test} PRIVATE /W4 /WX /UNDEBUG) diff --git a/docs/en/gamedata.md b/docs/en/gamedata.md index 645976a..d9e59c8 100644 --- a/docs/en/gamedata.md +++ b/docs/en/gamedata.md @@ -88,7 +88,7 @@ The Career round-end ESC fix additionally requires the client `CCSCareerRoundEnd vtable and the virtual slots of `vgui2::Frame::OnKeyCodePressed(vgui2::KeyCode)` and `vgui2::Frame::OnKeyCodeTyped(vgui2::KeyCode)`, requested in [GoldSrc_VibeSignatures #397](https://github.com/HLND2T/GoldSrc_VibeSignatures/issues/397). -Only the RoundEnd table is hooked: ESC leaves the pending choice intact while the +The RoundEnd table is hooked: ESC leaves the pending choice intact while the engine toggles GameUI; other keys and the original button commands are forwarded. Both key-code paths are protected, including the Typed app-modal close path. These three records are published in `v20261023a`. An older catalog fails the @@ -104,6 +104,16 @@ retain their native handlers. This fourth record is published in `v20261023b`; deploy the final DLL with that catalog or newer. `v20261023a` does not satisfy the extended manifest. +The MatchEnd ESC fix also requires the client `CCSCareerMatchEndMenu` vtable and +`CCSCareerMatchEndMenu::OnCommand(char const*)` virtual slot. It applies the same +key and generic-command guards to MatchEnd's own table, preserving the current +music and pending choice while GameUI toggles. Finish, retry and Credits retain +their native handlers. Both Frame key slots are shared with RoundEnd; no shared +Frame implementation or music handler is patched. These two MatchEnd records, +requested in [GoldSrc_VibeSignatures #409](https://github.com/HLND2T/GoldSrc_VibeSignatures/issues/409), +are published in `v20261026a`. Deploy that catalog or newer with this DLL on +`czero`; older catalogs fail the extended manifest. + The native build editor DPI hooks require `v20261025b` or newer. They hook the GameUI constructor and the editor/list vtables, retaining native editing, saving and ownership. Only editor lists receive the row/column and scrollbar correction. @@ -115,19 +125,19 @@ casting native containers or the dialog to the SDK's private classes. The declaration is [scripts/manifests/vgui2extension.json](../../scripts/manifests/vgui2extension.json). The list below follows its exact module, name and kind and has been checked against the -source calls. It contains **256 distinct explicit records** across 5 modules, +source calls. It contains **258 distinct explicit records** across 5 modules, including the initial `_0` records of the conditional numbered patch sets -(23 declarations). Each numbered set can consume additional records; 256 is not +(23 declarations). Each numbered set can consume additional records; 258 is not the total size of every installed catalog. | Module | Explicit entries | | --- | ---: | | `engine` | 14 | -| `client` | 37 | +| `client` | 39 | | `gameui` | 195 | | `serverbrowser` | 9 | | `vgui2` | 1 | -| Total | 256 | +| Total | 258 | A GameSymbol is identified by its module and name. Identical names in `engine`, `client`, `gameui` and `serverbrowser` refer to separate records. Conditions below describe the @@ -227,6 +237,8 @@ Sources: [ClientVGUI.cpp](../../src/ClientVGUI.cpp) and [privatefuncs.cpp](../.. | `CCareerBaseBox::s_currentBox` | `global` | G24 | | `CCSCareerRoundEndMenu` | `vtable` | G24 | | `CCSCareerRoundEndMenu::OnCommand(char const*)` | `virtualFunction` | G24 | +| `CCSCareerMatchEndMenu` | `vtable` | G24 | +| `CCSCareerMatchEndMenu::OnCommand(char const*)` | `virtualFunction` | G24 | | `vgui2::Frame::OnKeyCodePressed(vgui2::KeyCode)` | `virtualFunction` | G24 | | `vgui2::Frame::OnKeyCodeTyped(vgui2::KeyCode)` | `virtualFunction` | G24 | | `CounterStrikeViewport.m_pCSBackGround` | `structMember` | G8 | diff --git a/docs/zh-CN/gamedata.md b/docs/zh-CN/gamedata.md index 77ffd9a..a16f996 100644 --- a/docs/zh-CN/gamedata.md +++ b/docs/zh-CN/gamedata.md @@ -73,7 +73,7 @@ CRT locale,也不经过原有的 31 字节截断。无效 UTF-8 或缺失本 `vgui2::Frame::OnKeyCodePressed(vgui2::KeyCode)`、 `vgui2::Frame::OnKeyCodeTyped(vgui2::KeyCode)` 的虚槽位,已通过 [GoldSrc_VibeSignatures #397](https://github.com/HLND2T/GoldSrc_VibeSignatures/issues/397) -请求补齐。仅修改 RoundEnd 的虚表:ESC 保留待确认窗口,引擎仍切换主菜单;其他按键与 +请求补齐。修改 RoundEnd 的虚表:ESC 保留待确认窗口,引擎仍切换主菜单;其他按键与 原有按钮命令照常处理。两个 KeyCode 入口均受保护,包括 Typed 的 app-modal 关闭路径。 这三项记录已在 `v20261023a` 发布,旧 catalog 无法通过扩展后的 manifest,也无法在 `czero` 中运行此 DLL;应搭配匹配 catalog 部署。其他游戏不解析或安装这组 hook。 @@ -84,6 +84,14 @@ CRT locale,也不经过原有的 31 字节截断。无效 UTF-8 或缺失本 保留待确认的 Career 选择;业务命令继续由原生处理。这第四项记录已在 `v20261023b` 发布;最终 DLL 应搭配该版本或更新 catalog,`v20261023a` 不满足扩展后的 manifest。 +MatchEnd 的 ESC 修复还需要 client 的 `CCSCareerMatchEndMenu` 虚表与 +`CCSCareerMatchEndMenu::OnCommand(char const*)` 虚槽位。相同的按键和通用命令保护安装在 +MatchEnd 自己的虚表上,切换 GameUI 时保留当前音乐和待确认选择;完成、重试、Credits +继续交给原生处理。两个 Frame 按键槽位与 RoundEnd 共用,不修改共享 Frame 实现或音乐处理。 +这两项记录已通过 [GoldSrc_VibeSignatures #409](https://github.com/HLND2T/GoldSrc_VibeSignatures/issues/409) +请求补齐,已在 `v20261026a` 发布。在 `czero` 使用此 DLL 时,应部署该版本或更新的 +catalog;旧版本无法通过扩展后的 manifest 校验。 + 原生构建编辑器 DPI hook 需要 `v20261025b` 或更新版本。它们 hook GameUI 构造入口和 编辑器/列表虚表,保留原生编辑、保存和所有权行为;行列布局及滚动条修正只作用于编辑器列表。 原生编辑器继承 Frame 的 scheme 处理,子控件已使用比例字体,因此不修改共享 Frame 函数。 @@ -92,17 +100,17 @@ CRT locale,也不经过原有的 31 字节截断。无效 UTF-8 或缺失本 声明来源为 [scripts/manifests/vgui2extension.json](../../scripts/manifests/vgui2extension.json)。 以下清单保留 manifest 中的完整 module、名称与 kind,并已对照源码调用核对。 -共 5 个 module、**256 条不同的显式记录**,其中包含条件编号补丁组的首条 `_0` 记录 -(共 23 处声明)。每组编号补丁还可能消费后续记录,因此 256 不是每份安装 catalog 的记录总数。 +共 5 个 module、**258 条不同的显式记录**,其中包含条件编号补丁组的首条 `_0` 记录 +(共 23 处声明)。每组编号补丁还可能消费后续记录,因此 258 不是每份安装 catalog 的记录总数。 | Module | 显式记录数 | | --- | ---: | | `engine` | 14 | -| `client` | 37 | +| `client` | 39 | | `gameui` | 195 | | `serverbrowser` | 9 | | `vgui2` | 1 | -| 合计 | 256 | +| 合计 | 258 | GameSymbol 由 module 与名称共同定位。`engine`、`client`、`gameui`、`serverbrowser` 中的同名符号是独立记录。下列条件表示 manifest 的 catalog 裁剪与校验范围; @@ -199,6 +207,8 @@ GameSymbol 由 module 与名称共同定位。`engine`、`client`、`gameui`、` | `CCareerBaseBox::s_currentBox` | `global` | G24 | | `CCSCareerRoundEndMenu` | `vtable` | G24 | | `CCSCareerRoundEndMenu::OnCommand(char const*)` | `virtualFunction` | G24 | +| `CCSCareerMatchEndMenu` | `vtable` | G24 | +| `CCSCareerMatchEndMenu::OnCommand(char const*)` | `virtualFunction` | G24 | | `vgui2::Frame::OnKeyCodePressed(vgui2::KeyCode)` | `virtualFunction` | G24 | | `vgui2::Frame::OnKeyCodeTyped(vgui2::KeyCode)` | `virtualFunction` | G24 | | `CounterStrikeViewport.m_pCSBackGround` | `structMember` | G8 | diff --git a/memory/build_and_verification.md b/memory/build_and_verification.md index 32fe6fd..8449c93 100644 --- a/memory/build_and_verification.md +++ b/memory/build_and_verification.md @@ -187,3 +187,17 @@ Chinese numeric-format follow-up: GetCurrentGameLanguage() values schinese/tchin - Verification: MSVC x86 Release build,9 CTests (new geometry bounds/font/resize/repeated-calculation tests),11 Python tests, pinned70-file format-check and git diff --check passed. Live final layout:720p300x400/row24,1080p450x600/row36,1440p600x800/row48, no horizontal row overflow or clipped scrollbar. Native mouse resized600x800 to780x940, moved to[780,220], and Exit/reopen retained that exact pixel geometry. Selecting Options ApplyButton rebuilt rows at48px; native button drag/save wrote xpos614/ypos391/wide72/tall24 for a144x48px button, and subsequent launches loaded the saved resource. Scroll drag reached the last rows. Final source cleanup only named the same design constants and removed a duplicate idempotent row-sizing pass; compilation/tests reran afterward, the full live matrix preceded that cleanup. - Verification limits: activation used the existing Options BuildGroup SetEnabled on the main RunFrame thread, then debugger detachment; physical Shift+Ctrl+Alt+B routing remains unverified because CLI injection did not trigger creation. Requested3840x2160 was clamped to3440x1440, so4K is not runtime verified. Existing DpiManagerInternal::InitClient forces support true for HL25 after command-line flags; -no_high_dpi therefore did not test an actual disabled-DPI path. Other builds/custom schemes have catalog coverage only. - Cleanup/evidence: ten task-owned processes exited normally via RCON code0; no debugger sessions remain.72 original DLL/PDB/catalog/config/resource/appid files restored with SHA256 verification;11 task screenshots moved into ignored build/issue954-fix-runtime/screenshots. Evidence includes installed-hooks.txt,matrix.json,result.md,saved-options-dialog.res and restoration.json. Do not publish config backups or screenshots automatically. Original game deployment is restored. + + +## Issue 957: MatchEnd ESC replays MatchWin (2026-10-26) + +- Trigger: czero engine10210 Career match victory; repeated ESC/GameUI round trips replay MatchWin. MatchEnd is outside bea92c5's RoundEnd-only hooks. +- Root cause/constraint: inherited Frame ESC posts Cancel (Pressed) and has an app-modal Close path (Typed). MatchEnd::OnCommand treats generic commands as native completion: unpause, OnClose, OnMatchEndMenuClose(false). GameUI sets canUnpause=true and calls FadeOutTrack, which immediately resets currentTrack=INVALID while issuing cd fadeout. The completed server match keeps its due m_fCareerMatchMenuTime; Think resends CZCareer/MATCH and CZCareerHUD/MATCH after unpause. MatchEnd::Update -> OnMatchEndMenuOpen -> StartTrack(5,false) then restarts because the current-track identity was reset. Engine ESC toggling can still cause repeated MATCH after this fix; preserving currentTrack=5 lets the native same-track guard suppress actual playback. +- Static evidence: requested Windows/Linux10210 client/GameUI IDBs plus czero10210/server/cs.so.i64 inspected read-only. Windows MatchEnd AP0x100C8688, OnCommand0x1002B810, Update0x1002B9C0; Linux AP0x17F34C, OnCommand0x82610, Update0x81CE0. GameUI MatchClose0x10014490/0x624F0, FadeOut0x10030820/0x8A990, MatchOpen0x100144F0/0x62490, StartTrack0x10030AB0/0x8A7E0. Server send/pause block0xACE50-0xACF68 leaves the deadline intact. Addresses are analysis observations, not runtime hardcodes. All task IDA workers closed without saving. +- Correct implementation: czero excluding CZDS; three hooks on MatchEnd's own primary table share ForwardCareerEndMenuKey/Command with RoundEnd. Ignore ESC Pressed/Typed and exact case-insensitive generic Close/Cancel; forward other keys/business commands unchanged. Six-hook rollback/shutdown restores slots and resets pointers. Resolve shared Frame key slots and new MatchEnd table/OnCommand through required host gamedata. No music/server/global Frame patch. Manifest and bilingual inventory258 explicit records (client39). +- Published dependency: user-authorized upstream request GoldSrc_VibeSignatures #409 (https://github.com/HLND2T/GoldSrc_VibeSignatures/issues/409), fulfilled by v20261026a. Official Pages sync now downloads23 snapshots; real sync/prune and manifest validation all23 pass. Reconfigured VGUI2EXTENSION_SYNC_GAMEDATA=ON (local cache had been OFF), then normal MSVC x86 Release build of VGUI2Extension and career_end_menu_tests passed with project dependencies enabled. Old catalog remains incompatible. No local fake records or hand-patched runtime catalog. +- Local verification: forwarding test red before helper, then green; all10 CTests and11 Python tests passed, pinned clang-format checked72 files with zero differences, git diff --check passed. Existing signedness and /UNDEBUG warnings remain. Mock tests prove filtering and original argument/callback forwarding only. +- Runtime verification: official catalog + final code on Windows czero10210. Actual native table inspection verified all six installed slots. Independent temporary profile and backed-up Easy first-map fixture (one win/one margin, no task) used native Career menus and real server round completion, not injected MATCH. First victory baseline counters [StartTrack5 entry=1, Fade=1, Update=2, native MatchCommand=1, ESC pressed/typed=0/0, actual play=1]; prior Fade/MatchCommand came from defeat Retry. After6 ESC round trips: [7,1,8,1,6/6,1], currentTrack=5. Thus updates/StartTrack entries repeat but actual playback, fade and native completion do not. Second victory plus4 later ESC round trips: total actual plays2 (one per match), currentTrack5. Track file duration29.276188s; later switching occurred after that duration. Earlier process tested4 early round trips with currentTrack5 and zero fade/native command; actual-play trace was installed afterward, so do not attribute that counter to the earlier run. +- Business/lifecycle verification: native defeat Retry forwarded career_restart and restarted; victory Enter default forwarded disconnect and returned CareerMapFrame. At shutdown, breakpoint after UninstallCareerEndMenuHooks confirmed all six slots restored to native client functions (Match command RVA0x2B810; Round command0x2CBC0; both pressed0x7BFB0/typed0x7C0A0). All three task processes16768/34656/35904 exited normally, code0. Debuggers detached/closed. +- Cleanup/evidence:44 original DLL/PDB/catalog/config/profile/appid files restored and SHA256 verified. Task-created selected files and BMPs moved into ignored build/issue957-runtime, preserving original files/screenshots. Evidence result.json, restoration.json and screenshots remain local; do not publish backups automatically. Only the plugin source, regression tests, manifest, documentation and this verification note belong in the change; ignored runtime evidence and game backups remain local. +- Limits/applicability: no captured audio or stream-position trace; playback-command instrumentation establishes no additional play call, not audible output quality. Credits and RoundEnd choice/default-button/surrender branches were not reached in live verification (normal campaign trial did not show RoundEnd); forwarding unit tests and static boundary cover those commands only. Windows8684 has official catalog validation; Linux has static evidence; neither has runtime qualification. No broader compatibility claim. diff --git a/scripts/manifests/vgui2extension.json b/scripts/manifests/vgui2extension.json index 546e50d..3a1d27a 100644 --- a/scripts/manifests/vgui2extension.json +++ b/scripts/manifests/vgui2extension.json @@ -564,6 +564,8 @@ "CCareerBaseBox::s_currentBox": "global", "CCSCareerRoundEndMenu": "vtable", "CCSCareerRoundEndMenu::OnCommand(char const*)": "virtualFunction", + "CCSCareerMatchEndMenu": "vtable", + "CCSCareerMatchEndMenu::OnCommand(char const*)": "virtualFunction", "vgui2::Frame::OnKeyCodePressed(vgui2::KeyCode)": "virtualFunction", "vgui2::Frame::OnKeyCodeTyped(vgui2::KeyCode)": "virtualFunction" } diff --git a/src/CareerEndMenuInput.h b/src/CareerEndMenuInput.h new file mode 100644 index 0000000..d5d5232 --- /dev/null +++ b/src/CareerEndMenuInput.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +//ESC only switches GameUI. Keep the pending Career choice and music state +//intact when the completed match sends MATCH again. +template +void ForwardCareerEndMenuKey(Handler original, void* self, int dummy, KeyCode key, KeyCode escapeKey) +{ + if (key != escapeKey) + original(self, dummy, key); +} + +template +void ForwardCareerEndMenuCommand(Handler original, void* self, int dummy, const char* command) +{ + //Child dialogs can forward the same generic close commands as Frame. + if (command && (!_stricmp(command, "Close") || !_stricmp(command, "Cancel"))) + return; + + original(self, dummy, command); +} diff --git a/src/ClientVGUI.cpp b/src/ClientVGUI.cpp index d56cfcd..1a34c0f 100644 --- a/src/ClientVGUI.cpp +++ b/src/ClientVGUI.cpp @@ -18,6 +18,7 @@ #include "exportfuncs.h" #include "ComboBoxUnicode.h" #include "CareerPopupOrder.h" +#include "CareerEndMenuInput.h" #include #include "DpiManagerInternal.h" @@ -71,8 +72,8 @@ static vgui::IClientPanel** g_pCareerCurrentBox = nullptr; static bool g_bInCareerViewportTick = false; static void** g_pCareerRoundEndVTable = nullptr; -static DWORD g_CareerRoundEndKeyPressedSlot = 0; -static DWORD g_CareerRoundEndKeyTypedSlot = 0; +static DWORD g_CareerEndMenuKeyPressedSlot = 0; +static DWORD g_CareerEndMenuKeyTypedSlot = 0; static DWORD g_CareerRoundEndOnCommandSlot = 0; static hook_t* g_phook_CareerRoundEnd_OnKeyCodePressed = nullptr; static hook_t* g_phook_CareerRoundEnd_OnKeyCodeTyped = nullptr; @@ -81,28 +82,65 @@ static void(__fastcall* g_pfnCareerRoundEnd_OnKeyCodePressed)(void*, int, vgui:: static void(__fastcall* g_pfnCareerRoundEnd_OnKeyCodeTyped)(void*, int, vgui::KeyCode) = nullptr; static void(__fastcall* g_pfnCareerRoundEnd_OnCommand)(void*, int, const char*) = nullptr; -//The engine still handles ESC to toggle GameUI. Closing the Career frame here -//would lose the user's pending choice without sending career_continue. +static void** g_pCareerMatchEndVTable = nullptr; +static DWORD g_CareerMatchEndOnCommandSlot = 0; +static hook_t* g_phook_CareerMatchEnd_OnKeyCodePressed = nullptr; +static hook_t* g_phook_CareerMatchEnd_OnKeyCodeTyped = nullptr; +static hook_t* g_phook_CareerMatchEnd_OnCommand = nullptr; +static void(__fastcall* g_pfnCareerMatchEnd_OnKeyCodePressed)(void*, int, vgui::KeyCode) = nullptr; +static void(__fastcall* g_pfnCareerMatchEnd_OnKeyCodeTyped)(void*, int, vgui::KeyCode) = nullptr; +static void(__fastcall* g_pfnCareerMatchEnd_OnCommand)(void*, int, const char*) = nullptr; + static void __fastcall CareerRoundEnd_OnKeyCodePressed(void* pthis, int dummy, vgui::KeyCode code) { - if (code != vgui::KEY_ESCAPE) - g_pfnCareerRoundEnd_OnKeyCodePressed(pthis, dummy, code); + ForwardCareerEndMenuKey(g_pfnCareerRoundEnd_OnKeyCodePressed, pthis, dummy, code, vgui::KEY_ESCAPE); } static void __fastcall CareerRoundEnd_OnKeyCodeTyped(void* pthis, int dummy, vgui::KeyCode code) { - if (code != vgui::KEY_ESCAPE) - g_pfnCareerRoundEnd_OnKeyCodeTyped(pthis, dummy, code); + ForwardCareerEndMenuKey(g_pfnCareerRoundEnd_OnKeyCodeTyped, pthis, dummy, code, vgui::KEY_ESCAPE); } static void __fastcall CareerRoundEnd_OnCommand(void* pthis, int dummy, const char* command) { - //A child surrender confirmation can also forward Frame's generic ESC - //commands to RoundEnd. Only business commands may finish the pending choice. - if (command && (!stricmp(command, "Close") || !stricmp(command, "Cancel"))) - return; + ForwardCareerEndMenuCommand(g_pfnCareerRoundEnd_OnCommand, pthis, dummy, command); +} + +static void __fastcall CareerMatchEnd_OnKeyCodePressed(void* pthis, int dummy, vgui::KeyCode code) +{ + ForwardCareerEndMenuKey(g_pfnCareerMatchEnd_OnKeyCodePressed, pthis, dummy, code, vgui::KEY_ESCAPE); +} - g_pfnCareerRoundEnd_OnCommand(pthis, dummy, command); +static void __fastcall CareerMatchEnd_OnKeyCodeTyped(void* pthis, int dummy, vgui::KeyCode code) +{ + ForwardCareerEndMenuKey(g_pfnCareerMatchEnd_OnKeyCodeTyped, pthis, dummy, code, vgui::KEY_ESCAPE); +} + +static void __fastcall CareerMatchEnd_OnCommand(void* pthis, int dummy, const char* command) +{ + ForwardCareerEndMenuCommand(g_pfnCareerMatchEnd_OnCommand, pthis, dummy, command); +} + +static void UninstallCareerEndMenuHooks() +{ + Uninstall_Hook(CareerMatchEnd_OnCommand); + Uninstall_Hook(CareerMatchEnd_OnKeyCodeTyped); + Uninstall_Hook(CareerMatchEnd_OnKeyCodePressed); + Uninstall_Hook(CareerRoundEnd_OnCommand); + Uninstall_Hook(CareerRoundEnd_OnKeyCodeTyped); + Uninstall_Hook(CareerRoundEnd_OnKeyCodePressed); + g_pCareerMatchEndVTable = nullptr; + g_pfnCareerMatchEnd_OnKeyCodePressed = nullptr; + g_pfnCareerMatchEnd_OnKeyCodeTyped = nullptr; + g_pfnCareerMatchEnd_OnCommand = nullptr; + g_CareerMatchEndOnCommandSlot = 0; + g_pCareerRoundEndVTable = nullptr; + g_pfnCareerRoundEnd_OnKeyCodePressed = nullptr; + g_pfnCareerRoundEnd_OnKeyCodeTyped = nullptr; + g_pfnCareerRoundEnd_OnCommand = nullptr; + g_CareerEndMenuKeyPressedSlot = 0; + g_CareerEndMenuKeyTypedSlot = 0; + g_CareerRoundEndOnCommandSlot = 0; } struct CareerPopupPanels @@ -1123,12 +1161,16 @@ void NativeClientUI_FillAddress(const mh_dll_info_t& RealDllInfo) g_pCareerRoundEndVTable = (void**)GamedataResolvePtr( RealDllInfo.ImageBase, "client", "CCSCareerRoundEndMenu", MH_GAMESYMBOL_KIND_VTABLE); - g_CareerRoundEndKeyPressedSlot = GamedataResolveVFuncIndex( + g_CareerEndMenuKeyPressedSlot = GamedataResolveVFuncIndex( RealDllInfo.ImageBase, "client", "vgui2::Frame::OnKeyCodePressed(vgui2::KeyCode)"); - g_CareerRoundEndKeyTypedSlot = GamedataResolveVFuncIndex( + g_CareerEndMenuKeyTypedSlot = GamedataResolveVFuncIndex( RealDllInfo.ImageBase, "client", "vgui2::Frame::OnKeyCodeTyped(vgui2::KeyCode)"); g_CareerRoundEndOnCommandSlot = GamedataResolveVFuncIndex( RealDllInfo.ImageBase, "client", "CCSCareerRoundEndMenu::OnCommand(char const*)"); + g_pCareerMatchEndVTable = (void**)GamedataResolvePtr( + RealDllInfo.ImageBase, "client", "CCSCareerMatchEndMenu", MH_GAMESYMBOL_KIND_VTABLE); + g_CareerMatchEndOnCommandSlot = GamedataResolveVFuncIndex( + RealDllInfo.ImageBase, "client", "CCSCareerMatchEndMenu::OnCommand(char const*)"); } } @@ -1181,38 +1223,40 @@ void NativeClientUI_InstallHooks(void) if (!g_phook_CareerRoundEnd_OnKeyCodePressed) g_phook_CareerRoundEnd_OnKeyCodePressed = g_pMetaHookAPI->VFTHookEx( - g_pCareerRoundEndVTable, g_CareerRoundEndKeyPressedSlot, CareerRoundEnd_OnKeyCodePressed, + g_pCareerRoundEndVTable, g_CareerEndMenuKeyPressedSlot, CareerRoundEnd_OnKeyCodePressed, (void**)&g_pfnCareerRoundEnd_OnKeyCodePressed); if (!g_phook_CareerRoundEnd_OnKeyCodeTyped) g_phook_CareerRoundEnd_OnKeyCodeTyped = g_pMetaHookAPI->VFTHookEx( - g_pCareerRoundEndVTable, g_CareerRoundEndKeyTypedSlot, CareerRoundEnd_OnKeyCodeTyped, + g_pCareerRoundEndVTable, g_CareerEndMenuKeyTypedSlot, CareerRoundEnd_OnKeyCodeTyped, (void**)&g_pfnCareerRoundEnd_OnKeyCodeTyped); if (!g_phook_CareerRoundEnd_OnCommand) g_phook_CareerRoundEnd_OnCommand = g_pMetaHookAPI->VFTHookEx( g_pCareerRoundEndVTable, g_CareerRoundEndOnCommandSlot, CareerRoundEnd_OnCommand, (void**)&g_pfnCareerRoundEnd_OnCommand); - if (!g_phook_CareerRoundEnd_OnKeyCodePressed || !g_phook_CareerRoundEnd_OnKeyCodeTyped || !g_phook_CareerRoundEnd_OnCommand) + if (!g_phook_CareerMatchEnd_OnKeyCodePressed) + g_phook_CareerMatchEnd_OnKeyCodePressed = g_pMetaHookAPI->VFTHookEx( + g_pCareerMatchEndVTable, g_CareerEndMenuKeyPressedSlot, CareerMatchEnd_OnKeyCodePressed, + (void**)&g_pfnCareerMatchEnd_OnKeyCodePressed); + if (!g_phook_CareerMatchEnd_OnKeyCodeTyped) + g_phook_CareerMatchEnd_OnKeyCodeTyped = g_pMetaHookAPI->VFTHookEx( + g_pCareerMatchEndVTable, g_CareerEndMenuKeyTypedSlot, CareerMatchEnd_OnKeyCodeTyped, + (void**)&g_pfnCareerMatchEnd_OnKeyCodeTyped); + if (!g_phook_CareerMatchEnd_OnCommand) + g_phook_CareerMatchEnd_OnCommand = g_pMetaHookAPI->VFTHookEx( + g_pCareerMatchEndVTable, g_CareerMatchEndOnCommandSlot, CareerMatchEnd_OnCommand, + (void**)&g_pfnCareerMatchEnd_OnCommand); + if (!g_phook_CareerRoundEnd_OnKeyCodePressed || !g_phook_CareerRoundEnd_OnKeyCodeTyped || !g_phook_CareerRoundEnd_OnCommand || + !g_phook_CareerMatchEnd_OnKeyCodePressed || !g_phook_CareerMatchEnd_OnKeyCodeTyped || !g_phook_CareerMatchEnd_OnCommand) { - Uninstall_Hook(CareerRoundEnd_OnCommand); - Uninstall_Hook(CareerRoundEnd_OnKeyCodeTyped); - Uninstall_Hook(CareerRoundEnd_OnKeyCodePressed); - Sys_Error("Could not install client Career round-end ESC hooks."); + UninstallCareerEndMenuHooks(); + Sys_Error("Could not install client Career end-menu ESC hooks."); } } } void NativeClientUI_UninstallHooks(void) { - Uninstall_Hook(CareerRoundEnd_OnCommand); - Uninstall_Hook(CareerRoundEnd_OnKeyCodeTyped); - Uninstall_Hook(CareerRoundEnd_OnKeyCodePressed); - g_pCareerRoundEndVTable = nullptr; - g_pfnCareerRoundEnd_OnKeyCodePressed = nullptr; - g_pfnCareerRoundEnd_OnKeyCodeTyped = nullptr; - g_pfnCareerRoundEnd_OnCommand = nullptr; - g_CareerRoundEndKeyPressedSlot = 0; - g_CareerRoundEndKeyTypedSlot = 0; - g_CareerRoundEndOnCommandSlot = 0; + UninstallCareerEndMenuHooks(); Uninstall_Hook(CounterStrikeViewport_OnTick); Uninstall_Hook(CCareerBaseBox_BringToFront); g_pCareerCurrentBox = nullptr; diff --git a/tests/career_end_menu_tests.cpp b/tests/career_end_menu_tests.cpp new file mode 100644 index 0000000..1b03d5a --- /dev/null +++ b/tests/career_end_menu_tests.cpp @@ -0,0 +1,69 @@ +#include "../src/CareerEndMenuInput.h" +#include +#include + +enum class TestKey +{ + Escape, + Enter, + Other +}; + +struct NativeCalls +{ + int keyCalls = 0; + int commandCalls = 0; + int dummy = 0; + TestKey key = TestKey::Escape; + const char* command = nullptr; +}; + +static void __fastcall NativeKey(void* self, int dummy, TestKey key) +{ + auto& calls = *static_cast(self); + ++calls.keyCalls; + calls.dummy = dummy; + calls.key = key; +} + +static void __fastcall NativeCommand(void* self, int dummy, const char* command) +{ + auto& calls = *static_cast(self); + ++calls.commandCalls; + calls.dummy = dummy; + calls.command = command; +} + +int main() +{ + NativeCalls roundEnd; + NativeCalls matchEnd; + for (auto* calls : {&roundEnd, &matchEnd}) + { + //Both Pressed and Typed adapters must leave native close/unpause handlers untouched. + for (int path = 0; path < 2; ++path) + { + ForwardCareerEndMenuKey(NativeKey, calls, path, TestKey::Escape, TestKey::Escape); + assert(0 == calls->keyCalls); + } + for (const char* command : {"Close", "Cancel", "cLoSe", "cAnCeL"}) + ForwardCareerEndMenuCommand(NativeCommand, calls, 0, command); + assert(0 == calls->commandCalls); + + for (TestKey key : {TestKey::Enter, TestKey::Other}) + { + ForwardCareerEndMenuKey(NativeKey, calls, 1, key, TestKey::Escape); + assert(key == calls->key && 1 == calls->dummy); + } + assert(2 == calls->keyCalls); + + //Native business commands must receive the original pointer without rewriting it. + for (const char* command : {"disconnect", "career_restart", "Credits", "career_continue", "surrender", "vguicancel", "CloseModal", "", static_cast(nullptr)}) + { + const auto previousCalls = calls->commandCalls; + ForwardCareerEndMenuCommand(NativeCommand, calls, 1, command); + assert(previousCalls + 1 == calls->commandCalls); + assert(command == calls->command && 1 == calls->dummy); + } + } +}