diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..df5ff76 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. +dotnet_diagnostic.CS8632.severity = silent diff --git a/Core/AssetChanges/AGRoomChanges.cs b/Core/AssetChanges/AGRoomChanges.cs new file mode 100644 index 0000000..8dbafbf --- /dev/null +++ b/Core/AssetChanges/AGRoomChanges.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace undead_universal_patch_il2cpp.Core.AssetChanges; + +public class RecChange +{ + [JsonPropertyName("Key")] + public string RecKey { get; set; } + + [JsonPropertyName("Value")] + public dynamic RecValue { get; set; } +} + +public static class AGRoomChanges +{ + public static string name = "AGRoomObjectChanges"; + public static DediConfig>> config = new(name, "[]", null); +} \ No newline at end of file diff --git a/Config.cs b/Core/Config.cs similarity index 74% rename from Config.cs rename to Core/Config.cs index a458332..a724b8b 100644 --- a/Config.cs +++ b/Core/Config.cs @@ -1,23 +1,45 @@ using System; using BepInEx.Configuration; -namespace undead_universal_patch_il2cpp +namespace undead_universal_patch_il2cpp.Core { public static class GenericConfig { + public static ConfigEntry PatchDebug; public static ConfigEntry LogAllRequests; public static ConfigEntry CertificatePatch; public static ConfigEntry HilePatch; public static ConfigEntry SignalRHandshakeFix; public static ConfigEntry ImageSignaturePatch; + public static ConfigEntry RegistrationPatch; } public static class GenericConfigDefaults { + public static bool PatchDebug = false; public static bool LogAllRequests = false; public static bool CertificatePatch = false; public static bool HilePatch = false; public static bool SignalRHandshakeFix = false; public static bool ImageSignaturePatch = false; + public static bool RegistrationPatch = false; + } + public static class AssetChangesConfig + { + public static ConfigEntry AGRoomChanges; + } + public static class AssetChangesConfigDefaults + { + public static bool AGRoomChanges = false; + } + public static class GameManagerConfig + { + public static ConfigEntry StaticGameTeamConfig; + public static ConfigEntry AnyGameFreeSpawn; + } + public static class GameManagerConfigDefaults + { + public static bool StaticGameTeamConfig; + public static bool AnyGameFreeSpawn; } public static class NameserverConfig { diff --git a/Core/DediConfig.cs b/Core/DediConfig.cs new file mode 100644 index 0000000..4ed9095 --- /dev/null +++ b/Core/DediConfig.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +namespace undead_universal_patch_il2cpp.Core; + +public class DediConfig +{ + private readonly string _default; + public readonly string _name; + public readonly string path; + private T? cached; + private bool loaded = false; + private readonly JsonSerializerOptions? _options; + + public DediConfig(string name, string def, JsonSerializerOptions? options) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name cannot be invalid"); + + _name = name; + path = $"BepInEx/config/Undead.{name}.json"; + _default = def; + _options = options; + } + + private string GetAlways() { + if (!File.Exists(path)) + { + File.WriteAllText(path, _default); + return _default; + } + return File.ReadAllText(path); + } + public T Get() + { + if (loaded) return cached; + + string data = GetAlways(); + cached = JsonSerializer.Deserialize(data, _options == null ? new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + } : _options); + + loaded = true; + return cached; + } +} \ No newline at end of file diff --git a/Galvanic.cs b/Core/Galvanic.cs similarity index 85% rename from Galvanic.cs rename to Core/Galvanic.cs index 97c6a9f..e35d466 100644 --- a/Galvanic.cs +++ b/Core/Galvanic.cs @@ -8,7 +8,7 @@ using System.IO; // this entire file could be better -namespace undead_universal_patch_il2cpp.Galvanic +namespace undead_universal_patch_il2cpp.Core { public class GalvanicAuth { @@ -16,20 +16,18 @@ namespace undead_universal_patch_il2cpp.Galvanic { PlayerPrefs.DeleteKey("GalvanicPublicKey"); PlayerPrefs.DeleteKey("GalvanicPrivateKey"); - using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256)) - { - byte[] publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); - string publicKeyBase64 = Convert.ToBase64String(publicKeyBytes); + using ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + byte[] publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); + string publicKeyBase64 = Convert.ToBase64String(publicKeyBytes); - byte[] privateKeyBytes = ecdsa.ExportPkcs8PrivateKey(); - string privateKeyBase64 = Convert.ToBase64String(privateKeyBytes); + byte[] privateKeyBytes = ecdsa.ExportPkcs8PrivateKey(); + string privateKeyBase64 = Convert.ToBase64String(privateKeyBytes); - PlayerPrefs.SetString("GalvanicPublicKey", publicKeyBase64); - PlayerPrefs.SetString("GalvanicPrivateKey", privateKeyBase64); - PlayerPrefs.Save(); + PlayerPrefs.SetString("GalvanicPublicKey", publicKeyBase64); + PlayerPrefs.SetString("GalvanicPrivateKey", privateKeyBase64); + PlayerPrefs.Save(); - Plugin.Log.LogInfo("Created new keypair for Galvanic authentication."); - } + Plugin.Log.LogInfo("Created new keypair for Galvanic authentication."); } public static string GetPubKey() @@ -61,15 +59,13 @@ namespace undead_universal_patch_il2cpp.Galvanic public static string SignPayload(string message) { - using (ECDsa ecdsa = GetPrivKey()) - { - if (ecdsa == null) return null; + using ECDsa ecdsa = GetPrivKey(); + if (ecdsa == null) return null; - byte[] payloadBytes = Encoding.UTF8.GetBytes(message); - byte[] signature = ecdsa.SignData(payloadBytes, HashAlgorithmName.SHA256); + byte[] payloadBytes = Encoding.UTF8.GetBytes(message); + byte[] signature = ecdsa.SignData(payloadBytes, HashAlgorithmName.SHA256); - return Convert.ToBase64String(signature); - } + return Convert.ToBase64String(signature); } public static void Export() { @@ -146,22 +142,24 @@ namespace undead_universal_patch_il2cpp.Galvanic public static void GetToken() { string url = NameserverConfig.NewUrl.Value; - UriBuilder uri = new(url); - uri.Path = "/user/auth"; - uri.Query = ""; + UriBuilder uri = new(url) + { + Path = "/user/auth", + Query = "" + }; var info = GalvanicAuth.GetServerInfo(); Plugin.Log.LogInfo($"Sending authentication request to server ID '{info.id}'"); if (uri.Scheme == "http") Plugin.Log.LogWarning("The server is not secure! Please use HTTPS."); - UserAuthPayload payload = new UserAuthPayload + UserAuthPayload payload = new() { timestamp = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds(), nonce = Guid.NewGuid().ToString(), server_id = info.id, }; - UserAuthRequest request = new UserAuthRequest + UserAuthRequest request = new() { message = payload, signature = GalvanicAuth.SignPayload($"{JsonSerializer.Serialize(payload)}"), diff --git a/Core/GameManager/GameManSpawns.cs b/Core/GameManager/GameManSpawns.cs new file mode 100644 index 0000000..8c47104 --- /dev/null +++ b/Core/GameManager/GameManSpawns.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace undead_universal_patch_il2cpp.Core.UndeadGameManager; + +public static class GameFreeSpawns +{ + static readonly string name = "GameFreeSpawns"; + public static DediConfig> config = new(name, "[]", null); +} \ No newline at end of file diff --git a/Core/GameManager/TeamConfigurator.cs b/Core/GameManager/TeamConfigurator.cs new file mode 100644 index 0000000..e836300 --- /dev/null +++ b/Core/GameManager/TeamConfigurator.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace undead_universal_patch_il2cpp.Core.UndeadGameManager; + +public class TeamConfiguration +{ + public int Size { get; set; } +} +public class GameConfiguration +{ + public string GameName { get; set; } + public List TeamConfigurations { get; set; } +} + +public static class TeamConfigurator +{ + static readonly string name = "TeamConfigurator"; + public static DediConfig> config = new(name, "[]", null); +} \ No newline at end of file diff --git a/Util.cs b/Core/Util.cs similarity index 78% rename from Util.cs rename to Core/Util.cs index fb080ef..9419a05 100644 --- a/Util.cs +++ b/Core/Util.cs @@ -2,7 +2,7 @@ using System.Reflection; using HarmonyLib; -namespace undead_universal_patch_il2cpp +namespace undead_universal_patch_il2cpp.Core { public class Util { diff --git a/Patches/AssetManager/AGConfigEntryPatches.cs b/Patches/AssetManager/AGConfigEntryPatches.cs new file mode 100644 index 0000000..3ca7450 --- /dev/null +++ b/Patches/AssetManager/AGConfigEntryPatches.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using HarmonyLib; +using undead_universal_patch_il2cpp.Core; +using undead_universal_patch_il2cpp.Core.AssetChanges; + +namespace undead_universal_patch_il2cpp.Patches.AssetManager +{ + public static class AGConfigEntryPatch_Common + { + public static Dictionary> Config = AGRoomChanges.config.Get(); + } + + [HarmonyPatch] + public class AGConfigEntryPatch_RoomScene + { + static readonly string TargetTypeName = "AGRoomSettings"; + static readonly string TargetMethodName = "TryGetRoomSceneConfigById"; + static readonly string Description = "AGRoomSettings data patch"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + public static bool Prepare() + { + if (!AssetChangesConfig.AGRoomChanges.Value) return false; + if (targetType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); + return false; + } + if (AccessTools.Method(targetType, TargetMethodName) == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + static MethodBase TargetMethod() => AccessTools.Method(targetType, TargetMethodName); + static void Postfix(ref string replicationId, ref AGRoomRuntimeConfig.Room roomConfig, ref AGRoomRuntimeConfig.RoomScene roomSceneConfig) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"RoomScene patch for repid '{replicationId}'"); + if (!AGConfigEntryPatch_Common.Config.ContainsKey(replicationId)) return; + + var changes = AGConfigEntryPatch_Common.Config[replicationId]; + foreach (var change in changes) + { + PropertyInfo prop = AccessTools.Property(roomSceneConfig.GetType(), change.RecKey); + if (prop == null) + { + Plugin.Log.LogWarning($"Key '{change.RecKey}' does not exist on the room scene for replication ID '{replicationId}'!"); + continue; + } + + if (change.RecValue is JsonElement element) + { + object value; + if (prop.PropertyType.IsEnum) value = Enum.Parse(prop.PropertyType, element.GetRawText()); + else value = JsonSerializer.Deserialize(element.GetRawText(), prop.PropertyType); + + prop.SetValue(roomSceneConfig, value); + } + else prop.SetValue(roomSceneConfig, change.RecValue); + + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"'{change.RecKey}' on repId '{replicationId}' is now '{change.RecValue}'"); + } + } + } + + [HarmonyPatch] + public class AGConfigEntryPatch_Location + { + static readonly string TargetTypeName = "AGRoomSettings"; + static readonly string TargetMethodName = "TryGetLocationConfigById"; + static readonly MethodInfo connectMethod = AccessTools.Method(AccessTools.TypeByName(TargetTypeName), TargetMethodName); + + static bool Prepare() => AGConfigEntryPatch_RoomScene.Prepare(); + + static MethodBase TargetMethod() => connectMethod; + static void Postfix(ref string replicationId, ref AGRoomRuntimeConfig.Location locationConfig) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"Location patch for repid '{replicationId}'"); + if (!AGConfigEntryPatch_Common.Config.ContainsKey(replicationId)) return; + + var changes = AGConfigEntryPatch_Common.Config[replicationId]; + foreach (var change in changes) + { + PropertyInfo prop = AccessTools.Property(locationConfig.GetType(), change.RecKey); + if (prop == null) + { + Plugin.Log.LogWarning($"Key '{change.RecKey}' does not exist on the room scene for replication ID '{replicationId}'!"); + continue; + } + + if (change.RecValue is JsonElement element) + { + object value; + if (prop.PropertyType.IsEnum) value = Enum.Parse(prop.PropertyType, element.GetRawText()); + else value = JsonSerializer.Deserialize(element.GetRawText(), prop.PropertyType); + + prop.SetValue(locationConfig, value); + } + else prop.SetValue(locationConfig, change.RecValue); + + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"'{change.RecKey}' on repId '{replicationId}' is now '{change.RecValue}'"); + } + } + } + + [HarmonyPatch] + public class AGConfigEntryPatch_Room + { + static readonly string TargetTypeName = "AGRoomSettings"; + static readonly string TargetMethodName = "TryGetRoomConfigById"; + static readonly MethodInfo connectMethod = AccessTools.Method(AccessTools.TypeByName(TargetTypeName), TargetMethodName); + + static bool Prepare() => AGConfigEntryPatch_RoomScene.Prepare(); + + static MethodBase TargetMethod() => connectMethod; + static void Postfix(ref string replicationId, ref AGRoomRuntimeConfig.Room roomConfig) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"Roomconfig patch for repid '{replicationId}'"); + if (!AGConfigEntryPatch_Common.Config.ContainsKey(replicationId)) return; + + var changes = AGConfigEntryPatch_Common.Config[replicationId]; + foreach (var change in changes) + { + PropertyInfo prop = AccessTools.Property(roomConfig.GetType(), change.RecKey); + if (prop == null) + { + Plugin.Log.LogWarning($"Key '{change.RecKey}' does not exist on the room scene for replication ID '{replicationId}'!"); + continue; + } + + if (change.RecValue is JsonElement element) + { + object value; + if (prop.PropertyType.IsEnum) value = Enum.Parse(prop.PropertyType, element.GetRawText()); + else value = JsonSerializer.Deserialize(element.GetRawText(), prop.PropertyType); + + prop.SetValue(roomConfig, value); + } + else prop.SetValue(roomConfig, change.RecValue); + + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"'{change.RecKey}' on repId '{replicationId}' is now '{change.RecValue}'"); + } + } + } + +} \ No newline at end of file diff --git a/BasePatches/BestHTTP.cs b/Patches/BestHTTP.cs similarity index 85% rename from BasePatches/BestHTTP.cs rename to Patches/BestHTTP.cs index 3f4532b..b53ccf3 100644 --- a/BasePatches/BestHTTP.cs +++ b/Patches/BestHTTP.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using HarmonyLib; +using undead_universal_patch_il2cpp.Core; namespace undead_universal_patch_il2cpp.Patches { @@ -12,7 +13,6 @@ namespace undead_universal_patch_il2cpp.Patches static string Description = "Unobfuscated BestHTTP request URL rewrite patch"; static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); static readonly Type requestType = AccessTools.TypeByName("BestHTTP.HTTPRequest"); - static readonly MethodInfo targetMethod = AccessTools.Method(targetType, TargetMethodName, [requestType]); static bool Prepare() { @@ -21,7 +21,12 @@ namespace undead_universal_patch_il2cpp.Patches Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); return false; } - if (targetMethod == null) + if (requestType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The request type for this patch was not found."); + return false; + } + if (AccessTools.Method(targetType, TargetMethodName, [requestType]) == null) { Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); return false; @@ -31,7 +36,7 @@ namespace undead_universal_patch_il2cpp.Patches return true; } - static MethodBase TargetMethod() => targetMethod; + static MethodBase TargetMethod() => AccessTools.Method(targetType, TargetMethodName, [requestType]); [HarmonyPrefix] static void Prefix(ref object request) @@ -72,11 +77,11 @@ namespace undead_universal_patch_il2cpp.Patches { // refresh the token if it expired // this is somewhat inefficient, but we don't hook into many requests (see above) so it should be fine - Galvanic.GalvanicWebAuth.TokenExpiry(); + GalvanicWebAuth.TokenExpiry(); Type httpRequestType = request.GetType(); MethodInfo addHeaderMethod = httpRequestType.GetMethod("AddHeader"); - addHeaderMethod.Invoke(request, ["GalvanicAuth", Galvanic.GalvanicWebAuth.Token]); + addHeaderMethod.Invoke(request, ["GalvanicAuth", GalvanicWebAuth.Token]); break; } diff --git a/Patches/GameManager/FreeSpawns.cs b/Patches/GameManager/FreeSpawns.cs new file mode 100644 index 0000000..841054c --- /dev/null +++ b/Patches/GameManager/FreeSpawns.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using undead_universal_patch_il2cpp.Core; +using undead_universal_patch_il2cpp.Core.UndeadGameManager; + +namespace undead_universal_patch_il2cpp.Patches.UndeadGameManager; + +[HarmonyPatch] +public class FreeSpawnsPatch_Array +{ + public static List config = GameFreeSpawns.config.Get(); + + public static string TargetTypeName = "GamePlayerSpawnPoint"; + public static string TargetMethodName = "Awake"; + public static string Description = "FreeSpawns event patch"; + public static Type targetType = AccessTools.TypeByName(TargetTypeName); + + public static bool Prepare() + { + if (!GameManagerConfig.AnyGameFreeSpawn.Value) return false; + if (AccessTools.Method(targetType, TargetMethodName) == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + public static MethodBase TargetMethod() => AccessTools.Method(targetType, TargetMethodName); + + public static void Postfix(ref GamePlayerSpawnPoint __instance) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug("Attempting FreeSpawns patch"); + GameManager man = NetworkedSingletonMonoBehaviour.instance; + + if (man.CurrentGameConfiguration == null) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug("CurrentGameConfiguration was null"); + return; + } + if (man.CurrentGameConfiguration.configurationData == null) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug("CurrentGameConfiguration.configurationData was null"); + return; + } + if (!config.Contains(man.CurrentGameConfiguration.configurationData.Name)) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"Game '{man.CurrentGameConfiguration.configurationData.Name}' is not specified by GameFreeSpawns"); + return; + } + + __instance.GameTeamPlayerIndex = GameTeamPlayerIndex.ANY_INDEX; + } +} \ No newline at end of file diff --git a/Patches/GameManager/TeamConfigurator.cs b/Patches/GameManager/TeamConfigurator.cs new file mode 100644 index 0000000..9cb560d --- /dev/null +++ b/Patches/GameManager/TeamConfigurator.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using RecRoom.Core.GameManagement; +using undead_universal_patch_il2cpp.Core; +using undead_universal_patch_il2cpp.Core.UndeadGameManager; + +namespace undead_universal_patch_il2cpp.Patches.UndeadGameManager; + +[HarmonyPatch] +public class TeamConfiguratorPatch +{ + static List gameConfig = TeamConfigurator.config.Get(); + + static string TargetTypeName = "GameConfigurationTool"; + static string TargetMethodName = "SetGameConfiguration"; + static string Description = "TeamConfigurator event patch"; + + static Type targetType = AccessTools.TypeByName(TargetTypeName); + static MethodInfo targetMethod = AccessTools.Method(targetType, TargetMethodName); + + public static bool Prepare() + { + if (!GameManagerConfig.StaticGameTeamConfig.Value) return false; + if (targetMethod == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + public static MethodBase TargetMethod() => targetMethod; + + public static void Prefix(ref GameConfigurationAsset config, ref bool showNotification) + { + var conf = config; + showNotification = GenericConfig.PatchDebug.Value; + + var teamConf = gameConfig.Find(match => match.GameName == conf.Name); + if (teamConf == null) + { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"'{config.Name}' does not have explicit team configurations"); + return; + } + + var arr = new Il2CppStructArray(teamConf.TeamConfigurations.Count); + for (int i = 0; i < arr.Length; i++) arr[i] = new TeamConfiguration { MaxTeamSize = teamConf.TeamConfigurations[i].Size }; + config.TeamConfigurations = arr; + + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"New team configuration length for '{config.Name}': {arr.Length}"); + } +} \ No newline at end of file diff --git a/BasePatches/HilePatch.cs b/Patches/HilePatch.cs similarity index 87% rename from BasePatches/HilePatch.cs rename to Patches/HilePatch.cs index f3cf6a5..d1695ae 100644 --- a/BasePatches/HilePatch.cs +++ b/Patches/HilePatch.cs @@ -1,5 +1,6 @@ using System.Reflection; using HarmonyLib; +using undead_universal_patch_il2cpp.Core; using UnityEngine; namespace undead_universal_patch_il2cpp.Patches @@ -10,11 +11,10 @@ namespace undead_universal_patch_il2cpp.Patches static readonly string TargetTypeName = "RecNet.Core"; static readonly string TargetMethodName = "ConnectToRecNet"; static readonly string Description = "Hile Patch event method"; // It's convenient. Could patch at a different time. But this part was easy. - static readonly MethodInfo connectMethod = AccessTools.Method(AccessTools.TypeByName(TargetTypeName), TargetMethodName); static bool Prepare() { - if (connectMethod == null) + if (AccessTools.Method(AccessTools.TypeByName(TargetTypeName), TargetMethodName) == null) { Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); return false; @@ -23,7 +23,7 @@ namespace undead_universal_patch_il2cpp.Patches Plugin.Log.LogInfo($"'{Description}' succeeded validation."); return true; } - static MethodBase TargetMethod() => connectMethod; + static MethodBase TargetMethod() => AccessTools.Method(AccessTools.TypeByName(TargetTypeName), TargetMethodName); static void Prefix() { if (GenericConfig.HilePatch.Value) HilePatch.Patch(); diff --git a/BasePatches/ImageSignature.cs b/Patches/ImageSignature.cs similarity index 80% rename from BasePatches/ImageSignature.cs rename to Patches/ImageSignature.cs index e501192..9c4ff90 100644 --- a/BasePatches/ImageSignature.cs +++ b/Patches/ImageSignature.cs @@ -1,6 +1,7 @@ using System; using HarmonyLib; using System.Reflection; +using undead_universal_patch_il2cpp.Core; namespace undead_universal_patch_il2cpp.Patches { @@ -11,7 +12,6 @@ namespace undead_universal_patch_il2cpp.Patches public static string TargetMethodName = "VerifySignature"; public static string Description = "Image signature patch"; public static Type targetType = AccessTools.TypeByName(TargetTypeName); - public static MethodBase targetMethod = targetType.GetMethod(TargetMethodName); public static bool Prepare() { @@ -21,7 +21,7 @@ namespace undead_universal_patch_il2cpp.Patches Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); return false; } - if (targetMethod == null) + if (targetType.GetMethod(TargetMethodName) == null) { Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); return false; @@ -31,11 +31,11 @@ namespace undead_universal_patch_il2cpp.Patches return true; } - public static MethodBase TargetMethod() => targetMethod; + public static MethodBase TargetMethod() => targetType.GetMethod(TargetMethodName); public static bool Prefix(ref bool __result) { - Plugin.Log.LogDebug("Verified image signature"); + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug("Verified image signature"); __result = true; return false; } diff --git a/BasePatches/PhotonPatch.cs b/Patches/PhotonPatch.cs similarity index 88% rename from BasePatches/PhotonPatch.cs rename to Patches/PhotonPatch.cs index 6ff76a8..34c3ed2 100644 --- a/BasePatches/PhotonPatch.cs +++ b/Patches/PhotonPatch.cs @@ -1,10 +1,7 @@ using System; using System.Reflection; -using BepInEx.Configuration; using HarmonyLib; -using Il2CppSystem.Security.Cryptography; -using RecNet; -using UnityEngine; +using undead_universal_patch_il2cpp.Core; namespace undead_universal_patch_il2cpp.Patches { @@ -15,7 +12,6 @@ namespace undead_universal_patch_il2cpp.Patches static readonly string TargetMethodName = "ConnectUsingSettings"; static readonly string Description = "Photon ConnectUsingSettings patch event"; static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); - static readonly MethodInfo targetMethod = AccessTools.Method(targetType, TargetMethodName); static bool Prepare() { @@ -25,7 +21,7 @@ namespace undead_universal_patch_il2cpp.Patches Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); return false; } - if (targetMethod == null) + if (AccessTools.Method(targetType, TargetMethodName) == null) { Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); return false; @@ -35,10 +31,11 @@ namespace undead_universal_patch_il2cpp.Patches return true; } - static MethodInfo TargetMethod() => targetMethod; + static MethodInfo TargetMethod() => AccessTools.Method(targetType, TargetMethodName); static void Prefix() { + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug("Attempting Photon patch"); PhotonPatch.Patch(); } } @@ -101,7 +98,7 @@ namespace undead_universal_patch_il2cpp.Patches __state.code = photonRegionIdProperty.GetValue(targetGameSession); photonRegionIdProperty.SetValue(targetGameSession, 4); - Plugin.Log.LogDebug("Forcing masterserver"); + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug("Forcing masterserver"); } static void Postfix(ref PUNNetworkManager __instance, ref HarmonyState __state) @@ -126,7 +123,7 @@ namespace undead_universal_patch_il2cpp.Patches } photonRegionIdProperty.SetValue(targetGameSession, __state.code); - Plugin.Log.LogDebug("Masterserver regionId roundtrip"); + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug("Masterserver regionId roundtrip"); } } @@ -144,7 +141,6 @@ namespace undead_universal_patch_il2cpp.Patches { Plugin.Log.LogError("Cannot patch Photon: PhotonNetwork or ServerSettings types were not found. Is this build supported?"); return; - // safe to reference enums proceeding this } PropertyInfo photonServerSettingsProperty = photonNetworkType.GetRuntimeProperty("PhotonServerSettings"); @@ -161,6 +157,8 @@ namespace undead_universal_patch_il2cpp.Patches PropertyInfo appIdProperty = serverSettingsType.GetRuntimeProperty("AppID"); PropertyInfo voiceAppIdProperty = serverSettingsType.GetRuntimeProperty("VoiceAppID"); + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"New Photon AppID: '{PhotonConfig.AppID.Value}'"); + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"New Photon AppID: '{PhotonConfig.VoiceAppID.Value}'"); appIdProperty.SetValue(serverSettings, PhotonConfig.AppID.Value); voiceAppIdProperty.SetValue(serverSettings, PhotonConfig.VoiceAppID.Value); @@ -177,8 +175,6 @@ namespace undead_universal_patch_il2cpp.Patches Type authValuesType = AccessTools.TypeByName("AuthenticationValues"); if (authValuesType != null) { - PropertyInfo userIdProperty = authValuesType.GetRuntimeProperty("UserId"); - PropertyInfo networkingPeerProperty = photonNetworkType.GetRuntimeProperty("networkingPeer"); if (networkingPeerProperty == null) { @@ -193,12 +189,14 @@ namespace undead_universal_patch_il2cpp.Patches } string id = Util.LocalInstanceGuid; - Plugin.Log.LogDebug($"Instance GUID is {id}"); + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"Instance GUID is {id}"); - PhotonNetwork.AuthValues = new AuthenticationValues(); - PhotonNetwork.AuthValues.UserId = id; + PhotonNetwork.AuthValues = new AuthenticationValues + { + UserId = id + }; - Plugin.Log.LogDebug($"Set the authValues userId to {id}"); + if (GenericConfig.PatchDebug.Value) Plugin.Log.LogDebug($"Set the authValues userId to {id}"); } Plugin.Log.LogInfo($"Photon patch ({(PhotonConfig.SelfHosted.Value ? "self-hosted" : "cloud")}) succeeded."); diff --git a/Patches/Registration.cs b/Patches/Registration.cs new file mode 100644 index 0000000..a1edc78 --- /dev/null +++ b/Patches/Registration.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using HarmonyLib; +using undead_universal_patch_il2cpp.Core; + +namespace undead_universal_patch_il2cpp.Patches +{ + [HarmonyPatch] + public class RegistrationPatch + { + static bool Prepare() + { + if (!GenericConfig.RegistrationPatch.Value) return false; + + Type unityEngineType = AccessTools.TypeByName("UnityEngine.PlayerPrefs"); + MethodInfo getMethod = unityEngineType.GetMethod("GetInt", [typeof(string), typeof(int)]); + + return unityEngineType != null && getMethod != null; + } + + static MethodInfo TargetMethod() + { + Type tutorialManagerType = AccessTools.TypeByName("UnityEngine.PlayerPrefs"); + return tutorialManagerType.GetMethod("GetInt", [ typeof(string), typeof(int) ]); + } + + static void Postfix(ref string key, ref int defaultValue, ref int __result) + { + if (key.StartsWith("IncompleteRegistration-")) + { + __result = 0; + Plugin.Log.LogInfo("Detour'd IncompleteRegistration pref key"); + } + } + } +} diff --git a/BasePatches/SignalRHandshakeFix.cs b/Patches/SignalRHandshakeFix.cs similarity index 85% rename from BasePatches/SignalRHandshakeFix.cs rename to Patches/SignalRHandshakeFix.cs index 71b810d..0d5916f 100644 --- a/BasePatches/SignalRHandshakeFix.cs +++ b/Patches/SignalRHandshakeFix.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using HarmonyLib; +using undead_universal_patch_il2cpp.Core; namespace undead_universal_patch_il2cpp.Patches { @@ -11,7 +12,6 @@ namespace undead_universal_patch_il2cpp.Patches public static string TargetMethodName = "WithSeparator"; public static string Description = "SignalR Handshake Fix (quotes vs apostrophes)"; public static Type targetType = AccessTools.TypeByName(TargetTypeName); - public static MethodBase targetMethod = AccessTools.Method(targetType, TargetMethodName); public static bool Prepare() { @@ -21,7 +21,7 @@ namespace undead_universal_patch_il2cpp.Patches Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); return false; } - if (targetMethod == null) + if (AccessTools.Method(targetType, TargetMethodName) == null) { Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); return false; @@ -30,7 +30,7 @@ namespace undead_universal_patch_il2cpp.Patches Plugin.Log.LogInfo($"'{Description}' succeeded validation."); return true; } - public static MethodBase TargetMethod() => targetMethod; + public static MethodBase TargetMethod() => AccessTools.Method(targetType, TargetMethodName); public static void Prefix(ref string str) { if (str.Contains("protocol':")) diff --git a/BasePatches/TLS.cs b/Patches/TLS.cs similarity index 81% rename from BasePatches/TLS.cs rename to Patches/TLS.cs index 0af88fe..258fb95 100644 --- a/BasePatches/TLS.cs +++ b/Patches/TLS.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using HarmonyLib; +using undead_universal_patch_il2cpp.Core; namespace undead_universal_patch_il2cpp.Patches { @@ -11,17 +12,16 @@ namespace undead_universal_patch_il2cpp.Patches static string TargetMethodName = "NotifyServerCertificate"; static string Description = "Certificate patch"; static Type targetType = AccessTools.TypeByName(TargetTypeName); - static MethodInfo targetMethod = AccessTools.Method(targetType, TargetMethodName); static bool Prepare() { if (!GenericConfig.CertificatePatch.Value) return false; - if (targetMethod == null) + if (targetType == null) { Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); return false; } - if (targetMethod == null) + if (AccessTools.Method(targetType, TargetMethodName) == null) { Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); return false; @@ -31,7 +31,7 @@ namespace undead_universal_patch_il2cpp.Patches return true; } - static MethodBase TargetMethod() => targetMethod; + static MethodBase TargetMethod() => AccessTools.Method(targetType, TargetMethodName); static bool Prefix() { diff --git a/Plugin.cs b/Plugin.cs index 8c6f9b0..ebec7b3 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -1,12 +1,16 @@ using System; +using System.Text.Json; using BepInEx; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using undead_universal_patch_il2cpp.Core; +using undead_universal_patch_il2cpp.Core.AssetChanges; +using undead_universal_patch_il2cpp.Core.UndeadGameManager; namespace undead_universal_patch_il2cpp; -[BepInPlugin("dev.proxnet.recroom.universalpatch.noneac.il2cpp", "Undead Universal Patch", "1.3.0")] +[BepInPlugin("dev.proxnet.recroom.universalpatch.noneac.il2cpp", "Undead Universal Patch", "1.4.0")] public class Plugin : BasePlugin { public static new readonly ManualLogSource Log = Logger.CreateLogSource("UUPatch"); @@ -24,20 +28,32 @@ public class Plugin : BasePlugin { Util.LocalInstanceGuid = Guid.NewGuid().ToString(); - Log.LogInfo("It's time to Room some Rec and chew bubble gum, and I'm all out of gum"); + Log.LogInfo("It's time to Room some Rec and chew bubble gum..... and I'm all out of gum"); + GenericConfig.PatchDebug = Config.Bind("Generic", "PatchDebug", GenericConfigDefaults.PatchDebug, + "Enable logging messages sent by patches for debugging. Usually not needed. Enable when submitting issues or bug reports."); GenericConfig.LogAllRequests = Config.Bind("Generic", "LogAllRequests", GenericConfigDefaults.LogAllRequests, - "Log all HTTP requests sent by the game."); + "Log all BestHTTP requests sent by the game."); GenericConfig.CertificatePatch = Config.Bind("Generic", "CertificatePatch", GenericConfigDefaults.CertificatePatch, "The game expects a certain certificate from rec.net when making HTTPS requests. Enable this to allow any valid certificate."); GenericConfig.HilePatch = Config.Bind("Generic", "HilePatch", GenericConfigDefaults.HilePatch, - "The game will close after a short period of time if BepInEx is found. Enable to stop this from happening."); + "The game will close after a short period of time if BepInEx is found. Enable to stop this from happening." + + "This will also enable the AGRoomRuntimeConfig patch. See the README for more info."); GenericConfig.SignalRHandshakeFix = Config.Bind("Generic", "SignalRHandshakeFix", GenericConfigDefaults.SignalRHandshakeFix, "Replace apostrophes with quotes in the initial SignalR handshake."); GenericConfig.ImageSignaturePatch = Config.Bind("Generic", "ImageSignaturePatch", GenericConfigDefaults.ImageSignaturePatch, "When enabled, all image signatures will be valid." + "\nWorks only if the server appends a properly formatted signature header (signature does not need to be valid)"); - GenericConfig + GenericConfig.RegistrationPatch = Config.Bind("Generic", "RegistrationPatch", GenericConfigDefaults.RegistrationPatch, + "Always disable the registration prompt."); + + AssetChangesConfig.AGRoomChanges = Config.Bind("AssetChangesConfig", "AGRoomChanges", AssetChangesConfigDefaults.AGRoomChanges, + $"Use patches from '{AGRoomChanges.config.path}' to change the properties of internal AGRooms. See README.md"); + GameManagerConfig.AnyGameFreeSpawn = Config.Bind("GameManagerConfig", "AnyGameFreeSpawn", GameManagerConfigDefaults.AnyGameFreeSpawn, + $"Use patches from '{GameFreeSpawns.config.path}' to spawn in any valid player spawnpoint in specified games. See README.md"); + GameManagerConfig.StaticGameTeamConfig = Config.Bind("GameManagerConfig", "StaticGameTeamConfig", GameManagerConfigDefaults.StaticGameTeamConfig, + $"Use patches from '{TeamConfigurator.config.path}' to set the static configuration of teams in specified games. See README.md"); + PhotonConfig.PatchPhotonIds = Config.Bind("Photon", "PatchPhotonIds", PhotonConfigDefaults.PatchPhotonIds, "Patch Photon configuration."); PhotonConfig.PunLogging = Config.Bind("Photon", "PunLogging", PhotonConfigDefaults.PunLogging, @@ -55,10 +71,12 @@ public class Plugin : BasePlugin "Address of the Photon master server (ignored if not using self-hosted)"); PhotonConfig.ServerPort = Config.Bind("Photon", "ServerPort", PhotonConfigDefaults.ServerPort, "Photon master server UDP port (ignored if not using self-hosted)"); + NameserverConfig.Rewrite = Config.Bind("Nameserver", "Rewrite", NameserverConfigDefaults.Rewrite, "Enable/disable rewriting the URL for nameserver requests."); NameserverConfig.NewUrl = Config.Bind("Nameserver", "NewUrl", NameserverConfigDefaults.NewUrl, "The new full URL to use when sending a nameserver request."); + GalvanicConfig.Enabled = Config.Bind("GalvanicCorrosion", "Enabled", GalvanicConfigDefaults.Enabled, "Use Galvanic Corrosion features. Authenticates with a GC server." + "\nThe host and protocol in Nameserver.NewUrl will be used for the server's base URL." + @@ -76,15 +94,35 @@ public class Plugin : BasePlugin "\nbe unnecessarily written and read to and from the disk."); _hi.PatchAll(); - if (GalvanicConfig.RegenerateKeypair.Value) + if (GalvanicConfig.Enabled.Value && GalvanicConfig.RegenerateKeypair.Value) { Log.LogInfo("Regenerating keypair"); - Galvanic.GalvanicAuth.PrepareKeys(); + GalvanicAuth.PrepareKeys(); } - if (GalvanicConfig.Export.Value == "IWantToExportMyKeys") Galvanic.GalvanicAuth.Export(); - if (GalvanicConfig.Import.Value) Galvanic.GalvanicAuth.Import(); - if (GalvanicConfig.Enabled.Value) Galvanic.GalvanicWebAuth.GetToken(); + if (GalvanicConfig.Enabled.Value && GalvanicConfig.Export.Value == "IWantToExportMyKeys") GalvanicAuth.Export(); + if (GalvanicConfig.Enabled.Value && GalvanicConfig.Import.Value) GalvanicAuth.Import(); + if (GalvanicConfig.Enabled.Value) GalvanicWebAuth.GetToken(); + + Log.LogInfo("PATCH LIST START ========================="); + foreach (var method in _hi.GetPatchedMethods()) Log.LogInfo($"- {method.ToString()}"); + Log.LogInfo("PATCH LIST END ========================="); Log.LogInfo("Undead Universal IL2CPP Patch for Rec Room by @zombieb; loaded."); + + try + { + CacheChangePatchConfigs(); + } + catch (Exception ex) + { + Log.LogError($"Could not load change patches: {ex}"); + } + } + + private static void CacheChangePatchConfigs() + { + if (AssetChangesConfig.AGRoomChanges.Value) AGRoomChanges.config.Get(); + if (GameManagerConfig.AnyGameFreeSpawn.Value) GameFreeSpawns.config.Get(); + if (GameManagerConfig.StaticGameTeamConfig.Value) TeamConfigurator.config.Get(); } } \ No newline at end of file diff --git a/README.md b/README.md index d63b810..33b5876 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Run BepInEx interop on your build and add the following assemblies to new folder * `BepInEx/interop/Photon3Unity3D.dll` * `BepInEx/interop/RecRoom.Datastructures.Runtime` * `BepInEx/interop/UnityEngine.CoreModule.dll` +* `UniverseLib.BIE.IL2CPP.Interop.dll` [UniverseLib](https://github.com/yukieiji/UniverseLib); (can be obtained from UnityExplorer) ### Linux users Wine currently has problems generating the keypair used in Galvanic authentication. @@ -34,4 +35,76 @@ If you're unsure how to start your build on linux: * Add Rec Room as a non-steam game * Set the compatibility mode to the latest stable version of proton * Use protontricks to add the doorstop DLL -* Start Rec Room through Steam \ No newline at end of file +* Start Rec Room through Steam + +### AGRoomRuntimeConfig Patches +Properties of the object in the patch file will be used to set the properties of the respective object type in the game's assembly. + +Each object type can be configured as follows: +* `AGRoomRuntimeConfig.Room` or `AGRoomRuntimeConfig.RoomScene` or `AGRoomRuntimeConfig.Location` + - Set the key to the replicationId of the object you want to change + * Any object with a replicationId can be changed + - Objects in the list have properties "Key" and "Value" + * The key represents the name of the property to modify + * The value will replace the current value in the assembly + - Example: + ```json + {"76d98498-60a1-430c-ab76-b54a29b7a163": [ + { + "Key": "ReleaseStatus", + "Value": 0 + } + ]} + ``` + will prevent you from loading into the dorm, because you can't go to editor-only rooms. + + Although you can set rooms with ReleaseStatus:0 to 2 if you'd like to go to those rooms. +* `GameConfigurationAsset` + - Set the key to the name of the object you want to change + * Ex. "Crescendo Of The Blood Moon" (CrescendoOfTheBloodMoon_Config, 20200306) + - Value must match a certain schema (example): + ```json + { + "Name": "Crescendo Of The Blood Moon", + "TeamConfigurations": [ + { + "MaxTeamSize": 1 + }, + { + "MaxTeamSize": 2 + }, + { + "MaxTeamSize": 3 + } + ], + "TeamSelectionMethod": 0, + "ManualGameStartRequirement": { + "MinPlayerCount": 1, + "MinTeamCount": 1, + "MinTeamSize": 1 + }, + "AutomaticGameStartSupported": false, + "AutomaticGameStartRequirement": { + "MinPlayerCount": -1, + "MinTeamCount": -1, + "MinTeamSize": -1 + }, + "GameStartDelay": 10.0, + "EndGameResultsDuration": 15.0 + } + ``` + * You must set every property. + * Only `MaxTeamSize` is supported for every team in `TeamConfigurations`. + * See `TeamSelectionMethod` enum members for more information. + * Changes to arrays (lists) are made in order. To change the value of a team size in the middle of an array, you must duplicate the previous data. + - Example: + ```json + { "Crescendo Of The Blood Moon": { + "AutomaticGameStartSupported": true, + "TeamConfigurations": [ + { + "MaxTeamSize": 18 + } + ] + } } + ``` \ No newline at end of file diff --git a/undead-universal-patch-il2cpp.csproj b/undead-universal-patch-il2cpp.csproj index bd2a222..7ff94b3 100644 --- a/undead-universal-patch-il2cpp.csproj +++ b/undead-universal-patch-il2cpp.csproj @@ -4,7 +4,7 @@ net6.0 undead_universal_patch_il2cpp Non-EAC, IL2CPP build patcher for Rec Room (Late 2018*-*April-2020) - 1.3.0 + 1.4.0 true latest @@ -38,9 +38,8 @@ AssemblyReferences\UnityEngine.CoreModule.dll - - - - + + AssemblyReferences\Google.Protobuf.dll +