From 200858848a4adfb737506a3932109593d02459d2 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Mon, 5 Oct 2026 13:19:19 -0400 Subject: [PATCH 1/2] chore: load Swiftly signatures from the gamedata file the validator checks --- apps/gamedata-validator/README.md | 6 +++--- apps/gamedata-validator/main.py | 8 +++++++- apps/player-management-sw/src/ConnectGate.cs | 17 +++++------------ .../src/PlayerManagement.csproj | 3 +++ .../src/FiveStack.Events/InitConnect.cs | 19 +++---------------- .../src/FiveStack.Utilities/ClanTagUtility.cs | 11 +++-------- apps/swiftly/src/FiveStack.csproj | 3 +++ apps/utility-sw/src/Events/PracticeConnect.cs | 13 +++---------- apps/utility-sw/src/UtilityPractice.csproj | 3 +++ shared/gamedata/fivestack.gamedata.json | 4 ++-- shared/gamedata/swiftly/signatures.jsonc | 13 +++++++++++++ 11 files changed, 48 insertions(+), 52 deletions(-) create mode 100644 shared/gamedata/swiftly/signatures.jsonc diff --git a/apps/gamedata-validator/README.md b/apps/gamedata-validator/README.md index 24758dce..fa2509f0 100644 --- a/apps/gamedata-validator/README.md +++ b/apps/gamedata-validator/README.md @@ -12,13 +12,13 @@ runtimes, since 5Stack runs Swiftly and CounterStrikeSharp side by side: | Set | Runtime | Source | | --- | --- | --- | -| `fivestack` | both | `shared/gamedata/fivestack.gamedata.json` | +| `fivestack` | CounterStrikeSharp | `shared/gamedata/fivestack.gamedata.json` | +| `fivestack` | Swiftly | `shared/gamedata/swiftly/signatures.jsonc`, the file the Swiftly plugins ship and load | | `upstream-ccs` | CounterStrikeSharp | fetched at image build from CounterStrikeSharp | | `upstream-swiftly` | Swiftly | fetched at run time for the version `apps/swiftly` pins | Entries in `fivestack.gamedata.json` may carry a `runtimes` key naming the runtimes that -actually use them — the Swiftly port hooks `ConnectClient` but not the two vtable offsets. -Everything else defaults to its set's runtime. +actually use them. Everything else defaults to its set's runtime. `--runtime swiftlys2` or `--runtime counterstrikesharp` narrows a run to one runtime. diff --git a/apps/gamedata-validator/main.py b/apps/gamedata-validator/main.py index 01dce58f..b5a26a8d 100644 --- a/apps/gamedata-validator/main.py +++ b/apps/gamedata-validator/main.py @@ -20,7 +20,13 @@ "name": "fivestack", "path": path.join(BASE_DIR, "gamedata/fivestack.gamedata.json"), "format": "ccs", - "runtimes": [RUNTIME_CCS, RUNTIME_SWIFTLY], + "runtimes": [RUNTIME_CCS], + }, + { + "name": "fivestack", + "path": path.join(BASE_DIR, "gamedata/swiftly/signatures.jsonc"), + "format": "swiftly-signatures", + "runtimes": [RUNTIME_SWIFTLY], }, { "name": "upstream-ccs", diff --git a/apps/player-management-sw/src/ConnectGate.cs b/apps/player-management-sw/src/ConnectGate.cs index 487f9a3a..e1100c93 100644 --- a/apps/player-management-sw/src/ConnectGate.cs +++ b/apps/player-management-sw/src/ConnectGate.cs @@ -25,9 +25,8 @@ bool isLowViolence public partial class PlayerManagementPlugin { /** - * FiveStack_ConnectClient in shared/gamedata/fivestack.gamedata.json, which - * the gamedata validator checks after every game update. Signature near: - * "CNetworkGameServerBase::ConnectClient( name='%s', remote='%s' )\n" + * Resolved from shared/gamedata/swiftly/signatures.jsonc, which ships with + * the plugin and which the gamedata validator checks after every game update. * * Function signature: *
@@ -43,11 +42,7 @@ public partial class PlayerManagementPlugin
      * );
      * 
*/ - private static readonly string ConnectClientSignature = RuntimeInformation.IsOSPlatform( - OSPlatform.Linux - ) - ? "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?" - : "48 89 5C 24 18 44 89 4C 24 20 55 41 54 41 55 41 56 41 57 48 8D 6C 24 F1 48 81 EC ? ? ? ? 81 64 24 4C FF FF 0F FF"; + private const string ConnectClientSignature = "FiveStack_ConnectClient"; private IUnmanagedFunction? _connectClientFunc; private Guid _connectClientHookId; @@ -59,9 +54,7 @@ private void InstallConnectGate() { try { - nint? found = Core.Memory.GetAddressBySignature(Library.Engine, ConnectClientSignature); - - if (found == null || found == nint.Zero) + if (!Core.GameData.TryGetSignature(ConnectClientSignature, out nint found)) { _logger.LogError( "ConnectClient signature not found; access lists are only enforced by kicking after join" @@ -70,7 +63,7 @@ private void InstallConnectGate() } _connectClientFunc = Core.Memory.GetUnmanagedFunctionByAddress( - found.Value + found ); _connectClientHookId = _connectClientFunc.AddHook(next => diff --git a/apps/player-management-sw/src/PlayerManagement.csproj b/apps/player-management-sw/src/PlayerManagement.csproj index 660a2998..8bdb0ad5 100644 --- a/apps/player-management-sw/src/PlayerManagement.csproj +++ b/apps/player-management-sw/src/PlayerManagement.csproj @@ -34,5 +34,8 @@ PreserveNewest + + PreserveNewest + diff --git a/apps/swiftly/src/FiveStack.Events/InitConnect.cs b/apps/swiftly/src/FiveStack.Events/InitConnect.cs index 319d1dbf..a71d931d 100644 --- a/apps/swiftly/src/FiveStack.Events/InitConnect.cs +++ b/apps/swiftly/src/FiveStack.Events/InitConnect.cs @@ -28,9 +28,6 @@ public partial class FiveStackPlugin public static Dictionary PendingPlayers = new(); /** - * Signature near: - * "CNetworkGameServerBase::ConnectClient( name='%s', remote='%s' )\n" - * * Function signature: *
      * virtual CServerSideClientBase* CNetworkGameServerBase::ConnectClient(
@@ -45,9 +42,7 @@ public partial class FiveStackPlugin
      * );
      * 
*/ - private static string ConnectClientSignature = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?" - : "48 89 5C 24 18 44 89 4C 24 20 55 41 54 41 55 41 56 41 57 48 8D 6C 24 F1 48 81 EC ? ? ? ? 81 64 24 4C FF FF 0F FF"; + private const string ConnectClientSignature = "FiveStack_ConnectClient"; private IUnmanagedFunction? _connectClientFunc; private Guid _connectClientHookId; @@ -61,22 +56,14 @@ private void InitializeConnectClientHook() return; } - if (string.IsNullOrEmpty(ConnectClientSignature)) - { - _logger.LogWarning("ConnectClient signature is not available for this platform"); - return; - } - - var address = Core.Memory.GetAddressBySignature(Library.Engine, ConnectClientSignature); - - if (address == null || address == nint.Zero) + if (!Core.GameData.TryGetSignature(ConnectClientSignature, out nint address)) { _logger.LogWarning("Failed to find ConnectClient signature"); return; } _connectClientFunc = Core.Memory.GetUnmanagedFunctionByAddress( - address.Value + address ); if (_connectClientFunc == null) diff --git a/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs b/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs index 8e7532f8..ed50c7f4 100644 --- a/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs +++ b/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs @@ -17,24 +17,19 @@ public static class ClanTagUtility // any nonzero id; the client only shows the tag when one is set private const uint CustomClanId = 1; - private static readonly string SetClanSignature = RuntimeInformation.IsOSPlatform( - OSPlatform.Linux - ) - ? "55 48 89 E5 41 57 41 56 41 55 49 89 D5 41 54 53 48 89 FB 48 81 EC ? ? ? ? 39 B7 ? ? ? ?" - : "48 89 5C 24 10 48 89 6C 24 18 48 89 74 24 20 57 48 83 EC 20 49 8B E8 8B FA 48 8B F1 39 91 ? ? ? ? 74 ? BA FF FF FF FF"; + private const string SetClanSignature = "FiveStack_CCSPlayerController_SetClan"; private static IUnmanagedFunction? _setClan; private static Guid _hookId; public static void Hook(ISwiftlyCore core) { - nint? address = core.Memory.GetAddressBySignature(Library.Server, SetClanSignature); - if (address == null || address == nint.Zero) + if (!core.GameData.TryGetSignature(SetClanSignature, out nint address)) { throw new InvalidOperationException("SetClan signature not found"); } - _setClan = core.Memory.GetUnmanagedFunctionByAddress(address.Value); + _setClan = core.Memory.GetUnmanagedFunctionByAddress(address); _hookId = _setClan.AddHook(next => (controller, clanId, tag) => { }); } diff --git a/apps/swiftly/src/FiveStack.csproj b/apps/swiftly/src/FiveStack.csproj index e69dd252..492415e5 100644 --- a/apps/swiftly/src/FiveStack.csproj +++ b/apps/swiftly/src/FiveStack.csproj @@ -27,5 +27,8 @@ PreserveNewest + + PreserveNewest + diff --git a/apps/utility-sw/src/Events/PracticeConnect.cs b/apps/utility-sw/src/Events/PracticeConnect.cs index aecf6737..d7fd00ef 100644 --- a/apps/utility-sw/src/Events/PracticeConnect.cs +++ b/apps/utility-sw/src/Events/PracticeConnect.cs @@ -30,9 +30,6 @@ public partial class UtilityPracticePlugin public static Dictionary PendingPlayers = new(); /** - * Signature near: - * "CNetworkGameServerBase::ConnectClient( name='%s', remote='%s' )\n" - * * Function signature: *
      * virtual CServerSideClientBase* CNetworkGameServerBase::ConnectClient(
@@ -47,9 +44,7 @@ public partial class UtilityPracticePlugin
      * );
      * 
*/ - private static string ConnectClientSignature = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?" - : "48 89 5C 24 18 44 89 4C 24 20 55 41 54 41 55 41 56 41 57 48 8D 6C 24 F1 48 81 EC ? ? ? ? 81 64 24 4C FF FF 0F FF"; + private const string ConnectClientSignature = "FiveStack_ConnectClient"; private IUnmanagedFunction? _connectClientFunc; private Guid _connectClientHookId; @@ -63,16 +58,14 @@ private void InitializeConnectClientHook() return; } - var address = Core.Memory.GetAddressBySignature(Library.Engine, ConnectClientSignature); - - if (address == null || address == nint.Zero) + if (!Core.GameData.TryGetSignature(ConnectClientSignature, out nint address)) { _logger.LogWarning("Failed to find ConnectClient signature"); return; } _connectClientFunc = Core.Memory.GetUnmanagedFunctionByAddress( - address.Value + address ); if (_connectClientFunc == null) diff --git a/apps/utility-sw/src/UtilityPractice.csproj b/apps/utility-sw/src/UtilityPractice.csproj index df55efa6..1243d7cc 100644 --- a/apps/utility-sw/src/UtilityPractice.csproj +++ b/apps/utility-sw/src/UtilityPractice.csproj @@ -70,6 +70,9 @@ + diff --git a/shared/gamedata/fivestack.gamedata.json b/shared/gamedata/fivestack.gamedata.json index 546b5fc0..e6f67fe4 100644 --- a/shared/gamedata/fivestack.gamedata.json +++ b/shared/gamedata/fivestack.gamedata.json @@ -1,6 +1,6 @@ { "FiveStack_ConnectClient": { - "runtimes": ["counterstrikesharp", "swiftlys2"], + "runtimes": ["counterstrikesharp"], "signatures": { "library": "engine2", "linux": "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?", @@ -8,7 +8,7 @@ } }, "FiveStack_CCSPlayerController_SetClan": { - "runtimes": ["counterstrikesharp", "swiftlys2"], + "runtimes": ["counterstrikesharp"], "signatures": { "library": "server", "linux": "55 48 89 E5 41 57 41 56 41 55 49 89 D5 41 54 53 48 89 FB 48 81 EC ? ? ? ? 39 B7 ? ? ? ?", diff --git a/shared/gamedata/swiftly/signatures.jsonc b/shared/gamedata/swiftly/signatures.jsonc new file mode 100644 index 00000000..f6263e91 --- /dev/null +++ b/shared/gamedata/swiftly/signatures.jsonc @@ -0,0 +1,13 @@ +{ + // "CNetworkGameServerBase::ConnectClient( name='%s', remote='%s' )\n" + "FiveStack_ConnectClient": { + "lib": "engine2", + "windows": "48 89 5C 24 18 44 89 4C 24 20 55 41 54 41 55 41 56 41 57 48 8D 6C 24 F1 48 81 EC ? ? ? ? 81 64 24 4C FF FF 0F FF", + "linux": "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?" + }, + "FiveStack_CCSPlayerController_SetClan": { + "lib": "server", + "windows": "48 89 5C 24 10 48 89 6C 24 18 48 89 74 24 20 57 48 83 EC 20 49 8B E8 8B FA 48 8B F1 39 91 ? ? ? ? 74 ? BA FF FF FF FF", + "linux": "55 48 89 E5 41 57 41 56 41 55 49 89 D5 41 54 53 48 89 FB 48 81 EC ? ? ? ? 39 B7 ? ? ? ?" + } +} From d90c13ac0744e1cfa94204a27cdd9c482bcaf00c Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Mon, 5 Oct 2026 13:51:16 -0400 Subject: [PATCH 2/2] chore: read signatures from fivestack.gamedata.json instead of a second file --- apps/gamedata-validator/README.md | 6 +-- apps/gamedata-validator/main.py | 8 +-- apps/player-management-sw/src/ConnectGate.cs | 19 +++++-- .../src/PlayerManagement.csproj | 3 +- .../src/FiveStack.Events/InitConnect.cs | 14 ++++- .../src/FiveStack.Utilities/ClanTagUtility.cs | 11 +++- apps/swiftly/src/FiveStack.csproj | 2 +- apps/swiftly/test/GamedataUtilityTests.cs | 46 +++++++++++++++++ apps/utility-sw/src/Events/PracticeConnect.cs | 14 ++++- apps/utility-sw/src/UtilityPractice.csproj | 5 +- .../FiveStack.Utilities/GamedataUtility.cs | 51 +++++++++++++++++++ shared/gamedata/fivestack.gamedata.json | 4 +- shared/gamedata/swiftly/signatures.jsonc | 13 ----- 13 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 apps/swiftly/test/GamedataUtilityTests.cs create mode 100644 shared/dotnet/FiveStack.Utilities/GamedataUtility.cs delete mode 100644 shared/gamedata/swiftly/signatures.jsonc diff --git a/apps/gamedata-validator/README.md b/apps/gamedata-validator/README.md index fa2509f0..24758dce 100644 --- a/apps/gamedata-validator/README.md +++ b/apps/gamedata-validator/README.md @@ -12,13 +12,13 @@ runtimes, since 5Stack runs Swiftly and CounterStrikeSharp side by side: | Set | Runtime | Source | | --- | --- | --- | -| `fivestack` | CounterStrikeSharp | `shared/gamedata/fivestack.gamedata.json` | -| `fivestack` | Swiftly | `shared/gamedata/swiftly/signatures.jsonc`, the file the Swiftly plugins ship and load | +| `fivestack` | both | `shared/gamedata/fivestack.gamedata.json` | | `upstream-ccs` | CounterStrikeSharp | fetched at image build from CounterStrikeSharp | | `upstream-swiftly` | Swiftly | fetched at run time for the version `apps/swiftly` pins | Entries in `fivestack.gamedata.json` may carry a `runtimes` key naming the runtimes that -actually use them. Everything else defaults to its set's runtime. +actually use them — the Swiftly port hooks `ConnectClient` but not the two vtable offsets. +Everything else defaults to its set's runtime. `--runtime swiftlys2` or `--runtime counterstrikesharp` narrows a run to one runtime. diff --git a/apps/gamedata-validator/main.py b/apps/gamedata-validator/main.py index b5a26a8d..01dce58f 100644 --- a/apps/gamedata-validator/main.py +++ b/apps/gamedata-validator/main.py @@ -20,13 +20,7 @@ "name": "fivestack", "path": path.join(BASE_DIR, "gamedata/fivestack.gamedata.json"), "format": "ccs", - "runtimes": [RUNTIME_CCS], - }, - { - "name": "fivestack", - "path": path.join(BASE_DIR, "gamedata/swiftly/signatures.jsonc"), - "format": "swiftly-signatures", - "runtimes": [RUNTIME_SWIFTLY], + "runtimes": [RUNTIME_CCS, RUNTIME_SWIFTLY], }, { "name": "upstream-ccs", diff --git a/apps/player-management-sw/src/ConnectGate.cs b/apps/player-management-sw/src/ConnectGate.cs index e1100c93..a8207c57 100644 --- a/apps/player-management-sw/src/ConnectGate.cs +++ b/apps/player-management-sw/src/ConnectGate.cs @@ -25,8 +25,9 @@ bool isLowViolence public partial class PlayerManagementPlugin { /** - * Resolved from shared/gamedata/swiftly/signatures.jsonc, which ships with - * the plugin and which the gamedata validator checks after every game update. + * FiveStack_ConnectClient in shared/gamedata/fivestack.gamedata.json, which + * ships with the plugin and which the gamedata validator checks after every + * game update. * * Function signature: *
@@ -54,7 +55,17 @@ private void InstallConnectGate()
     {
         try
         {
-            if (!Core.GameData.TryGetSignature(ConnectClientSignature, out nint found))
+            GamedataSignature? signature = GamedataUtility.Load(
+                Core.PluginPath,
+                ConnectClientSignature
+            );
+
+            nint? found =
+                signature == null
+                    ? null
+                    : Core.Memory.GetAddressBySignature(signature.Library, signature.Pattern);
+
+            if (found == null || found == nint.Zero)
             {
                 _logger.LogError(
                     "ConnectClient signature not found; access lists are only enforced by kicking after join"
@@ -63,7 +74,7 @@ private void InstallConnectGate()
             }
 
             _connectClientFunc = Core.Memory.GetUnmanagedFunctionByAddress(
-                found
+                found.Value
             );
 
             _connectClientHookId = _connectClientFunc.AddHook(next =>
diff --git a/apps/player-management-sw/src/PlayerManagement.csproj b/apps/player-management-sw/src/PlayerManagement.csproj
index 8bdb0ad5..f815e910 100644
--- a/apps/player-management-sw/src/PlayerManagement.csproj
+++ b/apps/player-management-sw/src/PlayerManagement.csproj
@@ -28,13 +28,14 @@
     
     
     
+    
   
 
   
     
       PreserveNewest
     
-    
+    
       PreserveNewest
     
   
diff --git a/apps/swiftly/src/FiveStack.Events/InitConnect.cs b/apps/swiftly/src/FiveStack.Events/InitConnect.cs
index a71d931d..4f22e2c2 100644
--- a/apps/swiftly/src/FiveStack.Events/InitConnect.cs
+++ b/apps/swiftly/src/FiveStack.Events/InitConnect.cs
@@ -56,14 +56,24 @@ private void InitializeConnectClientHook()
                 return;
             }
 
-            if (!Core.GameData.TryGetSignature(ConnectClientSignature, out nint address))
+            GamedataSignature? signature = GamedataUtility.Load(
+                Core.PluginPath,
+                ConnectClientSignature
+            );
+
+            nint? address =
+                signature == null
+                    ? null
+                    : Core.Memory.GetAddressBySignature(signature.Library, signature.Pattern);
+
+            if (address == null || address == nint.Zero)
             {
                 _logger.LogWarning("Failed to find ConnectClient signature");
                 return;
             }
 
             _connectClientFunc = Core.Memory.GetUnmanagedFunctionByAddress(
-                address
+                address.Value
             );
 
             if (_connectClientFunc == null)
diff --git a/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs b/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs
index ed50c7f4..819864b9 100644
--- a/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs
+++ b/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs
@@ -24,12 +24,19 @@ public static class ClanTagUtility
 
     public static void Hook(ISwiftlyCore core)
     {
-        if (!core.GameData.TryGetSignature(SetClanSignature, out nint address))
+        GamedataSignature? signature = GamedataUtility.Load(core.PluginPath, SetClanSignature);
+
+        nint? address =
+            signature == null
+                ? null
+                : core.Memory.GetAddressBySignature(signature.Library, signature.Pattern);
+
+        if (address == null || address == nint.Zero)
         {
             throw new InvalidOperationException("SetClan signature not found");
         }
 
-        _setClan = core.Memory.GetUnmanagedFunctionByAddress(address);
+        _setClan = core.Memory.GetUnmanagedFunctionByAddress(address.Value);
         _hookId = _setClan.AddHook(next => (controller, clanId, tag) => { });
     }
 
diff --git a/apps/swiftly/src/FiveStack.csproj b/apps/swiftly/src/FiveStack.csproj
index 492415e5..47594292 100644
--- a/apps/swiftly/src/FiveStack.csproj
+++ b/apps/swiftly/src/FiveStack.csproj
@@ -27,7 +27,7 @@
     
       PreserveNewest
     
-    
+    
       PreserveNewest
     
   
diff --git a/apps/swiftly/test/GamedataUtilityTests.cs b/apps/swiftly/test/GamedataUtilityTests.cs
new file mode 100644
index 00000000..7f2beeec
--- /dev/null
+++ b/apps/swiftly/test/GamedataUtilityTests.cs
@@ -0,0 +1,46 @@
+using FiveStack.Utilities;
+using Xunit;
+
+public class GamedataUtilityTests
+{
+    // The file the plugin ships, copied beside the tests by the project reference.
+    private static string Shipped()
+    {
+        return File.ReadAllText(GamedataUtility.PathIn(AppContext.BaseDirectory));
+    }
+
+    [Theory]
+    [InlineData("FiveStack_ConnectClient", "engine2", true)]
+    [InlineData("FiveStack_ConnectClient", "engine2", false)]
+    [InlineData("FiveStack_CCSPlayerController_SetClan", "server", true)]
+    [InlineData("FiveStack_CCSPlayerController_SetClan", "server", false)]
+    public void EverySignatureAPluginAsksForShipsWithIt(string name, string library, bool linux)
+    {
+        GamedataSignature? signature = GamedataUtility.Parse(Shipped(), name, linux);
+
+        Assert.NotNull(signature);
+        Assert.Equal(library, signature.Library);
+        Assert.Matches("^[0-9A-F?]{1,2}( [0-9A-F?]{1,2})+$", signature.Pattern);
+    }
+
+    [Fact]
+    public void LinuxAndWindowsGetTheirOwnPattern()
+    {
+        string json = """
+            { "Thing": { "signatures": { "library": "server", "linux": "55 48", "windows": "48 89" } } }
+            """;
+
+        Assert.Equal("55 48", GamedataUtility.Parse(json, "Thing", linux: true)?.Pattern);
+        Assert.Equal("48 89", GamedataUtility.Parse(json, "Thing", linux: false)?.Pattern);
+    }
+
+    [Theory]
+    [InlineData("""{ }""")]
+    [InlineData("""{ "Thing": { "offsets": { "linux": 24 } } }""")]
+    [InlineData("""{ "Thing": { "signatures": { "library": "server", "windows": "48 89" } } }""")]
+    [InlineData("""{ "Thing": { "signatures": { "library": "server", "linux": " " } } }""")]
+    public void AnEntryWithNoPatternForThisPlatformIsNotFound(string json)
+    {
+        Assert.Null(GamedataUtility.Parse(json, "Thing", linux: true));
+    }
+}
diff --git a/apps/utility-sw/src/Events/PracticeConnect.cs b/apps/utility-sw/src/Events/PracticeConnect.cs
index d7fd00ef..35c165a8 100644
--- a/apps/utility-sw/src/Events/PracticeConnect.cs
+++ b/apps/utility-sw/src/Events/PracticeConnect.cs
@@ -58,14 +58,24 @@ private void InitializeConnectClientHook()
                 return;
             }
 
-            if (!Core.GameData.TryGetSignature(ConnectClientSignature, out nint address))
+            GamedataSignature? signature = GamedataUtility.Load(
+                Core.PluginPath,
+                ConnectClientSignature
+            );
+
+            nint? address =
+                signature == null
+                    ? null
+                    : Core.Memory.GetAddressBySignature(signature.Library, signature.Pattern);
+
+            if (address == null || address == nint.Zero)
             {
                 _logger.LogWarning("Failed to find ConnectClient signature");
                 return;
             }
 
             _connectClientFunc = Core.Memory.GetUnmanagedFunctionByAddress(
-                address
+                address.Value
             );
 
             if (_connectClientFunc == null)
diff --git a/apps/utility-sw/src/UtilityPractice.csproj b/apps/utility-sw/src/UtilityPractice.csproj
index 1243d7cc..5c0d94ac 100644
--- a/apps/utility-sw/src/UtilityPractice.csproj
+++ b/apps/utility-sw/src/UtilityPractice.csproj
@@ -56,6 +56,7 @@
     
     
     
+    
     
     
     
@@ -70,8 +71,8 @@
     
-    
     
     
diff --git a/shared/dotnet/FiveStack.Utilities/GamedataUtility.cs b/shared/dotnet/FiveStack.Utilities/GamedataUtility.cs
new file mode 100644
index 00000000..59be6c45
--- /dev/null
+++ b/shared/dotnet/FiveStack.Utilities/GamedataUtility.cs
@@ -0,0 +1,51 @@
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace FiveStack.Utilities;
+
+public record GamedataSignature(string Library, string Pattern);
+
+// Signatures come from shared/gamedata/fivestack.gamedata.json, which ships
+// beside the plugin. It is the file the gamedata validator checks after every
+// game update, so what a plugin scans for is what was validated.
+public static class GamedataUtility
+{
+    public static string PathIn(string pluginPath)
+    {
+        return Path.Join(pluginPath, "resources", "gamedata", "fivestack.gamedata.json");
+    }
+
+    public static GamedataSignature? Load(string pluginPath, string name)
+    {
+        return Parse(
+            File.ReadAllText(PathIn(pluginPath)),
+            name,
+            RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
+        );
+    }
+
+    public static GamedataSignature? Parse(string json, string name, bool linux)
+    {
+        using JsonDocument document = JsonDocument.Parse(json);
+
+        if (
+            !document.RootElement.TryGetProperty(name, out JsonElement entry)
+            || !entry.TryGetProperty("signatures", out JsonElement signatures)
+            || !signatures.TryGetProperty("library", out JsonElement library)
+            || !signatures.TryGetProperty(linux ? "linux" : "windows", out JsonElement pattern)
+        )
+        {
+            return null;
+        }
+
+        string? libraryName = library.GetString();
+        string? bytes = pattern.GetString();
+
+        if (string.IsNullOrWhiteSpace(libraryName) || string.IsNullOrWhiteSpace(bytes))
+        {
+            return null;
+        }
+
+        return new GamedataSignature(libraryName, bytes);
+    }
+}
diff --git a/shared/gamedata/fivestack.gamedata.json b/shared/gamedata/fivestack.gamedata.json
index e6f67fe4..546b5fc0 100644
--- a/shared/gamedata/fivestack.gamedata.json
+++ b/shared/gamedata/fivestack.gamedata.json
@@ -1,6 +1,6 @@
 {
   "FiveStack_ConnectClient": {
-    "runtimes": ["counterstrikesharp"],
+    "runtimes": ["counterstrikesharp", "swiftlys2"],
     "signatures": {
       "library": "engine2",
       "linux": "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?",
@@ -8,7 +8,7 @@
     }
   },
   "FiveStack_CCSPlayerController_SetClan": {
-    "runtimes": ["counterstrikesharp"],
+    "runtimes": ["counterstrikesharp", "swiftlys2"],
     "signatures": {
       "library": "server",
       "linux": "55 48 89 E5 41 57 41 56 41 55 49 89 D5 41 54 53 48 89 FB 48 81 EC ? ? ? ? 39 B7 ? ? ? ?",
diff --git a/shared/gamedata/swiftly/signatures.jsonc b/shared/gamedata/swiftly/signatures.jsonc
deleted file mode 100644
index f6263e91..00000000
--- a/shared/gamedata/swiftly/signatures.jsonc
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-    // "CNetworkGameServerBase::ConnectClient( name='%s', remote='%s' )\n"
-    "FiveStack_ConnectClient": {
-        "lib": "engine2",
-        "windows": "48 89 5C 24 18 44 89 4C 24 20 55 41 54 41 55 41 56 41 57 48 8D 6C 24 F1 48 81 EC ? ? ? ? 81 64 24 4C FF FF 0F FF",
-        "linux": "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?"
-    },
-    "FiveStack_CCSPlayerController_SetClan": {
-        "lib": "server",
-        "windows": "48 89 5C 24 10 48 89 6C 24 18 48 89 74 24 20 57 48 83 EC 20 49 8B E8 8B FA 48 8B F1 39 91 ? ? ? ? 74 ? BA FF FF FF FF",
-        "linux": "55 48 89 E5 41 57 41 56 41 55 49 89 D5 41 54 53 48 89 FB 48 81 EC ? ? ? ? 39 B7 ? ? ? ?"
-    }
-}