diff --git a/.github/workflows/build-windows-api18.yml b/.github/workflows/build-windows-api18.yml new file mode 100644 index 000000000..54eb5ffbe --- /dev/null +++ b/.github/workflows/build-windows-api18.yml @@ -0,0 +1,77 @@ +name: Build Windows API18 release + +on: + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + permissions: + contents: write + steps: + - name: Check out API18 source + uses: actions/checkout@v4 + with: + ref: metamod-api18 + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Prepare AMBuild and Metamod + shell: pwsh + run: | + git clone https://github.com/alliedmodders/ambuild.git "$env:RUNNER_TEMP\ambuild" + python -m pip install "$env:RUNNER_TEMP\ambuild" + git clone https://github.com/alliedmodders/metamod-source.git "$env:RUNNER_TEMP\mmsource-2.0" + git -C "$env:RUNNER_TEMP\mmsource-2.0" checkout 05c5c63 + git -C "$env:RUNNER_TEMP\mmsource-2.0" submodule update --init --recursive + $manifest = Join-Path $env:RUNNER_TEMP 'mmsource-2.0\hl2sdk-manifests\manifests\cs2.json' + Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/alliedmodders/hl2sdk-manifests/master/manifests/cs2.json' -OutFile $manifest + if ((Get-Content -Raw $manifest) -match 'tier1\.lib') { throw 'CS2 manifest still requests obsolete tier1.lib' } + $khook = Join-Path $env:RUNNER_TEMP 'mmsource-2.0\third_party\khook\include' + Copy-Item (Join-Path $khook 'khook.hpp') (Join-Path $env:RUNNER_TEMP 'mmsource-2.0\core\khook.hpp') + Copy-Item (Join-Path $khook 'khook') (Join-Path $env:RUNNER_TEMP 'mmsource-2.0\core\khook') -Recurse + "MMSOURCE20=$env:RUNNER_TEMP\mmsource-2.0" | Out-File -FilePath $env:GITHUB_ENV -Append + + - name: Download Windows SDK libraries + shell: bash + run: | + mkdir -p "$RUNNER_TEMP/dependencies/metamod-source" + cd "$RUNNER_TEMP/dependencies" + "$RUNNER_TEMP/mmsource-2.0/support/checkout-deps.sh" -s cs2 + echo "HL2SDKCS2=$RUNNER_TEMP/dependencies/hl2sdk-cs2" >> "$GITHUB_ENV" + + - name: Build Windows DLL + shell: pwsh + run: | + New-Item -ItemType Directory -Force build | Out-Null + Set-Location build + python ..\configure.py --enable-optimize --sdks cs2 --targets x86_64 --mms_path "$env:MMSOURCE20" --hl2sdk-root "$(Split-Path $env:HL2SDKCS2)" --hl2sdk-manifests "$env:MMSOURCE20\hl2sdk-manifests" + ambuild + + - name: Package Windows asset + shell: pwsh + run: | + $dll = Get-ChildItem -Recurse -Path build\package -Filter stfixes-metamod.dll | Select-Object -First 1 + if (-not $dll) { throw 'Build produced no stfixes-metamod.dll' } + $stage = Join-Path $env:RUNNER_TEMP 'stfixes-stage' + New-Item -ItemType Directory -Force "$stage\addons\stfixes-metamod\bin\win64", "$stage\addons\stfixes-metamod\gamedata", "$stage\addons\metamod" | Out-Null + Copy-Item $dll.FullName "$stage\addons\stfixes-metamod\bin\win64\stfixes-metamod.dll" + Copy-Item gamedata\cs2fixes.games.txt "$stage\addons\stfixes-metamod\gamedata\cs2fixes.games.txt" + @' + "Metamod Plugin" + { + "alias" "stfixes-metamod" + "file" "addons/stfixes-metamod/bin/win64/stfixes-metamod" + } + '@ | Set-Content "$stage\addons\metamod\stfixes-metamod.vdf" + Compress-Archive -Path "$stage\*" -DestinationPath STFixes-metamod-api18-windows-x86_64.zip -Force + + - name: Upload release asset + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload api18-1.0.0 STFixes-metamod-api18-windows-x86_64.zip --repo "${{ github.repository }}" --clobber diff --git a/AMBuilder b/AMBuilder index 500d4cc23..6be8dcf3a 100644 --- a/AMBuilder +++ b/AMBuilder @@ -11,6 +11,12 @@ for sdk_target in MMSPlugin.sdk_targets: binary = MMSPlugin.HL2Library(builder, cxx, MMSPlugin.plugin_name, sdk) + # Current CS2 SDK removed tier1.a; Metamod's older build helper still + # appends it to the link inputs. Drop only that obsolete archive. + binary.compiler.postlink = [ + item for item in binary.compiler.postlink if not str(item).endswith('/tier1.a') + ] + try: version = subprocess.check_output(['git', 'describe', '--tags', '--long']).decode('ascii').strip() except subprocess.SubprocessError as e: @@ -70,6 +76,7 @@ for sdk_target in MMSPlugin.sdk_targets: ] protoc_builder = builder.tools.Protoc(protoc = sdk_target.protoc, sources = [ + os.path.join(sdk['path'], 'common', 'valveextensions.proto'), os.path.join(sdk['path'], 'common', 'network_connection.proto'), os.path.join(sdk['path'], 'common', 'networkbasetypes.proto'), os.path.join(sdk['path'], 'common', 'engine_gcmessages.proto'), diff --git a/STFixes-API18-BUILD.md b/STFixes-API18-BUILD.md new file mode 100644 index 000000000..e8df8d453 --- /dev/null +++ b/STFixes-API18-BUILD.md @@ -0,0 +1,24 @@ +# STFixes Metamod API 18 port + +This branch ports STFixes-metamod to Metamod:Source 2.0 plugin API 18. + +It preserves the upstream GPLv3 license and attribution. The port replaces +the removed SourceHook hook declarations with KHook, builds against a current +CS2 SDK, and removes obsolete string-token imports from the old SDK. + +## Reproducible build + +The port was validated against the CS2 Linux `libserver.so` build used on +2026-09-26 and Metamod:Source build `05c5c63`. Build with the Steam Runtime +Sniper SDK and Clang 16. Do not load the resulting binary on a different CS2 +build without rechecking its gamedata and ABI. + +```bash +git clone --recurse-submodules https://github.com/SudoGool/STFixes-metamod.git +cd STFixes-metamod +git checkout metamod-api18 +git -C sdk fetch origin dadd5d70170fa94cf360ed3cbfffa511dfca87b4 +git -C sdk checkout --detach dadd5d70170fa94cf360ed3cbfffa511dfca87b4 +``` + +The original upstream project is [SharpTimer/STFixes-metamod](https://github.com/SharpTimer/STFixes-metamod). diff --git a/sdk b/sdk index 0df1f3b8e..dadd5d701 160000 --- a/sdk +++ b/sdk @@ -1 +1 @@ -Subproject commit 0df1f3b8e39a47b8840f5d5545ef5de37761683c +Subproject commit dadd5d70170fa94cf360ed3cbfffa511dfca87b4 diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index 2bb2ae49d..98deb822c 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -18,6 +18,7 @@ */ #include "cs2fixes.h" +#include "khook.hpp" #include "iserver.h" #include "appframework/IAppSystem.h" @@ -76,11 +77,6 @@ void Panic(const char *msg, ...) class GameSessionConfiguration_t { }; -SH_DECL_HOOK4_void(IServerGameClients, ClientActive, SH_NOATTRIB, 0, CPlayerSlot, bool, const char *, uint64); -SH_DECL_HOOK3_void(INetworkServerService, StartupServer, SH_NOATTRIB, 0, const GameSessionConfiguration_t&, ISource2WorldSession*, const char*); -SH_DECL_MANUALHOOK1_void(CTriggerGravityPrecache, 0, 0, 0, CEntityPrecacheContext*); -SH_DECL_MANUALHOOK1_void(CTriggerGravityEndTouch, 0, 0, 0, CBaseEntity*); - CS2Fixes g_CS2Fixes; CGameEntitySystem* g_pEntitySystem = nullptr; @@ -89,8 +85,51 @@ CPlayerManager *g_playerManager = nullptr; IVEngineServer2 *g_pEngineServer2 = nullptr; CGameConfig *g_GameConfig = nullptr; CCSGameRules *g_pGameRules = nullptr; -int g_iCTriggerGravityPrecacheId = -1; -int g_iCTriggerGravityEndTouchId = -1; + +// Metamod 2.0 build 1472 uses KHook (plugin API 18). SourceHook hooks from +// the original STFixes build cannot be loaded by this Metamod version. +static KHook::Return OnClientActivePost(IServerGameClients*, CPlayerSlot slot, + bool bLoadGame, const char* pszName, uint64 xuid) +{ + if (g_playerManager) + g_CS2Fixes.Hook_ClientActive(slot, bLoadGame, pszName, xuid); + return {KHook::Action::Ignore}; +} + +static KHook::Return OnStartupServerPost(INetworkServerService*, + const GameSessionConfiguration_t& config, ISource2WorldSession* pSession, + const char* pszMapName) +{ + if (g_pEntityListener) + g_CS2Fixes.Hook_StartupServer(config, pSession, pszMapName); + return {KHook::Action::Ignore}; +} + +static KHook::Return OnGravityPrecachePost(CBaseEntity* pThis, + CEntityPrecacheContext* param) +{ + g_CS2Fixes.Hook_CTriggerGravityPrecache(pThis, param); + return {KHook::Action::Ignore}; +} + +static KHook::Return OnGravityEndTouchPost(CBaseEntity* pThis, + CBaseEntity* pOther) +{ + g_CS2Fixes.Hook_CTriggerGravityEndTouch(pThis, pOther); + return {KHook::Action::Ignore}; +} + +static KHook::Virtual + g_ClientActiveHook(&IServerGameClients::ClientActive, nullptr, OnClientActivePost); +static KHook::Virtual + g_StartupServerHook(&INetworkServerService::StartupServer, nullptr, OnStartupServerPost); +static KHook::Virtual + g_GravityPrecacheHook(nullptr, OnGravityPrecachePost); +static KHook::Virtual + g_GravityEndTouchHook(nullptr, OnGravityEndTouchPost); +// KHook::AddGlobal reads a vtable pointer from the first word of this carrier. +static void* g_TriggerGravityVTable = nullptr; CGameEntitySystem* GameEntitySystem() { @@ -148,10 +187,6 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI *ismm, char *error, size_t maxlen, bool return false; } - SH_ADD_HOOK(IServerGameClients, ClientActive, g_pSource2GameClients, SH_MEMBER(this, &CS2Fixes::Hook_ClientActive), true); - SH_ADD_HOOK(INetworkServerService, StartupServer, g_pNetworkServerService, SH_MEMBER(this, &CS2Fixes::Hook_StartupServer), true); - META_CONPRINTF( "All hooks started!\n" ); - bool bRequiredInitLoaded = true; if (!addresses::Initialize(g_GameConfig)) @@ -179,33 +214,35 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI *ismm, char *error, size_t maxlen, bool if (!pTriggerGravityVTable) { snprintf(error, maxlen, "Failed to find TriggerGravity vtable\n"); - bRequiredInitLoaded = false; + return false; } int offset = g_GameConfig->GetOffset("CBaseEntity::Precache"); if (offset == -1) { snprintf(error, maxlen, "Failed to find CBaseEntity::Precache\n"); - bRequiredInitLoaded = false; + return false; } - SH_MANUALHOOK_RECONFIGURE(CTriggerGravityPrecache, offset, 0, 0); - g_iCTriggerGravityPrecacheId = SH_ADD_MANUALDVPHOOK(CTriggerGravityPrecache, pTriggerGravityVTable, SH_MEMBER(this, &CS2Fixes::Hook_CTriggerGravityPrecache), true); + g_GravityPrecacheHook.Configure(offset); offset = g_GameConfig->GetOffset("CBaseEntity::EndTouch"); if (offset == -1) { snprintf(error, maxlen, "Failed to find CBaseEntity::EndTouch\n"); - bRequiredInitLoaded = false; + return false; } - SH_MANUALHOOK_RECONFIGURE(CTriggerGravityEndTouch, offset, 0, 0); - g_iCTriggerGravityEndTouchId = SH_ADD_MANUALDVPHOOK(CTriggerGravityEndTouch, pTriggerGravityVTable, SH_MEMBER(this, &CS2Fixes::Hook_CTriggerGravityEndTouch), true); - - Message( "All hooks started!\n" ); + g_GravityEndTouchHook.Configure(offset); ConVar_Register(); g_playerManager = new CPlayerManager(); g_pEntityListener = new CEntityListener(); + g_TriggerGravityVTable = reinterpret_cast(pTriggerGravityVTable); + g_ClientActiveHook.Add(g_pSource2GameClients); + g_StartupServerHook.Add(g_pNetworkServerService); + g_GravityPrecacheHook.AddGlobal(reinterpret_cast(&g_TriggerGravityVTable)); + g_GravityEndTouchHook.AddGlobal(reinterpret_cast(&g_TriggerGravityVTable)); + Message("All KHook hooks started!\n"); // run our cfg g_pEngineServer2->ServerCommand("exec stfixes-metamod/cs2fixes"); @@ -225,10 +262,10 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI *ismm, char *error, size_t maxlen, bool bool CS2Fixes::Unload(char *error, size_t maxlen) { - SH_REMOVE_HOOK(IServerGameClients, ClientActive, g_pSource2GameClients, SH_MEMBER(this, &CS2Fixes::Hook_ClientActive), true); - SH_REMOVE_HOOK(INetworkServerService, StartupServer, g_pNetworkServerService, SH_MEMBER(this, &CS2Fixes::Hook_StartupServer), true); - SH_REMOVE_HOOK_ID(g_iCTriggerGravityPrecacheId); - SH_REMOVE_HOOK_ID(g_iCTriggerGravityEndTouchId); + g_ClientActiveHook.Remove(g_pSource2GameClients); + g_StartupServerHook.Remove(g_pNetworkServerService); + g_GravityPrecacheHook.RemoveGlobal(reinterpret_cast(&g_TriggerGravityVTable)); + g_GravityEndTouchHook.RemoveGlobal(reinterpret_cast(&g_TriggerGravityVTable)); ConVar_Unregister(); @@ -291,17 +328,15 @@ void CS2Fixes::Hook_StartupServer(const GameSessionConfiguration_t& config, ISou gpGlobals = g_pEngineServer2->GetServerGlobals(); } -void CS2Fixes::Hook_CTriggerGravityPrecache(CEntityPrecacheContext* param) +void CS2Fixes::Hook_CTriggerGravityPrecache(CBaseEntity* pThis, CEntityPrecacheContext* param) { const auto kv = param->m_pKeyValues; - CTriggerGravityHandler::OnPrecache(META_IFACEPTR(CBaseEntity), kv); - RETURN_META(MRES_IGNORED); + CTriggerGravityHandler::OnPrecache(pThis, kv); } -void CS2Fixes::Hook_CTriggerGravityEndTouch(CBaseEntity* pOther) +void CS2Fixes::Hook_CTriggerGravityEndTouch(CBaseEntity* pThis, CBaseEntity* pOther) { - CTriggerGravityHandler::OnEndTouch(META_IFACEPTR(CBaseEntity), pOther); - RETURN_META(MRES_IGNORED); + CTriggerGravityHandler::OnEndTouch(pThis, pOther); } void CS2Fixes::OnLevelInit(char const* pMapName, diff --git a/src/cs2fixes.h b/src/cs2fixes.h index 30d3c8507..7060a4a64 100644 --- a/src/cs2fixes.h +++ b/src/cs2fixes.h @@ -23,6 +23,8 @@ #include "networksystem/inetworkserializer.h" #include +class CEntityPrecacheContext; + class CS2Fixes : public ISmmPlugin, public IMetamodListener { public: @@ -34,8 +36,8 @@ class CS2Fixes : public ISmmPlugin, public IMetamodListener public: //hooks void Hook_ClientActive( CPlayerSlot slot, bool bLoadGame, const char *pszName, uint64 xuid ); void Hook_StartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession *pSession, const char *pszMapName); - void Hook_CTriggerGravityPrecache(CEntityPrecacheContext* param); - void Hook_CTriggerGravityEndTouch(CBaseEntity* pOther); + void Hook_CTriggerGravityPrecache(CBaseEntity* pThis, CEntityPrecacheContext* param); + void Hook_CTriggerGravityEndTouch(CBaseEntity* pThis, CBaseEntity* pOther); void OnLevelInit(char const* pMapName, char const* pMapEntities, char const* pOldLevel,