From e549d76a937a446150ada5ef33a1b88c97723bac Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Mon, 10 Jul 2023 04:33:47 +0100 Subject: [PATCH 1/5] stop coroutines on game restart --- CHANGELOG.md | 1 + .../GameRestart/CoroutinesStopOnRestart.cs | 30 +++++++++++++ .../CoroutineRunningObjectsTrackerPatch.cs | 43 +++++++++++++++++++ .../ICoroutineRunningObjectsTracker.cs | 8 ++++ 4 files changed, 82 insertions(+) create mode 100644 UniTAS/Patcher/Implementations/GameRestart/CoroutinesStopOnRestart.cs create mode 100644 UniTAS/Patcher/Patches/Harmony/CoroutineRunningObjectsTrackerPatch.cs create mode 100644 UniTAS/Patcher/Services/Trackers/ICoroutineRunningObjectsTracker.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 175dffd6..eb080729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed some games using switch statement skips over UniTAS tracking the end of static constructors - Fixed not using the right ILCode for returning a default value from a method - Fixed duplicate AssetBundle loading causing an exception +- Fixed coroutines causing exception on restart ## Changed diff --git a/UniTAS/Patcher/Implementations/GameRestart/CoroutinesStopOnRestart.cs b/UniTAS/Patcher/Implementations/GameRestart/CoroutinesStopOnRestart.cs new file mode 100644 index 00000000..3ccea4d4 --- /dev/null +++ b/UniTAS/Patcher/Implementations/GameRestart/CoroutinesStopOnRestart.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using UniTAS.Patcher.Interfaces.DependencyInjection; +using UniTAS.Patcher.Interfaces.Events.SoftRestart; +using UniTAS.Patcher.Services.Trackers; +using UnityEngine; + +namespace UniTAS.Patcher.Implementations.GameRestart; + +[Singleton] +public class CoroutinesStopOnRestart : ICoroutineRunningObjectsTracker, IOnPreGameRestart +{ + private readonly List _instances = new(); + + public void NewCoroutine(MonoBehaviour instance) + { + if (!_instances.Contains(instance) && instance != null) + { + _instances.Add(instance); + } + } + + public void OnPreGameRestart() + { + foreach (var coroutine in _instances) + { + if (coroutine == null) continue; + coroutine.StopAllCoroutines(); + } + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Patches/Harmony/CoroutineRunningObjectsTrackerPatch.cs b/UniTAS/Patcher/Patches/Harmony/CoroutineRunningObjectsTrackerPatch.cs new file mode 100644 index 00000000..33624af5 --- /dev/null +++ b/UniTAS/Patcher/Patches/Harmony/CoroutineRunningObjectsTrackerPatch.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using UniTAS.Patcher.Interfaces.Patches.PatchTypes; +using UniTAS.Patcher.Services.Trackers; +using UniTAS.Patcher.Utils; +using UnityEngine; + +namespace UniTAS.Patcher.Patches.Harmony; + +[RawPatch] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +[SuppressMessage("ReSharper", "UnusedMember.Local")] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class CoroutineRunningObjectsTrackerPatch +{ + private static readonly ICoroutineRunningObjectsTracker Tracker = + ContainerStarter.Kernel.GetInstance(); + + [HarmonyPatch] + private class RunCoroutine + { + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static IEnumerable TargetMethods() + { + // just patch them all, duplicate instances will be detected anyway + return AccessTools.GetDeclaredMethods(typeof(MonoBehaviour)) + .Where(x => !x.IsStatic && x.Name == "StartCoroutine").Select(x => (MethodBase)x); + } + + private static void Prefix(MonoBehaviour __instance) + { + Tracker.NewCoroutine(__instance); + } + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/Trackers/ICoroutineRunningObjectsTracker.cs b/UniTAS/Patcher/Services/Trackers/ICoroutineRunningObjectsTracker.cs new file mode 100644 index 00000000..33763a53 --- /dev/null +++ b/UniTAS/Patcher/Services/Trackers/ICoroutineRunningObjectsTracker.cs @@ -0,0 +1,8 @@ +using UnityEngine; + +namespace UniTAS.Patcher.Services.Trackers; + +public interface ICoroutineRunningObjectsTracker +{ + void NewCoroutine(MonoBehaviour instance); +} \ No newline at end of file From 7485c7e9d3d18c676caed1b8facd9e6b5fe3a3c5 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Mon, 10 Jul 2023 04:35:56 +0100 Subject: [PATCH 2/5] swap error log with warnings --- UniTAS/Patcher/Interfaces/TASRenderer/GameRender.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UniTAS/Patcher/Interfaces/TASRenderer/GameRender.cs b/UniTAS/Patcher/Interfaces/TASRenderer/GameRender.cs index bc5186b4..ed10e502 100644 --- a/UniTAS/Patcher/Interfaces/TASRenderer/GameRender.cs +++ b/UniTAS/Patcher/Interfaces/TASRenderer/GameRender.cs @@ -67,7 +67,7 @@ public GameRender(ILogger logger, IEnumerable videoRenderers, if (_videoRenderer == null) { - _logger.LogError("No video renderer available"); + _logger.LogWarning("No video renderer available"); return; } @@ -75,7 +75,7 @@ public GameRender(ILogger logger, IEnumerable videoRenderers, if (audioRenderer == null) { - _logger.LogError("No audio renderer available"); + _logger.LogWarning("No audio renderer available"); return; } @@ -83,7 +83,7 @@ public GameRender(ILogger logger, IEnumerable videoRenderers, if (!ffmpegProcessFactory.Available) { - _logger.LogError("ffmpeg not available"); + _logger.LogWarning("ffmpeg not available"); return; } From a5d5289d8fcbaa43d532e4bf6f703ad3a0331aef Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Mon, 10 Jul 2023 04:40:11 +0100 Subject: [PATCH 3/5] swap error with warn --- UniTAS/Patcher/Implementations/TASRenderer/GameVideoRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UniTAS/Patcher/Implementations/TASRenderer/GameVideoRenderer.cs b/UniTAS/Patcher/Implementations/TASRenderer/GameVideoRenderer.cs index da3aef69..74c137b7 100644 --- a/UniTAS/Patcher/Implementations/TASRenderer/GameVideoRenderer.cs +++ b/UniTAS/Patcher/Implementations/TASRenderer/GameVideoRenderer.cs @@ -41,7 +41,7 @@ public GameVideoRenderer(ILogger logger, IFfmpegProcessFactory ffmpegProcessFact if (!ffmpegProcessFactory.Available) { - _logger.LogError("ffmpeg not available"); + _logger.LogWarning("ffmpeg not available"); Available = false; return; } From 17922fbce4933b757d5b376ea2f1a667a2589c9a Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Mon, 10 Jul 2023 05:39:58 +0100 Subject: [PATCH 4/5] cleaned up trash code --- .../Patches/Preloader/MonoBehaviourPatch.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs b/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs index 4a55c2cc..4b9ccc15 100644 --- a/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs +++ b/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs @@ -204,17 +204,25 @@ public override void Patch(ref AssemblyDefinition assembly) { foreach (var eventMethodPair in PauseEventMethods) { + var eventMethodName = eventMethodPair.Key; + + // try finding method with no parameters + var eventMethodsMatch = type.GetMethods().Where(x => x.Name == eventMethodName).ToList(); var foundMethod = - type.Methods.FirstOrDefault(m => m.Name == eventMethodPair.Key && !m.HasParameters); + eventMethodsMatch.FirstOrDefault(m => !m.HasParameters); + // ok try finding method with parameters one by one + // it doesn't matter if the method only has part of the parameters, it just matters it comes in the right order if (foundMethod == null) { - for (var i = 0; i < eventMethodPair.Value.Length; i++) + var eventMethodArgs = eventMethodPair.Value; + + for (var i = 0; i < eventMethodArgs.Length; i++) { - var parameterTypes = eventMethodPair.Value.Take(i + 1).ToArray(); - foundMethod = type.Methods.FirstOrDefault(m => - m.Name == eventMethodPair.Key && m.HasParameters && - m.Parameters.Select(x => x.ParameterType.FullName).SequenceEqual(parameterTypes)); + var parameterTypes = eventMethodArgs.Take(i + 1).ToArray(); + foundMethod = eventMethodsMatch.FirstOrDefault(m => + m.HasParameters && m.Parameters.Select(x => x.ParameterType.FullName) + .SequenceEqual(parameterTypes)); if (foundMethod != null) break; } From 68c716269e6fde19db23ea182e65670afe994bc9 Mon Sep 17 00:00:00 2001 From: Eddio0141 Date: Mon, 10 Jul 2023 05:49:33 +0100 Subject: [PATCH 5/5] clean up useless trash --- .../Patches/Preloader/MonoBehaviourPatch.cs | 103 +++++++----------- 1 file changed, 42 insertions(+), 61 deletions(-) diff --git a/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs b/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs index 4b9ccc15..16eb0a74 100644 --- a/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs +++ b/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs @@ -153,22 +153,6 @@ public class MonoBehaviourPatch : PreloadPatcher new("OnGUI", new string[0]) }; - private static readonly string[] ExcludeNamespaces = - { - // TODO remove this exclusion, i dont know why i added it - "TMPro", - "UnityEngine", - "Unity", - "UniTAS.Plugin", - "UniTAS.Patcher", - "BepInEx" - }; - - private static readonly string[] IncludeNamespaces = - { - "UnityEngine.AI" - }; - public override void Patch(ref AssemblyDefinition assembly) { var types = assembly.Modules.SelectMany(m => m.GetAllTypes()); @@ -200,65 +184,62 @@ public override void Patch(ref AssemblyDefinition assembly) StaticLogger.Log.LogDebug($"Patching MonoBehaviour type: {type.FullName}"); // method invoke pause - if (!ExcludeNamespaces.Any(type.Namespace.StartsWith) || IncludeNamespaces.Any(type.Namespace.StartsWith)) + foreach (var eventMethodPair in PauseEventMethods) { - foreach (var eventMethodPair in PauseEventMethods) - { - var eventMethodName = eventMethodPair.Key; + var eventMethodName = eventMethodPair.Key; - // try finding method with no parameters - var eventMethodsMatch = type.GetMethods().Where(x => x.Name == eventMethodName).ToList(); - var foundMethod = - eventMethodsMatch.FirstOrDefault(m => !m.HasParameters); + // try finding method with no parameters + var eventMethodsMatch = type.GetMethods().Where(x => x.Name == eventMethodName).ToList(); + var foundMethod = + eventMethodsMatch.FirstOrDefault(m => !m.HasParameters); - // ok try finding method with parameters one by one - // it doesn't matter if the method only has part of the parameters, it just matters it comes in the right order - if (foundMethod == null) - { - var eventMethodArgs = eventMethodPair.Value; + // ok try finding method with parameters one by one + // it doesn't matter if the method only has part of the parameters, it just matters it comes in the right order + if (foundMethod == null) + { + var eventMethodArgs = eventMethodPair.Value; - for (var i = 0; i < eventMethodArgs.Length; i++) - { - var parameterTypes = eventMethodArgs.Take(i + 1).ToArray(); - foundMethod = eventMethodsMatch.FirstOrDefault(m => - m.HasParameters && m.Parameters.Select(x => x.ParameterType.FullName) - .SequenceEqual(parameterTypes)); + for (var i = 0; i < eventMethodArgs.Length; i++) + { + var parameterTypes = eventMethodArgs.Take(i + 1).ToArray(); + foundMethod = eventMethodsMatch.FirstOrDefault(m => + m.HasParameters && m.Parameters.Select(x => x.ParameterType.FullName) + .SequenceEqual(parameterTypes)); - if (foundMethod != null) break; - } + if (foundMethod != null) break; } + } - if (foundMethod == null) continue; + if (foundMethod == null) continue; - StaticLogger.Log.LogDebug($"Patching method for pausing execution {foundMethod.FullName}"); + StaticLogger.Log.LogDebug($"Patching method for pausing execution {foundMethod.FullName}"); - var il = foundMethod.Body.GetILProcessor(); - var firstInstruction = il.Body.Instructions.First(); + var il = foundMethod.Body.GetILProcessor(); + var firstInstruction = il.Body.Instructions.First(); - // return early check - il.InsertBefore(firstInstruction, il.Create(OpCodes.Call, pauseExecutionReference)); - il.InsertBefore(firstInstruction, il.Create(OpCodes.Brfalse_S, firstInstruction)); + // return early check + il.InsertBefore(firstInstruction, il.Create(OpCodes.Call, pauseExecutionReference)); + il.InsertBefore(firstInstruction, il.Create(OpCodes.Brfalse_S, firstInstruction)); - // if the return type isn't void, we need to return a default value - if (foundMethod.ReturnType != assembly.MainModule.TypeSystem.Void) + // if the return type isn't void, we need to return a default value + if (foundMethod.ReturnType != assembly.MainModule.TypeSystem.Void) + { + // if value type, we need to return a default value + if (foundMethod.ReturnType.IsValueType) { - // if value type, we need to return a default value - if (foundMethod.ReturnType.IsValueType) - { - var local = new VariableDefinition(foundMethod.ReturnType); - il.Body.Variables.Add(local); - il.InsertBefore(firstInstruction, il.Create(OpCodes.Ldloca_S, local)); - il.InsertBefore(firstInstruction, il.Create(OpCodes.Initobj, foundMethod.ReturnType)); - il.InsertBefore(firstInstruction, il.Create(OpCodes.Ldloc_S, local)); - } - else - { - il.InsertBefore(firstInstruction, il.Create(OpCodes.Ldnull)); - } + var local = new VariableDefinition(foundMethod.ReturnType); + il.Body.Variables.Add(local); + il.InsertBefore(firstInstruction, il.Create(OpCodes.Ldloca_S, local)); + il.InsertBefore(firstInstruction, il.Create(OpCodes.Initobj, foundMethod.ReturnType)); + il.InsertBefore(firstInstruction, il.Create(OpCodes.Ldloc_S, local)); + } + else + { + il.InsertBefore(firstInstruction, il.Create(OpCodes.Ldnull)); } - - il.InsertBefore(firstInstruction, il.Create(OpCodes.Ret)); } + + il.InsertBefore(firstInstruction, il.Create(OpCodes.Ret)); } // event methods invoke