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); + } +}