diff --git a/apps/player-management-sw/src/ConnectGate.cs b/apps/player-management-sw/src/ConnectGate.cs
index 487f9a3a..a8207c57 100644
--- a/apps/player-management-sw/src/ConnectGate.cs
+++ b/apps/player-management-sw/src/ConnectGate.cs
@@ -26,8 +26,8 @@ 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"
+ * ships with the plugin and which the gamedata validator checks after every
+ * game update.
*
* Function signature:
*
@@ -43,11 +43,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,7 +55,15 @@ private void InstallConnectGate()
{
try
{
- nint? found = Core.Memory.GetAddressBySignature(Library.Engine, ConnectClientSignature);
+ 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)
{
diff --git a/apps/player-management-sw/src/PlayerManagement.csproj b/apps/player-management-sw/src/PlayerManagement.csproj
index 660a2998..f815e910 100644
--- a/apps/player-management-sw/src/PlayerManagement.csproj
+++ b/apps/player-management-sw/src/PlayerManagement.csproj
@@ -28,11 +28,15 @@
+
PreserveNewest
+
+ PreserveNewest
+
diff --git a/apps/swiftly/src/FiveStack.Events/InitConnect.cs b/apps/swiftly/src/FiveStack.Events/InitConnect.cs
index 319d1dbf..4f22e2c2 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,13 +56,15 @@ private void InitializeConnectClientHook()
return;
}
- if (string.IsNullOrEmpty(ConnectClientSignature))
- {
- _logger.LogWarning("ConnectClient signature is not available for this platform");
- return;
- }
+ GamedataSignature? signature = GamedataUtility.Load(
+ Core.PluginPath,
+ ConnectClientSignature
+ );
- var address = Core.Memory.GetAddressBySignature(Library.Engine, ConnectClientSignature);
+ nint? address =
+ signature == null
+ ? null
+ : Core.Memory.GetAddressBySignature(signature.Library, signature.Pattern);
if (address == null || address == nint.Zero)
{
diff --git a/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs b/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs
index 8e7532f8..819864b9 100644
--- a/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs
+++ b/apps/swiftly/src/FiveStack.Utilities/ClanTagUtility.cs
@@ -17,18 +17,20 @@ 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);
+ 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");
diff --git a/apps/swiftly/src/FiveStack.csproj b/apps/swiftly/src/FiveStack.csproj
index e69dd252..47594292 100644
--- a/apps/swiftly/src/FiveStack.csproj
+++ b/apps/swiftly/src/FiveStack.csproj
@@ -27,5 +27,8 @@
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 aecf6737..35c165a8 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,7 +58,15 @@ private void InitializeConnectClientHook()
return;
}
- var address = Core.Memory.GetAddressBySignature(Library.Engine, ConnectClientSignature);
+ 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)
{
diff --git a/apps/utility-sw/src/UtilityPractice.csproj b/apps/utility-sw/src/UtilityPractice.csproj
index df55efa6..5c0d94ac 100644
--- a/apps/utility-sw/src/UtilityPractice.csproj
+++ b/apps/utility-sw/src/UtilityPractice.csproj
@@ -56,6 +56,7 @@
+
@@ -70,6 +71,9 @@
+
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);
+ }
+}