diff --git a/CHANGELOG.md b/CHANGELOG.md index ee93fefd5..590965ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed Baldi's basics itch.io throwing exceptions during TAS playback - Fixed Resonance Of The Ocean not saving some information at the start +- Fixed Cat Quest 2 from soft locking in the first cave +- Fixed Untitled Goose Game breaking when restarting level ### UniTAS @@ -21,7 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Compatibility -- **MAYBE BREAKING**: Setting async operation activation to true won't instantly load, which was the case before which is inaccurate +- **BREAKING**: Async scene load is not supposed to be instant, there is a frame of delay before loading +- **BREAKING**: Async operations in unity now follows a queue of operations, so if a scene load operation happens, rest of the operations stop until scene loads +- **BREAKING**: Setting async operation activation to true won't instantly load, which was the case before which is inaccurate - AsyncOperation's InvokeCompletionEvent is only disabled for tracked AsyncOperation instances - Implemented LoadScene forcing pending LoadSceneAsync to be all loaded - Fixed accidentally obliterating return value of LoadScene instances diff --git a/UniTAS/Patcher.Tests/Extensions/MethodDelegateTests.cs b/UniTAS/Patcher.Tests/Extensions/MethodDelegateTests.cs new file mode 100644 index 000000000..ff1ca0a8a --- /dev/null +++ b/UniTAS/Patcher.Tests/Extensions/MethodDelegateTests.cs @@ -0,0 +1,47 @@ +using System; +using UniTAS.Patcher.Extensions; + +namespace Patcher.Tests.Extensions; + +public class MethodDelegateTests +{ + private struct TestStruct + { + public int Value; + public void Increment() => Value++; + public string ReturnValue() => Value.ToString(); + } + + private delegate void TestStructRefDelegate(ref TestStruct testStruct); + + private delegate void TestStructObjectRefDelegate(ref object testStruct); + + [Fact] + public void StructRefDelegateMutate() + { + var increment = typeof(TestStruct).GetMethod(nameof(TestStruct.Increment))!; + + var incrementDel = increment.MethodDelegate(delegateArgs: + [typeof(TestStruct).MakeByRefType()]); + var instance = new TestStruct(); + incrementDel(ref instance); + Assert.Equal(1, instance.Value); + + var incrementDel2 = increment.MethodDelegate(delegateArgs: + [typeof(object).MakeByRefType()]); + var instanceWrap = (object)instance; + incrementDel2(ref instanceWrap); + Assert.Equal(2, ((TestStruct)instanceWrap).Value); + } + + [Fact] + public void StructDelegateInvoke() + { + var instance = new TestStruct(); + instance.Value++; + + var returnValue = typeof(TestStruct).GetMethod(nameof(TestStruct.ReturnValue))!; + var returnValueDel = returnValue.MethodDelegate>(); + Assert.Equal("1", returnValueDel(instance)); + } +} \ No newline at end of file diff --git a/UniTAS/Patcher.Tests/KernelUtils.cs b/UniTAS/Patcher.Tests/KernelUtils.cs index da43c7660..a5334c978 100644 --- a/UniTAS/Patcher.Tests/KernelUtils.cs +++ b/UniTAS/Patcher.Tests/KernelUtils.cs @@ -199,13 +199,18 @@ public void RemoveBackendEntry(string key) } [Register(IncludeDifferentAssembly = true)] - public class SceneManagerWrapperDummy : ISceneWrapper + public class ISceneManagerManagerWrapperDummy : ISceneManagerWrapper { public void LoadSceneAsync(string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, LocalPhysicsMode localPhysicsMode, bool mustCompleteNextFrame) { } + public void UnloadSceneAsync(string sceneName, int sceneBuildIndex, object options, bool immediate, out bool success) + { + success = false; + } + public void LoadScene(int buildIndex) { } @@ -217,8 +222,14 @@ public void LoadScene(string name) public int TotalSceneCount => 0; public int ActiveSceneIndex => 0; public string ActiveSceneName => ""; - public int SceneCount { get; set; } - public bool TrackSceneCount { get; set; } + public int LoadedSceneCountDummy { get; set; } + public bool TrackSceneCountDummy { get; set; } + public int SceneCount => 0; + + public SceneWrapper GetSceneAt(int index) + { + throw new NotImplementedException(); + } } [Register(IncludeDifferentAssembly = true)] diff --git a/UniTAS/Patcher.Tests/Patcher.Tests.csproj b/UniTAS/Patcher.Tests/Patcher.Tests.csproj index fca1285b3..5bb05cf12 100644 --- a/UniTAS/Patcher.Tests/Patcher.Tests.csproj +++ b/UniTAS/Patcher.Tests/Patcher.Tests.csproj @@ -14,7 +14,7 @@ 12 - ReleaseTest + ReleaseTest;DebugTest AnyCPU @@ -23,6 +23,10 @@ true + + false + + diff --git a/UniTAS/Patcher/ContainerBindings/GameExecutionControllers/MonoBehaviourController.cs b/UniTAS/Patcher/ContainerBindings/GameExecutionControllers/MonoBehaviourController.cs index b9997bdf2..fc0fe5238 100644 --- a/UniTAS/Patcher/ContainerBindings/GameExecutionControllers/MonoBehaviourController.cs +++ b/UniTAS/Patcher/ContainerBindings/GameExecutionControllers/MonoBehaviourController.cs @@ -1,3 +1,5 @@ +using System.Collections; +using System.Collections.Generic; using UniTAS.Patcher.Models.DependencyInjection; using UniTAS.Patcher.Services.GameExecutionControllers; using UniTAS.Patcher.Utils; @@ -25,4 +27,6 @@ public static bool PausedUpdate get => _monoBehaviourController.PausedUpdate; set => _monoBehaviourController.PausedUpdate = value; } + + public static HashSet IgnoreCoroutines => _monoBehaviourController.IgnoreCoroutines; } \ No newline at end of file diff --git a/UniTAS/Patcher/Extensions/MethodBaseExtensions.cs b/UniTAS/Patcher/Extensions/MethodBaseExtensions.cs index ec4c66b2b..7a2804401 100644 --- a/UniTAS/Patcher/Extensions/MethodBaseExtensions.cs +++ b/UniTAS/Patcher/Extensions/MethodBaseExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; @@ -97,7 +98,20 @@ public static TDelegateType MethodDelegate( parameterTypes[0] = declaringType; for (var i = 0; i < numParameters; i++) parameterTypes[i + 1] = parameters[i].ParameterType; - var delegateArgsResolved = delegateArgs ?? delegateType.GetGenericArguments(); + // special handling for Func + var delegateArgsResolved = delegateArgs; + if (delegateArgsResolved == null && delegateType.IsGenericType && delegateType.GetGenericTypeDefinition() + .SaneFullName().StartsWith("System.Func`")) + { + var args = delegateType.GetGenericArguments().ToList(); + args.RemoveAt(args.Count - 1); // return + delegateArgsResolved = args.ToArray(); + } + else if (delegateArgsResolved == null) + { + delegateArgsResolved = delegateType.GetGenericArguments(); + } + var dynMethodReturn = delegateArgsResolved.Length < parameterTypes.Length ? parameterTypes : delegateArgsResolved; @@ -109,10 +123,37 @@ public static TDelegateType MethodDelegate( // OwnerType = declaringType }; var ilGen = dmd.GetILGenerator(); - if (declaringType is { IsValueType: true } && !delegateArgsResolved[0].IsByRef) + LocalBuilder valueTypeHolder = null; + if (declaringType is { IsValueType: true } && !delegateArgsResolved[0].IsByRef && + (declaringType == delegateArgsResolved[0] || delegateArgsResolved[0].IsSubclassOf(declaringType))) ilGen.Emit(OpCodes.Ldarga_S, 0); + // if `ref object` for instance, you would want a return + else if (declaringType is { IsValueType: true } && + !(delegateArgsResolved[0].GetElementType() ?? delegateArgsResolved[0]).IsValueType) + { + if (delegateArgsResolved[0].IsByRef) + { + valueTypeHolder = ilGen.DeclareLocal(declaringType); + + // unbox and store to temp + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Ldind_Ref); + ilGen.Emit(OpCodes.Unbox_Any, declaringType); + ilGen.Emit(OpCodes.Stloc, valueTypeHolder); + ilGen.Emit(OpCodes.Ldloca, valueTypeHolder); + } + else + { + var tempHolder = ilGen.DeclareLocal(declaringType); + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Unbox_Any, declaringType); + ilGen.Emit(OpCodes.Stloc, tempHolder); + ilGen.Emit(OpCodes.Ldloca, tempHolder); + } + } else ilGen.Emit(OpCodes.Ldarg_0); + for (var i = 1; i < parameterTypes.Length; i++) { ilGen.Emit(OpCodes.Ldarg, i); @@ -125,8 +166,18 @@ public static TDelegateType MethodDelegate( } ilGen.Emit(OpCodes.Call, method); + + if (valueTypeHolder != null) + { + // replace ref + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Ldloc, valueTypeHolder); + ilGen.Emit(OpCodes.Box, declaringType); + ilGen.Emit(OpCodes.Stind_Ref); + } + ilGen.Emit(OpCodes.Ret); - return (TDelegateType)dmd.Generate().CreateDelegate(delegateType); + return dmd.Generate().CreateDelegate(); } // Closed instance method delegate that virtually calls diff --git a/UniTAS/Patcher/Extensions/ObjectExtensions.cs b/UniTAS/Patcher/Extensions/ObjectExtensions.cs new file mode 100644 index 000000000..eba5c91b3 --- /dev/null +++ b/UniTAS/Patcher/Extensions/ObjectExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections; +using System.Reflection; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop.Converters; + +namespace UniTAS.Patcher.Extensions; + +public static class ObjectExtensions +{ + public static DynValue ToDynValue(this object obj, Script script) + { + if (obj == null) + return DynValue.Nil; + + DynValue v = null; + + if (obj is Type type) + v = UserData.CreateStatic(type); + + // unregistered enums go as integers + if (obj is Enum) + // ReSharper disable once PossibleInvalidCastException + return DynValue.NewNumber((int)obj); + + if (v != null) return v; + + if (obj is Delegate @delegate) + return DynValue.NewCallback(CallbackFunction.FromDelegate(script, @delegate)); + + if (obj is MethodInfo { IsStatic: true } mi) + { + return DynValue.NewCallback(CallbackFunction.FromMethodInfo(script, mi)); + } + + if (obj is IList list) + { + var converted = new DynValue[list.Count]; + for (var i = 0; i < list.Count; i++) + { + var o = list[i]; + converted[i] = o.ToDynValue(script); + } + + return DynValue.NewTable(script, converted); + } + + var enumerator = ClrToScriptConversions.EnumerationToDynValue(script, obj); + if (enumerator != null) return enumerator; + + return DynValue.FromObject(script, obj); + } + + public static IntPtr Addr(this object obj) + { + var tr = __makeref(obj); + unsafe + { +#pragma warning disable CS8500 + var ptr = *(IntPtr*)(&tr); +#pragma warning restore CS8500 + return ptr; + } + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Extensions/TypeExtensions.cs b/UniTAS/Patcher/Extensions/TypeExtensions.cs index 010e374a0..c203ff30f 100644 --- a/UniTAS/Patcher/Extensions/TypeExtensions.cs +++ b/UniTAS/Patcher/Extensions/TypeExtensions.cs @@ -6,7 +6,7 @@ public static class TypeExtensions { public static string SaneFullName(this Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type == null) return "null"; var name = type.Name; if (type.IsGenericParameter) return name; diff --git a/UniTAS/Patcher/Implementations/Coroutine/WaitForCoroutine.cs b/UniTAS/Patcher/Implementations/Coroutine/WaitForCoroutine.cs index 644d67a32..641333dd7 100644 --- a/UniTAS/Patcher/Implementations/Coroutine/WaitForCoroutine.cs +++ b/UniTAS/Patcher/Implementations/Coroutine/WaitForCoroutine.cs @@ -1,21 +1,20 @@ using System.Collections.Generic; using UniTAS.Patcher.Interfaces.Coroutine; using UniTAS.Patcher.Services; +using UniTAS.Patcher.Services.Logging; namespace UniTAS.Patcher.Implementations.Coroutine; -public class WaitForCoroutine : CoroutineWait +public class WaitForCoroutine(IEnumerable coroutineMethod) : CoroutineWait { - private readonly IEnumerable _coroutineMethod; - - public WaitForCoroutine(IEnumerable coroutineMethod) - { - _coroutineMethod = coroutineMethod; - } - protected override void HandleWait() { - var coroutineStatus = Container.GetInstance().Start(_coroutineMethod); - coroutineStatus.OnComplete += _ => RunNext(); + var coroutineStatus = Container.GetInstance().Start(coroutineMethod); + coroutineStatus.OnComplete += status => + { + if (status.Exception != null) + Container.GetInstance().LogFatal($"coroutine exception: {status.Exception}"); + RunNext(); + }; } } \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/FrameAdvancing/FrameAdvancing.cs b/UniTAS/Patcher/Implementations/FrameAdvancing/FrameAdvancing.cs index ba0fa6103..60fc5c894 100644 --- a/UniTAS/Patcher/Implementations/FrameAdvancing/FrameAdvancing.cs +++ b/UniTAS/Patcher/Implementations/FrameAdvancing/FrameAdvancing.cs @@ -205,7 +205,11 @@ private void FrameAdvanceUpdate(bool update) // do we have new frame advance to do? don't bother pausing then if (_pendingPauseFrames != 0) return; - _coroutine.Start(Pause(update)); + _coroutine.Start(Pause(update)).OnComplete += status => + { + if (status.Exception != null) + _logger.LogFatal($"exception occurs during frame advance coroutine: {status.Exception}"); + }; } private void CheckAndAddPendingFrameAdvances() @@ -297,4 +301,4 @@ private void UpdateOffsetSyncFix() _logger.LogDebug("fixed update offset"); PauseActual(); } -} +} \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/GUI/Windows/ObjectTrackerInstanceWindow.cs b/UniTAS/Patcher/Implementations/GUI/Windows/ObjectTrackerInstanceWindow.cs index b6360ef5e..152c2a2e6 100644 --- a/UniTAS/Patcher/Implementations/GUI/Windows/ObjectTrackerInstanceWindow.cs +++ b/UniTAS/Patcher/Implementations/GUI/Windows/ObjectTrackerInstanceWindow.cs @@ -25,12 +25,12 @@ public class ObjectTrackerInstanceWindow : Window private readonly IConfig _config; private readonly IToolBar _toolBar; - private readonly ISceneWrapper _sceneWrapper; + private readonly ISceneManagerWrapper _iSceneManagerWrapper; private readonly IWindowFactory _windowFactory; public ObjectTrackerInstanceWindow(WindowDependencies windowDependencies, UnityObjectIdentifier identifier, - IOnSceneLoadEvent onSceneLoadEvent, ISceneWrapper sceneWrapper, IWindowFactory windowFactory) : base( + IOnSceneLoadEvent onSceneLoadEvent, ISceneManagerWrapper iSceneManagerWrapper, IWindowFactory windowFactory) : base( windowDependencies, new WindowConfig(defaultWindowRect: GUIUtils.WindowRect(200, 200), showByDefault: true, removeConfigOnClose: true), @@ -39,7 +39,7 @@ public ObjectTrackerInstanceWindow(WindowDependencies windowDependencies, _config = windowDependencies.Config; _toolBar = windowDependencies.ToolBar; _unityObjectIdentifier = identifier; - _sceneWrapper = sceneWrapper; + _iSceneManagerWrapper = iSceneManagerWrapper; _windowFactory = windowFactory; onSceneLoadEvent.OnSceneLoadEvent += UpdateInstance; windowDependencies.UpdateEvents.OnLateUpdateActual += OnLateUpdateActual; @@ -94,7 +94,7 @@ private void UpdateInstance() if (_instance == null) { - _instance = _unityObjectIdentifier.FindObject(_trackSettings.ObjectSearch, _sceneWrapper, TrackedObjects); + _instance = _unityObjectIdentifier.FindObject(_trackSettings.ObjectSearch, _iSceneManagerWrapper, TrackedObjects); updateComponents = true; } diff --git a/UniTAS/Patcher/Implementations/GUI/Windows/TestTab.cs b/UniTAS/Patcher/Implementations/GUI/Windows/TestTab.cs index e676cd8f8..73b68928d 100644 --- a/UniTAS/Patcher/Implementations/GUI/Windows/TestTab.cs +++ b/UniTAS/Patcher/Implementations/GUI/Windows/TestTab.cs @@ -17,7 +17,7 @@ public class TestTab( WindowDependencies windowDependencies, IGameRender gameRender, IGameRestart gameRestart, - ISceneWrapper sceneWrapper, + ISceneManagerWrapper iSceneManagerWrapper, IMonoBehaviourController monoBehaviourController, IRuntimeTestAndLog runtimeTestAndLog) : Window(windowDependencies, @@ -48,7 +48,7 @@ protected override void OnGUI() if (GUILayout.Button("Scene 0", GUIUtils.EmptyOptions)) { - sceneWrapper.LoadScene(0); + iSceneManagerWrapper.LoadScene(0); } if (GUILayout.Button("Toggle MonoBehaviour pause", GUIUtils.EmptyOptions)) diff --git a/UniTAS/Patcher/Implementations/GameRestart/CoroutinesStopOnRestart.cs b/UniTAS/Patcher/Implementations/GameRestart/CoroutinesStopOnRestart.cs deleted file mode 100644 index 1d3279f06..000000000 --- a/UniTAS/Patcher/Implementations/GameRestart/CoroutinesStopOnRestart.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using UniTAS.Patcher.Interfaces.DependencyInjection; -using UniTAS.Patcher.Interfaces.Events.SoftRestart; -using UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; -using UnityEngine; - -namespace UniTAS.Patcher.Implementations.GameRestart; - -[Singleton] -public class CoroutinesStopOnRestart : ICoroutineRunningObjectsTracker, IOnPreGameRestart -{ - private readonly HashSet _instances = []; - - public void NewCoroutine(MonoBehaviour instance) - { - if (instance == null) return; - _instances.Add(instance); - } - - public void OnPreGameRestart() - { - foreach (var coroutine in _instances) - { - if (coroutine == null) continue; - coroutine.StopAllCoroutines(); - } - - _instances.Clear(); - } -} \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/GameRestart/GameRestart.cs b/UniTAS/Patcher/Implementations/GameRestart/GameRestart.cs index 6cf3b580c..75824caaf 100644 --- a/UniTAS/Patcher/Implementations/GameRestart/GameRestart.cs +++ b/UniTAS/Patcher/Implementations/GameRestart/GameRestart.cs @@ -30,7 +30,7 @@ public class GameRestart : IGameRestart, IOnAwakeUnconditional, IOnEnableUncondi private DateTime _softRestartTime; private readonly ISyncFixedUpdateCycle _syncFixedUpdate; - private readonly ISceneWrapper _sceneWrapper; + private readonly ISceneManagerWrapper _iSceneManagerWrapper; private readonly IMonoBehaviourController _monoBehaviourController; private readonly ILogger _logger; private readonly IFinalizeSuppressor _finalizeSuppressor; @@ -46,14 +46,14 @@ public class GameRestart : IGameRestart, IOnAwakeUnconditional, IOnEnableUncondi private bool _pendingResumePausedExecution; [SuppressMessage("ReSharper", "MemberCanBeProtected.Global")] - public GameRestart(ISyncFixedUpdateCycle syncFixedUpdate, ISceneWrapper sceneWrapper, + public GameRestart(ISyncFixedUpdateCycle syncFixedUpdate, ISceneManagerWrapper iSceneManagerWrapper, IMonoBehaviourController monoBehaviourController, ILogger logger, IOnGameRestart[] onGameRestart, IOnGameRestartResume[] onGameRestartResume, IOnPreGameRestart[] onPreGameRestart, IStaticFieldManipulator staticFieldManipulator, ITimeEnv timeEnv, IFinalizeSuppressor finalizeSuppressor, IUpdateInvokeOffset updateInvokeOffset, IObjectTracker objectTracker, ICoroutine coroutine, IGameInfo gameInfo) { _syncFixedUpdate = syncFixedUpdate; - _sceneWrapper = sceneWrapper; + _iSceneManagerWrapper = iSceneManagerWrapper; _monoBehaviourController = monoBehaviourController; _logger = logger; _staticFieldManipulator = staticFieldManipulator; @@ -106,7 +106,11 @@ public void SoftRestart(DateTime time) { if (_pendingRestart && !_pendingResumePausedExecution) return; - _coroutine.Start(SoftRestartCoroutine(time)); + _coroutine.Start(SoftRestartCoroutine(time)).OnComplete += status => + { + if (status.Exception != null) + _logger.LogFatal($"failed to soft restart: {status.Exception}"); + }; } private IEnumerable SoftRestartCoroutine(DateTime time) @@ -166,7 +170,7 @@ private void SoftRestartOperation() _logger.LogInfo("Soft restarting"); OnGameRestart?.Invoke(_softRestartTime, true); - _sceneWrapper.LoadScene(0); + _iSceneManagerWrapper.LoadScene(0); OnGameRestart?.Invoke(_softRestartTime, false); _pendingRestart = false; diff --git a/UniTAS/Patcher/Implementations/MonoBehaviourController.cs b/UniTAS/Patcher/Implementations/MonoBehaviourController.cs index bde866c56..3540121d2 100644 --- a/UniTAS/Patcher/Implementations/MonoBehaviourController.cs +++ b/UniTAS/Patcher/Implementations/MonoBehaviourController.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using UniTAS.Patcher.Interfaces.DependencyInjection; using UniTAS.Patcher.Interfaces.Events.SoftRestart; using UniTAS.Patcher.Models.DependencyInjection; @@ -11,9 +13,10 @@ namespace UniTAS.Patcher.Implementations; public class MonoBehaviourController : IMonoBehaviourController, IOnGameRestartResume { public bool PausedExecution { get; set; } - public bool PausedUpdate { get; set; } + public HashSet IgnoreCoroutines { get; } = new(); + public void OnGameRestartResume(DateTime startupTime, bool preMonoBehaviourResume) { // make sure we reset the paused update state on restart diff --git a/UniTAS/Patcher/Implementations/Movie/MovieRunner.cs b/UniTAS/Patcher/Implementations/Movie/MovieRunner.cs index 4f1d97fce..e89d12b5d 100644 --- a/UniTAS/Patcher/Implementations/Movie/MovieRunner.cs +++ b/UniTAS/Patcher/Implementations/Movie/MovieRunner.cs @@ -140,7 +140,11 @@ public void InputUpdateActual(bool fixedUpdate, bool newInputSystemUpdate) { _timeEnv.FrameTime = 0; MovieRunningStatusChange(false); - _coroutine.Start(FinishMovieCleanup()); + _coroutine.Start(FinishMovieCleanup()).OnComplete += status => + { + if (status.Exception != null) + _logger.LogFatal($"exception occurs during movie runner coroutine: {status.Exception}"); + }; MovieLogger.LogInfo("movie end"); } } diff --git a/UniTAS/Patcher/Implementations/PatchReverseInvoker.cs b/UniTAS/Patcher/Implementations/PatchReverseInvoker.cs index c3de0d091..d110366b1 100644 --- a/UniTAS/Patcher/Implementations/PatchReverseInvoker.cs +++ b/UniTAS/Patcher/Implementations/PatchReverseInvoker.cs @@ -1,16 +1,33 @@ using System; +using System.Diagnostics; using System.Threading; using UniTAS.Patcher.Interfaces.DependencyInjection; using UniTAS.Patcher.Services; +using UniTAS.Patcher.Utils; namespace UniTAS.Patcher.Implementations; // ReSharper disable once ClassNeverInstantiated.Global [Singleton] +// TODO: probably make this into a manual service public class PatchReverseInvoker : IPatchReverseInvoker { private readonly ThreadLocal _invoking = new(() => false); - public bool Invoking => _invoking.Value; + + public bool Invoking + { + get => _invoking.Value; + set + { + if (_invoking.Value == value) + { + StaticLogger.LogWarning($"reverse patcher is already in use for this thread at {new StackTrace(true)}"); + return; + } + + _invoking.Value = value; + } + } public void Invoke(Action method) { @@ -26,6 +43,13 @@ public void Invoke(Action method, T1 arg1) _invoking.Value = false; } + public void Invoke(Action method, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + _invoking.Value = true; + method(arg1, arg2, arg3, arg4); + _invoking.Value = false; + } + public void Invoke(IPatchReverseInvoker.Action5 method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { diff --git a/UniTAS/Patcher/Implementations/PreloadPatcherProcessor.cs b/UniTAS/Patcher/Implementations/PreloadPatcherProcessor.cs index 7006b10ef..dbfaaf0c5 100644 --- a/UniTAS/Patcher/Implementations/PreloadPatcherProcessor.cs +++ b/UniTAS/Patcher/Implementations/PreloadPatcherProcessor.cs @@ -13,6 +13,7 @@ public class PreloadPatcherProcessor new FinalizeSuppressionPatch(), new SteamAPIPatch(), new SerializationCallbackPatch(), + new CoroutinePatch(), new FunctionCallTrace() // this must run last, it hooks logs on start and ret ]; } \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/Proxies/TraverseProxy.cs b/UniTAS/Patcher/Implementations/Proxies/TraverseProxy.cs index ae5f31add..5bf2ad463 100644 --- a/UniTAS/Patcher/Implementations/Proxies/TraverseProxy.cs +++ b/UniTAS/Patcher/Implementations/Proxies/TraverseProxy.cs @@ -4,6 +4,7 @@ using System.Reflection; using HarmonyLib; using MoonSharp.Interpreter; +using UniTAS.Patcher.Extensions; namespace UniTAS.Patcher.Implementations.Proxies; @@ -17,9 +18,10 @@ public class TraverseProxy(Traverse traverse) private static readonly AccessTools.FieldRef TraverseInfoField = AccessTools.FieldRefAccess(typeof(Traverse), "_info"); - public object GetValue(params object[] args) + public DynValue GetValue(Script script, params object[] args) { - return args.Length == 0 ? traverse.GetValue() : traverse.GetValue(args); + var ret = args.Length == 0 ? traverse.GetValue() : traverse.GetValue(args); + return ret.ToDynValue(script); } public Traverse SetValue(DynValue value) diff --git a/UniTAS/Patcher/Implementations/StaticFieldStorage.cs b/UniTAS/Patcher/Implementations/StaticFieldStorage.cs index 30e5e4158..0302dd077 100644 --- a/UniTAS/Patcher/Implementations/StaticFieldStorage.cs +++ b/UniTAS/Patcher/Implementations/StaticFieldStorage.cs @@ -22,11 +22,16 @@ public void ResetStaticFields() { logger.LogDebug("resetting static fields"); - UnityEngine.Resources.UnloadUnusedAssets(); + // UnityEngine.Resources.UnloadUnusedAssets(); var bench = Bench.Measure(); - foreach (var field in classStaticInfoTracker.StaticFields) + + var fieldsCount = classStaticInfoTracker.StaticFields.Count; + var ctorInvokeCount = classStaticInfoTracker.StaticCtorInvokeOrder.Count; + + for (var i = 0; i < fieldsCount; i++) { + var field = classStaticInfoTracker.StaticFields[i]; var typeName = field.DeclaringType?.FullName ?? "unknown_type"; logger.LogDebug($"resetting static field: {typeName}.{field.Name}"); @@ -49,11 +54,10 @@ public void ResetStaticFields() GC.Collect(); GC.WaitForPendingFinalizers(); - var count = classStaticInfoTracker.StaticCtorInvokeOrder.Count; - logger.LogDebug($"calling {count} static constructors"); + logger.LogDebug($"calling {ctorInvokeCount} static constructors"); bench = Bench.Measure(); - for (var i = 0; i < count; i++) + for (var i = 0; i < ctorInvokeCount; i++) { var staticCtorType = classStaticInfoTracker.StaticCtorInvokeOrder[i]; var cctor = staticCtorType.TypeInitializer; diff --git a/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.InputSystemEvents.cs b/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.InputSystemEvents.cs index 8e4ee5d56..ed4fa2ac3 100644 --- a/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.InputSystemEvents.cs +++ b/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.InputSystemEvents.cs @@ -6,7 +6,6 @@ using UnityEngine.InputSystem; #if TRACE using UniTAS.Patcher.Utils; -using UniTAS.Patcher.Services; using UnityEngine; #endif @@ -106,55 +105,55 @@ public void InputSystemChangeUpdate(InputSettings.UpdateMode updateMode) switch (updateMode) { case InputSettings.UpdateMode.ProcessEventsInDynamicUpdate: + { + var registerOnBeforeUpdate = !AlreadyRegisteredOnEvent; + + if (!_usingMonoBehFixedUpdate) { - var registerOnBeforeUpdate = !AlreadyRegisteredOnEvent; - - if (!_usingMonoBehFixedUpdate) - { - _inputFixedUpdate = () => InputUpdate(true, false); - AddEventToFixedUpdateUnconditional(); - _usingMonoBehFixedUpdate = true; - } - - if (_usingMonoBehUpdate) - { - OnUpdateUnconditional -= _inputUpdate; - _usingMonoBehUpdate = false; - } - - if (registerOnBeforeUpdate) - { - _inputUpdate = () => InputUpdate(false, true); - InputSystem.onBeforeUpdate += _inputUpdate; - } - - break; + _inputFixedUpdate = () => InputUpdate(true, false); + AddEventToFixedUpdateUnconditional(); + _usingMonoBehFixedUpdate = true; } + + if (_usingMonoBehUpdate) + { + OnUpdateUnconditional -= _inputUpdate; + _usingMonoBehUpdate = false; + } + + if (registerOnBeforeUpdate) + { + _inputUpdate = () => InputUpdate(false, true); + InputSystem.onBeforeUpdate += _inputUpdate; + } + + break; + } case InputSettings.UpdateMode.ProcessEventsInFixedUpdate: + { + var registerOnBeforeUpdate = !AlreadyRegisteredOnEvent; + + if (!_usingMonoBehUpdate) { - var registerOnBeforeUpdate = !AlreadyRegisteredOnEvent; - - if (!_usingMonoBehUpdate) - { - _inputUpdate = () => InputUpdate(false, false); - AddEventToUpdateUnconditional(); - _usingMonoBehUpdate = true; - } - - if (_usingMonoBehFixedUpdate) - { - OnFixedUpdateUnconditional -= _inputFixedUpdate; - _usingMonoBehFixedUpdate = false; - } - - if (registerOnBeforeUpdate) - { - _inputFixedUpdate = () => InputUpdate(true, true); - InputSystem.onBeforeUpdate += _inputFixedUpdate; - } - - break; + _inputUpdate = () => InputUpdate(false, false); + AddEventToUpdateUnconditional(); + _usingMonoBehUpdate = true; } + + if (_usingMonoBehFixedUpdate) + { + OnFixedUpdateUnconditional -= _inputFixedUpdate; + _usingMonoBehFixedUpdate = false; + } + + if (registerOnBeforeUpdate) + { + _inputFixedUpdate = () => InputUpdate(true, true); + InputSystem.onBeforeUpdate += _inputFixedUpdate; + } + + break; + } case InputSettings.UpdateMode.ProcessEventsManually: if (AlreadyRegisteredOnEvent) { @@ -188,9 +187,8 @@ public void InputSystemChangeUpdate(InputSettings.UpdateMode updateMode) private void InputUpdate(bool fixedUpdate, bool newInputSystemUpdate) { #if TRACE - _patchReverseInvoker ??= ContainerStarter.Kernel.GetInstance(); StaticLogger.Trace( - $"InputUpdate, time: {_patchReverseInvoker.Invoke(() => Time.time)} fixed update: {fixedUpdate}, new input system update: {newInputSystemUpdate}"); + $"InputUpdate, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)} fixed update: {fixedUpdate}, new input system update: {newInputSystemUpdate}"); #endif for (var i = 0; i < _inputUpdatesUnconditional.Count; i++) @@ -205,4 +203,4 @@ private void InputUpdate(bool fixedUpdate, bool newInputSystemUpdate) _inputUpdatesActual[i](fixedUpdate, newInputSystemUpdate); } } -} +} \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.cs b/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.cs index 9cad1e823..42e562fd4 100644 --- a/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.cs +++ b/UniTAS/Patcher/Implementations/UnityEvents/UnityEvents.cs @@ -11,9 +11,9 @@ using UniTAS.Patcher.Services.GameExecutionControllers; using UniTAS.Patcher.Services.InputSystemOverride; using UniTAS.Patcher.Services.UnityEvents; +using UnityEngine; #if TRACE using UniTAS.Patcher.Utils; -using UnityEngine; #endif namespace UniTAS.Patcher.Implementations.UnityEvents; @@ -27,7 +27,10 @@ namespace UniTAS.Patcher.Implementations.UnityEvents; [Singleton(timing: RegisterTiming.Entry)] public partial class UnityEvents : IUpdateEvents, IMonoBehEventInvoker, IInputEventInvoker { + private readonly IPatchReverseInvoker _patchReverseInvoker; + public UnityEvents(IEnumerable onAwakesUnconditional, + IEnumerable onAwakeActual, IEnumerable onStartsUnconditional, IEnumerable onEnablesUnconditional, IEnumerable onPreUpdatesUnconditional, @@ -42,16 +45,14 @@ public UnityEvents(IEnumerable onAwakesUnconditional, IEnumerable onInputUpdatesUnconditional, IEnumerable onLateUpdatesUnconditional, IEnumerable onLastUpdatesUnconditional, - IEnumerable onLastUpdatesActual, IGameRestart gameRestart, - IInputSystemState newInputSystemExists, IMonoBehaviourController monoBehaviourController -#if TRACE - , IPatchReverseInvoker patchReverseInvoker -#endif + IEnumerable onLastUpdatesActual, + IEnumerable onEndOfFrameActual, + IGameRestart gameRestart, + IInputSystemState newInputSystemExists, IMonoBehaviourController monoBehaviourController, + IPatchReverseInvoker patchReverseInvoker ) { -#if TRACE _patchReverseInvoker = patchReverseInvoker; -#endif _newInputSystemExists = newInputSystemExists; _monoBehaviourController = monoBehaviourController; @@ -69,6 +70,11 @@ public UnityEvents(IEnumerable onAwakesUnconditional, RegisterMethod(onAwake, onAwake.AwakeUnconditional, CallbackUpdate.AwakeUnconditional); } + foreach (var onAwake in onAwakeActual) + { + RegisterMethod(onAwake, onAwake.AwakeActual, CallbackUpdate.AwakeActual); + } + foreach (var onStart in onStartsUnconditional) { RegisterMethod(onStart, onStart.StartUnconditional, CallbackUpdate.StartUnconditional); @@ -138,6 +144,11 @@ public UnityEvents(IEnumerable onAwakesUnconditional, RegisterMethod(onLastUpdateActual, onLastUpdateActual.OnLastUpdateActual, CallbackUpdate.LastUpdateActual); } + foreach (var endOfFrameActual in onEndOfFrameActual) + { + RegisterMethod(endOfFrameActual, endOfFrameActual.OnEndOfFrame, CallbackUpdate.EndOfFrameActual); + } + // input system events init foreach (var onInputUpdateActual in onInputUpdatesActual) { @@ -176,6 +187,7 @@ private void RegisterMethod(object processingCallback, Action callback, Callback CallbackUpdate.LastUpdateUnconditional => _lastUpdatesUnconditional, CallbackUpdate.LateUpdateUnconditional => _lateUpdatesUnconditional, CallbackUpdate.LateUpdateActual => _lateUpdatesActual, + CallbackUpdate.EndOfFrameActual => _endOfFramesActual, _ => throw new ArgumentOutOfRangeException(nameof(update), update, null) }; @@ -379,14 +391,20 @@ public void AddPriorityCallback(CallbackUpdate callbackUpdate, Action callback, private readonly PriorityList _lastUpdatesUnconditional = new(); private readonly PriorityList _lastUpdatesActual = new(); + private readonly PriorityList _endOfFramesActual = new(); + private bool _updated; - private bool _calledFixedUpdate; private bool _calledPreUpdate; private readonly IMonoBehaviourController _monoBehaviourController; public void InvokeLastUpdate() { +#if TRACE + StaticLogger.Trace($"InvokeLastUpdate, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution}"); +#endif + for (var i = 0; i < _lastUpdatesUnconditional.Count; i++) { _lastUpdatesUnconditional[i](); @@ -403,6 +421,11 @@ public void InvokeLastUpdate() // calls awake before any other script public void InvokeAwake() { +#if TRACE + StaticLogger.Trace($"InvokeAwake, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution}"); +#endif + for (var i = 0; i < _awakesUnconditional.Count; i++) { _awakesUnconditional[i](); @@ -419,6 +442,11 @@ public void InvokeAwake() // calls onEnable before any other script public void InvokeOnEnable() { +#if TRACE + StaticLogger.Trace($"InvokeOnEnable, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution}"); +#endif + for (var i = 0; i < _enablesUnconditional.Count; i++) { _enablesUnconditional[i](); @@ -435,6 +463,11 @@ public void InvokeOnEnable() // calls start before any other script public void InvokeStart() { +#if TRACE + StaticLogger.Trace($"InvokeStart, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution}"); +#endif + for (var i = 0; i < _startsUnconditional.Count; i++) { _startsUnconditional[i](); @@ -448,17 +481,14 @@ public void InvokeStart() } } -#if TRACE - private IPatchReverseInvoker _patchReverseInvoker; -#endif - public void InvokeUpdate() { if (_updated) return; _updated = true; #if TRACE - StaticLogger.Trace($"InvokeUpdate, time: {_patchReverseInvoker.Invoke(() => Time.time)}"); + StaticLogger.Trace($"InvokeUpdate, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution || _monoBehaviourController.PausedUpdate}"); #endif if (!_calledPreUpdate) @@ -467,6 +497,8 @@ public void InvokeUpdate() InvokeCallOnPreUpdate(); } + _endOfFrameUpdated = false; + for (var i = 0; i < _updatesUnconditional.Count; i++) { _updatesUnconditional[i](); @@ -484,6 +516,11 @@ public void InvokeUpdate() // right now I don't call this update before other scripts so I don't need to check if it was already called public void InvokeLateUpdate() { +#if TRACE + StaticLogger.Trace($"InvokeLateUpdate, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution}"); +#endif + _updated = false; _calledPreUpdate = false; @@ -501,19 +538,20 @@ public void InvokeLateUpdate() } } - // isn't called at the very first yield WaitForFixedUpdate, but this is enough - public void CoroutineFixedUpdate() - { - _calledFixedUpdate = false; - } + private float _prevFixedTime = -1; public void InvokeFixedUpdate() { - if (_calledFixedUpdate) return; - _calledFixedUpdate = true; +#if !UNIT_TESTS + var fixedTime = _patchReverseInvoker.Invoke(() => Time.fixedTime); + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (_prevFixedTime == fixedTime) return; + _prevFixedTime = fixedTime; +#endif #if TRACE - StaticLogger.Trace($"InvokeFixedUpdate, time: {_patchReverseInvoker.Invoke(() => Time.time)}"); + StaticLogger.Trace( + $"InvokeFixedUpdate, time: {fixedTime}, paused: {_monoBehaviourController.PausedExecution}"); #endif InvokeCallOnPreUpdate(); @@ -533,6 +571,11 @@ public void InvokeFixedUpdate() public void InvokeOnGUI() { +// #if TRACE +// StaticLogger.Trace($"InvokeOnGUI, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + +// $"paused: {_monoBehaviourController.PausedExecution}"); +// #endif + // currently, this doesn't get called before other scripts for (var i = 0; i < _guisUnconditional.Count; i++) { @@ -549,6 +592,11 @@ public void InvokeOnGUI() private void InvokeCallOnPreUpdate() { +#if TRACE + StaticLogger.Trace($"InvokeCallOnPreUpdate, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution}"); +#endif + for (var i = 0; i < _preUpdatesUnconditional.Count; i++) { _preUpdatesUnconditional[i](); @@ -561,4 +609,30 @@ private void InvokeCallOnPreUpdate() preUpdate(); } } + + private bool _endOfFrameUpdated; + + public void InvokeEndOfFrame() + { + if (_endOfFrameUpdated) return; + _endOfFrameUpdated = true; + +#if TRACE + StaticLogger.Trace($"InvokeEndOfFrame, time: {_patchReverseInvoker.Invoke(() => Time.frameCount)}, " + + $"paused: {_monoBehaviourController.PausedExecution}"); +#endif + + // for (var i = 0; i < _endOfFramesUnconditional.Count; i++) + // { + // _endOfFramesUnconditional[i](); + // } + + for (var i = 0; i < _endOfFramesActual.Count; i++) + { + var endOfFrame = _endOfFramesActual[i]; + if (_monoBehaviourController.PausedExecution || + _monoBehaviourController.PausedUpdate) continue; + endOfFrame(); + } + } } \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnityFix/ClearLoadedAssetBundlesOnRestart.cs b/UniTAS/Patcher/Implementations/UnityFix/ClearLoadedAssetBundlesOnRestart.cs deleted file mode 100644 index f1f0439ea..000000000 --- a/UniTAS/Patcher/Implementations/UnityFix/ClearLoadedAssetBundlesOnRestart.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using UniTAS.Patcher.Interfaces.DependencyInjection; -using UniTAS.Patcher.Interfaces.Events.SoftRestart; -using UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; -using UnityEngine; - -namespace UniTAS.Patcher.Implementations.UnityFix; - -[Singleton] -public class ClearLoadedAssetBundlesOnRestart : IAssetBundleTracker, IOnPreGameRestart -{ - private readonly List _trackedAssetBundles = new(); - private readonly List _trackedAssetBundleCreateRequests = new(); - - public void NewInstance(AssetBundle assetBundle) - { - _trackedAssetBundles.Add(assetBundle); - } - - public void NewInstance(AssetBundleCreateRequest assetBundleCreateRequest) - { - _trackedAssetBundleCreateRequests.Add(assetBundleCreateRequest); - } - - public void OnPreGameRestart() - { - foreach (var assetBundle in _trackedAssetBundles) - { - if (assetBundle == null) continue; - assetBundle.Unload(true); - } - - foreach (var assetBundleCreateRequest in _trackedAssetBundleCreateRequests) - { - if (assetBundleCreateRequest?.assetBundle == null) continue; - assetBundleCreateRequest.assetBundle.Unload(true); - } - - _trackedAssetBundles.Clear(); - _trackedAssetBundleCreateRequests.Clear(); - } -} \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnityInfo/GameBuildScenesInfo.cs b/UniTAS/Patcher/Implementations/UnityInfo/GameBuildScenesInfo.cs new file mode 100644 index 000000000..b763b3c28 --- /dev/null +++ b/UniTAS/Patcher/Implementations/UnityInfo/GameBuildScenesInfo.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using UniTAS.Patcher.Interfaces.DependencyInjection; +using UniTAS.Patcher.Services.UnityInfo; +#if !UNIT_TESTS +using System.IO; +using AssetsTools.NET.Extra; +using BepInEx; +using UniTAS.Patcher.Services.Logging; +#endif + +namespace UniTAS.Patcher.Implementations.UnityInfo; + +[Singleton] +public class GameBuildScenesInfo : IGameBuildScenesInfo +{ +#if !UNIT_TESTS + public GameBuildScenesInfo(IAssetsManager assetsManager, ILogger logger) + { + var globalGameManagersPath = Directory.GetParent(Paths.ManagedPath)?.FullName; + if (globalGameManagersPath == null) + { + logger.LogError("Failed to get globalgamemanagers path"); + return; + } + + globalGameManagersPath = Path.Combine(globalGameManagersPath, "globalgamemanagers"); + + if (!File.Exists(globalGameManagersPath)) + { + logger.LogInfo("Failed to find globalgamemanagers file"); + return; + } + + var manager = assetsManager.Instance; + if (manager == null) + { + logger.LogError("Failed to get assets manager instance"); + return; + } + + var assetsFileInstance = manager.LoadAssetsFile(globalGameManagersPath, true); + var file = assetsFileInstance.file; + manager.LoadClassDatabaseFromPackage(file.Metadata.UnityVersion); + + var buildSettingsAssets = file.GetAssetsOfType(AssetClassID.BuildSettings); + if (buildSettingsAssets.Count == 0) + { + logger.LogError("Failed to find build settings asset"); + return; + } + + var buildSettingsAsset = buildSettingsAssets[0]; + var buildSettings = manager.GetBaseField(assetsFileInstance, buildSettingsAsset); + + var scenes = buildSettings["scenes.Array"]; + var useFullPath = new HashSet(); + var i = 0; + foreach (var scene in scenes) + { + var path = scene.AsString; + PathToIndex[path] = i; + var name = Path.GetFileNameWithoutExtension(path); + if (useFullPath.Contains(name)) + { + name = path; + } + else if (NameToPath.TryGetValue(name, out var fixPath)) + { + // duplicate name, full path must be used + useFullPath.Add(name); + + // also fix duplicate, previous entry will also need to use full path + var fixIndex = PathToIndex[fixPath]; + + PathToIndex.Remove(fixPath); + PathToName.Remove(fixPath); + NameToPath.Remove(name); + + PathToIndex[fixPath] = fixIndex; + PathToName[fixPath] = fixPath; + NameToPath[fixPath] = fixPath; + + name = path; + } + + PathToIndex[path] = i; + PathToName[path] = name; + NameToPath[name] = path; + ShortPathToPath[path.Remove(path.Length - ".unity".Length).Remove(0, "Assets/".Length)] = path; + IndexToPath.Add(path); + + i++; + } + } +#endif + + public Dictionary PathToIndex { get; } = new(); + public Dictionary PathToName { get; } = new(); + public Dictionary NameToPath { get; } = new(); + public Dictionary ShortPathToPath { get; } = new(); + public List IndexToPath { get; } = new(); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnityInfo/LegacyInputInfo.cs b/UniTAS/Patcher/Implementations/UnityInfo/LegacyInputInfo.cs index a72294647..0a0e9c601 100644 --- a/UniTAS/Patcher/Implementations/UnityInfo/LegacyInputInfo.cs +++ b/UniTAS/Patcher/Implementations/UnityInfo/LegacyInputInfo.cs @@ -21,7 +21,7 @@ public LegacyInputInfo(IAssetsManager assetsManager, ILogger logger, var globalGameManagersPath = Directory.GetParent(Paths.ManagedPath)?.FullName; if (globalGameManagersPath == null) { - logger.LogError("Failed to get globalGameManagers path"); + logger.LogError("Failed to get globalgamemanagers path"); return; } @@ -29,7 +29,7 @@ public LegacyInputInfo(IAssetsManager assetsManager, ILogger logger, if (!File.Exists(globalGameManagersPath)) { - logger.LogInfo("Failed to find globalGameManagers file"); + logger.LogInfo("Failed to find globalgamemanagers file"); return; } diff --git a/UniTAS/Patcher/Implementations/UnityManagers/UnityCoroutineManager.cs b/UniTAS/Patcher/Implementations/UnityManagers/UnityCoroutineManager.cs new file mode 100644 index 000000000..5cfc39f8b --- /dev/null +++ b/UniTAS/Patcher/Implementations/UnityManagers/UnityCoroutineManager.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using UniTAS.Patcher.Extensions; +using UniTAS.Patcher.Interfaces.DependencyInjection; +using UniTAS.Patcher.Services; +using UniTAS.Patcher.Services.GameExecutionControllers; +using UniTAS.Patcher.Services.Logging; +using UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; +using UniTAS.Patcher.Services.UnityAsyncOperationTracker; +using UniTAS.Patcher.Services.UnityEvents; +using UniTAS.Patcher.Utils; +using UnityEngine; + +namespace UniTAS.Patcher.Implementations.UnityManagers; + +[Singleton] +public class UnityCoroutineManager : ICoroutineTracker +{ + private readonly HashSet _instances = []; + private readonly HashSet _enumeratorInstances = []; + + public void NewCoroutine(object instance, IEnumerator routine) => + NewCoroutineHandle(instance as MonoBehaviour, routine); + + public void NewCoroutine(MonoBehaviour instance, IEnumerator routine) => NewCoroutineHandle(instance, routine); + + private readonly Dictionary> _patchedCoroutinesMethods = []; + private readonly ILogger _logger; + private readonly IHarmony _harmony; + + public UnityCoroutineManager(ILogger logger, IHarmony harmony, IGameRestart gameRestart) + { + _logger = logger; + _harmony = harmony; + // do not use interface for game restart, it fucks everything up + gameRestart.OnPreGameRestart += OnPreGameRestart; + } + + public void NewCoroutine(MonoBehaviour instance, string methodName, object value) + { + // find target method + var method = AccessTools.GetDeclaredMethods(instance.GetType()) + .FirstOrDefault(x => !x.IsStatic && x.Name == methodName); + if (method == null) return; + var requiredArgs = value == null ? 0 : 1; + if (method.GetParameters().Length != requiredArgs) return; + var instanceType = instance.GetType(); + if (_patchedCoroutinesMethods.TryGetValue(instanceType, out var coroutineMethods)) + { + if (coroutineMethods.Contains(method)) return; + coroutineMethods.Add(method); + } + else + { + _patchedCoroutinesMethods[instanceType] = [method]; + } + + _harmony.Harmony.Patch(method, postfix: NewCoroutinePostfixMethod); + } + + private void NewCoroutineHandle(MonoBehaviour instance, IEnumerator routine) + { + if (instance == null || routine == null) return; + // don't track ours + var routineType = routine.GetType(); + if (Equals(routineType.Assembly, typeof(UnityCoroutineManager).Assembly)) return; + + _logger.LogDebug( + $"new coroutine made in script {instance.GetType().SaneFullName()}, got IEnumerator {routineType.SaneFullName()}"); + StaticLogger.Trace($"call from {new StackTrace()}"); + + _instances.Add(instance); + _enumeratorInstances.Add(routine); + } + + private void OnPreGameRestart() + { + foreach (var coroutine in _instances) + { + if (coroutine == null) continue; + coroutine.StopAllCoroutines(); + } + + _instances.Clear(); + _enumeratorInstances.Clear(); + // no need, but better clean it up + DoneFirstMoveNext.Clear(); + } + + private static readonly HarmonyMethod NewCoroutinePostfixMethod = + new(typeof(UnityCoroutineManager), nameof(NewCoroutinePostfix)); + + private static readonly IMonoBehaviourController MonoBehaviourController = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IAsyncOperationTracker AsyncOperationTracker = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IPatchReverseInvoker ReverseInvoker = + ContainerStarter.Kernel.GetInstance(); + + // ReSharper disable InconsistentNaming + // TODO: for both, handle time based coroutines and check the rest + + private class YieldNone : IEnumerator + { + public bool MoveNext() => false; + + public void Reset() + { + throw new NotImplementedException(); + } + + public object Current => null; + } + + private static readonly YieldNone NoYield = new(); + + private static readonly IMonoBehEventInvoker MonoBehEventInvoker = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IAsyncOperationOverride AsyncOperationOverride = + ContainerStarter.Kernel.GetInstance(); + + public void CoroutineCurrentPostfix(IEnumerator __instance, ref object __result) + { + if (!_enumeratorInstances.Contains(__instance)) return; + + StaticLogger.Trace($"coroutine get_Current: {__instance.GetType().SaneFullName()}"); + + if (__result == null) return; + if (ReverseInvoker.Invoking) return; + + // managed async operation? + if (__result is AsyncOperation op && AsyncOperationOverride.Yield(op)) + { + if (op.isDone) + { + __result = NoYield; + return; + } + + StaticLogger.Trace("paused execution for AsyncOperation Current, result is managed by unitas" + + $", and it isn't complete, replaced result with null: {new StackTrace()}"); + __result = null; + return; + } + + if (MonoBehaviourController.PausedExecution) + { + // TODO: i can probably just manually check for CoreModule / UnityEngine since its not like there's many of this + if (__result.GetType().Assembly.GetName().Name.StartsWith("UnityEngine")) + { + StaticLogger.Trace("paused execution for coroutine Current" + + $", result is unity type: {__result.GetType().SaneFullName()}" + + $", replaced result with null: {new StackTrace()}"); + __result = null; + } + + return; + } + + if (__result is WaitForEndOfFrame) + { + // MoveNext is invoked first, so code already ran, just run this here + MonoBehEventInvoker.InvokeEndOfFrame(); + + if (MonoBehaviourController.PausedUpdate) + { + StaticLogger.Trace("paused update execution for coroutine Current" + + $", result is type: {__result.GetType().SaneFullName()}" + + $", replaced result with null: {new StackTrace()}"); + __result = null; + // return; + } + } + } + + private static readonly HashSet DoneFirstMoveNext = []; + + public bool CoroutineMoveNextPrefix(IEnumerator __instance, ref bool __result) + { + if (!_enumeratorInstances.Contains(__instance)) return true; + + StaticLogger.Trace($"coroutine MoveNext: {__instance.GetType().SaneFullName()}"); + + if (!DoneFirstMoveNext.Contains(__instance)) + { + DoneFirstMoveNext.Add(__instance); + StaticLogger.Trace("first MoveNext"); + return true; + } + + var current = ReverseInvoker.Invoke(i => i.Current, __instance); + + // managed async operation? + if (current is AsyncOperation op && AsyncOperationTracker.ManagedInstance(op)) + { + var isDone = op.isDone; + StaticLogger.Trace("coroutine MoveNext with AsyncOperation, operation is managed by unitas" + + $", running MoveNext: {isDone}"); + if (!isDone) + __result = true; + return isDone; + } + + if (MonoBehaviourController.PausedExecution) + { + if (current is null) + { + StaticLogger.Trace("paused execution while coroutine MoveNext, Current is null, not running MoveNext"); + __result = true; + return false; + } + + // TODO: i can probably just manually check for CoreModule / UnityEngine since its not like there's many of this + if (current.GetType().Assembly.GetName().Name.StartsWith("UnityEngine")) + { + StaticLogger.Trace("paused execution while coroutine MoveNext" + + $", Current is type: {current.GetType().SaneFullName()}" + + " unity type, not running MoveNext"); + __result = true; + return false; + } + + return true; + } + + if (MonoBehaviourController.PausedUpdate && current is null or WaitForEndOfFrame) + { + StaticLogger.Trace("paused update execution while coroutine MoveNext" + + ", Current is null / WaitForEndOfFrame, not running MoveNext"); + __result = true; + return false; + } + + return true; + } + + private static readonly UnityCoroutineManager CoroutineManager = + ContainerStarter.Kernel.GetInstance(); + + private static void NewCoroutinePostfix(MonoBehaviour __instance, IEnumerator __result) + { + CoroutineManager.NewCoroutineHandle(__instance, __result); + } + + // ReSharper restore InconsistentNaming +} \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnityObjectIdentifierFactory.cs b/UniTAS/Patcher/Implementations/UnityObjectIdentifierFactory.cs index 172bc4830..6f29f958a 100644 --- a/UniTAS/Patcher/Implementations/UnityObjectIdentifierFactory.cs +++ b/UniTAS/Patcher/Implementations/UnityObjectIdentifierFactory.cs @@ -8,10 +8,10 @@ namespace UniTAS.Patcher.Implementations; [Singleton] [ExcludeRegisterIfTesting] -public class UnityObjectIdentifierFactory(ISceneWrapper sceneWrapper) : IUnityObjectIdentifierFactory +public class UnityObjectIdentifierFactory(ISceneManagerWrapper iSceneManagerWrapper) : IUnityObjectIdentifierFactory { public UnityObjectIdentifier NewUnityObjectIdentifier(Object o) { - return new UnityObjectIdentifier(o, this, sceneWrapper); + return new UnityObjectIdentifier(o, this, iSceneManagerWrapper); } } \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnityRuntimeInitAttributeInvoker.cs b/UniTAS/Patcher/Implementations/UnityRuntimeInitAttributeInvoker.cs index 6838d008c..fe4ee5df1 100644 --- a/UniTAS/Patcher/Implementations/UnityRuntimeInitAttributeInvoker.cs +++ b/UniTAS/Patcher/Implementations/UnityRuntimeInitAttributeInvoker.cs @@ -9,6 +9,7 @@ using UniTAS.Patcher.Services; using UniTAS.Patcher.Services.Logging; using UniTAS.Patcher.Services.UnityEvents; +using UniTAS.Patcher.Utils; namespace UniTAS.Patcher.Implementations; @@ -116,7 +117,7 @@ private void InvokeBeforeSceneLoad() foreach (var method in _beforeSceneLoad) { _logger.LogDebug($"Invoking BeforeSceneLoad method {method.DeclaringType?.Name}.{method.Name}"); - method.Invoke(null, null); + ExceptionUtils.UnityLogErrorOnThrow(m => m.Invoke(null, null), method); } } @@ -130,7 +131,7 @@ private void InvokeBeforeStart() foreach (var method in _beforeStart) { _logger.LogDebug($"Invoking BeforeStart method {method.DeclaringType?.Name}.{method.Name}"); - method.Invoke(null, null); + ExceptionUtils.UnityLogErrorOnThrow(m => m.Invoke(null, null), method); } } } \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagement/SceneWrapper.cs b/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagement/SceneWrapper.cs index 8d1d1f59b..94cd86157 100644 --- a/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagement/SceneWrapper.cs +++ b/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagement/SceneWrapper.cs @@ -1,25 +1,114 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Reflection.Emit; using HarmonyLib; +using MonoMod.Utils; +using UniTAS.Patcher.Extensions; using UniTAS.Patcher.Interfaces.UnitySafeWrappers; namespace UniTAS.Patcher.Implementations.UnitySafeWrappers.SceneManagement; [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] -public class SceneWrapper : UnityInstanceWrap +public class SceneWrapper(object instance) : UnityInstanceWrap(instance) { - private readonly Traverse _instanceTraverse; + protected override Type WrappedType { get; } = AccessTools.TypeByName("UnityEngine.SceneManagement.Scene"); - private const string BuildIndexField = "buildIndex"; - private const string NameField = "name"; + public bool IsValid => IsValidMethod(Instance); + public string Name => NameGetter(Instance); + public string Path => PathGetter(Instance); + public bool IsLoaded => IsLoadedGetter(Instance); - protected override Type WrappedType { get; } = AccessTools.TypeByName("UnityEngine.SceneManagement.Scene"); + public int BuildIndex => BuildIndexGetter(Instance); + // public bool IsDirty => IsDirtyGetter(Instance); + // public int RootCount => RootCountGetter(Instance); + + public int Handle + { + get => GetHandleField(Instance); + set + { + var i = Instance; + SetHandleField(ref i, value); + Instance = i; + } + } - public SceneWrapper(object instance) : base(instance) + // TODO: when does this exist from? + public bool IsSubScene { - _instanceTraverse = Traverse.Create(Instance); + get => IsSubSceneGetter(Instance); + set + { + var i = Instance; + IsSubSceneSetter(ref i, value); + Instance = i; + } } - public int? BuildIndex => _instanceTraverse?.Property(BuildIndexField).GetValue(); - public string Name => _instanceTraverse?.Property(NameField).GetValue(); -} + private static readonly Type SceneType = AccessTools.TypeByName("UnityEngine.SceneManagement.Scene"); + private static readonly Func NameGetter; + private static readonly Func PathGetter; + private static readonly Func BuildIndexGetter; + private static readonly Func IsValidMethod; + + private static readonly Func IsLoadedGetter; + + // private static readonly Func IsDirtyGetter; + private static readonly SetBoolDelegate IsSubSceneSetter; + + private static readonly Func IsSubSceneGetter; + + // private static readonly Func RootCountGetter; + private static readonly Func GetHandleField; + private static readonly SetIntDelegate SetHandleField; + + private delegate void SetBoolDelegate(ref object instance, bool value); + + private delegate void SetIntDelegate(ref object instance, int handle); + + static SceneWrapper() + { + if (SceneType == null) return; + var name = AccessTools.PropertyGetter(SceneType, "name"); + NameGetter = name.MethodDelegate>(); + var path = AccessTools.PropertyGetter(SceneType, "path"); + PathGetter = path.MethodDelegate>(); + var buildIndex = AccessTools.PropertyGetter(SceneType, "buildIndex"); + BuildIndexGetter = buildIndex.MethodDelegate>(); + var isValid = AccessTools.Method(SceneType, "IsValid"); + IsValidMethod = isValid.MethodDelegate>(); + var isLoaded = AccessTools.PropertyGetter(SceneType, "isLoaded"); + IsLoadedGetter = isLoaded.MethodDelegate>(); + // var isDirty = AccessTools.PropertyGetter(SceneType, "isDirty"); + // IsDirtyGetter = isDirty.MethodDelegate>(); + // var rootCount = AccessTools.PropertyGetter(SceneType, "rootCount"); + // RootCountGetter = rootCount.MethodDelegate>(); + // TODO: does it exist + var subScene = AccessTools.Property(SceneType, "isSubScene"); + if (subScene != null) + { + IsSubSceneGetter = subScene.GetGetMethod().MethodDelegate>(); + IsSubSceneSetter = subScene.GetSetMethod().MethodDelegate(delegateArgs: + [typeof(object).MakeByRefType(), typeof(bool)]); + } + + var handleField = AccessTools.Field(SceneType, "m_Handle"); + var dmd = new DynamicMethodDefinition("m_Handle_get", typeof(int), [typeof(object)]); + var il = dmd.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, SceneType); + il.Emit(OpCodes.Ldfld, handleField); + il.Emit(OpCodes.Ret); + GetHandleField = dmd.Generate().CreateDelegate>(); + + dmd = new DynamicMethodDefinition("m_Handle_set", null, [typeof(object).MakeByRefType(), typeof(int)]); + il = dmd.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldind_Ref); + il.Emit(OpCodes.Unbox, SceneType); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, handleField); + il.Emit(OpCodes.Ret); + SetHandleField = dmd.Generate().CreateDelegate(); + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagerWrapper.cs b/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagerWrapper.cs index 182083e24..b701c8b27 100644 --- a/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagerWrapper.cs +++ b/UniTAS/Patcher/Implementations/UnitySafeWrappers/SceneManagerWrapper.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using HarmonyLib; +using UniTAS.Patcher.Extensions; using UniTAS.Patcher.Implementations.UnitySafeWrappers.SceneManagement; using UniTAS.Patcher.Interfaces.DependencyInjection; using UniTAS.Patcher.Interfaces.Events.SoftRestart; @@ -15,7 +16,7 @@ namespace UniTAS.Patcher.Implementations.UnitySafeWrappers; // ReSharper disable once ClassNeverInstantiated.Global [Singleton] [ExcludeRegisterIfTesting] -public class SceneManagerWrapper : ISceneWrapper, IOnPreGameRestart +public class SceneManagerWrapper : ISceneManagerWrapper, IOnPreGameRestart { private readonly IUnityInstanceWrapFactory _unityInstanceWrapFactory; private readonly IPatchReverseInvoker _patchReverseInvoker; @@ -38,14 +39,12 @@ public class SceneManagerWrapper : ISceneWrapper, IOnPreGameRestart // fallback load level async 1 private readonly MethodInfo _loadSceneAsyncNameIndexInternal; - // fallback load level async 2 - private readonly Func _applicationLoadLevelAsync; - - // non-async load level - private readonly MethodInfo _loadSceneByIndex; - private readonly MethodInfo _loadSceneByName; + private readonly MethodInfo _unloadSceneNameIndexInternal; + private readonly bool _unloadSceneNameIndexInternalHasOptions; private readonly MethodInfo _getActiveScene; + private readonly MethodInfo _getSceneAt; + private readonly Func _sceneCount; public SceneManagerWrapper(IUnityInstanceWrapFactory unityInstanceWrapFactory, IPatchReverseInvoker patchReverseInvoker) @@ -64,17 +63,6 @@ public SceneManagerWrapper(IUnityInstanceWrapFactory unityInstanceWrapFactory, [typeof(string), typeof(int), _loadSceneParametersType, typeof(bool)], null); } - _loadSceneByIndex = _sceneManager?.GetMethod("LoadScene", AccessTools.all, null, [typeof(int)], null); - _loadSceneByName = _sceneManager?.GetMethod("LoadScene", AccessTools.all, null, [typeof(string)], null); - - var loadLevelAsync = AccessTools.Method(typeof(Application), "LoadLevelAsync", - [typeof(string), typeof(int), typeof(bool), typeof(bool)]); - if (loadLevelAsync != null) - { - _applicationLoadLevelAsync = - AccessTools.MethodDelegate>(loadLevelAsync); - } - if (_loadSceneParametersType != null) { var usingType = _sceneManagerAPIInternal ?? _sceneManager; @@ -92,22 +80,47 @@ public SceneManagerWrapper(IUnityInstanceWrapFactory unityInstanceWrapFactory, } _getActiveScene = _sceneManager?.GetMethod("GetActiveScene", AccessTools.all); + _getSceneAt = _sceneManager?.GetMethod("GetSceneAt", AccessTools.all, null, [typeof(int)], null); + _sceneCount = _sceneManager?.GetProperty("sceneCount", AccessTools.all)?.GetGetMethod() + ?.MethodDelegate>(); + + var unloadSceneOptions = AccessTools.TypeByName($"{SceneManagementNamespace}.UnloadSceneOptions"); + _unloadSceneNameIndexInternal = unloadSceneOptions == null + ? null + : AccessTools.Method(_sceneManagerAPIInternal ?? _sceneManager, + "UnloadSceneNameIndexInternal", + [typeof(string), typeof(int), typeof(bool), unloadSceneOptions, typeof(bool).MakeByRefType()]) ?? + AccessTools.Method(_sceneManagerAPIInternal ?? _sceneManager, + "UnloadSceneNameIndexInternal", + [typeof(string), typeof(int), typeof(bool), typeof(bool).MakeByRefType()]); + + if (unloadSceneOptions != null) + { + _unloadSceneNameIndexInternal = AccessTools.Method(_sceneManagerAPIInternal ?? _sceneManager, + "UnloadSceneNameIndexInternal", + [typeof(string), typeof(int), typeof(bool), unloadSceneOptions, typeof(bool).MakeByRefType()]); + _unloadSceneNameIndexInternalHasOptions = _unloadSceneNameIndexInternal != null; + } + + _unloadSceneNameIndexInternal ??= AccessTools.Method(_sceneManagerAPIInternal ?? _sceneManager, + "UnloadSceneNameIndexInternal", + [typeof(string), typeof(int), typeof(bool), typeof(bool).MakeByRefType()]); } public void OnPreGameRestart() { - SceneCount = 1; + LoadedSceneCountDummy = 1; } public void LoadSceneAsync(string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, LocalPhysicsMode localPhysicsMode, bool mustCompleteNextFrame) { - if (TrackSceneCount) + if (TrackSceneCountDummy) { if (loadSceneMode == LoadSceneMode.Additive) - SceneCount++; + LoadedSceneCountDummy++; else - SceneCount = 1; + LoadedSceneCountDummy = 1; } if (_loadSceneAsyncNameIndexInternalInjected != null && _loadSceneParametersType != null) @@ -134,45 +147,43 @@ public void LoadSceneAsync(string sceneName, int sceneBuildIndex, LoadSceneMode return; } - if (_applicationLoadLevelAsync != null) - { - _patchReverseInvoker.Invoke( - (load, sceneNameInner, sceneBuildIndexInner, additive, mustCompleteNextFrameInner) => - load.Invoke(sceneNameInner, sceneBuildIndexInner, additive, mustCompleteNextFrameInner), - _applicationLoadLevelAsync, sceneName, sceneBuildIndex, loadSceneMode == LoadSceneMode.Additive, - mustCompleteNextFrame); - return; - } - - throw new InvalidOperationException("Could not find any more alternative load async methods"); + _patchReverseInvoker.Invoke( + (sceneNameInner, sceneBuildIndexInner, additive, mustCompleteNextFrameInner) => + Application.LoadLevelAsync(sceneNameInner, sceneBuildIndexInner, additive, + mustCompleteNextFrameInner), sceneName, sceneBuildIndex, + loadSceneMode == LoadSceneMode.Additive, mustCompleteNextFrame); } public void LoadScene(int buildIndex) { - if (TrackSceneCount) - SceneCount = 1; - - if (_loadSceneByIndex != null) - { - _patchReverseInvoker.Invoke((load, index) => load.Invoke(null, [index]), _loadSceneByIndex, buildIndex); - return; - } - - _patchReverseInvoker.Invoke(Application.LoadLevel, buildIndex); + _patchReverseInvoker.Invoke( + index => LoadSceneAsync(null, index, LoadSceneMode.Single, LocalPhysicsMode.None, true), buildIndex); } public void LoadScene(string name) { - if (TrackSceneCount) - SceneCount = 1; + _patchReverseInvoker.Invoke(n => LoadSceneAsync(n, -1, LoadSceneMode.Single, LocalPhysicsMode.None, true), + name); + } - if (_loadSceneByName != null) + public void UnloadSceneAsync(string sceneName, int sceneBuildIndex, object options, bool immediate, + out bool success) + { + object[] args; + if (_unloadSceneNameIndexInternalHasOptions) { - _patchReverseInvoker.Invoke((load, n) => load.Invoke(null, [n]), _loadSceneByName, name); - return; + args = [sceneName, sceneBuildIndex, immediate, options, null]; + } + else + { + args = [sceneName, sceneBuildIndex, immediate, null]; } - _patchReverseInvoker.Invoke(Application.LoadLevel, name); + var op = _patchReverseInvoker.Invoke((m, a) => m.Invoke(null, a), _unloadSceneNameIndexInternal, args); + success = (bool)args[args.Length - 1]; + + if (TrackSceneCountDummy && (!immediate || op != null) && success) + LoadedSceneCountDummy = Math.Max(1, LoadedSceneCountDummy - 1); } public int TotalSceneCount => _totalSceneCount?.Invoke() ?? Application.levelCount; @@ -185,7 +196,7 @@ public int ActiveSceneIndex { var sceneWrapInstance = _unityInstanceWrapFactory.Create(_getActiveScene.Invoke(null, null)); - return sceneWrapInstance.BuildIndex ?? Application.loadedLevel; + return sceneWrapInstance.BuildIndex; } return Application.loadedLevel; @@ -207,6 +218,20 @@ public string ActiveSceneName } } - public int SceneCount { get; set; } = 1; - public bool TrackSceneCount { get; set; } + public int LoadedSceneCountDummy { get; set; } = 1; + public bool TrackSceneCountDummy { get; set; } + + public SceneWrapper GetSceneAt(int index) + { + return _getSceneAt == null ? null : new SceneWrapper(_getSceneAt.Invoke(null, [index])); + } + + public int SceneCount + { + get + { + if (_sceneCount == null) return -1; + return _patchReverseInvoker.Invoke(call => call(), _sceneCount); + } + } } \ No newline at end of file diff --git a/UniTAS/Patcher/Implementations/UnitySafeWrappers/UnityInstanceWrapFactory.cs b/UniTAS/Patcher/Implementations/UnitySafeWrappers/UnityInstanceWrapFactory.cs index 6cb340227..472ceac55 100644 --- a/UniTAS/Patcher/Implementations/UnitySafeWrappers/UnityInstanceWrapFactory.cs +++ b/UniTAS/Patcher/Implementations/UnitySafeWrappers/UnityInstanceWrapFactory.cs @@ -4,7 +4,6 @@ using UniTAS.Patcher.Implementations.UnitySafeWrappers.Unity.Collections; using UniTAS.Patcher.Interfaces.DependencyInjection; using UniTAS.Patcher.Services.UnitySafeWrappers; -using UniTAS.Patcher.Services.UnitySafeWrappers.Wrappers; namespace UniTAS.Patcher.Implementations.UnitySafeWrappers; diff --git a/UniTAS/Patcher/Interfaces/Events/UnityEvents/DontRunIfPaused/IOnAwakeActual.cs b/UniTAS/Patcher/Interfaces/Events/UnityEvents/DontRunIfPaused/IOnAwakeActual.cs new file mode 100644 index 000000000..c967bdbe6 --- /dev/null +++ b/UniTAS/Patcher/Interfaces/Events/UnityEvents/DontRunIfPaused/IOnAwakeActual.cs @@ -0,0 +1,6 @@ +namespace UniTAS.Patcher.Interfaces.Events.UnityEvents.DontRunIfPaused; + +public interface IOnAwakeActual +{ + void AwakeActual(); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Interfaces/Events/UnityEvents/DontRunIfPaused/IOnEndOfFrameActual.cs b/UniTAS/Patcher/Interfaces/Events/UnityEvents/DontRunIfPaused/IOnEndOfFrameActual.cs new file mode 100644 index 000000000..13f266d41 --- /dev/null +++ b/UniTAS/Patcher/Interfaces/Events/UnityEvents/DontRunIfPaused/IOnEndOfFrameActual.cs @@ -0,0 +1,10 @@ +namespace UniTAS.Patcher.Interfaces.Events.UnityEvents.DontRunIfPaused; + +/// +/// End of frame, not the last update +/// This simply runs code on start of `yield WaitForEndOfFrame` +/// +public interface IOnEndOfFrameActual +{ + void OnEndOfFrame(); +} \ No newline at end of file diff --git a/UniTAS/Patcher/ManualServices/CoroutineManagerManual.cs b/UniTAS/Patcher/ManualServices/CoroutineManagerManual.cs new file mode 100644 index 000000000..2d45e5efc --- /dev/null +++ b/UniTAS/Patcher/ManualServices/CoroutineManagerManual.cs @@ -0,0 +1,28 @@ +using System.Collections; +using UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; +using UniTAS.Patcher.Utils; + +namespace UniTAS.Patcher.ManualServices; + +public static class CoroutineManagerManual +{ + private static ICoroutineTracker _coroutineTracker; + + private static ICoroutineTracker CoroutineTracker + { + get + { + _coroutineTracker ??= ContainerStarter.Kernel.GetInstance(); + return _coroutineTracker; + } + } + + public static void MonoBehNewCoroutine(object instance, IEnumerator routine) => + CoroutineTracker.NewCoroutine(instance, routine); + + public static bool CoroutineMoveNextPrefix(IEnumerator instance, ref bool result) => + CoroutineTracker.CoroutineMoveNextPrefix(instance, ref result); + + public static void CoroutineCurrentPostfix(IEnumerator instance, ref object result) => + CoroutineTracker.CoroutineCurrentPostfix(instance, ref result); +} \ No newline at end of file diff --git a/UniTAS/Patcher/ManualServices/Trackers/SerializationCallbackTracker.cs b/UniTAS/Patcher/ManualServices/Trackers/SerializationCallbackTracker.cs index 9b1c4d451..5fe72f759 100644 --- a/UniTAS/Patcher/ManualServices/Trackers/SerializationCallbackTracker.cs +++ b/UniTAS/Patcher/ManualServices/Trackers/SerializationCallbackTracker.cs @@ -70,7 +70,7 @@ public static void InvokeAllAfterDeserialization() foreach (var obj in AfterDeserializationInvoked) { - _afterDeserialization.Invoke(obj, null); + ExceptionUtils.UnityLogErrorOnThrow((m, o) => m.Invoke(o, null), _afterDeserialization, obj); AfterSerializationManuallyInvoked.Add(obj); } diff --git a/UniTAS/Patcher/Models/EventSubscribers/CallbackUpdate.cs b/UniTAS/Patcher/Models/EventSubscribers/CallbackUpdate.cs index f07dfdab5..2812f188e 100644 --- a/UniTAS/Patcher/Models/EventSubscribers/CallbackUpdate.cs +++ b/UniTAS/Patcher/Models/EventSubscribers/CallbackUpdate.cs @@ -18,6 +18,7 @@ public enum CallbackUpdate GUIActual, LateUpdateUnconditional, LateUpdateActual, + EndOfFrameActual, LastUpdateUnconditional, LastUpdateActual } \ No newline at end of file diff --git a/UniTAS/Patcher/Models/UnityObjectIdentifier.cs b/UniTAS/Patcher/Models/UnityObjectIdentifier.cs index 55a6fb21b..288521013 100644 --- a/UniTAS/Patcher/Models/UnityObjectIdentifier.cs +++ b/UniTAS/Patcher/Models/UnityObjectIdentifier.cs @@ -33,7 +33,7 @@ private UnityObjectIdentifier() /// Initialise from object /// public UnityObjectIdentifier(Object o, IUnityObjectIdentifierFactory unityObjectIdentifierFactory, - ISceneWrapper sceneWrapper) + ISceneManagerWrapper iSceneManagerWrapper) { _objectType = o.GetType(); @@ -58,7 +58,7 @@ public UnityObjectIdentifier(Object o, IUnityObjectIdentifierFactory unityObject _componentTypes = t?.GetComponents()?.Select(x => x.GetType()).ToArray() ?? []; - _foundScene = sceneWrapper.ActiveSceneIndex; + _foundScene = iSceneManagerWrapper.ActiveSceneIndex; var parent = t?.parent?.gameObject; if (parent == null) return; @@ -97,15 +97,15 @@ public UnityObjectIdentifier(Object o, IUnityObjectIdentifierFactory unityObject /// Finds runtime object /// /// Search settings to loosen or strict search - /// + /// /// Array of already tracked objects /// Objects to search from, otherwise it will automatically search /// Objects with matching type as identifier, otherwise it will search automatically - public Object FindObject(SearchSettings searchSettings, ISceneWrapper sceneWrapper, + public Object FindObject(SearchSettings searchSettings, ISceneManagerWrapper iSceneManagerWrapper, HashSet alreadyTrackedObjects, Object[] allObjects = null, Object[] allObjectsWithType = null) { - if (searchSettings.SceneMatch && _foundScene != sceneWrapper.ActiveSceneIndex) return null; + if (searchSettings.SceneMatch && _foundScene != iSceneManagerWrapper.ActiveSceneIndex) return null; allObjects ??= Resources.FindObjectsOfTypeAll(_objectType); allObjectsWithType ??= allObjects.Where(x => x.GetType() == _objectType).ToArray(); @@ -144,8 +144,8 @@ public Object FindObject(SearchSettings searchSettings, ISceneWrapper sceneWrapp // must fail match because it could mess up parent matching completely parentFindSettings.MultipleMatchHandle = MultipleMatchHandle.FailMatch; var parent = - _parent.FindObject(parentFindSettings, sceneWrapper, alreadyTrackedObjects, allObjects) as GameObject; - parent ??= _parent.FindObject(new(), sceneWrapper, alreadyTrackedObjects, allObjects) as GameObject; + _parent.FindObject(parentFindSettings, iSceneManagerWrapper, alreadyTrackedObjects, allObjects) as GameObject; + parent ??= _parent.FindObject(new(), iSceneManagerWrapper, alreadyTrackedObjects, allObjects) as GameObject; if (parent == null) return null; var parentTransform = parent.transform; var childCount = parentTransform.childCount; diff --git a/UniTAS/Patcher/MonoBehaviourScripts/MonoBehaviourUpdateInvoker.cs b/UniTAS/Patcher/MonoBehaviourScripts/MonoBehaviourUpdateInvoker.cs index 872f78683..36601ce4a 100644 --- a/UniTAS/Patcher/MonoBehaviourScripts/MonoBehaviourUpdateInvoker.cs +++ b/UniTAS/Patcher/MonoBehaviourScripts/MonoBehaviourUpdateInvoker.cs @@ -1,4 +1,5 @@ using System.Collections; +using UniTAS.Patcher.Services.GameExecutionControllers; using UniTAS.Patcher.Services.Logging; using UniTAS.Patcher.Services.UnityEvents; using UniTAS.Patcher.Services.UnityInfo; @@ -22,8 +23,17 @@ private void Awake() _monoBehEventInvoker.InvokeAwake(); - StartCoroutine(EndOfFrameCoroutine()); - StartCoroutine(FixedUpdateCoroutine()); + var controller = kernel.GetInstance(); + + var endOfFrame = EndOfFrameCoroutine(); + var fixedUpdate = FixedUpdateCoroutine(); + var update = UpdateCoroutine(); + controller.IgnoreCoroutines.Add(endOfFrame); + controller.IgnoreCoroutines.Add(fixedUpdate); + controller.IgnoreCoroutines.Add(update); + StartCoroutine(endOfFrame); + StartCoroutine(fixedUpdate); + StartCoroutine(update); } private bool _quitting; @@ -79,11 +89,22 @@ private void OnApplicationFocus(bool hasFocus) private readonly WaitForEndOfFrame _waitForEndOfFrame = new(); private readonly WaitForFixedUpdate _waitForFixedUpdate = new(); + private IEnumerator UpdateCoroutine() + { + while (true) + { + yield return null; + _monoBehEventInvoker.InvokeUpdate(); + } + // ReSharper disable once IteratorNeverReturns + } + private IEnumerator EndOfFrameCoroutine() { while (true) { yield return _waitForEndOfFrame; + _monoBehEventInvoker.InvokeEndOfFrame(); _monoBehEventInvoker.InvokeLastUpdate(); } // ReSharper disable once IteratorNeverReturns @@ -94,7 +115,7 @@ private IEnumerator FixedUpdateCoroutine() while (true) { yield return _waitForFixedUpdate; - _monoBehEventInvoker.CoroutineFixedUpdate(); + _monoBehEventInvoker.InvokeFixedUpdate(); } // ReSharper disable once IteratorNeverReturns } diff --git a/UniTAS/Patcher/Patcher.csproj b/UniTAS/Patcher/Patcher.csproj index 2cf0a59af..bbaf0013a 100644 --- a/UniTAS/Patcher/Patcher.csproj +++ b/UniTAS/Patcher/Patcher.csproj @@ -14,7 +14,7 @@ https://api.nuget.org/v3/index.json; https://nuget.bepinex.dev/v3/index.json - Debug;Release;ReleaseTrace;ReleaseBench;ReleaseTest + Debug;DebugTest;Release;ReleaseTrace;ReleaseBench;ReleaseTest AnyCPU false false @@ -23,15 +23,31 @@ + true bin/Release/BepInEx/patchers/UniTAS/ full true + true + + + + BENCH;UNIT_TESTS; bin/Debug/BepInEx/patchers/UniTAS/ + ; + full + + + + bin/Debug/BepInEx/patchers/UniTAS/ + full + + true + BENCH;UNIT_TESTS @@ -58,6 +74,7 @@ full true + true diff --git a/UniTAS/Patcher/Patches/Harmony/UnityInit/AssetBundleTracker.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/AssetBundleTracker.cs deleted file mode 100644 index 3cbdb8f6f..000000000 --- a/UniTAS/Patcher/Patches/Harmony/UnityInit/AssetBundleTracker.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using HarmonyLib; -using UniTAS.Patcher.Extensions; -using UniTAS.Patcher.Interfaces.Patches.PatchTypes; -using UniTAS.Patcher.Services.Logging; -using UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; -using UniTAS.Patcher.Utils; -using UnityEngine; - -namespace UniTAS.Patcher.Patches.Harmony.UnityInit; - -[RawPatchUnityInit] -[SuppressMessage("ReSharper", "InconsistentNaming")] -[SuppressMessage("ReSharper", "UnusedMember.Local")] -[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] -public class AssetBundleTracker -{ - private static readonly IAssetBundleTracker Tracker = ContainerStarter.Kernel.GetInstance(); - private static readonly ILogger Logger = ContainerStarter.Kernel.GetInstance(); - - [HarmonyPatch] - private static class LoadMethods - { - private static Exception Cleanup(MethodBase original, Exception ex) - { - return PatchHelper.CleanupIgnoreFail(original, ex); - } - - private static IEnumerable TargetMethods() - { - // target methods with signatures: - // static extern AssetBundle - - var methods = AccessTools.GetDeclaredMethods(typeof(AssetBundle)) - .Where(x => x.IsStatic && - (x.ReturnType == typeof(AssetBundle) || x.ReturnType == typeof(AssetBundleCreateRequest)) && - x.IsExtern()) - .Select(x => (MethodBase)x); - - foreach (var method in methods) - { - Logger.LogDebug($"Target method for tracking AssetBundle load: AssetBundle.{method.Name}"); - yield return method; - } - } - - private static void Postfix(object __result) - { - switch (__result) - { - case AssetBundle assetBundle: - Tracker.NewInstance(assetBundle); - break; - case AssetBundleCreateRequest assetBundleCreateRequest: - Tracker.NewInstance(assetBundleCreateRequest); - break; - } - } - } -} \ No newline at end of file diff --git a/UniTAS/Patcher/Patches/Harmony/UnityInit/AsyncOperationPatch.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/AsyncOperationPatch.cs index c125cdc71..3e209dc78 100644 --- a/UniTAS/Patcher/Patches/Harmony/UnityInit/AsyncOperationPatch.cs +++ b/UniTAS/Patcher/Patches/Harmony/UnityInit/AsyncOperationPatch.cs @@ -4,12 +4,16 @@ using System.IO; using System.Reflection; using HarmonyLib; +using UniTAS.Patcher.Extensions; using UniTAS.Patcher.Interfaces.Patches.PatchTypes; using UniTAS.Patcher.Services.Logging; using UniTAS.Patcher.Services.UnityAsyncOperationTracker; using UniTAS.Patcher.Utils; using UnityEngine; using Object = UnityEngine.Object; +#if TRACE +using System.Collections.Generic; +#endif namespace UniTAS.Patcher.Patches.Harmony.UnityInit; @@ -37,8 +41,68 @@ public class AsyncOperationPatch private static readonly IAsyncOperationIsInvokingOnComplete AsyncOperationIsInvokingOnComplete = ContainerStarter.Kernel.GetInstance(); + private static readonly IAssetBundleTracker AssetBundleTracker = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IAsyncOperationOverride AsyncOperationOverride = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IResourceAsyncTracker ResourceAsyncTracker = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IAssetBundleTracker Tracker = ContainerStarter.Kernel.GetInstance(); + private static readonly ILogger Logger = ContainerStarter.Kernel.GetInstance(); +#if TRACE + [HarmonyPatch] + private class AsyncOperationReturnTrace + { + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static IEnumerable TargetMethods() + { + foreach (var type in AccessTools.AllTypes()) + { + if (Equals(type.Assembly, typeof(AsyncOperationReturnTrace).Assembly)) continue; + + IEnumerable methods; + try + { + methods = AccessTools.GetDeclaredMethods(type); + } + catch (Exception) + { + continue; + } + + foreach (var method in methods) + { + Type ret; + try + { + ret = method.ReturnType; + } + catch (Exception) + { + continue; + } + + if (ret == typeof(AsyncOperation)) yield return method; + } + } + } + + private static void Postfix(AsyncOperation __result) + { + StaticLogger.Trace($"new AsyncOperation return: {DebugHelp.PrintClass(__result)}, {new StackTrace()}"); + } + } +#endif + [HarmonyPatch(typeof(AsyncOperation), "InvokeCompletionEvent")] private class InvokeCompletionEvent { @@ -50,7 +114,7 @@ private static Exception Cleanup(MethodBase original, Exception ex) private static bool Prefix(AsyncOperation __instance) { StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - return AsyncOperationIsInvokingOnComplete.IsInvokingOnComplete(__instance, out var invoking) && invoking; + return !AsyncOperationIsInvokingOnComplete.IsInvokingOnComplete(__instance, out var invoking) || invoking; } } @@ -92,7 +156,7 @@ private static bool Prefix(ref bool __result, AsyncOperation __instance) } [HarmonyPatch(typeof(AsyncOperation), nameof(AsyncOperation.priority), MethodType.Setter)] - private class prioritySetter + private class set_priority { private static Exception Cleanup(MethodBase original, Exception ex) { @@ -101,8 +165,23 @@ private static Exception Cleanup(MethodBase original, Exception ex) private static bool Prefix(int value, ref AsyncOperation __instance) { - StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - Logger.LogDebug($"Priority set to {value} for instance hash {__instance.GetHashCode()}, skipping"); + return !AsyncOperationOverride.SetPriority(__instance, value); + } + } + + [HarmonyPatch(typeof(AsyncOperation), nameof(AsyncOperation.priority), MethodType.Getter)] + private class get_priority + { + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(ref int __result, ref AsyncOperation __instance) + { + if (!AsyncOperationOverride.GetPriority(__instance, out var priority)) return true; + + __result = priority; return false; } } @@ -123,7 +202,7 @@ private static bool Prefix(ref float __result, AsyncOperation __instance) // 0.9f if the scene is loaded and ready, doesn't matter if allowSceneActivation is true, // it will be at 0.9 till the scene loads in the next frame - if (!SceneLoadTracker.Progress(__instance, out var progress)) + if (!AsyncOperationOverride.Progress(__instance, out var progress)) { // not tracked instance, proceed as original return true; @@ -146,7 +225,7 @@ private static bool Prefix(ref bool __result, AsyncOperation __instance) { StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - if (!SceneLoadTracker.IsDone(__instance, out var isDone)) + if (!AsyncOperationOverride.IsDone(__instance, out var isDone)) { StaticLogger.Trace("didn't find a tracked AsyncOperation instance"); return true; @@ -166,12 +245,11 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static bool Prefix(AsyncOperation __instance) + private static bool Prefix(IntPtr ___m_Ptr) { StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - var instanceTraverse = new Traverse(__instance).Field("m_Ptr"); - return instanceTraverse.GetValue() != IntPtr.Zero; + return ___m_Ptr != IntPtr.Zero; } } @@ -198,22 +276,12 @@ private static MethodInfo TargetMethod() [typeof(string), typeof(uint), typeof(ulong)]); } - private static readonly MethodInfo _loadFromFile = - AccessTools.Method(typeof(AssetBundle), "LoadFromFile_Internal", - [typeof(string), typeof(uint), typeof(ulong)]) ?? - AccessTools.Method(typeof(AssetBundle), "LoadFromFile", - [typeof(string), typeof(uint), typeof(ulong)]); - private static bool Prefix(string path, uint crc, ulong offset, ref AssetBundleCreateRequest __result) { - StaticLogger.Trace( - $"patch prefix invoke (path = {path}, crc = {crc}, offset = {offset})\n{new StackTrace()}"); - - // LoadFromFile fails with null return if operation fails, __result.assetBundle will also reflect that if async load fails too - var loadResult = _loadFromFile.Invoke(null, [path, crc, offset]) as AssetBundle; - // create a new instance - __result = new(); - AssetBundleCreateRequestTracker.NewAssetBundleCreateRequest(__result, loadResult); + StaticLogger.LogDebug($"Async op, load file async, path: {path}"); + StaticLogger.Trace(new StackTrace()); + __result = new AssetBundleCreateRequest(); + AssetBundleCreateRequestTracker.NewAssetBundleCreateRequest(__result, path, crc, offset); return false; } } @@ -227,17 +295,11 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static readonly MethodBase _loadFromMemoryInternal = AccessTools.Method(typeof(AssetBundle), - "LoadFromMemory_Internal", - [typeof(byte[]), typeof(uint)]); - private static bool Prefix(byte[] binary, uint crc, ref AssetBundleCreateRequest __result) { - StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - - var loadResult = _loadFromMemoryInternal.Invoke(null, [binary, crc]) as AssetBundle; - __result = new(); - AssetBundleCreateRequestTracker.NewAssetBundleCreateRequest(__result, loadResult); + StaticLogger.LogDebug("Async op, load memory async"); + __result = new AssetBundleCreateRequest(); + AssetBundleCreateRequestTracker.NewAssetBundleCreateRequest(__result, binary, crc); return false; } } @@ -251,20 +313,12 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static readonly MethodBase _loadFromStreamInternal = AccessTools.Method(typeof(AssetBundle), - "LoadFromStreamInternal", - [typeof(Stream), typeof(uint), typeof(uint)]); - private static bool Prefix(Stream stream, uint crc, uint managedReadBufferSize, ref AssetBundleCreateRequest __result) { - StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - - var loadResult = - _loadFromStreamInternal.Invoke(null, [stream, crc, managedReadBufferSize]) as - AssetBundle; - __result = new(); - AssetBundleCreateRequestTracker.NewAssetBundleCreateRequest(__result, loadResult); + StaticLogger.LogDebug("Async op, load stream async"); + __result = new AssetBundleCreateRequest(); + AssetBundleCreateRequestTracker.NewAssetBundleCreateRequest(__result, stream, crc, managedReadBufferSize); return false; } } @@ -278,17 +332,11 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static readonly MethodBase _loadAssetInternal = AccessTools.Method(typeof(AssetBundle), - "LoadAsset_Internal", - [typeof(string), typeof(Type)]); - private static bool Prefix(AssetBundle __instance, string name, Type type, ref AssetBundleRequest __result) { - StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - - var loadResult = _loadAssetInternal.Invoke(__instance, [name, type]) as Object; - __result = new(); - AssetBundleRequestTracker.NewAssetBundleRequest(__result, loadResult); + StaticLogger.LogDebug($"Async op, load asset async, name: {name}, type: {type.SaneFullName()}"); + __result = new AssetBundleRequest(); + AssetBundleRequestTracker.NewAssetBundleRequest(__result, __instance, name, type, false); return false; } } @@ -302,18 +350,12 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static readonly MethodBase _loadAssetWithSubAssetsInternal = AccessTools.Method(typeof(AssetBundle), - "LoadAssetWithSubAssets_Internal", - [typeof(string), typeof(Type)]); - private static bool Prefix(AssetBundle __instance, string name, Type type, ref AssetBundleRequest __result) { - StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - - var loadResult = - _loadAssetWithSubAssetsInternal.Invoke(__instance, [name, type]) as Object[]; - __result = new(); - AssetBundleRequestTracker.NewAssetBundleRequestMultiple(__result, loadResult); + StaticLogger.LogDebug( + $"Async op, load asset with sub assets async, name: {name}, type: {type.SaneFullName()}"); + __result = new AssetBundleRequest(); + AssetBundleRequestTracker.NewAssetBundleRequest(__result, __instance, name, type, true); return false; } } @@ -326,19 +368,33 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static readonly MethodBase _unload = - AccessTools.Method(typeof(AssetBundle), "Unload", [typeof(bool)]); + private static readonly Type AssetBundleUnloadOperationType = + AccessTools.TypeByName("UnityEngine.AssetBundleUnloadOperation"); - private static bool Prefix(bool unloadAllLoadedObjects, ref object __result) + private static bool Prefix(AssetBundle __instance, bool unloadAllLoadedObjects, ref object __result) { - StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - - _unload.Invoke(null, [unloadAllLoadedObjects]); - __result = AccessTools.CreateInstance(typeof(AssetBundle)); + StaticLogger.LogDebug("Async op, unload AssetBundle"); + __result = AccessTools.CreateInstance(AssetBundleUnloadOperationType); + AssetBundleTracker.UnloadBundleAsync((AsyncOperation)__result, __instance, unloadAllLoadedObjects); return false; } } + [HarmonyPatch(typeof(AssetBundle), nameof(AssetBundle.Unload))] + private class Unload + { + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static void Postfix(AssetBundle __instance) + { + StaticLogger.LogDebug("non async op, unload AssetBundle"); + AssetBundleTracker.Unload(__instance); + } + } + // TODO there's no non-async alternative of this // private static extern AssetBundleRecompressOperation RecompressAssetBundleAsync_Internal_Injected(string inputPath, string outputPath, ref BuildCompression method, uint expectedCRC, ThreadPriority priority); @@ -350,12 +406,11 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static bool Prefix(AssetBundleRequest __instance, ref object __result) + private static bool Prefix(AssetBundleRequest __instance, ref Object __result) { StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - __result = AssetBundleRequestTracker.GetAssetBundleRequest(__instance); - return false; + return !AssetBundleRequestTracker.GetAssetBundleRequest(__instance, out __result); } } @@ -367,15 +422,15 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static bool Prefix(AssetBundleRequest __instance, ref object __result) + private static bool Prefix(AssetBundleRequest __instance, ref Object[] __result) { - __result = AssetBundleRequestTracker.GetAssetBundleRequestMultiple(__instance); - return __result == null; + StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); + + return !AssetBundleRequestTracker.GetAssetBundleRequestMultiple(__instance, out __result); } } - [HarmonyPatch(typeof(AssetBundleCreateRequest), nameof(AssetBundleCreateRequest.assetBundle), - MethodType.Getter)] + [HarmonyPatch(typeof(AssetBundleCreateRequest), nameof(AssetBundleCreateRequest.assetBundle), MethodType.Getter)] private class get_assetBundle { private static Exception Cleanup(MethodBase original, Exception ex) @@ -387,29 +442,46 @@ private static bool Prefix(AssetBundleCreateRequest __instance, ref AssetBundle { StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); - __result = AssetBundleCreateRequestTracker.GetAssetBundleCreateRequest(__instance); - return false; + return !AssetBundleCreateRequestTracker.GetAssetBundleCreateRequest(__instance, out __result); } } - [HarmonyPatch(typeof(Resources), "LoadAsyncInternal")] + [HarmonyPatch] private class LoadAsyncInternalPatch { + private static readonly MethodBase Resources_LoadAsyncInternal = + AccessTools.Method(AccessTools.TypeByName("UnityEngine.ResourcesAPIInternal") ?? typeof(Resources), + "LoadAsyncInternal"); + + private static MethodBase TargetMethod() => Resources_LoadAsyncInternal; + private static Exception Cleanup(MethodBase original, Exception ex) { return PatchHelper.CleanupIgnoreFail(original, ex); } - private static bool Prefix(string path, Type type, ref object __result) + private static bool Prefix(string path, Type type, ref AsyncOperation __result) { - StaticLogger.Trace($"patch prefix invoke\n{new StackTrace()}"); + StaticLogger.LogDebug("Resources async op, load"); + __result = (AsyncOperation)AccessTools.CreateInstance(_resourceRequest); + ResourceAsyncTracker.ResourceLoadAsync(__result, path, type); + return false; + } + } - // returns ResourceRequest - // should be fine with my instance and no tinkering - __result = AccessTools.CreateInstance(_resourceRequest); - var resultTraverse = Traverse.Create(__result); - _ = resultTraverse.Field("m_Path").SetValue(path); - _ = resultTraverse.Field("m_Type").SetValue(type); + [HarmonyPatch(typeof(Resources), nameof(Resources.UnloadUnusedAssets))] + private class Resources_UnloadUnusedAssets + { + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(ref AsyncOperation __result) + { + StaticLogger.LogDebug("Resources async op, unload unused assets"); + __result = new AsyncOperation(); + ResourceAsyncTracker.ResourceUnloadAsync(__result); return false; } } diff --git a/UniTAS/Patcher/Patches/Harmony/UnityInit/CoroutineRunningObjectsTrackerPatch.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/CoroutineRunningObjectsTrackerPatch.cs deleted file mode 100644 index 58e22d757..000000000 --- a/UniTAS/Patcher/Patches/Harmony/UnityInit/CoroutineRunningObjectsTrackerPatch.cs +++ /dev/null @@ -1,43 +0,0 @@ -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.UpdateTrackInfo; -using UniTAS.Patcher.Utils; -using UnityEngine; - -namespace UniTAS.Patcher.Patches.Harmony.UnityInit; - -[RawPatchUnityInit] -[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/Harmony/UnityInit/CoroutineTrackerPatch.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/CoroutineTrackerPatch.cs new file mode 100644 index 000000000..361771b71 --- /dev/null +++ b/UniTAS/Patcher/Patches/Harmony/UnityInit/CoroutineTrackerPatch.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; +using UniTAS.Patcher.Interfaces.Patches.PatchTypes; +using UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; +using UniTAS.Patcher.Utils; +using UnityEngine; + +namespace UniTAS.Patcher.Patches.Harmony.UnityInit; + +[RawPatchUnityInit] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +[SuppressMessage("ReSharper", "UnusedMember.Local")] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class CoroutineTrackerPatch +{ + private static readonly ICoroutineTracker Tracker = + ContainerStarter.Kernel.GetInstance(); + + [HarmonyPatch(typeof(MonoBehaviour), nameof(MonoBehaviour.StartCoroutine), typeof(IEnumerator))] + private class StartCoroutine_IEnumerator + { + private static Exception Cleanup(MethodBase original, Exception ex) => + PatchHelper.CleanupIgnoreFail(original, ex); + + private static void Prefix(MonoBehaviour __instance, IEnumerator routine) + { + Tracker.NewCoroutine(__instance, routine); + } + } + + [HarmonyPatch(typeof(MonoBehaviour), nameof(MonoBehaviour.StartCoroutine), typeof(string), typeof(object))] + private class StartCoroutine_string_object + { + private static Exception Cleanup(MethodBase original, Exception ex) => + PatchHelper.CleanupIgnoreFail(original, ex); + + private static void Prefix(MonoBehaviour __instance, string methodName, object value) + { + Tracker.NewCoroutine(__instance, methodName, value); + } + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Patches/Harmony/UnityInit/LegacyAsyncSceneLoadPatch.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/LegacyAsyncSceneLoadPatch.cs deleted file mode 100644 index 34e6dc596..000000000 --- a/UniTAS/Patcher/Patches/Harmony/UnityInit/LegacyAsyncSceneLoadPatch.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Reflection; -using HarmonyLib; -using UniTAS.Patcher.Interfaces.Patches.PatchTypes; -using UniTAS.Patcher.Services; -using UniTAS.Patcher.Services.UnityEvents; -using UniTAS.Patcher.Utils; -using UnityEngine; - -// ReSharper disable UnusedMember.Local -// ReSharper disable RedundantAssignment - -namespace UniTAS.Patcher.Patches.Harmony.UnityInit; - -[RawPatchUnityInit] -public static class LegacyAsyncSceneLoadPatch -{ - private static readonly ISceneLoadInvoke SceneLoadInvoke = ContainerStarter.Kernel.GetInstance(); - - private static readonly IPatchReverseInvoker PatchReverseInvoker = - ContainerStarter.Kernel.GetInstance(); - - [HarmonyPatch] - private class LoadLevelAsync - { - private static MethodBase TargetMethod() - { - return AccessTools.Method(typeof(Application), nameof(Application.LoadLevelAsync), - [typeof(string), typeof(int), typeof(bool), typeof(bool)]); - } - - private static Exception Cleanup(MethodBase original, Exception ex) - { - return PatchHelper.CleanupIgnoreFail(original, ex); - } - - private static void Prefix(ref bool mustCompleteNextFrame) - { - if (!PatchReverseInvoker.Invoking) - mustCompleteNextFrame = true; - SceneLoadInvoke.SceneLoadCall(); - } - } -} \ No newline at end of file diff --git a/UniTAS/Patcher/Patches/Harmony/UnityInit/SceneManagerAsyncLoadPatch.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/SceneManagerAsyncLoadPatch.cs index 4f0ddf6f3..0e33871dd 100644 --- a/UniTAS/Patcher/Patches/Harmony/UnityInit/SceneManagerAsyncLoadPatch.cs +++ b/UniTAS/Patcher/Patches/Harmony/UnityInit/SceneManagerAsyncLoadPatch.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using HarmonyLib; using UniTAS.Patcher.Implementations.UnitySafeWrappers; @@ -11,6 +13,7 @@ using UniTAS.Patcher.Services.Logging; using UniTAS.Patcher.Services.UnityAsyncOperationTracker; using UniTAS.Patcher.Services.UnityEvents; +using UniTAS.Patcher.Services.UnitySafeWrappers; using UniTAS.Patcher.Services.UnitySafeWrappers.Wrappers; using UniTAS.Patcher.Utils; using UnityEngine; @@ -27,6 +30,21 @@ namespace UniTAS.Patcher.Patches.Harmony.UnityInit; [SuppressMessage("ReSharper", "InconsistentNaming")] public class SceneManagerAsyncLoadPatch { + static SceneManagerAsyncLoadPatch() + { + if (UnloadSceneOptions != null) + { + UnloadSceneNameIndexInternal = AccessTools.Method(SceneManagerAPIInternal ?? SceneManager, + "UnloadSceneNameIndexInternal", + [typeof(string), typeof(int), typeof(bool), UnloadSceneOptions, typeof(bool).MakeByRefType()]); + UnloadSceneNameIndexInternalHasOptions = UnloadSceneNameIndexInternal != null; + } + + UnloadSceneNameIndexInternal ??= AccessTools.Method(SceneManagerAPIInternal ?? SceneManager, + "UnloadSceneNameIndexInternal", + [typeof(string), typeof(int), typeof(bool), typeof(bool).MakeByRefType()]); + } + private const string Namespace = "UnityEngine.SceneManagement"; private static readonly Type SceneManager = AccessTools.TypeByName($"{Namespace}.SceneManager"); private static readonly Type UnloadSceneOptions = AccessTools.TypeByName($"{Namespace}.UnloadSceneOptions"); @@ -36,14 +54,8 @@ public class SceneManagerAsyncLoadPatch private static readonly Type SceneManagerAPIInternal = AccessTools.TypeByName($"{Namespace}.SceneManagerAPIInternal"); - private static readonly MethodInfo UnloadSceneNameIndexInternal = UnloadSceneOptions == null - ? null - : AccessTools.Method(SceneManagerAPIInternal ?? SceneManager, - "UnloadSceneNameIndexInternal", - [typeof(string), typeof(int), typeof(bool), UnloadSceneOptions, typeof(bool).MakeByRefType()]) ?? - AccessTools.Method(SceneManagerAPIInternal ?? SceneManager, - "UnloadSceneNameIndexInternal", - [typeof(string), typeof(int), typeof(bool), typeof(bool).MakeByRefType()]); + private static readonly MethodInfo UnloadSceneNameIndexInternal; + private static readonly bool UnloadSceneNameIndexInternalHasOptions; private static readonly MethodInfo LoadSceneAsyncNameIndexInternalInjected = SceneManagerAPIInternal == null || LoadSceneParametersType == null @@ -52,11 +64,43 @@ public class SceneManagerAsyncLoadPatch SceneManagerAPIInternal, "LoadSceneAsyncNameIndexInternal_Injected", [typeof(string), typeof(int), LoadSceneParametersType.MakeByRefType(), typeof(bool)]); + private static readonly MethodInfo GetSceneByBuildIndex = + SceneManager?.GetMethod("GetSceneByBuildIndex", AccessTools.all, null, [typeof(int)], null); + + private static readonly MethodInfo SceneGetName = SceneType?.GetProperty("name", AccessTools.all)?.GetGetMethod(); + + private static readonly MethodInfo SetActiveSceneInjected = SceneType == null + ? null + : SceneManager?.GetMethod("SetActiveScene_Injected", AccessTools.all, + null, [SceneType.MakeByRefType()], null); + + private static readonly MethodInfo GetSceneByNameInjected = SceneType == null + ? null + : SceneManager?.GetMethod("GetSceneByName_Injected", AccessTools.all, null, + [typeof(string), SceneType.MakeByRefType()], null); + + private static readonly MethodInfo GetSceneByPathInjected = SceneType == null + ? null + : SceneManager?.GetMethod("GetSceneByPath_Injected", AccessTools.all, + null, + [typeof(string), SceneType.MakeByRefType()], null); + + private static readonly MethodInfo GetSceneByBuildIndexInjected = SceneType == null + ? null + : (SceneManagerAPIInternal ?? SceneManager)?.GetMethod("GetSceneByBuildIndex_Injected", AccessTools.all, + null, + [typeof(int), SceneType.MakeByRefType()], null); + + private static readonly MethodInfo GetSceneAtInjected = SceneType == null + ? null + : SceneManager?.GetMethod("GetSceneAt_Injected", AccessTools.all, null, + [typeof(int), SceneType.MakeByRefType()], null); + private static readonly ISceneLoadTracker SceneLoadTracker = ContainerStarter.Kernel.GetInstance(); - private static readonly ISceneWrapper SceneWrapper = - ContainerStarter.Kernel.GetInstance(); + private static readonly ISceneManagerWrapper SceneManagerWrapper = + ContainerStarter.Kernel.GetInstance(); private static readonly UnityInstanceWrapFactory UnityInstanceWrapFactory = ContainerStarter.Kernel.GetInstance(); @@ -68,6 +112,9 @@ public class SceneManagerAsyncLoadPatch private static readonly IPatchReverseInvoker ReverseInvoker = ContainerStarter.Kernel.GetInstance(); + private static readonly IUnityInstanceWrapFactory WrapFactory = + ContainerStarter.Kernel.GetInstance(); + private static bool AsyncSceneLoad(bool mustCompleteNextFrame, string sceneName, int sceneBuildIndex, object parameters, bool? isAdditive, ref AsyncOperation __result) { @@ -97,8 +144,6 @@ private static bool AsyncSceneLoad(bool mustCompleteNextFrame, string sceneName, if (!mustCompleteNextFrame) { __result = new(); - - Logger.LogDebug($"async scene load, instance id: {__result.GetHashCode()}"); } if (parameters != null) @@ -122,7 +167,7 @@ private static bool AsyncSceneLoad(bool mustCompleteNextFrame, string sceneName, else { SceneLoadTracker.AsyncSceneLoad(sceneName, sceneBuildIndex, (LoadSceneMode)loadSceneModeValue, - (LocalPhysicsMode)localPhysicsModeValue, __result); + (LocalPhysicsMode)localPhysicsModeValue, ref __result); } return false; @@ -142,15 +187,17 @@ private static bool AsyncSceneLoad(bool mustCompleteNextFrame, string sceneName, { SceneLoadTracker.AsyncSceneLoad(sceneName, sceneBuildIndex, isAdditive.Value ? LoadSceneMode.Additive : LoadSceneMode.Single, - LocalPhysicsMode.None, __result); + LocalPhysicsMode.None, ref __result); } return false; } [HarmonyPatch] - private class UnloadSceneNameIndexInternalPatch + private class UnloadSceneNameIndexInternalPatchOptions { + private static bool Prepare() => UnloadSceneNameIndexInternalHasOptions; + private static MethodBase TargetMethod() { return UnloadSceneNameIndexInternal; @@ -161,18 +208,43 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static void Prefix(ref bool immediately, out bool __state) + private static bool Prefix(string sceneName, int sceneBuildIndex, ref bool outSuccess, object options, + ref AsyncOperation __result) { - __state = immediately; - immediately = true; + if (ReverseInvoker.Invoking) return true; + + __result = new AsyncOperation(); + SceneLoadTracker.AsyncSceneUnload(ref __result, sceneBuildIndex >= 0 ? sceneBuildIndex : sceneName, + options); + outSuccess = __result != null; + return false; } + } + + [HarmonyPatch] + private class UnloadSceneNameIndexInternalPatch + { + private static bool Prepare() => !UnloadSceneNameIndexInternalHasOptions; - private static void Postfix(ref AsyncOperation __result, bool __state) + private static MethodBase TargetMethod() { - if (__state) return; + return UnloadSceneNameIndexInternal; + } - __result = new(); - SceneLoadTracker.AsyncSceneUnload(__result); + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(string sceneName, int sceneBuildIndex, ref bool outSuccess, + ref AsyncOperation __result) + { + if (ReverseInvoker.Invoking) return true; + + __result = new AsyncOperation(); + SceneLoadTracker.AsyncSceneUnload(ref __result, sceneBuildIndex >= 0 ? sceneBuildIndex : sceneName, null); + outSuccess = __result != null; + return false; } } @@ -189,29 +261,12 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static readonly MethodInfo _getName = SceneType?.GetProperty("name", AccessTools.all)?.GetGetMethod(); - private static bool Prefix(object scene, ref AsyncOperation __result) { - var sceneName = (string)_getName.Invoke(scene, null); - - StaticLogger.LogDebug($"async scene unload, forcing scene `{sceneName}` to unload"); - StaticLogger.LogWarning( - "THIS OPERATION MIGHT BREAK THE GAME, scene unloading patch is using an unstable unity function, and it may fail"); - var args = new object[] { sceneName, -1, true, null }; - UnloadSceneNameIndexInternal.Invoke(null, args); - if (!(bool)args[3]) - StaticLogger.LogError("async unload most likely failed, prepare for game to go nuts"); - __result = new(); + SceneLoadTracker.AsyncSceneUnload(ref __result, scene, null); return false; } - - private static void Postfix(ref AsyncOperation __result) - { - __result = new(); - SceneLoadTracker.AsyncSceneUnload(__result); - } } [HarmonyPatch] @@ -228,30 +283,14 @@ private static Exception Cleanup(MethodBase original, Exception ex) return PatchHelper.CleanupIgnoreFail(original, ex); } - private static readonly MethodInfo _getName = - SceneType?.GetProperty("name", AccessTools.all)?.GetGetMethod(); - private static bool Prefix(object scene, object options, ref AsyncOperation __result) { - var sceneName = (string)_getName.Invoke(scene, null); - - StaticLogger.LogDebug($"async scene unload, forcing scene `{sceneName}` to unload"); - StaticLogger.LogWarning( - "THIS OPERATION MIGHT BREAK THE GAME, scene unloading patch is using an unstable unity function, and it may fail"); - var args = new[] { sceneName, -1, true, options, null }; - UnloadSceneNameIndexInternal.Invoke(null, args); - if (!(bool)args[4]) - StaticLogger.LogError("async unload most likely failed, prepare for game to go nuts"); + if (ReverseInvoker.Invoking) return true; __result = new(); + SceneLoadTracker.AsyncSceneUnload(ref __result, scene, options); return false; } - - private static void Postfix(ref AsyncOperation __result) - { - __result = new(); - SceneLoadTracker.AsyncSceneUnload(__result); - } } [HarmonyPatch] @@ -328,6 +367,27 @@ private static bool Prefix(string sceneName, int sceneBuildIndex, object paramet } } + [HarmonyPatch] + private class LoadLevelAsync + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(typeof(Application), nameof(Application.LoadLevelAsync), + [typeof(string), typeof(int), typeof(bool), typeof(bool)]); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(string monoLevelName, int index, bool additive, bool mustCompleteNextFrame, + ref AsyncOperation __result) + { + return AsyncSceneLoad(mustCompleteNextFrame, monoLevelName, index, null, additive, ref __result); + } + } + [HarmonyPatch] private class get_loadedSceneCount { @@ -346,7 +406,7 @@ private static Exception Cleanup(MethodBase original, Exception ex) private static bool Prefix(ref int __result) { - __result = SceneWrapper.SceneCount; + __result = SceneManagerWrapper.LoadedSceneCountDummy; return false; } } @@ -369,20 +429,175 @@ private static Exception Cleanup(MethodBase original, Exception ex) private static bool Prefix(ref int __result) { - // check if it came from the specific method, since we want real data for this - var frames = new StackTrace().GetFrames(); - if (frames != null) + if (ReverseInvoker.Invoking) return true; + __result = SceneManagerWrapper.LoadedSceneCountDummy + SceneLoadTracker.LoadingSceneCount; + return false; + } + } + + [HarmonyPatch] + private class GetSceneAt_Injected + { + private static MethodBase TargetMethod() + { + return GetSceneAtInjected; + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int index, ref object ret) + { + var sceneCount = SceneManagerWrapper.SceneCount; + if (index < sceneCount) return true; + + // check loading ones + var loadIndex = index - sceneCount; + if (loadIndex >= SceneLoadTracker.LoadingScenes.Count) return true; + + ret = SceneLoadTracker.LoadingScenes[loadIndex].DummySceneStruct; + return false; + } + } + + [HarmonyPatch] + private class GetSceneByName_Injected + { + private static MethodBase TargetMethod() + { + return GetSceneByNameInjected; + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(string name, ref object ret) + { + foreach (var loading in SceneLoadTracker.LoadingScenes) { - foreach (var frame in frames) - { - var method = frame.GetMethod(); - if (method.DeclaringType == SceneManager && method.Name == "LoadScene") - return true; - } + if (loading.LoadingScene.Name != name) continue; + ret = loading.DummySceneStruct; + return false; } - __result = SceneWrapper.SceneCount + SceneLoadTracker.LoadingSceneCount; - return false; + return true; + } + } + + [HarmonyPatch] + private class GetSceneByBuildIndex_Injected + { + private static MethodBase TargetMethod() + { + return GetSceneByBuildIndexInjected; + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int buildIndex, ref object ret) + { + foreach (var loading in SceneLoadTracker.LoadingScenes) + { + if (loading.LoadingScene.BuildIndex != buildIndex) continue; + ret = loading.DummySceneStruct; + return false; + } + + return true; + } + } + + [HarmonyPatch] + private class GetSceneByPath_Injected + { + private static MethodBase TargetMethod() + { + return GetSceneByPathInjected; + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(string scenePath, ref object ret) + { + foreach (var loading in SceneLoadTracker.LoadingScenes) + { + if (loading.LoadingScene.Path != scenePath) continue; + ret = loading.DummySceneStruct; + return false; + } + + return true; + } + } + + [HarmonyPatch] + private class SetActiveScene_Injected + { + private static MethodBase TargetMethod() + { + return SetActiveSceneInjected; + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static void Prefix(ref object scene) + { + StaticLogger.Trace($"prefix invoke: {new StackTrace()}"); + var handle = WrapFactory.Create(scene).Handle; + foreach (var loading in SceneLoadTracker.LoadingScenes) + { + if (loading.TrackingHandle != handle) continue; + // scene is still loading, so do the intended error + throw new ArgumentException( + $"SceneManager.SetActiveScene failed; scene '{loading.LoadingScene.Name}' is not loaded and therefore cannot be set active"); + } + + foreach (var dummy in SceneLoadTracker.DummyScenes) + { + if (dummy.dummyScene.TrackingHandle != handle) continue; + scene = dummy.actualScene.Instance; + return; + } + } + } + + [HarmonyPatch] + private class ReversePatches + { + private static IEnumerable TargetMethods() + { + return new[] + { + AccessTools.Method(SceneManager, "Internal_SceneLoaded") + }.Where(x => x != null).Select(MethodBase (x) => x); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static void Prefix() + { + ReverseInvoker.Invoking = true; + } + + private static void Postfix() + { + ReverseInvoker.Invoking = false; } } } \ No newline at end of file diff --git a/UniTAS/Patcher/Patches/Harmony/UnityInit/SceneStructPatch.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/SceneStructPatch.cs new file mode 100644 index 000000000..4b1692656 --- /dev/null +++ b/UniTAS/Patcher/Patches/Harmony/UnityInit/SceneStructPatch.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using UniTAS.Patcher.Implementations.UnitySafeWrappers.SceneManagement; +using UniTAS.Patcher.Interfaces.Patches.PatchTypes; +using UniTAS.Patcher.Services; +using UniTAS.Patcher.Services.UnityAsyncOperationTracker; +using UniTAS.Patcher.Services.UnitySafeWrappers; +using UniTAS.Patcher.Utils; + +namespace UniTAS.Patcher.Patches.Harmony.UnityInit; + +[RawPatchUnityInit] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +[SuppressMessage("ReSharper", "UnusedMember.Local")] +[SuppressMessage("ReSharper", "RedundantAssignment")] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class SceneStructPatch +{ + private static readonly Type SceneType = AccessTools.TypeByName("UnityEngine.SceneManagement.Scene"); + + private static readonly ISceneLoadTracker SceneLoadTracker = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IPatchReverseInvoker PatchReverseInvoker = + ContainerStarter.Kernel.GetInstance(); + + private static readonly IUnityInstanceWrapFactory WrapFactory = + ContainerStarter.Kernel.GetInstance(); + + private static readonly ISceneOverride SceneOverride = ContainerStarter.Kernel.GetInstance(); + + [HarmonyPatch] + private class IsValidInternal + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "IsValidInternal"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(ref bool __result, int sceneHandle) + { + return CheckAndSetDefault(sceneHandle, ref __result, _ => true, actual => actual.IsValid); + } + } + + [HarmonyPatch] + private class GetNameInternal + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "GetNameInternal"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(ref string __result, int sceneHandle) + { + return CheckAndSetDefault(sceneHandle, ref __result, x => x.Name, x => x.Name); + } + } + + [HarmonyPatch] + private class SetNameInternal + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "SetNameInternal"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int sceneHandle) + { + if (PatchReverseInvoker.Invoking) return true; + + foreach (var loading in SceneLoadTracker.LoadingScenes) + { + if (loading.TrackingHandle != sceneHandle) continue; + CheckAndWarnAPIUsage(); + // well this ain't the proper error, but it should do the same thing + throw new InvalidOperationException( + $"Setting a name on a saved scene is not allowed (the filename is used as name). Scene: '{loading.LoadingScene.Path}'"); + } + + return true; + } + } + + [HarmonyPatch] + private class GetBuildIndexInternal + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "GetBuildIndexInternal"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int sceneHandle, ref int __result) + { + return CheckAndSetDefault(sceneHandle, ref __result, dummy => dummy.BuildIndex, + actual => actual.BuildIndex); + } + } + + [HarmonyPatch] + private class GetPathInternal + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "GetPathInternal"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int sceneHandle, ref string __result) + { + return CheckAndSetDefault(sceneHandle, ref __result, dummy => dummy.Path, actual => actual.Path); + } + } + + [HarmonyPatch] + private class op_Equality + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "op_Equality"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(ref object lhs, ref object rhs, ref bool __result) + { + if (PatchReverseInvoker.Invoking) return true; + + var rhsAddr = WrapFactory.Create(rhs).Handle; + var lhsAddr = WrapFactory.Create(lhs).Handle; + if (rhsAddr == lhsAddr) + { + __result = true; + return false; + } + + foreach (var loading in SceneLoadTracker.DummyScenes) + { + if (loading.dummyScene.TrackingHandle == lhsAddr) + { + __result = loading.actualScene != null && loading.actualScene.Handle == rhsAddr; + return false; + } + + if (loading.dummyScene.TrackingHandle == rhsAddr) + { + __result = loading.actualScene != null && loading.actualScene.Handle == lhsAddr; + return false; + } + } + + return true; + } + } + + [HarmonyPatch] + private class op_Inequality + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "op_Inequality"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(ref object lhs, ref object rhs, ref bool __result) + { + if (PatchReverseInvoker.Invoking) return true; + + var rhsAddr = WrapFactory.Create(rhs).Handle; + var lhsAddr = WrapFactory.Create(lhs).Handle; + if (rhsAddr == lhsAddr) + { + __result = false; + return false; + } + + foreach (var loading in SceneLoadTracker.DummyScenes) + { + if (loading.dummyScene.TrackingHandle == lhsAddr) + { + __result = loading.actualScene != null && loading.actualScene.Handle != rhsAddr; + return false; + } + + if (loading.dummyScene.TrackingHandle == rhsAddr) + { + __result = loading.actualScene != null && loading.actualScene.Handle != lhsAddr; + return false; + } + } + + return true; + } + } + + [HarmonyPatch] + private class GetHashCodePatch + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "GetHashCode"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int ___m_Handle, ref int __result) + { + return CheckAndSetDefault(___m_Handle, ref __result, _ => 0, actual => actual.Instance.GetHashCode()); + } + } + + [HarmonyPatch] + private class EqualsPatch + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "Equals"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int ___m_Handle, object other, ref bool __result) + { + if (PatchReverseInvoker.Invoking) return true; + + var otherHandle = WrapFactory.Create(other).Handle; + if (___m_Handle == otherHandle) + { + __result = true; + return false; + } + + foreach (var loading in SceneLoadTracker.DummyScenes) + { + if (loading.dummyScene.TrackingHandle == ___m_Handle) + { + __result = loading.actualScene != null && loading.actualScene.Handle == otherHandle; + return false; + } + + if (loading.dummyScene.TrackingHandle == otherHandle) + { + __result = loading.actualScene != null && loading.actualScene.Handle == ___m_Handle; + return false; + } + } + + return true; + } + } + + [HarmonyPatch] + private class GetIsLoadedInternal + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "GetIsLoadedInternal"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int sceneHandle, ref bool __result) + { + if (SceneOverride.IsLoaded(sceneHandle, out var loaded)) + { + __result = loaded; + return false; + } + + return CheckAndSetDefault(sceneHandle, ref __result, _ => false, actual => actual.IsLoaded); + } + } + + [HarmonyPatch] + private class IsSubScene + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "IsSubScene"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int sceneHandle, ref bool __result) + { + if (SceneOverride.IsSubScene(sceneHandle, out var sub)) + { + __result = sub; + return false; + } + + return CheckAndSetDefault(sceneHandle, ref __result, _ => false, actual => actual.IsSubScene); + } + } + + [HarmonyPatch] + private class SetIsSubScene + { + private static MethodBase TargetMethod() + { + return AccessTools.Method(SceneType, "SetIsSubScene"); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(int sceneHandle, bool value) + { + return !SceneOverride.SetSubScene(sceneHandle, value); + } + } + + [HarmonyPatch] + private class FixHandleOrReturnDefault + { + private static IEnumerable TargetMethods() + { + return new[] + { + AccessTools.Method(SceneType, "GetRootGameObjectsInternal"), + AccessTools.Method(SceneType, "GetRootCountInternal"), + AccessTools.Method(SceneType, "GetDirtyID"), + AccessTools.Method(SceneType, "GetIsDirtyInternal"), + + // these shouldn't matter + AccessTools.Method(SceneType, "GetLoadingStateInternal"), + AccessTools.Method(SceneType, "GetGUIDInternal"), + AccessTools.Method(SceneType, "SetPathAndGUIDInternal"), + }.Where(x => x != null).Select(MethodBase (x) => x); + } + + private static Exception Cleanup(MethodBase original, Exception ex) + { + return PatchHelper.CleanupIgnoreFail(original, ex); + } + + private static bool Prefix(ref int sceneHandle) + { + return CheckAndSetHandleArg(ref sceneHandle); + } + } + + private static bool CheckAndSetHandleArg(ref int sceneHandle) + { + foreach (var dummy in SceneLoadTracker.DummyScenes) + { + if (dummy.dummyScene.TrackingHandle != sceneHandle) continue; + CheckAndWarnAPIUsage(); + if (dummy.actualScene == null) + return false; + + sceneHandle = dummy.actualScene.Handle; + return true; + } + + return true; + } + + private static bool CheckAndSetHandleArg(ref int sceneHandle, ref T __result, Func dummySet) + { + foreach (var dummy in SceneLoadTracker.DummyScenes) + { + if (dummy.dummyScene.TrackingHandle != sceneHandle) continue; + CheckAndWarnAPIUsage(); + if (dummy.actualScene == null) + { + __result = dummySet(); + return false; + } + + sceneHandle = dummy.actualScene.Handle; + return true; + } + + return true; + } + + private static bool CheckAndSetDefault(int mHandle, ref T __result, + Func dummySet, Func actualSet) + { + if (PatchReverseInvoker.Invoking) return true; + + foreach (var loading in SceneLoadTracker.LoadingScenes) + { + if (loading.TrackingHandle != mHandle) continue; + CheckAndWarnAPIUsage(); + __result = dummySet(loading.LoadingScene); + return false; + } + + foreach (var loading in SceneLoadTracker.DummyScenes) + { + if (loading.dummyScene.TrackingHandle != mHandle) continue; + __result = PatchReverseInvoker.Invoke((a, b) => a(b), actualSet, loading.actualScene); + return false; + } + + return true; + } + + private static void CheckAndWarnAPIUsage() + { + var frames = new StackTrace(true).GetFrames(); + if (frames == null) return; + + foreach (var frame in frames.Skip(1)) + { + var method = frame.GetMethod(); + if (method.DeclaringType?.Namespace == typeof(HarmonyLib.Harmony).Namespace) continue; + + if (method.DeclaringType != SceneType) + { + StaticLogger.LogWarning( + $"Something is calling scene struct API that is managed by UniTAS outside of Scene struct, this could be bad, stacktrace: {new StackTrace()}"); + } + + return; + } + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Patches/Harmony/UnityInit/TimePatch.cs b/UniTAS/Patcher/Patches/Harmony/UnityInit/TimePatch.cs index cd3758b6e..08534d407 100644 --- a/UniTAS/Patcher/Patches/Harmony/UnityInit/TimePatch.cs +++ b/UniTAS/Patcher/Patches/Harmony/UnityInit/TimePatch.cs @@ -31,7 +31,7 @@ private static bool CalledFromFixedUpdate() foreach (var frame in frames) { var method = frame.GetMethod(); - if (method?.Name is not "FixedUpdate") return true; + if (method?.Name is not "FixedUpdate") continue; var declType = method.DeclaringType; while (declType != null) @@ -39,8 +39,6 @@ private static bool CalledFromFixedUpdate() if (declType.IsSubclassOf(typeof(MonoBehaviour))) return true; declType = declType.DeclaringType; } - - return true; } return false; @@ -156,6 +154,7 @@ private static Exception Cleanup(MethodBase original, Exception ex) private static void Postfix(ref int __result) { + if (ReverseInvoker.Invoking) return; __result = (int)((ulong)__result - TimeEnv.FrameCountRestartOffset); } } @@ -256,6 +255,7 @@ private static Exception Cleanup(MethodBase original, Exception ex) private static bool Prefix(ref float __result) { + if (ReverseInvoker.Invoking) return true; __result = (float)TimeEnv.ScaledFixedTime; return false; } diff --git a/UniTAS/Patcher/Patches/Preloader/CoroutinePatch.cs b/UniTAS/Patcher/Patches/Preloader/CoroutinePatch.cs new file mode 100644 index 000000000..9d3879fba --- /dev/null +++ b/UniTAS/Patcher/Patches/Preloader/CoroutinePatch.cs @@ -0,0 +1,81 @@ +using System.Collections; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using UniTAS.Patcher.Extensions; +using UniTAS.Patcher.Interfaces; +using UniTAS.Patcher.ManualServices; +using UniTAS.Patcher.Utils; + +namespace UniTAS.Patcher.Patches.Preloader; + +public class CoroutinePatch : PreloadPatcher +{ + public override void Patch(ref AssemblyDefinition assembly) + { + var types = assembly.MainModule.GetAllTypes(); + foreach (var type in types) + { + if (type.IsValueType) continue; + if (type.Interfaces.All(x => x.InterfaceType.FullName != typeof(IEnumerator).FullName)) continue; + + StaticLogger.LogDebug($"coroutine patch: patching type {type.FullName}"); + + var moveNext = type.Methods.FirstOrDefault(x => + x.Name is "MoveNext" or "System.Collections.IEnumerator.MoveNext" && + x.Parameters.Count == 0); + var current = + type.Properties.FirstOrDefault(x => x.Name is "System.Collections.IEnumerator.Current" or "Current") + ?.GetMethod; + + // MoveNext + if (moveNext?.HasBody is true) + { + var moveNextPrefix = assembly.MainModule.ImportReference( + typeof(CoroutineManagerManual).GetMethod(nameof(CoroutineManagerManual.CoroutineMoveNextPrefix))); + + var body = moveNext.Body; + body.SimplifyMacros(); + var il = body.GetILProcessor(); + var first = body.Instructions.First(); + var resultVar = new VariableDefinition(assembly.MainModule.TypeSystem.Boolean); + body.Variables.Add(resultVar); + // TODO: create a utility where I can use harmony-type hooks but with preload patcher + il.InsertBefore(first, il.Create(OpCodes.Ldc_I4_0)); + il.InsertBefore(first, il.Create(OpCodes.Stloc, resultVar)); + il.InsertBefore(first, il.Create(OpCodes.Ldarg_0)); + il.InsertBefore(first, il.Create(OpCodes.Ldloca, resultVar)); + il.InsertBefore(first, il.Create(OpCodes.Call, moveNextPrefix)); + il.InsertBefore(first, il.Create(OpCodes.Brtrue, first)); + il.InsertBefore(first, il.Create(OpCodes.Ldloc, resultVar)); + il.InsertBefore(first, il.Create(OpCodes.Ret)); + body.Optimize(); + } + + if (current?.HasBody is not true) continue; + + var currentPostfix = assembly.MainModule.ImportReference( + typeof(CoroutineManagerManual).GetMethod(nameof(CoroutineManagerManual.CoroutineCurrentPostfix))); + + // get_Current + var body2 = current.Body; + body2.SimplifyMacros(); + var il2 = body2.GetILProcessor(); + var resultVar2 = new VariableDefinition(assembly.MainModule.TypeSystem.Object); + body2.Variables.Add(resultVar2); + + foreach (var inst in body2.Instructions.ToArray()) + { + if (inst.OpCode != OpCodes.Ret) continue; + il2.InsertBeforeInstructionReplace(inst, il2.Create(OpCodes.Stloc, resultVar2)); + il2.InsertBeforeInstructionReplace(inst, il2.Create(OpCodes.Ldarg_0)); + il2.InsertBeforeInstructionReplace(inst, il2.Create(OpCodes.Ldloca, resultVar2)); + il2.InsertBeforeInstructionReplace(inst, il2.Create(OpCodes.Call, currentPostfix)); + il2.InsertBeforeInstructionReplace(inst, il2.Create(OpCodes.Ldloc, resultVar2)); + } + + body2.Optimize(); + } + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs b/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs index 8d7b3fe27..e5b69f7ba 100644 --- a/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs +++ b/UniTAS/Patcher/Patches/Preloader/MonoBehaviourPatch.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Linq; using System.Reflection; using HarmonyLib; @@ -8,6 +9,7 @@ using UniTAS.Patcher.ContainerBindings.UnityEvents; using UniTAS.Patcher.Extensions; using UniTAS.Patcher.Interfaces; +using UniTAS.Patcher.ManualServices; using UniTAS.Patcher.Utils; namespace UniTAS.Patcher.Patches.Preloader; @@ -139,7 +141,7 @@ public override void Patch(ref AssemblyDefinition assembly) var (eventMethodName, eventMethodArgs) = eventMethodPair; // try finding method with no parameters - var eventMethodsMatch = type.GetMethods().Where(x => x.Name == eventMethodName).ToList(); + var eventMethodsMatch = type.GetMethods().Where(x => x.Name == eventMethodName && !x.IsStatic).ToList(); var foundMethod = eventMethodsMatch.FirstOrDefault(m => !m.HasParameters); @@ -195,9 +197,38 @@ public override void Patch(ref AssemblyDefinition assembly) } } - il.InsertBeforeInstructionReplace(firstInstruction, il.Create(OpCodes.Ret), + var ignoreRet = il.Create(OpCodes.Ret); + il.InsertBeforeInstructionReplace(firstInstruction, ignoreRet, InstructionReplaceFixType.ExceptionRanges); + // is it an IEnumerator + if (foundMethod.ReturnType.FullName == typeof(IEnumerator).SaneFullName()) + { + StaticLogger.Trace($"this method is an IEnumerator"); + + var iEnumeratorRef = assembly.MainModule.ImportReference(typeof(IEnumerator)); + var rets = il.Body.Instructions.Where(x => x != ignoreRet && x.OpCode == OpCodes.Ret).ToArray(); + var trackerInvoke = assembly.MainModule.ImportReference( + typeof(CoroutineManagerManual).GetMethod(nameof(CoroutineManagerManual.MonoBehNewCoroutine))); + + var returnTemp = new VariableDefinition(iEnumeratorRef); + il.Body.Variables.Add(returnTemp); + + foreach (var ret in rets) + { + // 1. dupe return value + // 2. store return value + // 3. load `this` + // 4. load return value + // 5. call tracker + il.InsertBeforeInstructionReplace(ret, il.Create(OpCodes.Dup)); + il.InsertBeforeInstructionReplace(ret, il.Create(OpCodes.Stloc, returnTemp)); + il.InsertBeforeInstructionReplace(ret, il.Create(OpCodes.Ldarg_0)); + il.InsertBeforeInstructionReplace(ret, il.Create(OpCodes.Ldloc, returnTemp)); + il.InsertBeforeInstructionReplace(ret, il.Create(OpCodes.Call, trackerInvoke)); + } + } + foundMethod.Body.OptimizeMacros(); } diff --git a/UniTAS/Patcher/Patches/Preloader/StaticCtorHeaders.cs b/UniTAS/Patcher/Patches/Preloader/StaticCtorHeaders.cs index ed2375731..f8d70c3ae 100644 --- a/UniTAS/Patcher/Patches/Preloader/StaticCtorHeaders.cs +++ b/UniTAS/Patcher/Patches/Preloader/StaticCtorHeaders.cs @@ -240,7 +240,7 @@ public static void StaticCtorStart() var stackCount = invokeStack.Count; var typeSaneFullName = type.SaneFullName(); - StaticLogger.Log.LogDebug( + StaticLogger.Trace( $"Start of static ctor {typeSaneFullName}, stack count: {stackCount}, thread id: {Thread.CurrentThread.ManagedThreadId}"); if (IsNotFirstInvoke(type)) return; StaticLogger.Trace("First static ctor invoke"); @@ -281,21 +281,20 @@ public static void StaticCtorEnd() throw new NullReferenceException("Could not find type of static ctor, something went horribly wrong"); } - var threadId = Thread.CurrentThread.ManagedThreadId; - - StaticLogger.Log.LogDebug($"End of static ctor {type.SaneFullName()}, thread id: {threadId}"); + StaticLogger.Trace( + $"End of static ctor {type.SaneFullName()}, thread id: {Thread.CurrentThread.ManagedThreadId}"); var invokeStack = CctorInvokeStack.Value; invokeStack.RemoveAt(invokeStack.Count - 1); - StaticLogger.Log.LogDebug($"stack count: {invokeStack.Count}"); + StaticLogger.Trace($"stack count: {invokeStack.Count}"); if (IsNotFirstInvoke(type)) return; // add only if not in ignore list if (PendingIgnoreAddingInvokeList.Value.Remove(type)) return; - StaticLogger.Log.LogDebug($"Adding type {type} to static ctor invoke list"); + StaticLogger.Trace($"Adding type {type} to static ctor invoke list"); ClassStaticInfoTracker.AddStaticCtorForTracking(type); } diff --git a/UniTAS/Patcher/Patches/Preloader/UnityInitInvoke.cs b/UniTAS/Patcher/Patches/Preloader/UnityInitInvoke.cs index ab2a82d4b..f5f3ae31c 100644 --- a/UniTAS/Patcher/Patches/Preloader/UnityInitInvoke.cs +++ b/UniTAS/Patcher/Patches/Preloader/UnityInitInvoke.cs @@ -108,7 +108,7 @@ private void TryHookRuntimeInits(AssemblyDefinition assembly) ILCodeUtils.MethodInvokeHook(assembly, initMethod, AccessTools.Method(typeof(InvokeTracker), nameof(InvokeTracker.OnUnityInit))); - LogHook(assembly, type.Name, initMethod.Name); + LogHook(assembly, type.FullName, initMethod.Name); } } } @@ -197,7 +197,7 @@ private static void TryHookUnityEvent(AssemblyDefinition assembly) { ILCodeUtils.MethodInvokeHook(assembly, method, AccessTools.Method(typeof(InvokeTracker), nameof(InvokeTracker.OnUnityInit))); - LogHook(assembly, type.Name, "Awake"); + LogHook(assembly, type.FullName, "Awake"); } } } diff --git a/UniTAS/Patcher/SafeAPI/UnityEngine/AssetBundle.cs b/UniTAS/Patcher/SafeAPI/UnityEngine/AssetBundle.cs new file mode 100644 index 000000000..d46f2bf77 --- /dev/null +++ b/UniTAS/Patcher/SafeAPI/UnityEngine/AssetBundle.cs @@ -0,0 +1,12 @@ +using System; +using HarmonyLib; +using UniTAS.Patcher.Extensions; +using ue = UnityEngine; + +namespace UniTAS.Patcher.SafeAPI.UnityEngine; + +public static class AssetBundle +{ + public static readonly Action UnloadAllAssetBundles = + AccessTools.Method(typeof(ue.AssetBundle), "UnloadAllAssetBundles")?.MethodDelegate>(); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/GameExecutionControllers/IMonoBehaviourController.cs b/UniTAS/Patcher/Services/GameExecutionControllers/IMonoBehaviourController.cs index 9aba0fb62..2f06d8f47 100644 --- a/UniTAS/Patcher/Services/GameExecutionControllers/IMonoBehaviourController.cs +++ b/UniTAS/Patcher/Services/GameExecutionControllers/IMonoBehaviourController.cs @@ -1,7 +1,11 @@ +using System.Collections; +using System.Collections.Generic; + namespace UniTAS.Patcher.Services.GameExecutionControllers; public interface IMonoBehaviourController { bool PausedExecution { get; set; } bool PausedUpdate { get; set; } + HashSet IgnoreCoroutines { get; } } \ No newline at end of file diff --git a/UniTAS/Patcher/Services/IPatchReverseInvoker.cs b/UniTAS/Patcher/Services/IPatchReverseInvoker.cs index c322f1caa..1eeb9e7dd 100644 --- a/UniTAS/Patcher/Services/IPatchReverseInvoker.cs +++ b/UniTAS/Patcher/Services/IPatchReverseInvoker.cs @@ -4,13 +4,14 @@ namespace UniTAS.Patcher.Services; public interface IPatchReverseInvoker { - bool Invoking { get; } + bool Invoking { get; set; } void Invoke(Action method); void Invoke(Action method, T1 arg1); + void Invoke(Action method, T1 arg1, T2 arg2, T3 arg3, T4 arg4); void Invoke(Action5 method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5); TRet Invoke(Func method); TRet Invoke(Func method, T arg1); TRet Invoke(Func method, T1 arg1, T2 arg2); - delegate void Action5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5); + delegate void Action5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5); } \ No newline at end of file diff --git a/UniTAS/Patcher/Services/Trackers/UpdateTrackInfo/ICoroutineRunningObjectsTracker.cs b/UniTAS/Patcher/Services/Trackers/UpdateTrackInfo/ICoroutineRunningObjectsTracker.cs deleted file mode 100644 index b32b77b8d..000000000 --- a/UniTAS/Patcher/Services/Trackers/UpdateTrackInfo/ICoroutineRunningObjectsTracker.cs +++ /dev/null @@ -1,8 +0,0 @@ -using UnityEngine; - -namespace UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; - -public interface ICoroutineRunningObjectsTracker -{ - void NewCoroutine(MonoBehaviour instance); -} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/Trackers/UpdateTrackInfo/ICoroutineTracker.cs b/UniTAS/Patcher/Services/Trackers/UpdateTrackInfo/ICoroutineTracker.cs new file mode 100644 index 000000000..22bf53493 --- /dev/null +++ b/UniTAS/Patcher/Services/Trackers/UpdateTrackInfo/ICoroutineTracker.cs @@ -0,0 +1,15 @@ +using System.Collections; +using UnityEngine; + +namespace UniTAS.Patcher.Services.Trackers.UpdateTrackInfo; + +public interface ICoroutineTracker +{ + void NewCoroutine(MonoBehaviour instance, IEnumerator routine); + + // `object` replacement for accessing this function in preload patcher + void NewCoroutine(object instance, IEnumerator routine); + void NewCoroutine(MonoBehaviour instance, string methodName, object value); + bool CoroutineMoveNextPrefix(IEnumerator instance, ref bool result); + void CoroutineCurrentPostfix(IEnumerator instance, ref object result); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/AsyncOperationTracker.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/AsyncOperationTracker.cs index 1c137fc6e..333491b30 100644 --- a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/AsyncOperationTracker.cs +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/AsyncOperationTracker.cs @@ -1,305 +1,658 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Reflection; +using System.Text; using HarmonyLib; +using UniTAS.Patcher.Extensions; +using UniTAS.Patcher.Implementations.UnitySafeWrappers.SceneManagement; using UniTAS.Patcher.Interfaces.DependencyInjection; using UniTAS.Patcher.Interfaces.Events.SoftRestart; using UniTAS.Patcher.Interfaces.Events.UnityEvents.DontRunIfPaused; -using UniTAS.Patcher.Interfaces.Events.UnityEvents.RunEvenPaused; using UniTAS.Patcher.Models.UnitySafeWrappers.SceneManagement; using UniTAS.Patcher.Models.Utils; using UniTAS.Patcher.Services.Logging; +using UniTAS.Patcher.Services.UnityInfo; +using UniTAS.Patcher.Services.UnitySafeWrappers; using UniTAS.Patcher.Services.UnitySafeWrappers.Wrappers; using UniTAS.Patcher.Utils; using UnityEngine; +using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; // ReSharper disable once ClassNeverInstantiated.Global [Singleton] -public class AsyncOperationTracker(ISceneWrapper sceneWrapper, ILogger logger) - : ISceneLoadTracker, IAssetBundleCreateRequestTracker, IAssetBundleRequestTracker, - IOnLastUpdateUnconditional, IAsyncOperationIsInvokingOnComplete, IOnPreGameRestart, IOnUpdateActual, - IOnStartActual, IOnFixedUpdateActual +public class AsyncOperationTracker : IAsyncOperationTracker, ISceneLoadTracker, IAssetBundleCreateRequestTracker, + IAssetBundleRequestTracker, IOnLastUpdateActual, IAsyncOperationIsInvokingOnComplete, IOnPreGameRestart, + IOnUpdateActual, IOnEndOfFrameActual, IOnFixedUpdateActual, IOnStartActual, IOnAwakeActual, + IAssetBundleTracker, ISceneOverride, IAsyncOperationOverride, + IResourceAsyncTracker { - private readonly HashSet _tracked = new(new HashUtils.ReferenceComparer()); + private bool _isInvokingOnComplete; + private readonly ISceneManagerWrapper _sceneManagerWrapper; + private readonly ILogger _logger; + private readonly IGameBuildScenesInfo _gameBuildScenesInfo; + private readonly IUnityInstanceWrapFactory _wrapFactory; + + public AsyncOperationTracker(ISceneManagerWrapper sceneManagerWrapper, ILogger logger, + IGameBuildScenesInfo gameBuildScenesInfo, IUnityInstanceWrapFactory wrapFactory) + { + _sceneManagerWrapper = sceneManagerWrapper; + _logger = logger; + _gameBuildScenesInfo = gameBuildScenesInfo; + _wrapFactory = wrapFactory; + _loaded = [GetSceneInfo(0)]; + } - // any scene related things are stored as a list - // it is a list of scenes in load order - private readonly List _asyncLoads = new(); + private readonly Dictionary _tracked = []; - // not allowed to be disabled anymore - private readonly HashSet _allowSceneActivationNotAllowed = new(); + // all async operations queued up in order + private readonly List _ops = []; + private readonly List _pendingLoadCallbacks = []; - // this can have unload operations too, which is why its Either - private readonly List> _pendingLoadCallbacks = new(); - private readonly List _asyncLoadStalls = new(); private readonly Dictionary _assetBundleCreateRequests = new(); + private readonly Dictionary _assetBundleRequests = new(); - private readonly Dictionary _assetBundleRequests = new(); + private readonly Dictionary _bundleSceneNames = new(); // path -> name + private readonly Dictionary> _bundleSceneShortPaths = new(); // path -> short path - private readonly Dictionary _allowSceneActivationValue = - new(new HashUtils.ReferenceComparer()); + private readonly Dictionary> _bundleScenePaths = + new(new HashUtils.ReferenceComparer()); - private readonly HashSet _isDone = new(new HashUtils.ReferenceComparer()); + private bool _sceneLoadSync; - private class AssetBundleRequestData(Object singleResult = null, Object[] multipleResults = null) - { - public Object SingleResult { get; } = singleResult; - public Object[] MultipleResults { get; } = multipleResults; - } + private static readonly Func GetAllScenePaths = AccessTools + .Method(typeof(AssetBundle), "GetAllScenePaths")?.MethodDelegate>(); + + private int _sceneStructHandleId = int.MaxValue / 2; + + // loaded in this game session + private readonly HashSet _loaded; public void OnPreGameRestart() { - _asyncLoads.Clear(); + SafeAPI.UnityEngine.AssetBundle.UnloadAllAssetBundles?.Invoke(true); + + foreach (var bundle in _assetBundleCreateRequests.Values) + { + if (bundle == null) continue; + bundle.Unload(true); + } + + _ops.Clear(); _pendingLoadCallbacks.Clear(); - _asyncLoadStalls.Clear(); _assetBundleCreateRequests.Clear(); _assetBundleRequests.Clear(); - _allowSceneActivationValue.Clear(); - _isDone.Clear(); _tracked.Clear(); - _allowSceneActivationNotAllowed.Clear(); + _bundleScenePaths.Clear(); + _bundleSceneNames.Clear(); + _bundleSceneShortPaths.Clear(); + DummyScenes.Clear(); + LoadingScenes.Clear(); LoadingSceneCount = 0; + _sceneLoadSync = false; + _sceneStructHandleId = int.MaxValue / 2; + _loaded.Clear(); + _loaded.Add(GetSceneInfo(0)); + _firstMatchUnloadPaths.Clear(); + _subSceneTracker.Clear(); } - public void OnLastUpdateUnconditional() + public void OnLastUpdateActual() { - if (_asyncLoads.Count == 0) return; + _firstMatchUnloadPaths.Clear(); + + if (_sceneLoadSync) + { + foreach (var loadData in _ops) + { + LoadOp(loadData); + } + + _ops.Clear(); + return; + } - foreach (var loadData in _asyncLoads) + if (_ops.Count == 0) return; + + var removes = new List(); + var foundLoad = false; + for (var i = 0; i < _ops.Count; i++) { - logger.LogDebug( - $"force loading scene via OnLastUpdate, name: {loadData.SceneName}, index: {loadData.SceneBuildIndex}, manually loading in loop"); - sceneWrapper.TrackSceneCount = false; - sceneWrapper.LoadSceneAsync(loadData.SceneName, loadData.SceneBuildIndex, loadData.LoadSceneMode, - loadData.LocalPhysicsMode, true); - sceneWrapper.TrackSceneCount = true; + var load = _ops[i]; + + if (!foundLoad) + { + if (load is AsyncSceneLoadData data) + { + foundLoad = true; + if (data.DelayFrame) break; - _pendingLoadCallbacks.Add(loadData); - var op = loadData.AsyncOperation; - if (op == null) continue; - _allowSceneActivationNotAllowed.Remove(op); + var op = load.Op; + if (op != null) + { + var trackState = _tracked[load.Op]; + if (trackState.NotAllowedToStall || !trackState.AllowSceneActivation) break; + } + } + + LoadOp(load); + removes.Add(i); + continue; + } + + if (load is AsyncSceneLoadData) break; + // add the rest of the operations + LoadOp(load); + removes.Add(i); } - _asyncLoads.Clear(); + foreach (var i in ((IEnumerable)removes).Reverse()) + { + _ops.RemoveAt(i); + } } - public void FixedUpdateActual() => CallPendingCallbacks(); - public void UpdateActual() => CallPendingCallbacks(); - public void StartActual() => CallPendingCallbacks(); - - private void CallPendingCallbacks() + private void ProcessOpsUntilOp(AsyncOperation op) { - if (_pendingLoadCallbacks.Count == 0) return; + if (_ops.Count == 0) return; + + var foundIdx = -1; + var pendingCallbacks = new List(); + for (var i = 0; i < _ops.Count; i++) + { + var load = _ops[i]; + + _logger.LogDebug($"processing operation {op}"); + load.Load(); + pendingCallbacks.Add(load); + + if (load.Op != op) continue; + foundIdx = i; + break; + } + + if (foundIdx < 0) + { + _logger.LogError($"operation was not found in the _ops list, did you actually check for tracked?"); + return; + } + + _ops.RemoveRange(0, foundIdx + 1); // to allow the scene to be findable, invoke when scene loads on update - var callbacks = new List>(_pendingLoadCallbacks.Count); - foreach (var data in _pendingLoadCallbacks) + if (pendingCallbacks.Count > 0) + _logger.LogDebug($"async op: {pendingCallbacks.Count} callbacks to be processed"); + + foreach (var data in pendingCallbacks) { - callbacks.Add(data); - var op = data.IsLeft ? data.Left.AsyncOperation : data.Right; - if (op == null) continue; - _isDone.Add(op); + var dataOp = data.Op; + if (dataOp == null) continue; + var state = _tracked[dataOp]; + state.IsDone = true; + _tracked[dataOp] = state; } - // prevent allowSceneActivation to be False on event callback - _pendingLoadCallbacks.Clear(); +#if TRACE + var loadOrUnload = false; +#endif - foreach (var data in callbacks) + foreach (var data in pendingCallbacks) { - LoadingSceneCount--; - AsyncOperation op; - if (data.IsLeft) - { - var left = data.Left; - if (left.LoadSceneMode == LoadSceneMode.Additive) - sceneWrapper.SceneCount++; - else - sceneWrapper.SceneCount = 1; + data.Callback(); +#if TRACE + if (!loadOrUnload && data is AsyncSceneLoadData or AsyncSceneUnloadData) + loadOrUnload = true; +#endif + if (data.Op == null) continue; + InvokeOnComplete(data.Op); + } - op = left.AsyncOperation; - } - else +#if TRACE + if (!loadOrUnload) return; + + StaticLogger.Trace("scene stack has changed"); + + var sceneCount = _sceneManagerWrapper.SceneCount; + for (var i = 0; i < sceneCount; i++) + { + var scene = _sceneManagerWrapper.GetSceneAt(i); + StaticLogger.Trace($"scene: name = `{scene.Name}`, path = `{scene.Path}`, index = `{scene.BuildIndex}`"); + } +#endif + } + + public bool Yield(AsyncOperation asyncOperation) + { + if (!_tracked.TryGetValue(asyncOperation, out var data)) + { + WarnAsyncOperationAPI(asyncOperation); + return false; + } + + _logger.LogDebug($"yield on async operation {asyncOperation}"); + if (data.Yield) return true; + data.Yield = true; + _tracked[asyncOperation] = data; + return true; + } + + public void OnEndOfFrame() => CallPendingCallbacks(true); + + public void AwakeActual() => CallPendingCallbacks(false); + + public void StartActual() => CallPendingCallbacks(false); + + public void FixedUpdateActual() => CallPendingCallbacks(false); + + private void LoadOp(IAsyncOperation op) + { + _logger.LogDebug($"processing operation {op}"); + op.Load(); + _pendingLoadCallbacks.Add(op); + } + + public void UpdateActual() + { + _sceneLoadSync = false; + + // doesn't matter, as long as next frame happens, before the game update adds more scenes, delay is gone + foreach (var op in _ops) + { + if (op is AsyncSceneLoadData data) + data.DelayFrame = false; + } + + CallPendingCallbacks(false); + } + + private void CallPendingCallbacks(bool endOfFrame) + { + if (_pendingLoadCallbacks.Count == 0) return; + + // to allow the scene to be findable, invoke when scene loads on update + var callbacks = new List<(int, IAsyncOperation )>(); + for (var i = 0; i < _pendingLoadCallbacks.Count; i++) + { + var data = _pendingLoadCallbacks[i]; + var op = data.Op; + if (op != null) { - op = data.Right; + var state = _tracked[op]; + state.IsDone = true; + _tracked[op] = state; + if (endOfFrame != state.Yield) continue; } - if (op == null) continue; - InvokeOnComplete(op); + callbacks.Add((i, data)); + } + + // separate loop since I wanna work backwards just here + foreach (var (i, _) in ((IEnumerable<(int, IAsyncOperation)>)callbacks).Reverse()) + { + _pendingLoadCallbacks.RemoveAt(i); + } + +#if TRACE + var loadOrUnload = false; +#endif + + _logger.LogDebug($"async op: {callbacks.Count} callbacks to be processed, endOfFrame: {endOfFrame}"); + + foreach (var (_, data) in callbacks) + { + data.Callback(); +#if TRACE + if (!loadOrUnload && data is AsyncSceneLoadData or AsyncSceneUnloadData) + loadOrUnload = true; +#endif + if (data.Op == null) continue; + InvokeOnComplete(data.Op); } + +#if TRACE + if (!loadOrUnload) return; + + StaticLogger.Trace("scene stack has changed"); + + var sceneCount = _sceneManagerWrapper.SceneCount; + for (var i = 0; i < sceneCount; i++) + { + var scene = _sceneManagerWrapper.GetSceneAt(i); + StaticLogger.Trace($"scene: name = `{scene.Name}`, path = `{scene.Path}`, index = `{scene.BuildIndex}`"); + } +#endif } - public void NewAssetBundleRequest(AsyncOperation asyncOperation, Object assetBundleRequest) + public void NewAssetBundleRequest(AsyncOperation op, AssetBundle assetBundle, string name, Type type, + bool withSubAssets) { - _assetBundleRequests.Add(asyncOperation, new(assetBundleRequest)); - _tracked.Add(asyncOperation); - _isDone.Add(asyncOperation); - InvokeOnComplete(asyncOperation); + _tracked.Add(op, new AsyncOperationData()); + _ops.Add(new NewAssetBundleRequestData(op, assetBundle, name, type, withSubAssets, this)); } - public void NewAssetBundleRequestMultiple(AsyncOperation asyncOperation, Object[] assetBundleRequestArray) + public void NewAssetBundleCreateRequest(AsyncOperation op, string path, uint crc, ulong offset) { - _assetBundleRequests.Add(asyncOperation, new(multipleResults: assetBundleRequestArray)); - _tracked.Add(asyncOperation); - _isDone.Add(asyncOperation); - InvokeOnComplete(asyncOperation); + _tracked.Add(op, new AsyncOperationData()); + _ops.Add(new NewAssetBundleFromFileData(op, path, crc, offset, this)); } - public void NewAssetBundleCreateRequest(AsyncOperation asyncOperation, AssetBundle assetBundle) + public void NewAssetBundleCreateRequest(AsyncOperation op, byte[] binary, uint crc) { - _assetBundleCreateRequests.Add(asyncOperation, assetBundle); - _tracked.Add(asyncOperation); - _isDone.Add(asyncOperation); - InvokeOnComplete(asyncOperation); + _tracked.Add(op, new AsyncOperationData()); + _ops.Add(new NewAssetBundleFromMemoryData(op, binary, crc, this)); } - public void AsyncSceneUnload(AsyncOperation asyncOperation) + public void NewAssetBundleCreateRequest(AsyncOperation op, Stream stream, uint crc, uint managedReadBufferSize) + { + _tracked.Add(op, new AsyncOperationData()); + _ops.Add(new NewAssetBundleFromStreamData(op, stream, crc, managedReadBufferSize, this)); + } + + // silly but simple and efficient enough check + private readonly HashSet _firstMatchUnloadPaths = []; + + public void AsyncSceneUnload(ref AsyncOperation asyncOperation, Either scene, object options) { - logger.LogDebug("async scene unload"); + _logger.LogDebug("async scene unload, " + (scene.IsLeft ? $"name: {scene.Left}" : $"index: {scene.Right}")); - _tracked.Add(asyncOperation); - _pendingLoadCallbacks.Add(asyncOperation); - sceneWrapper.SceneCount--; + var sceneInfo = GetSceneInfo(scene); + if (sceneInfo == null || !_loaded.Contains(sceneInfo)) + { + throw new ArgumentException("Scene to unload is invalid"); + } + + if (_sceneManagerWrapper.LoadedSceneCountDummy + LoadingSceneCount == 1) + { + _logger.LogDebug("there is only 1 scene loaded, cannot unload, skipping this operation"); + asyncOperation = null; + return; + } + + if (_firstMatchUnloadPaths.Contains(sceneInfo.Path)) + { + _logger.LogDebug("scene is already to be unloaded, skipping this operation"); + asyncOperation = null; + return; + } + + _firstMatchUnloadPaths.Add(sceneInfo.Path); + + SceneWrapper sceneFound = null; + var sceneCount = _sceneManagerWrapper.SceneCount; + for (var i = 0; i < sceneCount; i++) + { + // shouldn't be null, if unity version is so old Scene struct doesn't exist, async unload also shouldn't exist neither + var sceneWrapper = _sceneManagerWrapper.GetSceneAt(i); + if (sceneWrapper.Path != sceneInfo.Path) continue; + sceneFound = sceneWrapper; + break; + } + + _tracked.Add(asyncOperation, new AsyncOperationData()); + _ops.Add(new AsyncSceneUnloadData(asyncOperation, options, sceneFound, ResolveDummyHandle(sceneFound!.Handle), + this)); + _sceneManagerWrapper.LoadedSceneCountDummy--; LoadingSceneCount++; } + public void AsyncSceneUnload(ref AsyncOperation asyncOperation, object scene, object options) + { + var sceneWrap = _wrapFactory.Create(scene); + _logger.LogDebug($"async scene unload, {sceneWrap.Path}"); + + if (_sceneManagerWrapper.LoadedSceneCountDummy + LoadingSceneCount == 1) + { + _logger.LogDebug("there is only 1 scene loaded, cannot unload, skipping this operation"); + asyncOperation = null; + return; + } + + var handle = ResolveDummyHandle(sceneWrap.Handle); + + if (_ops.Any(x => x is AsyncSceneUnloadData d && d.SceneWrapper.Handle == handle)) + { + _logger.LogDebug("scene is already to be unloaded, skipping this operation"); + asyncOperation = null; + return; + } + + _firstMatchUnloadPaths.Add(sceneWrap.Path); + + _tracked.Add(asyncOperation, new AsyncOperationData()); + _ops.Add(new AsyncSceneUnloadData(asyncOperation, options, sceneWrap, handle, this)); + _sceneManagerWrapper.LoadedSceneCountDummy--; + LoadingSceneCount++; + } + + private int ResolveDummyHandle(int handle) + { + var dummySceneIndex = DummyScenes.FindIndex(x => x.dummyScene.TrackingHandle == handle); + return dummySceneIndex >= 0 ? DummyScenes[dummySceneIndex].actualScene?.Handle ?? handle : handle; + } + public void AsyncSceneLoad(string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, - LocalPhysicsMode localPhysicsMode, AsyncOperation asyncOperation) + LocalPhysicsMode localPhysicsMode, ref AsyncOperation asyncOperation) { + Either scene = sceneBuildIndex >= 0 ? sceneBuildIndex : sceneName; + if (InvalidSceneLoadAndLog(scene, out var sceneInfo)) + { + asyncOperation = null; + return; + } + LoadingSceneCount++; + CreateDummySceneStruct(scene, asyncOperation); + + _tracked.Add(asyncOperation, new AsyncOperationData()); + + _logger.LogDebug( + $"async scene load, {sceneName}, index: {sceneBuildIndex}, loadSceneMode: {loadSceneMode}, localPhysicsMode: {localPhysicsMode}"); + var loadData = + new AsyncSceneLoadData(sceneName, sceneBuildIndex, loadSceneMode, localPhysicsMode, asyncOperation, + sceneInfo, this); + _ops.Add(loadData); - _tracked.Add(asyncOperation); + if (!_sceneLoadSync) return; + _logger.LogDebug("load scene sync is happening for next frame"); - logger.LogDebug($"async scene load, {asyncOperation.GetHashCode()}"); - _asyncLoads.Add(new(sceneName, sceneBuildIndex, loadSceneMode, localPhysicsMode, asyncOperation)); + // sync load is going to happen, make this load too + loadData.DelayFrame = false; + var data = _tracked[asyncOperation]; + data.NotAllowedToStall = true; + _tracked[asyncOperation] = data; } public void NonAsyncSceneLoad(string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, LocalPhysicsMode localPhysicsMode) { - LoadingSceneCount++; + Either scene = sceneBuildIndex >= 0 ? sceneBuildIndex : sceneName; + if (InvalidSceneLoadAndLog(scene, out var sceneInfo)) return; + _logger.LogDebug( + $"scene load, {sceneName}, index: {sceneBuildIndex}, loadSceneMode: {loadSceneMode}, localPhysicsMode: {localPhysicsMode}"); - // first, all stalls are moved to load - _asyncLoads.AddRange(_asyncLoadStalls); - foreach (var sceneData in _asyncLoads) + if (!_sceneLoadSync) { - logger.LogDebug( - $"force loading scene via non-async scene load, name: {sceneData.SceneName}, index: {sceneData.SceneBuildIndex}"); - _allowSceneActivationNotAllowed.Add(sceneData.AsyncOperation); - } + _sceneLoadSync = true; + + // first, all stalls are moved to load + foreach (var pair in _tracked.ToList()) + { + var op = pair.Key; + var data = pair.Value; + data.AllowSceneActivation = true; + data.NotAllowedToStall = true; + _tracked[op] = data; + } + + foreach (var sceneData in _ops) + { + // delays are all gone now + if (sceneData is not AsyncSceneLoadData data) continue; - _asyncLoadStalls.Clear(); + data.DelayFrame = false; + _logger.LogDebug("force loading scene via non-async scene load"); + } + } // next, queue the non async scene load - logger.LogDebug($"scene load, {sceneName}, index: {sceneBuildIndex}"); - _asyncLoads.Add(new(sceneName, sceneBuildIndex, loadSceneMode, localPhysicsMode, null)); + LoadingSceneCount++; + CreateDummySceneStruct(scene, null); + + _ops.Add(new AsyncSceneLoadData(sceneName, sceneBuildIndex, loadSceneMode, localPhysicsMode, null, + sceneInfo, this)); } - public void AllowSceneActivation(bool allow, AsyncOperation asyncOperation) + private bool InvalidSceneLoadAndLog(Either scene, out SceneInfo sceneInfo) { - logger.LogDebug($"allow scene activation {allow}, {new StackTrace()}"); + sceneInfo = GetSceneInfo(scene); + if (sceneInfo != null) return false; - if (allow) - { - var stallIndex = _asyncLoadStalls.FindIndex(d => d.AsyncOperation == asyncOperation); - if (stallIndex < 0) return; - StoreAllowSceneActivation(asyncOperation, true); - if (_allowSceneActivationNotAllowed.Contains(asyncOperation)) return; - _asyncLoads.Add(_asyncLoadStalls[stallIndex]); - _asyncLoadStalls.RemoveAt(stallIndex); - logger.LogDebug("restored scene activation"); - } - else - { - var loadIndex = _asyncLoads.FindIndex(d => d.AsyncOperation == asyncOperation); - if (loadIndex < 0) return; - StoreAllowSceneActivation(asyncOperation, false); - if (_allowSceneActivationNotAllowed.Contains(asyncOperation)) return; - _asyncLoadStalls.Add(_asyncLoads[loadIndex]); - _asyncLoads.RemoveAt(loadIndex); - logger.LogDebug("Added scene to stall list"); - } + var errorBuilder = new StringBuilder(); + + errorBuilder.AppendLine( + scene.IsLeft + ? $"Scene '{scene.Left}' couldn't be loaded because it has not been added to the build settings or the AssetBundle has not been loaded." + : $"Scene with build index: {scene.Right} couldn't be loaded because it has not been added to the build settings."); + + errorBuilder.Append("To add a scene to the build settings use the menu File->Build Settings..."); + Debug.LogError(errorBuilder); + return true; } - private void StoreAllowSceneActivation(AsyncOperation asyncOperation, bool allow) + public void AllowSceneActivation(bool allow, AsyncOperation asyncOperation) { - _allowSceneActivationValue[asyncOperation] = allow; + if (!_tracked.TryGetValue(asyncOperation, out var state)) + { + WarnAsyncOperationAPI(asyncOperation); + return; + } + + _logger.LogDebug($"allow scene activation {allow}, {new StackTrace()}"); + + state.AllowSceneActivation = allow; + _tracked[asyncOperation] = state; + if (state.NotAllowedToStall) return; + + _logger.LogDebug(allow ? "restored scene activation" : "scene load is stalled"); } public bool GetAllowSceneActivation(AsyncOperation asyncOperation, out bool state) { - if (_allowSceneActivationValue.TryGetValue(asyncOperation, out state)) + if (_tracked.TryGetValue(asyncOperation, out var data)) { + state = data.AllowSceneActivation; return true; } - // not found, is it even tracked? + WarnAsyncOperationAPI(asyncOperation); state = false; - return _tracked.Contains(asyncOperation); + return false; } public bool IsDone(AsyncOperation asyncOperation, out bool isDone) { - if (_isDone.Contains(asyncOperation)) + if (_tracked.TryGetValue(asyncOperation, out var data)) { - isDone = true; + isDone = data.IsDone; return true; } + WarnAsyncOperationAPI(asyncOperation); isDone = false; - return _tracked.Contains(asyncOperation); + return false; } public bool Progress(AsyncOperation asyncOperation, out float progress) { - if (_isDone.Contains(asyncOperation)) + if (_ops.Any(load => load is AsyncSceneUnloadData && ReferenceEquals(load.Op, asyncOperation))) { - progress = 1f; + progress = 0f; return true; } - progress = 0.9f; - return _tracked.Contains(asyncOperation); + if (_tracked.TryGetValue(asyncOperation, out var data)) + { + progress = data.IsDone ? 1f : 0.9f; + return true; + } + + WarnAsyncOperationAPI(asyncOperation); + progress = 0f; + return false; } - public AssetBundle GetAssetBundleCreateRequest(AsyncOperation asyncOperation) + public bool GetAssetBundleCreateRequest(AsyncOperation asyncOperation, out AssetBundle assetBundle) { - return _assetBundleCreateRequests.TryGetValue(asyncOperation, out var assetBundle) - ? assetBundle - : null; + if (_assetBundleCreateRequests.TryGetValue(asyncOperation, out assetBundle)) return true; + + // bundle access before operation completes, force load + if (_tracked.ContainsKey(asyncOperation)) + { + ProcessOpsUntilOp(asyncOperation); + _assetBundleCreateRequests.TryGetValue(asyncOperation, out assetBundle); + return true; + } + + WarnAsyncOperationAPI(asyncOperation); + return false; } - public object GetAssetBundleRequest(AsyncOperation asyncOperation) + public bool GetAssetBundleRequest(AsyncOperation asyncOperation, out Object obj) { - return _assetBundleRequests.TryGetValue(asyncOperation, out var assetBundleRequest) - ? assetBundleRequest.SingleResult - : null; + if (_assetBundleRequests.TryGetValue(asyncOperation, out var data)) + { + obj = data[0]; + return true; + } + + // access before op completion, force load + if (_tracked.ContainsKey(asyncOperation)) + { + ProcessOpsUntilOp(asyncOperation); + obj = _assetBundleRequests.TryGetValue(asyncOperation, out var objs) ? objs[0] : null; + return true; + } + + WarnAsyncOperationAPI(asyncOperation); + obj = null; + return false; } - public object GetAssetBundleRequestMultiple(AsyncOperation asyncOperation) + public bool GetAssetBundleRequestMultiple(AsyncOperation asyncOperation, out Object[] objects) { - return _assetBundleRequests.TryGetValue(asyncOperation, out var assetBundleRequest) - ? assetBundleRequest.MultipleResults - : null; + if (_assetBundleRequests.TryGetValue(asyncOperation, out objects)) + return true; + + // access before op completion, force load + if (_tracked.ContainsKey(asyncOperation)) + { + ProcessOpsUntilOp(asyncOperation); + _assetBundleRequests.TryGetValue(asyncOperation, out objects); + return true; + } + + WarnAsyncOperationAPI(asyncOperation); + return false; } private readonly MethodBase _invokeCompletionEvent = AccessTools.Method("UnityEngine.AsyncOperation:InvokeCompletionEvent", Type.EmptyTypes); - private bool _isInvokingOnComplete; - public bool IsInvokingOnComplete(AsyncOperation asyncOperation, out bool wasInvoked) { - if (_tracked.Contains(asyncOperation)) + if (_tracked.ContainsKey(asyncOperation)) { wasInvoked = _isInvokingOnComplete; return true; } - wasInvoked = default; + // WarnAsyncOperationAPI(asyncOperation); + wasInvoked = false; return false; } @@ -307,26 +660,601 @@ private void InvokeOnComplete(AsyncOperation asyncOperation) { if (_invokeCompletionEvent == null) return; _isInvokingOnComplete = true; - logger.LogDebug("invoking completion event"); - _invokeCompletionEvent.Invoke(asyncOperation, null); + try + { + _invokeCompletionEvent.Invoke(asyncOperation, null); + } + catch (Exception e) + { + Debug.LogError(e); + } + _isInvokingOnComplete = false; } - // I don't think I need to track unload, they get invoked immediately, but maybe I should delay unload till end of frame and track till then? // note: this property exists here, because this class handles async scene loading, not scene wrapper public int LoadingSceneCount { get; private set; } + public List<(DummyScene dummyScene, SceneWrapper actualScene)> DummyScenes { get; } = new(); + + public List LoadingScenes { get; } = new(); + + // tracks sub scene for dummy scenes during load + private readonly Dictionary _subSceneTracker = new(); + + public void Unload(AssetBundle assetBundle) + { + if (!_bundleScenePaths.TryGetValue(assetBundle, out var paths)) return; + foreach (var path in paths) + { + _bundleSceneNames.Remove(path); + _bundleSceneShortPaths.Remove(path); + } + + _bundleScenePaths.Remove(assetBundle); + } + + public void UnloadBundleAsync(AsyncOperation op, AssetBundle bundle, bool unloadAllLoadedObjects) + { + _tracked.Add(op, new AsyncOperationData()); + _ops.Add(new UnloadBundleAsyncData(op, bundle, unloadAllLoadedObjects)); + } + + public void ResourceLoadAsync(AsyncOperation op, string path, Type type) + { + _tracked.Add(op, new AsyncOperationData()); + _ops.Add(new ResourceLoadAsyncData(op, path, type)); + } + + public void ResourceUnloadAsync(AsyncOperation op) + { + _tracked.Add(op, new AsyncOperationData()); + _ops.Add(new DummyOpData(op)); + } + + private readonly Type _sceneStruct = AccessTools.TypeByName("UnityEngine.SceneManagement.Scene"); + + private void CreateDummySceneStruct(Either load, AsyncOperation op) + { + if (_sceneStruct == null) return; + var sceneInfo = GetSceneInfo(load); + if (sceneInfo == null) return; + + var instance = _wrapFactory.Create(null); + instance.Handle = _sceneStructHandleId; + var dummyScene = new DummyScene(instance.Instance, _sceneStructHandleId, sceneInfo, op); + LoadingScenes.Add(dummyScene); + DummyScenes.Add((dummyScene, null)); + _sceneStructHandleId++; + } + + private void ReplaceDummyScene(AsyncOperation op, string path) + { + var sceneCount = _sceneManagerWrapper.SceneCount; + if (sceneCount < 0) + { + _logger.LogWarning("Scene count is invalid"); + return; + } + + if (LoadingScenes.Count == 0) + { + _logger.LogWarning("Usually there are loading scenes, but there isn't"); + return; + } + + var actualScene = _sceneManagerWrapper.GetSceneAt(sceneCount - 1); + var loadingInfoIndex = op == null + ? LoadingScenes.FindIndex(x => x.Op == null && x.LoadingScene.Path == path) + : LoadingScenes.FindIndex(x => ReferenceEquals(x.Op, op)); + var dummyHandle = LoadingScenes[loadingInfoIndex].TrackingHandle; + LoadingScenes.RemoveAt(loadingInfoIndex); + var dummySceneIndex = DummyScenes.FindIndex(x => x.dummyScene.TrackingHandle == dummyHandle); + DummyScenes[dummySceneIndex] = (DummyScenes[dummySceneIndex].dummyScene, actualScene); + + // sub scene value copied + if (_subSceneTracker.TryGetValue(dummyHandle, out var sub)) + { + actualScene.IsSubScene = sub; + } + } + + private SceneInfo GetSceneInfo(Either scene) + { + if (scene.IsRight) + { + var buildIndex = scene.Right; + if (buildIndex >= _gameBuildScenesInfo.IndexToPath.Count) return null; + var path = _gameBuildScenesInfo.IndexToPath[buildIndex]; + return new SceneInfo(path, _gameBuildScenesInfo.PathToName[path], buildIndex); + } + + var name = scene.Left; + + // asset bundle scenes take priority in name based matching + if (_bundleScenePaths.Values.Any(scenes => scenes.Contains(name))) + { + return new SceneInfo(name, name, -1); + } + + var bundleScenePath2 = _bundleSceneShortPaths.FirstOrDefault(x => x.Value.Contains(name)); + if (bundleScenePath2.Key != null) + { + return new SceneInfo(bundleScenePath2.Key, name, -1); + } + + // note: asset bundle with same scene name as already loaded ones fails to load in the first place + var bundleScenePath = _bundleSceneNames.FirstOrDefault(x => x.Value == name); + if (bundleScenePath.Key != null) + { + return new SceneInfo(bundleScenePath.Key, name, -1); + } + + // fallback to builtin scenes + if (_gameBuildScenesInfo.PathToIndex.TryGetValue(name, out var index2)) + { + // sceneName is path + return new SceneInfo(name, _gameBuildScenesInfo.PathToName[name], index2); + } + + if (_gameBuildScenesInfo.ShortPathToPath.TryGetValue(name, out var path3)) + { + // sceneName is short path + return new SceneInfo(path3, name, _gameBuildScenesInfo.PathToIndex[path3]); + } + + if (_gameBuildScenesInfo.NameToPath.TryGetValue(name, out var path2)) + { + // sceneName is name + return new SceneInfo(path2, name, _gameBuildScenesInfo.PathToIndex[path2]); + } + + if (GetAllScenePaths == null) + { + throw new InvalidOperationException( + "Tried to search for scene name but scene wasn't found, this may be wrong as " + + "AssetBundle.GetAllScenePaths doesn't exist in this unity version"); + } + + return null; + } + + public bool IsLoaded(int handle, out bool loaded) + { + if (_ops.Any(x => x is AsyncSceneUnloadData data && data.RealHandle == ResolveDummyHandle(handle))) + { + loaded = false; + return true; + } + + loaded = false; + return false; + } + + public bool IsSubScene(int handle, out bool subScene) + { + if (_subSceneTracker.TryGetValue(handle, out var sub)) + { + subScene = sub; + return true; + } + + subScene = false; + return false; + } + + public bool SetSubScene(int handle, bool subScene) + { + if (DummyScenes.FindIndex(x => x.dummyScene.TrackingHandle == handle) < 0) return false; + _subSceneTracker[handle] = subScene; + return true; + } + + public bool GetPriority(AsyncOperation op, out int priority) + { + if (_tracked.TryGetValue(op, out var data)) + { + priority = data.Priority; + return true; + } + + priority = 0; + WarnAsyncOperationAPI(op); + return false; + } + + public bool SetPriority(AsyncOperation op, int priority) + { + if (!_tracked.TryGetValue(op, out var data)) + { + WarnAsyncOperationAPI(op); + return false; + } + + data.Priority = priority; + _tracked[op] = data; + return true; + } + + private void WarnAsyncOperationAPI(AsyncOperation op) + { + _logger.LogError( + $"found untracked async operation, hashcode: {op.GetHashCode()}, type name: {op.GetType().SaneFullName()}" + + $", class structure: {DebugHelp.PrintClass(op)}, API use at {new StackTrace()}"); + } + + public bool ManagedInstance(AsyncOperation asyncOperation) + { + return _tracked.ContainsKey(asyncOperation); + } + + private struct AsyncOperationData() + { + public bool IsDone = false; + public bool AllowSceneActivation = true; + public bool NotAllowedToStall = false; + public int Priority = 0; + public bool Yield = false; + } + + private class NewAssetBundleFromFileData( + AsyncOperation op, + string path, + uint crc, + ulong offset, + AsyncOperationTracker tracker) : IAsyncOperation + { + private static readonly Func LoadFromFile = + (AccessTools.Method(typeof(AssetBundle), "LoadFromFile_Internal", + [typeof(string), typeof(uint), typeof(ulong)]) ?? + AccessTools.Method(typeof(AssetBundle), "LoadFromFile", + [typeof(string), typeof(uint), typeof(ulong)])) + .MethodDelegate>(); + + public void Load() + { + var bundle = LoadFromFile(path, crc, offset); + if (bundle == null) return; + tracker._assetBundleCreateRequests.Add(Op, bundle); + tracker.AddScenesFromBundle(bundle); + } + + public void Callback() + { + } + + public AsyncOperation Op { get; } = op; + + public override string ToString() + { + return $"new asset bundle, path: {path}, crc: {crc}, offset: {offset}"; + } + } + + private class NewAssetBundleFromMemoryData( + AsyncOperation op, + byte[] binary, + uint crc, + AsyncOperationTracker tracker) : IAsyncOperation + { + private static readonly Func LoadFromMemoryInternal = AccessTools.Method( + typeof(AssetBundle), + "LoadFromMemory_Internal", + [typeof(byte[]), typeof(uint)]).MethodDelegate>(); + + public void Load() + { + var bundle = LoadFromMemoryInternal(binary, crc); + if (bundle == null) return; + tracker._assetBundleCreateRequests.Add(Op, bundle); + tracker.AddScenesFromBundle(bundle); + } + + public void Callback() + { + } + + public AsyncOperation Op { get; } = op; + + public override string ToString() + { + return $"new asset bundle, byte len: {binary.Length}, crc: {crc}"; + } + } + + private class NewAssetBundleFromStreamData( + AsyncOperation op, + Stream stream, + uint crc, + uint managedReadBufferSize, + AsyncOperationTracker tracker) : IAsyncOperation + { + private static readonly Func LoadFromStreamInternal = AccessTools.Method( + typeof(AssetBundle), + "LoadFromStreamInternal", [typeof(Stream), typeof(uint), typeof(uint)]) + .MethodDelegate>(); + + public void Load() + { + var bundle = LoadFromStreamInternal(stream, crc, managedReadBufferSize); + if (bundle == null) return; + tracker._assetBundleCreateRequests.Add(Op, bundle); + tracker.AddScenesFromBundle(bundle); + } + + public void Callback() + { + } + + public AsyncOperation Op { get; } = op; + + public override string ToString() + { + return $"new asset bundle, stream: {stream}, crc: {crc}, managedReadBufferSize: {managedReadBufferSize}"; + } + } + + private void AddScenesFromBundle(AssetBundle bundle) + { + if (GetAllScenePaths == null) + { + _logger.LogWarning( + "GetAllScenePaths function wasn't found, cannot discover scenes in asset bundles"); + return; + } + + var paths = GetAllScenePaths(bundle); + foreach (var path in paths) + { + _bundleSceneNames.Add(path, Path.GetFileNameWithoutExtension(path)); + } + + foreach (var path in paths) + { + var partial = path.Remove(path.Length - ".unity".Length); + var allPartialNames = new HashSet { partial }; + if (partial.StartsWith("Assets/")) + { + allPartialNames.Add(partial.Remove(0, "Assets/".Length)); + } + + _bundleSceneShortPaths.Add(path, allPartialNames); + } + + _bundleScenePaths[bundle] = [..paths]; + } + + private class UnloadBundleAsyncData(AsyncOperation op, AssetBundle bundle, bool unloadAllLoadedObjects) + : IAsyncOperation + { + public void Load() + { + bundle.Unload(unloadAllLoadedObjects); + } + + public void Callback() + { + } + + public AsyncOperation Op { get; } = op; + + public override string ToString() + { + return $"unload bundle, bundle: {bundle}, unload all objects: {unloadAllLoadedObjects}"; + } + } + + private class ResourceLoadAsyncData(AsyncOperation op, string path, Type type) : IAsyncOperation + { + public void Load() + { + var resultTraverse = Traverse.Create(Op); + resultTraverse.Field("m_Path").SetValue(path); + resultTraverse.Field("m_Type").SetValue(type); + } + + public void Callback() + { + } + + public AsyncOperation Op { get; } = op; + + public override string ToString() + { + return $"resource load, path: {path}, type: {type.SaneFullName()}"; + } + } + private class AsyncSceneLoadData( string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, LocalPhysicsMode localPhysicsMode, - AsyncOperation asyncOperation) + AsyncOperation asyncOperation, + SceneInfo sceneInfo, + AsyncOperationTracker tracker) : IAsyncOperation + { + public AsyncOperation Op { get; } = asyncOperation; + public bool DelayFrame = true; + + public void Load() + { + tracker._sceneManagerWrapper.TrackSceneCountDummy = false; + tracker._sceneManagerWrapper.LoadSceneAsync(sceneName, sceneBuildIndex, loadSceneMode, localPhysicsMode, + true); + tracker._sceneManagerWrapper.TrackSceneCountDummy = true; + + // call after scene is loaded + tracker.ReplaceDummyScene(Op, sceneInfo.Path); + + tracker._loaded.Add(sceneInfo); + if (Op == null || !tracker._tracked.TryGetValue(Op, out var state)) return; + state.NotAllowedToStall = false; + tracker._tracked[Op] = state; + } + + public void Callback() + { + tracker.LoadingSceneCount--; + if (loadSceneMode == LoadSceneMode.Additive) + tracker._sceneManagerWrapper.LoadedSceneCountDummy++; + else + tracker._sceneManagerWrapper.LoadedSceneCountDummy = 1; + } + + public override string ToString() + { + return $"scene load, name: {sceneName}, index: {sceneBuildIndex}"; + } + } + + private class AsyncSceneUnloadData( + AsyncOperation asyncOperation, + object options, + SceneWrapper sceneWrapper, + int realHandle, + AsyncOperationTracker tracker) : IAsyncOperation + { + public AsyncOperation Op { get; } = asyncOperation; + public int RealHandle { get; } = realHandle; + public SceneWrapper SceneWrapper { get; } = sceneWrapper; + + public void Load() + { + tracker._logger.LogWarning( + "THIS OPERATION MIGHT BREAK THE GAME, scene unloading patch is using an unstable unity function, and it may fail"); + tracker._sceneManagerWrapper.TrackSceneCountDummy = false; + tracker._sceneManagerWrapper.UnloadSceneAsync(SceneWrapper.Name, -1, options, true, + out var success); + tracker._sceneManagerWrapper.TrackSceneCountDummy = true; + if (success) return; + tracker._logger.LogError("async unload failed, prepare for game to maybe go nuts"); + } + + public void Callback() + { + tracker.LoadingSceneCount--; + } + + public override string ToString() + { + return $"scene unload, name: {SceneWrapper.Name}"; + } + } + + private class NewAssetBundleRequestData( + AsyncOperation op, + AssetBundle bundle, + string name, + Type type, + bool withSubAssets, + AsyncOperationTracker tracker) : IAsyncOperation + { + private static readonly Func LoadAssetInternal = AccessTools.Method( + typeof(AssetBundle), + "LoadAsset_Internal", + [typeof(string), typeof(Type)])?.MethodDelegate>(); + + private static readonly Func LoadAssetWithSubAssetsInternal = AccessTools + .Method(typeof(AssetBundle), + "LoadAssetWithSubAssets_Internal", + [typeof(string), typeof(Type)])?.MethodDelegate>(); + + public void Load() + { + var objs = withSubAssets + ? LoadAssetWithSubAssetsInternal(bundle, name, type) + : [LoadAssetInternal(bundle, name, type)]; + + tracker._assetBundleRequests.Add(Op, objs); + } + + public void Callback() + { + } + + public AsyncOperation Op { get; } = op; + } + + private class DummyOpData(AsyncOperation op) : IAsyncOperation { - public string SceneName { get; } = sceneName; - public int SceneBuildIndex { get; } = sceneBuildIndex; - public LoadSceneMode LoadSceneMode { get; } = loadSceneMode; - public LocalPhysicsMode LocalPhysicsMode { get; } = localPhysicsMode; - public AsyncOperation AsyncOperation { get; } = asyncOperation; + public void Load() + { + } + + public void Callback() + { + } + + public AsyncOperation Op { get; } = op; + } + + public class SceneInfo(string path, string name, int buildIndex) + { + private bool Equals(SceneInfo other) + { + return string.Equals(Path, other.Path, StringComparison.InvariantCulture) && + string.Equals(Name, other.Name, StringComparison.InvariantCulture) && BuildIndex == other.BuildIndex; + } + + public override bool Equals(object obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((SceneInfo)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Path != null ? StringComparer.InvariantCulture.GetHashCode(Path) : 0; + hashCode = (hashCode * 397) ^ (Name != null ? StringComparer.InvariantCulture.GetHashCode(Name) : 0); + hashCode = (hashCode * 397) ^ BuildIndex; + return hashCode; + } + } + + public static bool operator ==(SceneInfo left, SceneInfo right) + { + return Equals(left, right); + } + + public static bool operator !=(SceneInfo left, SceneInfo right) + { + return !Equals(left, right); + } + + public string Path { get; } = path; + public string Name { get; } = name; + public int BuildIndex { get; } = buildIndex; + } + + public readonly struct DummyScene( + object dummySceneStruct, + int trackingHandle, + SceneInfo loadingScene, + AsyncOperation op) + { + public readonly object DummySceneStruct = dummySceneStruct; + public readonly int TrackingHandle = trackingHandle; + public readonly SceneInfo LoadingScene = loadingScene; + public readonly AsyncOperation Op = op; + } + + private interface IAsyncOperation + { + /// + /// Invoked when operation has to be completed, which means you call the equivalent non-async operation for this operation + /// + void Load(); + + /// + /// Invoked when callback is about to happen + /// + void Callback(); + + AsyncOperation Op { get; } } } \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleCreateRequestTracker.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleCreateRequestTracker.cs index 98fba38ea..629854a30 100644 --- a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleCreateRequestTracker.cs +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleCreateRequestTracker.cs @@ -1,9 +1,12 @@ +using System.IO; using UnityEngine; namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; public interface IAssetBundleCreateRequestTracker { - void NewAssetBundleCreateRequest(AsyncOperation asyncOperation, AssetBundle assetBundle); - AssetBundle GetAssetBundleCreateRequest(AsyncOperation asyncOperation); + void NewAssetBundleCreateRequest(AsyncOperation op, string path, uint crc, ulong offset); + void NewAssetBundleCreateRequest(AsyncOperation op, byte[] binary, uint crc); + void NewAssetBundleCreateRequest(AsyncOperation op, Stream stream, uint crc, uint managedReadBufferSize); + bool GetAssetBundleCreateRequest(AsyncOperation asyncOperation, out AssetBundle assetBundle); } \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleRequestTracker.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleRequestTracker.cs index 11c986a21..9deae6bd0 100644 --- a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleRequestTracker.cs +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleRequestTracker.cs @@ -1,12 +1,12 @@ +using System; using UnityEngine; +using Object = UnityEngine.Object; namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; public interface IAssetBundleRequestTracker { - void NewAssetBundleRequest(AsyncOperation asyncOperation, Object assetBundleRequest); - void NewAssetBundleRequestMultiple(AsyncOperation asyncOperation, Object[] assetBundleRequestArray); - - object GetAssetBundleRequest(AsyncOperation asyncOperation); - object GetAssetBundleRequestMultiple(AsyncOperation asyncOperation); + void NewAssetBundleRequest(AsyncOperation op, AssetBundle assetBundle, string name, Type type, bool withSubAssets); + bool GetAssetBundleRequest(AsyncOperation asyncOperation, out Object obj); + bool GetAssetBundleRequestMultiple(AsyncOperation asyncOperation, out Object[] objects); } \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleTracker.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleTracker.cs new file mode 100644 index 000000000..9435b8907 --- /dev/null +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAssetBundleTracker.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; + +public interface IAssetBundleTracker +{ + void Unload(AssetBundle assetBundle); + void UnloadBundleAsync(AsyncOperation op, AssetBundle bundle, bool unloadAllLoadedObjects); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAsyncOperationOverride.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAsyncOperationOverride.cs new file mode 100644 index 000000000..01d90a4ce --- /dev/null +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAsyncOperationOverride.cs @@ -0,0 +1,31 @@ +using UnityEngine; + +namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; + +public interface IAsyncOperationOverride +{ + bool GetPriority(AsyncOperation op, out int priority); + bool SetPriority(AsyncOperation op, int priority); + + /// + /// Gets the progress + /// + /// The AsyncOperation to check + /// Progress of the AsyncOperation if it is tracked by UniTAS + /// True if is our tracked instance, otherwise it is some user created one + bool Progress(AsyncOperation asyncOperation, out float progress); + + /// + /// Gets if done + /// + /// The AsyncOperation to check + /// If IsDone is true or false, USE THIS NOT THE RETURN VALUE + /// True if is our tracked instance, otherwise it is some user created one + bool IsDone(AsyncOperation asyncOperation, out bool isDone); + + /// + /// For when the operation is yielded + /// + /// If instance is tracked + bool Yield(AsyncOperation asyncOperation); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAsyncOperationTracker.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAsyncOperationTracker.cs new file mode 100644 index 000000000..95c08b3b8 --- /dev/null +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IAsyncOperationTracker.cs @@ -0,0 +1,8 @@ +using UnityEngine; + +namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; + +public interface IAsyncOperationTracker +{ + bool ManagedInstance(AsyncOperation asyncOperation); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IResourceAsyncTracker.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IResourceAsyncTracker.cs new file mode 100644 index 000000000..68ca4e3b0 --- /dev/null +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/IResourceAsyncTracker.cs @@ -0,0 +1,10 @@ +using System; +using UnityEngine; + +namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; + +public interface IResourceAsyncTracker +{ + void ResourceLoadAsync(AsyncOperation op, string path, Type type); + void ResourceUnloadAsync(AsyncOperation op); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/ISceneLoadTracker.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/ISceneLoadTracker.cs index 0bd5d8d30..83581b165 100644 --- a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/ISceneLoadTracker.cs +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/ISceneLoadTracker.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; +using UniTAS.Patcher.Implementations.UnitySafeWrappers.SceneManagement; using UniTAS.Patcher.Models.UnitySafeWrappers.SceneManagement; +using UniTAS.Patcher.Models.Utils; using UnityEngine; namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; @@ -6,30 +9,16 @@ namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; public interface ISceneLoadTracker { void AsyncSceneLoad(string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, - LocalPhysicsMode localPhysicsMode, AsyncOperation asyncOperation); + LocalPhysicsMode localPhysicsMode, ref AsyncOperation asyncOperation); void NonAsyncSceneLoad(string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, LocalPhysicsMode localPhysicsMode); - void AsyncSceneUnload(AsyncOperation asyncOperation); + void AsyncSceneUnload(ref AsyncOperation asyncOperation, Either scene, object options); - void AllowSceneActivation(bool allow, AsyncOperation asyncOperation); - - /// - /// Gets if done - /// - /// The AsyncOperation to check - /// If IsDone is true or false, USE THIS NOT THE RETURN VALUE - /// True if is our tracked instance, otherwise it is some user created one - bool IsDone(AsyncOperation asyncOperation, out bool isDone); + void AsyncSceneUnload(ref AsyncOperation asyncOperation, object scene, object options); - /// - /// Gets the progress - /// - /// The AsyncOperation to check - /// Progress of the AsyncOperation if it is tracked by UniTAS - /// True if is our tracked instance, otherwise it is some user created one - bool Progress(AsyncOperation asyncOperation, out float progress); + void AllowSceneActivation(bool allow, AsyncOperation asyncOperation); /// /// Gets allowSceneActivation state @@ -40,4 +29,12 @@ void NonAsyncSceneLoad(string sceneName, int sceneBuildIndex, LoadSceneMode load bool GetAllowSceneActivation(AsyncOperation asyncOperation, out bool state); int LoadingSceneCount { get; } + + List<(AsyncOperationTracker.DummyScene dummyScene, SceneWrapper actualScene)> DummyScenes { get; } + + /// + /// The LoadingScene will provide dummy data for the fake instances, but once the scene is actually loaded, + /// SceneWrapper will give real information for the fake instance to use + /// + List LoadingScenes { get; } } \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityAsyncOperationTracker/ISceneOverride.cs b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/ISceneOverride.cs new file mode 100644 index 000000000..077115abd --- /dev/null +++ b/UniTAS/Patcher/Services/UnityAsyncOperationTracker/ISceneOverride.cs @@ -0,0 +1,16 @@ +namespace UniTAS.Patcher.Services.UnityAsyncOperationTracker; + +public interface ISceneOverride +{ + /// + /// Returns true if override loaded state + /// + bool IsLoaded(int handle, out bool loaded); + + /// + /// Returns true if override loaded state + /// + bool IsSubScene(int handle, out bool subScene); + + bool SetSubScene(int handle, bool subScene); +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityEvents/IMonoBehEventInvoker.cs b/UniTAS/Patcher/Services/UnityEvents/IMonoBehEventInvoker.cs index 2d700d1c8..d0f6bd358 100644 --- a/UniTAS/Patcher/Services/UnityEvents/IMonoBehEventInvoker.cs +++ b/UniTAS/Patcher/Services/UnityEvents/IMonoBehEventInvoker.cs @@ -10,5 +10,5 @@ public interface IMonoBehEventInvoker void InvokeLastUpdate(); void InvokeOnGUI(); void InvokeOnEnable(); - void CoroutineFixedUpdate(); + void InvokeEndOfFrame(); } \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnityInfo/IGameBuildScenesInfo.cs b/UniTAS/Patcher/Services/UnityInfo/IGameBuildScenesInfo.cs new file mode 100644 index 000000000..923c9c3ba --- /dev/null +++ b/UniTAS/Patcher/Services/UnityInfo/IGameBuildScenesInfo.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace UniTAS.Patcher.Services.UnityInfo; + +public interface IGameBuildScenesInfo +{ + Dictionary PathToIndex { get; } + Dictionary PathToName { get; } + Dictionary NameToPath { get; } + Dictionary ShortPathToPath { get; } + List IndexToPath { get; } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Services/UnitySafeWrappers/IUnityInstanceWrapFactory.cs b/UniTAS/Patcher/Services/UnitySafeWrappers/IUnityInstanceWrapFactory.cs index 47164354c..9711177a2 100644 --- a/UniTAS/Patcher/Services/UnitySafeWrappers/IUnityInstanceWrapFactory.cs +++ b/UniTAS/Patcher/Services/UnitySafeWrappers/IUnityInstanceWrapFactory.cs @@ -1,7 +1,4 @@ -using UniTAS.Patcher.Implementations.UnitySafeWrappers; -using UniTAS.Patcher.Services.UnitySafeWrappers.Wrappers; - -namespace UniTAS.Patcher.Services.UnitySafeWrappers; +namespace UniTAS.Patcher.Services.UnitySafeWrappers; /// /// A factory for creating unity instance wraps with extra functionality or for creating types that may or may not be present in the current unity version diff --git a/UniTAS/Patcher/Services/UnitySafeWrappers/Wrappers/ISceneWrapper.cs b/UniTAS/Patcher/Services/UnitySafeWrappers/Wrappers/ISceneManagerWrapper.cs similarity index 59% rename from UniTAS/Patcher/Services/UnitySafeWrappers/Wrappers/ISceneWrapper.cs rename to UniTAS/Patcher/Services/UnitySafeWrappers/Wrappers/ISceneManagerWrapper.cs index 2f7b6c71d..23cbfb836 100644 --- a/UniTAS/Patcher/Services/UnitySafeWrappers/Wrappers/ISceneWrapper.cs +++ b/UniTAS/Patcher/Services/UnitySafeWrappers/Wrappers/ISceneManagerWrapper.cs @@ -1,12 +1,16 @@ +using JetBrains.Annotations; +using UniTAS.Patcher.Implementations.UnitySafeWrappers.SceneManagement; using UniTAS.Patcher.Models.UnitySafeWrappers.SceneManagement; namespace UniTAS.Patcher.Services.UnitySafeWrappers.Wrappers; -public interface ISceneWrapper +public interface ISceneManagerWrapper { void LoadSceneAsync(string sceneName, int sceneBuildIndex, LoadSceneMode loadSceneMode, LocalPhysicsMode localPhysicsMode, bool mustCompleteNextFrame); + void UnloadSceneAsync(string sceneName, int sceneBuildIndex, object options, bool immediate, out bool success); + void LoadScene(int buildIndex); void LoadScene(string name); @@ -14,12 +18,17 @@ void LoadSceneAsync(string sceneName, int sceneBuildIndex, LoadSceneMode loadSce int ActiveSceneIndex { get; } + [UsedImplicitly] // for test runner string ActiveSceneName { get; } // not really an actual call, but to keep track of stuff - int SceneCount { get; set; } + int LoadedSceneCountDummy { get; set; } + /// /// Disabling this would not update SceneCount from any of the LoadScene functions here /// - bool TrackSceneCount { get; set; } + bool TrackSceneCountDummy { get; set; } + + int SceneCount { get; } + SceneWrapper GetSceneAt(int index); } \ No newline at end of file diff --git a/UniTAS/Patcher/Utils/ExceptionUtils.cs b/UniTAS/Patcher/Utils/ExceptionUtils.cs new file mode 100644 index 000000000..cc2d32665 --- /dev/null +++ b/UniTAS/Patcher/Utils/ExceptionUtils.cs @@ -0,0 +1,31 @@ +using System; +using UnityEngine; + +namespace UniTAS.Patcher.Utils; + +public static class ExceptionUtils +{ + public static void UnityLogErrorOnThrow(Action action, T arg) + { + try + { + action(arg); + } + catch (Exception e) + { + Debug.LogError(e); + } + } + + public static void UnityLogErrorOnThrow(Action action, T1 arg, T2 arg2) + { + try + { + action(arg, arg2); + } + catch (Exception e) + { + Debug.LogError(e); + } + } +} \ No newline at end of file diff --git a/UniTAS/Patcher/Utils/PatchHelper.cs b/UniTAS/Patcher/Utils/PatchHelper.cs index 6f3639f85..838615250 100644 --- a/UniTAS/Patcher/Utils/PatchHelper.cs +++ b/UniTAS/Patcher/Utils/PatchHelper.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using UniTAS.Patcher.Extensions; namespace UniTAS.Patcher.Utils; @@ -11,7 +12,7 @@ public static Exception CleanupIgnoreFail(MethodBase original, Exception ex) { StaticLogger.Log.LogDebug(original == null ? $"Failed to patch, exception: {ex}" - : $"Failed to patch {original}, exception: {ex}"); + : $"Failed to patch {original.DeclaringType.SaneFullName()}: {original}, exception: {ex}"); } return null; diff --git a/UniTAS/UniTAS.sln b/UniTAS/UniTAS.sln index bd36fa64a..56b65705d 100644 --- a/UniTAS/UniTAS.sln +++ b/UniTAS/UniTAS.sln @@ -17,6 +17,7 @@ Global ReleaseTrace|Any CPU = ReleaseTrace|Any CPU ReleaseBench|Any CPU = ReleaseBench|Any CPU ReleaseTest|Any CPU = ReleaseTest|Any CPU + DebugTest|Any CPU = DebugTest|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {47A3D730-E962-4CEA-9FAE-F020EF68649D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -29,6 +30,8 @@ Global {47A3D730-E962-4CEA-9FAE-F020EF68649D}.ReleaseBench|Any CPU.Build.0 = ReleaseBench|Any CPU {47A3D730-E962-4CEA-9FAE-F020EF68649D}.ReleaseTest|Any CPU.ActiveCfg = ReleaseTest|Any CPU {47A3D730-E962-4CEA-9FAE-F020EF68649D}.ReleaseTest|Any CPU.Build.0 = ReleaseTest|Any CPU + {47A3D730-E962-4CEA-9FAE-F020EF68649D}.DebugTest|Any CPU.ActiveCfg = DebugTest|Any CPU + {47A3D730-E962-4CEA-9FAE-F020EF68649D}.DebugTest|Any CPU.Build.0 = DebugTest|Any CPU {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.Debug|Any CPU.Build.0 = Debug|Any CPU {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -39,6 +42,8 @@ Global {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.ReleaseBench|Any CPU.Build.0 = Release|Any CPU {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.ReleaseTest|Any CPU.ActiveCfg = Release|Any CPU {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.ReleaseTest|Any CPU.Build.0 = Release|Any CPU + {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.DebugTest|Any CPU.ActiveCfg = Debug|Any CPU + {91EA9B9D-FE03-4273-BDAF-8AD42EDE1E59}.DebugTest|Any CPU.Build.0 = Debug|Any CPU {71232739-5507-4A37-BE37-1DD5CB3088D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {71232739-5507-4A37-BE37-1DD5CB3088D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {71232739-5507-4A37-BE37-1DD5CB3088D6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -49,6 +54,8 @@ Global {71232739-5507-4A37-BE37-1DD5CB3088D6}.ReleaseTrace|Any CPU.Build.0 = Release|Any CPU {71232739-5507-4A37-BE37-1DD5CB3088D6}.ReleaseTest|Any CPU.ActiveCfg = Release|Any CPU {71232739-5507-4A37-BE37-1DD5CB3088D6}.ReleaseTest|Any CPU.Build.0 = Release|Any CPU + {71232739-5507-4A37-BE37-1DD5CB3088D6}.DebugTest|Any CPU.ActiveCfg = Debug|Any CPU + {71232739-5507-4A37-BE37-1DD5CB3088D6}.DebugTest|Any CPU.Build.0 = Debug|Any CPU {E629E473-B4C5-491C-A773-4549CE9E32A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {E629E473-B4C5-491C-A773-4549CE9E32A3}.Release|Any CPU.Build.0 = Release|Any CPU {E629E473-B4C5-491C-A773-4549CE9E32A3}.ReleaseBench|Any CPU.ActiveCfg = Release|Any CPU @@ -59,6 +66,8 @@ Global {E629E473-B4C5-491C-A773-4549CE9E32A3}.Debug|Any CPU.Build.0 = Release|Any CPU {E629E473-B4C5-491C-A773-4549CE9E32A3}.ReleaseTest|Any CPU.ActiveCfg = Release|Any CPU {E629E473-B4C5-491C-A773-4549CE9E32A3}.ReleaseTest|Any CPU.Build.0 = Release|Any CPU + {E629E473-B4C5-491C-A773-4549CE9E32A3}.DebugTest|Any CPU.ActiveCfg = Release|Any CPU + {E629E473-B4C5-491C-A773-4549CE9E32A3}.DebugTest|Any CPU.Build.0 = Release|Any CPU {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.Debug|Any CPU.ActiveCfg = ReleaseTest|Any CPU {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.Debug|Any CPU.Build.0 = ReleaseTest|Any CPU {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.Release|Any CPU.ActiveCfg = ReleaseTest|Any CPU @@ -69,5 +78,7 @@ Global {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.ReleaseTrace|Any CPU.Build.0 = ReleaseTest|Any CPU {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.ReleaseTest|Any CPU.ActiveCfg = ReleaseTest|Any CPU {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.ReleaseTest|Any CPU.Build.0 = ReleaseTest|Any CPU + {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.DebugTest|Any CPU.ActiveCfg = DebugTest|Any CPU + {0C767EFA-674E-47F9-AE27-BE49C56AC85C}.DebugTest|Any CPU.Build.0 = DebugTest|Any CPU EndGlobalSection EndGlobal