using System.Diagnostics.CodeAnalysis; using ADOFAIRenderer.Core; using ADOFAIRenderer.Util.Audio; using HarmonyLib; using UnityEngine; namespace ADOFAIRenderer.Patch.Engine; /// /// Unity engine patches that put the game on the offline render clock and the render resolution. /// /// Render clock: while capturing (, stage >= Render) and /// RenderContext.TimePatch is on (default on; a RenderSession can turn it off), the Unity time getters return /// the value captured at the capture start (RenderManager.timeBase, unscaledTimeBase, dspTimeBase, /// frameCountBase, read in SetupPlay) plus the render time . Each rendered frame then /// advances game time by exactly 1 / RenderFPS seconds, independent of how long the frame took to render and encode, and the /// clocks continue from their real values without a jump. The patches only affect managed callers of these getters /// (game and mod scripts). /// /// /// All prefixes run on the thread that calls the getter (normally the main Unity thread) and read RenderManager state /// without locks. /// /// public static class RenderEnvironmentPatches { /// /// Render time of the current frame in seconds: RenderManager.frameCount / RenderFPS. /// frameCount is reset to 0 when capture starts and incremented at the start of every frame (FirstUpdateHook). /// private static double TimeSpent => RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.RenderFPS; /// /// Time.time (scaled game time in seconds, float): returns timeBase + while capturing with /// the time patch on. Time.timeScale is not applied to the render time. /// [HarmonyPatch(typeof(Time), "time", MethodType.Getter)] public static class TimePatch { /// Prefix of the Time.time getter. /// Replacement value in seconds. /// True to run the original getter; false when the render clock value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref float __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = (float)(TimeSpent + RenderManager.Instance.timeBase); return false; } } /// Time.timeAsDouble (double precision of Time.time): timeBase + . [HarmonyPatch(typeof(Time), "timeAsDouble", MethodType.Getter)] public static class TimeAsDoublePatch { /// Prefix of the getter; returns the render clock value while capturing with the time patch on. /// Replacement value in seconds. /// True to run the original getter; false when the render clock value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref double __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = TimeSpent + RenderManager.Instance.timeBase; return false; } } /// /// Time.unscaledTime (time in seconds independent of Time.timeScale, float): unscaledTimeBase + /// . Unscaled time is not advanced by Time.captureDeltaTime (it follows real time), so it is /// replaced as well. /// Reference: https://docs.unity3d.com/ScriptReference/Time-captureDeltaTime.html /// [HarmonyPatch(typeof(Time), "unscaledTime", MethodType.Getter)] public static class UnscaledTimePatch { /// Prefix of the getter; returns the render clock value while capturing with the time patch on. /// Replacement value in seconds. /// True to run the original getter; false when the render clock value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref float __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = (float)(TimeSpent + RenderManager.Instance.unscaledTimeBase); return false; } } /// /// Time.unscaledTimeAsDouble (double precision of Time.unscaledTime): unscaledTimeBase + . /// [HarmonyPatch(typeof(Time), "unscaledTimeAsDouble", MethodType.Getter)] public static class UnscaledTimeAsDoublePatch { /// Prefix of the getter; returns the render clock value while capturing with the time patch on. /// Replacement value in seconds. /// True to run the original getter; false when the render clock value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref double __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = TimeSpent + RenderManager.Instance.unscaledTimeBase; return false; } } /// /// AudioSettings.dspTime (audio system clock in seconds, double): dspTimeBase + . /// The game schedules the song and computes the song position from this clock (scrConductor), so the level advances /// by one render frame per frame. The offline audio mixer uses the same base (RenderManager.dspTimeBase). /// [HarmonyPatch(typeof(AudioSettings), "dspTime", MethodType.Getter)] public static class DspTimePatch { /// Prefix of the getter; returns the render clock value while capturing with the time patch on. /// Replacement value in seconds. /// True to run the original getter; false when the render clock value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref double __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = TimeSpent + RenderManager.Instance.dspTimeBase; return false; } } /// /// AudioSource.PlayScheduled(double time) (start playback at a dspTime): blocked for every AudioSource while /// (stage >= OnScene), so Unity plays no scheduled sound in real time. During /// capture the song and the sound effects are produced offline by OfflineAudioMixer; sound effects reach it through /// the AudioManager._Play and scrConductor.PlayWithEndTime patches (AudioPatches.cs). /// [HarmonyPatch(typeof(AudioSource), "PlayScheduled")] public static class PlayScheduledPatch { /// Prefix of AudioSource.PlayScheduled. /// The AudioSource (unused). /// Scheduled start in dspTime seconds (unused). /// True to run the original method (not rendering); false to skip it (rendering). // ReSharper disable once InconsistentNaming public static bool Prefix(AudioSource __instance, double time) { return !RenderManager.IsActive; } } /// /// Time.frameCount (frames since startup): frameCountBase + RenderManager.frameCount while capturing /// (stage >= Render) with RenderContext.TimePatch on. /// [HarmonyPatch(typeof(Time), "frameCount", MethodType.Getter)] public static class FrameCountPatch { /// Prefix of the getter; returns the replacement value under the conditions in the type summary. /// Replacement value. /// True to run the original getter; false when the replacement value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref int __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = (int)(RenderManager.Instance.frameCount + RenderManager.Instance.frameCountBase); return false; } } /// Time.deltaTime (seconds since the previous frame, float): 1 / RenderFPS. [HarmonyPatch(typeof(Time), "deltaTime", MethodType.Getter)] public static class DeltaTimePatch { /// Prefix of the getter; returns the render clock value while capturing with the time patch on. /// Replacement value in seconds. /// True to run the original getter; false when the render clock value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref float __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS); return false; } } /// Time.unscaledDeltaTime (timeScale-independent frame time in seconds, float): 1 / RenderFPS. [HarmonyPatch(typeof(Time), "unscaledDeltaTime", MethodType.Getter)] public static class UnscaledDeltaTimePatch { /// Prefix of the getter; returns the render clock value while capturing with the time patch on. /// Replacement value in seconds. /// True to run the original getter; false when the render clock value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref float __result) { if (!RenderManager.IsCapturingActive || !RenderContext.TimePatch) return true; __result = (float)(1 / RenderManager.Instance.Configuration.RenderFPS); return false; } } /// /// Screen.width (window width in pixels): RenderConfig.RenderWidth while /// (stage >= OnScene) and RenderContext.ScreenPatch is on (default on). Game code that sizes render textures from /// the screen size (for example scrCamera recreates camRT at Screen.width x Screen.height when the size /// differs) then works at the output resolution independent of the window size. /// [HarmonyPatch(typeof(Screen), "width", MethodType.Getter)] public static class ScreenWidthPatch { /// Prefix of the getter; returns the replacement value under the conditions in the type summary. /// Replacement value. /// True to run the original getter; false when the replacement value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref int __result) { if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true; __result = RenderManager.Instance.Configuration.RenderWidth; return false; } } /// /// Screen.height (window height in pixels): RenderConfig.RenderHeight; same conditions as . /// [HarmonyPatch(typeof(Screen), "height", MethodType.Getter)] public static class ScreenHeightPatch { /// Prefix of the getter; returns the replacement value under the conditions in the type summary. /// Replacement value. /// True to run the original getter; false when the replacement value is returned. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(ref int __result) { if (!RenderManager.IsActive || !RenderContext.ScreenPatch || !RenderManager.Instance) return true; __result = RenderManager.Instance.Configuration.RenderHeight; return false; } } /// /// AudioSource.GetSpectrumData(float[] samples, int channel, FFTWindow window) (FFT magnitudes of the source's current /// output): while capturing (stage >= Render) with AdvancedConfig.ProcessAudio on, fills samples with /// (spectrum of the song at the render time) for any AudioSource. /// No AudioSource plays during capture, so the original would return silence. The game calls it from /// scrConductor, scrBarMaker and the volume trackers for audio-reactive visuals. /// [HarmonyPatch(typeof(AudioSource), "GetSpectrumData", typeof(float[]), typeof(int), typeof(FFTWindow))] public static class GetSpectrumDataPatch { /// Prefix of AudioSource.GetSpectrumData. /// The AudioSource queried (not used; the song spectrum is returned for every source). /// Output array; its length is the number of spectrum bins. /// Channel index requested by the caller. /// FFT window type requested by the caller. /// True to run the original method; false when the generated spectrum was written. [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool Prefix(AudioSource __instance, float[] samples, int channel, FFTWindow window) { if (!RenderManager.IsCapturingActive || !RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) return true; AudioSpectrumGenerator.GetSpectrumData(samples, channel, window); return false; } } /// /// AudioManager.FindOrLoadAudioClipExternal(string path, bool mp3Streaming, float length, bool stream) (coroutine /// that loads an external audio file of the level into audioLib): from stage Load with /// AdvancedConfig.ProcessAudio on, forces stream to false. The game passes stream to /// DownloadHandlerAudioClip.streamAudio for ogg/wav/aif/aiff files, so these clips are loaded as non-streaming /// clips; the MP3 path uses mp3Streaming and ignores stream. /// Reference: https://docs.unity3d.com/ScriptReference/Networking.DownloadHandlerAudioClip-streamAudio.html /// [HarmonyPatch(typeof(AudioManager), "FindOrLoadAudioClipExternal", typeof(string), typeof(bool), typeof(float), typeof(bool))] public static class FindOrLoadAudioClipExternalPatch { /// Prefix of AudioManager.FindOrLoadAudioClipExternal; the original always runs. /// Streaming flag of the load; set to false under the conditions above. [SuppressMessage("ReSharper", "InconsistentNaming")] public static void Prefix(ref bool stream) { if (RenderManager.Instance && RenderManager.Instance.stage >= RenderStage.Load && RenderManager.Instance.Configuration.AdvancedConfig.ProcessAudio) stream = false; } } }