Files
ADOFAIRenderer/Patch/Engine/RenderEnvironmentPatches.cs
T
2026-10-15 16:17:13 +09:00

282 lines
16 KiB
C#

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