Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/build-windows-api18.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions AMBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'),
Expand Down
24 changes: 24 additions & 0 deletions STFixes-API18-BUILD.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 1 addition & 1 deletion sdk
Submodule sdk updated 4042 files
95 changes: 65 additions & 30 deletions src/cs2fixes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

#include "cs2fixes.h"
#include "khook.hpp"
#include "iserver.h"

#include "appframework/IAppSystem.h"
Expand Down Expand Up @@ -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;
Expand All @@ -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<void> 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<void> 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<void> OnGravityPrecachePost(CBaseEntity* pThis,
CEntityPrecacheContext* param)
{
g_CS2Fixes.Hook_CTriggerGravityPrecache(pThis, param);
return {KHook::Action::Ignore};
}

static KHook::Return<void> OnGravityEndTouchPost(CBaseEntity* pThis,
CBaseEntity* pOther)
{
g_CS2Fixes.Hook_CTriggerGravityEndTouch(pThis, pOther);
return {KHook::Action::Ignore};
}

static KHook::Virtual<IServerGameClients, void, CPlayerSlot, bool, const char*, uint64>
g_ClientActiveHook(&IServerGameClients::ClientActive, nullptr, OnClientActivePost);
static KHook::Virtual<INetworkServerService, void, const GameSessionConfiguration_t&,
ISource2WorldSession*, const char*>
g_StartupServerHook(&INetworkServerService::StartupServer, nullptr, OnStartupServerPost);
static KHook::Virtual<CBaseEntity, void, CEntityPrecacheContext*>
g_GravityPrecacheHook(nullptr, OnGravityPrecachePost);
static KHook::Virtual<CBaseEntity, void, CBaseEntity*>
g_GravityEndTouchHook(nullptr, OnGravityEndTouchPost);
// KHook::AddGlobal reads a vtable pointer from the first word of this carrier.
static void* g_TriggerGravityVTable = nullptr;

CGameEntitySystem* GameEntitySystem()
{
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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<void*>(pTriggerGravityVTable);
g_ClientActiveHook.Add(g_pSource2GameClients);
g_StartupServerHook.Add(g_pNetworkServerService);
g_GravityPrecacheHook.AddGlobal(reinterpret_cast<CBaseEntity*>(&g_TriggerGravityVTable));
g_GravityEndTouchHook.AddGlobal(reinterpret_cast<CBaseEntity*>(&g_TriggerGravityVTable));
Message("All KHook hooks started!\n");

// run our cfg
g_pEngineServer2->ServerCommand("exec stfixes-metamod/cs2fixes");
Expand All @@ -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<CBaseEntity*>(&g_TriggerGravityVTable));
g_GravityEndTouchHook.RemoveGlobal(reinterpret_cast<CBaseEntity*>(&g_TriggerGravityVTable));

ConVar_Unregister();

Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions src/cs2fixes.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
#include "networksystem/inetworkserializer.h"
#include <iserver.h>

class CEntityPrecacheContext;

class CS2Fixes : public ISmmPlugin, public IMetamodListener
{
public:
Expand All @@ -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,
Expand Down