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 Unity main 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;
}
}
}