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
15 changes: 14 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,23 @@ jobs:
uses: actions/checkout@v4
with:
repository: alliedmodders/metamod-source
ref: master
# Pinned, not master: this is the build the API 18 port was validated
# against, and Metamod's plugin ABI moves without warning.
ref: 05c5c63a9d595cb84021c3a51fe38366ba4a12a5
path: mmsource-2.0
submodules: recursive

# core/ISmmPlugin.h includes "khook.hpp" as a sibling, but Metamod keeps
# KHook in third_party/khook/include. Without this the build dies with
# "fatal error: 'khook.hpp' file not found" on the first translation unit.
- name: Stage KHook headers
shell: bash
run: |
khook=mmsource-2.0/third_party/khook/include
test -f "$khook/khook.hpp" || { echo "KHook headers missing at $khook"; exit 1; }
cp "$khook/khook.hpp" mmsource-2.0/core/khook.hpp
cp -r "$khook/khook" mmsource-2.0/core/khook

- name: Checkout AMBuild
uses: actions/checkout@v4
with:
Expand Down
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).
6 changes: 3 additions & 3 deletions gamedata/cs2fixes.games.txt
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
{
"library" "server"
"windows" "\x0F\x84\x2A\x2A\x2A\x2A\x80\xB8\x2A\x2A\x2A\x2A\x00\x0F\x84\x2A\x2A\x2A\x2A\x80\x3D\x2A\x2A\x2A\x2A\x00\x74\x15"
"linux" "\x0F\x84\x2A\x2A\x2A\x2A\x44\x0F\xB6\xB0\x2A\x2A\x2A\x2A\x45\x84\xF6\x0F\x84"
"linux" "\x0F\x84\x2A\x2A\x2A\x2A\x44\x0F\xB6\xB8\x2A\x2A\x2A\x2A\x45\x84\xFF\x0F\x84"
}
// Called right after this in windows "Entity %s(%s) is ambiguously parented to..."
"CBaseEntity_SetParent"
Expand Down Expand Up @@ -171,7 +171,7 @@
{
"library" "server"
"windows" "\xC8\x42\xEB\x2A\x4C\x8B\x77\x2A\x4D\x39\x66"
"linux" "\xC8\x42\x41\xC7\x85\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x41\xC7\x85\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\xE9"
"linux" "\xC8\x42\x41\xC7\x84\x24\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x41\xC7\x84\x24\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\xE9"
}
// idk a good way to find this again, i just brute forced the vtable. offset is 136 on CTriggerPush
"TriggerPush_Touch"
Expand Down Expand Up @@ -338,7 +338,7 @@
"BotNavIgnore"
{
"windows" "\xE9\x2C\x00\x00\x00\x90"
"linux" "\xE9\x25\x00\x00\x00\x90"
"linux" "\xE9\x22\x00\x00\x00\x90"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion sdk
Submodule sdk updated 4042 files
112 changes: 79 additions & 33 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 All @@ -104,9 +143,13 @@ INetworkGameServer* GetNetworkGameServer()
return g_pNetworkServerService->GetIGameServer();
}

// IVEngineServer2::GetServerGlobals returns null since the CS2 update of 2026-10-22 (1.41.9.2), so the
// first client to connect crashed the server on a null gpGlobals. The network game server still hands
// out the globals, which is how CounterStrikeSharp and upstream CS2Fixes read them.
CGlobalVars* GetGlobals()
{
return g_pEngineServer2->GetServerGlobals();
INetworkGameServer* pServer = GetNetworkGameServer();
return pServer ? pServer->GetGlobals() : nullptr;
}

PLUGIN_EXPOSE(CS2Fixes, g_CS2Fixes);
Expand Down Expand Up @@ -148,10 +191,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 +218,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 @@ -216,7 +257,7 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI *ismm, char *error, size_t maxlen, bool
{
g_pEntitySystem = GameEntitySystem();
g_pEntitySystem->AddListenerEntity(g_pEntityListener);
gpGlobals = g_pEngineServer2->GetServerGlobals();
gpGlobals = GetGlobals();
}


Expand All @@ -225,10 +266,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 @@ -281,27 +322,32 @@ CServerSideClient *GetClientBySlot(CPlayerSlot slot)
void CS2Fixes::Hook_ClientActive( CPlayerSlot slot, bool bLoadGame, const char *pszName, uint64 xuid )
{
Message( "Hook_ClientActive(%d, %d, \"%s\", %lli)\n", slot, bLoadGame, pszName, xuid );
if (!gpGlobals)
gpGlobals = GetGlobals();
if (!gpGlobals)
{
Message("No server globals yet, client %d not tracked\n", slot.Get());
return;
}
g_playerManager->OnClientConnected(slot, xuid);
}

void CS2Fixes::Hook_StartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession *pSession, const char *pszMapName)
{
g_pEntitySystem = GameEntitySystem();
g_pEntitySystem->AddListenerEntity(g_pEntityListener);
gpGlobals = g_pEngineServer2->GetServerGlobals();
gpGlobals = GetGlobals();
}

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
Loading