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/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; } 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; } 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/Patches/Preloader/MonoBehaviourPatch.cs b/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs index 4a55c2cc..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,57 +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; + + // 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 foundMethod = - type.Methods.FirstOrDefault(m => m.Name == eventMethodPair.Key && !m.HasParameters); + var eventMethodArgs = eventMethodPair.Value; - if (foundMethod == null) + for (var i = 0; i < eventMethodArgs.Length; i++) { - for (var i = 0; i < eventMethodPair.Value.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)); - - if (foundMethod != null) break; - } + 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) 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 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