diff --git a/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/Engine.Unity.Shared.csproj b/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/Engine.Unity.Shared.csproj index c78f22723..0e2de1886 100644 --- a/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/Engine.Unity.Shared.csproj +++ b/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/Engine.Unity.Shared.csproj @@ -9,10 +9,6 @@ embedded MelonLoader.Engine.Unity.Shared - - - - diff --git a/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/MelonCoroutines.cs b/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/MelonCoroutines.cs index cb1162a0d..3a78250f4 100644 --- a/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/MelonCoroutines.cs +++ b/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/MelonCoroutines.cs @@ -3,19 +3,23 @@ namespace MelonLoader { - internal abstract class MelonCoroutineInterop + public abstract class MelonCoroutineInterop { - internal abstract object Start(IEnumerator routine); - internal abstract void Stop(object coroutineToken); + public abstract object Start(IEnumerator routine); + public abstract void Stop(object coroutineToken); } public static class MelonCoroutines { - internal static List QueuedCoroutines = new List(); - internal static MelonCoroutineInterop Interop; + private static List QueuedCoroutines = new List(); + private static bool HasProcessedQueue; + public static MelonCoroutineInterop Interop; - internal static void ProcessQueue() + public static void ProcessQueue() { + if (HasProcessedQueue) + return; + HasProcessedQueue = true; foreach (var queuedCoroutine in QueuedCoroutines) Start(queuedCoroutine); QueuedCoroutines.Clear(); @@ -31,6 +35,17 @@ public static object Start(IEnumerator routine) { if (Interop != null) return Interop.Start(routine); + return Queue(routine); + } + + /// + /// Start a new coroutine.
+ /// Coroutines are called at the end of the game Update loops. + ///
+ /// The target routine + /// An object that can be passed to Stop to stop this coroutine + public static object Queue(IEnumerator routine) + { QueuedCoroutines.Add(routine); return routine; } @@ -43,8 +58,14 @@ public static void Stop(object coroutineToken) { if (Interop != null) Interop.Stop(coroutineToken); - else - QueuedCoroutines.Remove(coroutineToken as IEnumerator); + Dequeue(coroutineToken as IEnumerator); + } + + public static void Dequeue(object coroutineToken) + { + IEnumerator routine = coroutineToken as IEnumerator; + if (QueuedCoroutines.Contains(routine)) + QueuedCoroutines.Remove(routine); } } } \ No newline at end of file diff --git a/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/UnityInformationHandler.cs b/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/UnityInformationHandler.cs index 91b1e4b81..8e4b8e383 100644 --- a/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/UnityInformationHandler.cs +++ b/Dependencies/Modules/Engines/Unity/Engine.Unity.Shared/UnityInformationHandler.cs @@ -37,7 +37,7 @@ private static UnityVersion TryParse(string version) return returnval; } - internal static void Setup(string gameDataPath, string unityPlayerPath) + public static void Setup(string gameDataPath, string unityPlayerPath) { //if (!string.IsNullOrEmpty(LoaderConfig.Current.UnityEngine.VersionOverride)) // EngineVersion = TryParse(LoaderConfig.Current.UnityEngine.VersionOverride); diff --git a/Dependencies/Modules/Engines/Unity/Il2Cpp/Unity.Il2Cpp/Il2CppCoroutineInterop.cs b/Dependencies/Modules/Engines/Unity/Il2Cpp/Unity.Il2Cpp/Il2CppCoroutineInterop.cs index 23c87057d..dbe18c0c4 100644 --- a/Dependencies/Modules/Engines/Unity/Il2Cpp/Unity.Il2Cpp/Il2CppCoroutineInterop.cs +++ b/Dependencies/Modules/Engines/Unity/Il2Cpp/Unity.Il2Cpp/Il2CppCoroutineInterop.cs @@ -10,21 +10,20 @@ internal class Il2CppCoroutineInterop : MelonCoroutineInterop internal Il2CppCoroutineInterop(Il2CppSupportComponent component) => Component = component; - internal override object Start(IEnumerator coroutine) + public override object Start(IEnumerator coroutine) { if (Component != null) return Component.StartCoroutine(new Il2CppSystem.Collections.IEnumerator(new Il2CppEnumeratorWrapper(coroutine).Pointer)); - - MelonCoroutines.QueuedCoroutines.Add(coroutine); + MelonCoroutines.Queue(coroutine); return coroutine; } - internal override void Stop(object coroutineToken) + public override void Stop(object coroutineToken) { if (Component != null) Component.StopCoroutine(coroutineToken as Coroutine); else - MelonCoroutines.QueuedCoroutines.Remove(coroutineToken as IEnumerator); + MelonCoroutines.Dequeue(coroutineToken); } } } diff --git a/Dependencies/Modules/Engines/Unity/Mono/Libs/UnityEngine.dll b/Dependencies/Modules/Engines/Unity/Mono/Libs/UnityEngine.dll new file mode 100644 index 000000000..571442868 Binary files /dev/null and b/Dependencies/Modules/Engines/Unity/Mono/Libs/UnityEngine.dll differ diff --git a/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoCoroutineInterop.cs b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoCoroutineInterop.cs new file mode 100644 index 000000000..1a3c27937 --- /dev/null +++ b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoCoroutineInterop.cs @@ -0,0 +1,28 @@ +using System.Collections; +using UnityEngine; + +namespace MelonLoader.Engine.Unity.Mono +{ + internal class MonoCoroutineInterop : MelonCoroutineInterop + { + private MonoSupportComponent Component; + + internal MonoCoroutineInterop(MonoSupportComponent component) + => Component = component; + + public override object Start(IEnumerator coroutine) + { + if (Component != null) + return Component.StartCoroutine(coroutine); + return MelonCoroutines.Start(coroutine); + } + + public override void Stop(object coroutineToken) + { + if (Component != null) + Component.StopCoroutine(coroutineToken as Coroutine); + else + MelonCoroutines.Stop(coroutineToken); + } + } +} diff --git a/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSceneHandler.cs b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSceneHandler.cs new file mode 100644 index 000000000..fd9cc9236 --- /dev/null +++ b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSceneHandler.cs @@ -0,0 +1,100 @@ +using System; +using UnityEngine.SceneManagement; +using System.Collections.Generic; + +namespace MelonLoader.Engine.Unity.Mono +{ + internal class MonoSceneHandler : IDisposable + { + private MonoSupportModule SupportModule; + + internal class SceneInitEvent + { + internal int buildIndex; + internal string name; + internal bool wasLoadedThisTick; + } + private Queue scenesLoaded = new Queue(); + + internal MonoSceneHandler(MonoSupportModule supportModule) + { + SupportModule = supportModule; + + try + { + SceneManager.sceneLoaded += OnSceneLoad; + } + catch (Exception ex) { MelonLogger.Error($"SceneManager.sceneLoaded override failed: {ex}"); } + + try + { + SceneManager.sceneUnloaded += OnSceneUnload; + } + catch (Exception ex) { MelonLogger.Error($"SceneManager.sceneUnloaded override failed: {ex}"); } + } + + ~MonoSceneHandler() + => Dispose(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2013:Do not use ReferenceEquals with value types")] + public void Dispose() + { + try + { + SceneManager.sceneLoaded -= OnSceneLoad; + } + catch (Exception ex) { MelonLogger.Error($"SceneManager.sceneLoaded override failed: {ex}"); } + + try + { + SceneManager.sceneUnloaded -= OnSceneUnload; + } + catch (Exception ex) { MelonLogger.Error($"SceneManager.sceneUnloaded override failed: {ex}"); } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2013:Do not use ReferenceEquals with value types")] + private void OnSceneLoad(Scene scene, LoadSceneMode mode) + { + if (SupportModule.obj == null) + SupportModule.CreateGameObject(); + + if (ReferenceEquals(scene, null)) + return; + + MelonUnityEvents.OnSceneWasLoaded.Invoke(scene.buildIndex, scene.name); + scenesLoaded.Enqueue(new SceneInitEvent { buildIndex = scene.buildIndex, name = scene.name }); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2013:Do not use ReferenceEquals with value types")] + private void OnSceneUnload(Scene scene) + { + if (ReferenceEquals(scene, null)) + return; + + MelonUnityEvents.OnSceneWasUnloaded.Invoke(scene.buildIndex, scene.name); + } + + internal void OnUpdate() + { + if (scenesLoaded.Count > 0) + { + Queue requeue = new Queue(); + SceneInitEvent evt = null; + while ((scenesLoaded.Count > 0) && ((evt = scenesLoaded.Dequeue()) != null)) + { + if (evt.wasLoadedThisTick) + { + MelonUnityEvents.OnSceneWasInitialized.Invoke(evt.buildIndex, evt.name); + } + else + { + evt.wasLoadedThisTick = true; + requeue.Enqueue(evt); + } + } + while ((requeue.Count > 0) && ((evt = requeue.Dequeue()) != null)) + scenesLoaded.Enqueue(evt); + } + } + } +} diff --git a/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSupportComponent.cs b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSupportComponent.cs new file mode 100644 index 000000000..9a54402fe --- /dev/null +++ b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSupportComponent.cs @@ -0,0 +1,132 @@ +using System; +using System.Reflection; +using UnityEngine; +using MelonLoader.Modules; + +namespace MelonLoader.Engine.Unity.Mono +{ + internal class MonoSupportComponent : MonoBehaviour + { + private bool isQuitting; + private bool hadError; + + private MethodInfo SetAsLastSiblingMethod; + + MonoSupportComponent() + { + try + { + SetAsLastSiblingMethod = typeof(Transform).GetMethod("SetAsLastSibling", BindingFlags.Public | BindingFlags.Instance); + if (SetAsLastSiblingMethod == null) + throw new Exception("Unable to find UnityEngine.Transform::SetAsLastSibling"); + } + catch (Exception ex) { LogError("Getting UnityEngine.Transform::SetAsLastSibling", ex); } + } + + private void LogError(string cat, Exception ex) + { + hadError = true; + MelonLogger.Warning($"Exception while {cat}: {ex}"); + MelonLogger.Warning("Melon Events might run before some MonoBehaviour Events"); + } + + internal void SiblingFix() + { + if (hadError) + return; + + try + { + gameObject.transform.SetAsLastSibling(); + transform.SetAsLastSibling(); + } + catch (Exception ex) + { + LogError("Invoking UnityEngine.Transform::SetAsLastSibling", ex); + } + } + + void Start() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + SiblingFix(); + + MelonUnityEvents.OnApplicationLateStart.Invoke(); + } + + void Awake() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + MelonCoroutines.ProcessQueue(); + } + + void Update() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + isQuitting = false; + SiblingFix(); + + ((MonoSupportModule)ModuleInterop.Support).sceneHandler.OnUpdate(); + + MelonUnityEvents.OnUpdate.Invoke(); + } + + void OnDestroy() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + if (!isQuitting) + { + ((MonoSupportModule)ModuleInterop.Support).CreateGameObject(); + return; + } + + OnApplicationDefiniteQuit(); + } + + void OnApplicationQuit() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + isQuitting = true; + MelonUnityEvents.OnApplicationQuit.Invoke(); + } + + void OnApplicationDefiniteQuit() + { + MelonUnityEvents.OnApplicationDefiniteQuit.Invoke(); + } + + void FixedUpdate() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + MelonUnityEvents.OnFixedUpdate.Invoke(); + } + + void LateUpdate() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + MelonUnityEvents.OnLateUpdate.Invoke(); + } + + void OnGUI() + { + if ((ModuleInterop.Support == null) || (((MonoSupportModule)ModuleInterop.Support).component != this)) + return; + + MelonUnityEvents.OnGUI.Invoke(); + } + } +} diff --git a/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSupportModule.cs b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSupportModule.cs new file mode 100644 index 000000000..11692d21c --- /dev/null +++ b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoSupportModule.cs @@ -0,0 +1,41 @@ +using MelonLoader.Modules; +using UnityEngine; + +namespace MelonLoader.Engine.Unity.Mono +{ + internal class MonoSupportModule : MelonSupportModule + { + internal MonoSceneHandler sceneHandler; + + internal GameObject obj; + internal MonoSupportComponent component; + + public override void Initialize() + { + // Initialize Tomlet Unity Object Serialization + MonoTomletProvider.Initialize(); + + // Create Scene Handler + sceneHandler = new(this); + + // Create GameObject and Component + if (component == null) + CreateGameObject(); + } + + internal void CreateGameObject() + { + // Create Support GameObject + obj = new GameObject(); + GameObject.DontDestroyOnLoad(obj); + obj.hideFlags = HideFlags.DontSave; + + // Create Support Component + component = obj.AddComponent(); + component.SiblingFix(); + + // Create Interop for Coroutine Management + MelonCoroutines.Interop = new MonoCoroutineInterop(component); + } + } +} diff --git a/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoTomletProvider.cs b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoTomletProvider.cs new file mode 100644 index 000000000..8978311cd --- /dev/null +++ b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/MonoTomletProvider.cs @@ -0,0 +1,103 @@ +using Tomlet; +using Tomlet.Models; +using UnityEngine; + +namespace MelonLoader.Engine.Unity.Mono +{ + internal static class MonoTomletProvider + { + internal static void Initialize() + { + TomletMain.RegisterMapper(WriteColor, ReadColor); + TomletMain.RegisterMapper(WriteColor32, ReadColor32); + TomletMain.RegisterMapper(WriteVector2, ReadVector2); + TomletMain.RegisterMapper(WriteVector3, ReadVector3); + TomletMain.RegisterMapper(WriteVector4, ReadVector4); + TomletMain.RegisterMapper(WriteQuaternion, ReadQuaternion); + } + + private static Color ReadColor(TomlValue value) + { + float[] floats = MelonPreferences.Mapper.ReadArray(value); + if (floats == null || floats.Length != 4) + return default; + return new Color(floats[0] / 255f, floats[1] / 255f, floats[2] / 255f, floats[3] / 255f); + } + + private static TomlValue WriteColor(Color value) + { + float[] floats = new[] { value.r * 255, value.g * 255, value.b * 255, value.a * 255}; + return MelonPreferences.Mapper.WriteArray(floats); + } + + private static Color32 ReadColor32(TomlValue value) + { + byte[] bytes = MelonPreferences.Mapper.ReadArray(value); + if (bytes == null || bytes.Length != 4) + return default; + return new Color32(bytes[0], bytes[1], bytes[2], bytes[3]); + } + + private static TomlValue WriteColor32(Color32 value) + { + byte[] bytes = new[] { value.r, value.g, value.b, value.a }; + return MelonPreferences.Mapper.WriteArray(bytes); + } + + private static Vector2 ReadVector2(TomlValue value) + { + float[] floats = MelonPreferences.Mapper.ReadArray(value); + if (floats == null || floats.Length != 2) + return default; + return new Vector2(floats[0], floats[1]); + } + + private static TomlValue WriteVector2(Vector2 value) + { + float[] floats = new[] { value.x, value.y }; + return MelonPreferences.Mapper.WriteArray(floats); + } + + private static Vector3 ReadVector3(TomlValue value) + { + float[] floats = MelonPreferences.Mapper.ReadArray(value); + if (floats == null || floats.Length != 3) + return default; + return new Vector3(floats[0], floats[1], floats[2]); + } + + private static TomlValue WriteVector3(Vector3 value) + { + float[] floats = new[] { value.x, value.y, value.z }; + return MelonPreferences.Mapper.WriteArray(floats); + } + + private static Vector4 ReadVector4(TomlValue value) + { + float[] floats = MelonPreferences.Mapper.ReadArray(value); + if (floats == null || floats.Length != 4) + return default; + return new Vector4(floats[0], floats[1], floats[2], floats[3]); + } + + private static TomlValue WriteVector4(Vector4 value) + { + float[] floats = new[] { value.x, value.y, value.z, value.w }; + return MelonPreferences.Mapper.WriteArray(floats); + } + + private static Quaternion ReadQuaternion(TomlValue value) + { + float[] floats = MelonPreferences.Mapper.ReadArray(value); + if (floats == null || floats.Length != 4) + return default; + return new Quaternion(floats[0], floats[1], floats[2], floats[3]); + } + + private static TomlValue WriteQuaternion(Quaternion value) + { + float[] floats = new[] { value.x, value.y, value.z, value.w }; + return MelonPreferences.Mapper.WriteArray(floats); + } + } +} \ No newline at end of file diff --git a/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/Unity.Mono.csproj b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/Unity.Mono.csproj new file mode 100644 index 000000000..de52312bf --- /dev/null +++ b/Dependencies/Modules/Engines/Unity/Mono/Unity.Mono/Unity.Mono.csproj @@ -0,0 +1,29 @@ + + + MelonLoader.Unity.Mono + net35 + Latest + true + $(MLOutDir)/MelonLoader/Dependencies/Engines/Unity/ + true + embedded + MelonLoader.Unity.Mono + + + + False + + + False + + + + + ..\Libs\UnityEngine.dll + false + + + + + + \ No newline at end of file diff --git a/MelonLoader.sln b/MelonLoader.sln index bf122ad95..741115c1e 100644 --- a/MelonLoader.sln +++ b/MelonLoader.sln @@ -52,6 +52,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Mono", "Dependencie EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Mono.Shared", "Dependencies\Modules\Runtimes\Mono\Runtime.Mono.Shared\Runtime.Mono.Shared.csproj", "{AF553B0E-A74D-4F1F-857C-18741FB255E8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Mono", "Dependencies\Modules\Engines\Unity\Mono\Unity.Mono\Unity.Mono.csproj", "{27AD78CF-2EC4-4E02-8CEA-D2533D821C33}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -106,6 +108,10 @@ Global {AF553B0E-A74D-4F1F-857C-18741FB255E8}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF553B0E-A74D-4F1F-857C-18741FB255E8}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF553B0E-A74D-4F1F-857C-18741FB255E8}.Release|Any CPU.Build.0 = Release|Any CPU + {27AD78CF-2EC4-4E02-8CEA-D2533D821C33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27AD78CF-2EC4-4E02-8CEA-D2533D821C33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27AD78CF-2EC4-4E02-8CEA-D2533D821C33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27AD78CF-2EC4-4E02-8CEA-D2533D821C33}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -127,6 +133,7 @@ Global {AEB11019-A160-4A8C-AB56-06E890AF1BA4} = {EFD62F17-3C48-4D77-8F19-1D4BCD62CEFB} {CF071430-B106-4D7E-9AC5-D3E892479BD2} = {5B41B0FE-3B90-4F82-B1B0-061E39C27EBB} {AF553B0E-A74D-4F1F-857C-18741FB255E8} = {5B41B0FE-3B90-4F82-B1B0-061E39C27EBB} + {27AD78CF-2EC4-4E02-8CEA-D2533D821C33} = {3B132718-3D60-44B5-AAF8-BDA2E8BBEDBE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4AB93B1D-1C52-4A80-809D-C28770140E0A} diff --git a/MelonLoader/Modules/ModuleInterop.cs b/MelonLoader/Modules/ModuleInterop.cs index 2159d9157..5c1ae06cd 100644 --- a/MelonLoader/Modules/ModuleInterop.cs +++ b/MelonLoader/Modules/ModuleInterop.cs @@ -3,10 +3,10 @@ namespace MelonLoader.Modules { - internal static class ModuleInterop + public static class ModuleInterop { - internal static MelonEngineModule Engine { get; private set; } - internal static MelonSupportModule Support { get; private set; } + public static MelonEngineModule Engine { get; private set; } + public static MelonSupportModule Support { get; private set; } internal static void StartEngine() {