Document Harmony Patches

This commit is contained in:
copecone committed 2026-10-15 16:17:13 +09:00
1 parent 3684db6f51
commit 00407c1320
19 files changed
+499 -4

No files matched your search

+21
View File
@@ -3,10 +3,31 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Engine;
/// <summary>
/// Makes the async input clock offset follow the render clock immediately.
/// <para>
/// Patched member: <c>AsyncInputUtils.UpdateOffsetTime(long fixDivider = 1)</c>. The async input system maps wall-clock ticks
/// (<c>AsyncInputManager.currFrameTick</c> = <c>DateTime.Now.Ticks</c>, 100 ns units) to conductor time with
/// <c>AsyncInputManager.offsetTick</c> = currFrameTick - <c>scrConductor.dspTime</c> * 10^7. With <c>fixDivider</c> 1 the
/// offset is set directly; otherwise it moves by 1 / fixDivider of the difference per call. <c>scrConductor</c> calls it every
/// frame with divider 100 (damped); <c>scrController</c> calls it with divider 1.
/// </para>
/// <para>
/// Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene). From stage Render the conductor time follows the
/// patched <c>AudioSettings.dspTime</c> (offline render clock, a fixed step per frame independent of wall-clock time), so the
/// difference changes every frame; the damped update would lag behind it. The prefix always applies the divider-1 update.
/// </para>
/// <para>Reference: https://learn.microsoft.com/en-us/dotnet/api/system.datetime.ticks</para>
/// </summary>
[HarmonyPatch(typeof(AsyncInputUtils), "UpdateOffsetTime")]
// ReSharper disable once InconsistentNaming
public static class AsyncInputPatches
{
/// <summary>
/// Prefix of <c>AsyncInputUtils.UpdateOffsetTime</c>. While rendering, sets <c>offsetTick</c> to
/// currFrameTick - dspTime * 10^7 (10^7 = 100 ns ticks per second) and <c>offsetTickUpdated</c> to true, for any divider.
/// </summary>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
// ReSharper disable once InconsistentNaming
public static bool Prefix()
+117
View File
@@ -6,13 +6,40 @@ 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)
{
@@ -22,9 +49,13 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -34,9 +65,18 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -46,9 +86,15 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -58,9 +104,17 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -70,9 +124,19 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -80,9 +144,16 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -92,9 +163,13 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -104,9 +179,13 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -116,9 +195,18 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -128,9 +216,15 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -140,9 +234,22 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
@@ -152,9 +259,19 @@ public static class RenderEnvironmentPatches
}
}
/// <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)
{
+17
View File
@@ -3,10 +3,27 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Hides the detailed results panel (per-judgement hit counts) that the game shows on the clear screen.
/// <para>
/// Patched member: <c>DetailedResults.Show()</c>. The game calls it from <c>scrController.OnLandOnPortal</c> when a level is won
/// (and detailed results are enabled); it builds the result text per player, enables the component and activates
/// <c>scrController.detailedResults</c>.
/// </para>
/// <para>
/// Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene), so the panel does not appear in the rendered video
/// during the delay after the level end.
/// </para>
/// </summary>
[HarmonyPatch(typeof(DetailedResults))]
// ReSharper disable once InconsistentNaming
public static class DetailedResultsPatch
{
/// <summary>
/// Prefix of <c>DetailedResults.Show</c>. While rendering, disables the component and deactivates its GameObject.
/// </summary>
/// <param name="__instance">The DetailedResults component being shown.</param>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("Show")]
// ReSharper disable once InconsistentNaming
+34
View File
@@ -4,10 +4,29 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Suppresses the end-screen lanterns (level complete / accuracy / speed trial lamps) shown after a clear.
/// <para>
/// Patched members: <c>EndscreenLanterns.Show(scrPlayer, float, int, bool)</c>, which activates the lantern set, sets the
/// lantern texts and starts DOTween sequences (<c>chainSequence</c> for the chain drop, <c>lampSequences[3]</c> for the lamp
/// effects), returning the number of lanterns shown; and <c>EndscreenLanterns.Hide()</c>, which deactivates the GameObject.
/// </para>
/// <para>
/// Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene), so the lanterns and their tweens do not appear in
/// the rendered video during the delay after the level end. See also <see cref="scrUIControllerPatch"/>.
/// </para>
/// </summary>
[HarmonyPatch(typeof(EndscreenLanterns))]
// ReSharper disable once InconsistentNaming
public static class EndscreenLanternsPatch
{
/// <summary>
/// Prefix of <c>EndscreenLanterns.Show</c>. While rendering, hides the lantern set (which also runs
/// <see cref="PrefixHide"/>) and reports zero lanterns shown.
/// </summary>
/// <param name="__instance">The lantern set being shown.</param>
/// <param name="__result">Return value of Show: number of lanterns shown; set to 0 when the original is skipped.</param>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("Show")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
@@ -22,6 +41,21 @@ public static class EndscreenLanternsPatch
return false; // Skip original Show method
}
/// <summary>
/// Prefix of <c>EndscreenLanterns.Hide</c>. While rendering, releases the lantern tweens started by an earlier Show
/// before the original deactivates the GameObject.
/// <para>
/// The private fields <c>lampSequences</c> (DOTween <c>Sequence[]</c>) and <c>chainSequence</c> (<c>Sequence</c>) are read
/// through Harmony <see cref="Traverse"/>. For each sequence, a public instance method <c>Kill(bool)</c> is looked up on the
/// object's runtime type by reflection and invoked with <c>false</c> when found; then the field slot is set to null.
/// In the game's DOTween.dll, <c>Kill(this Tween, bool complete = false)</c> is an extension method of
/// <c>DG.Tweening.TweenExtensions</c>, so the lookup on <c>Sequence</c> returns null and no Kill is invoked; the effect is
/// that the fields stop referencing the sequences, while the sequences themselves keep running in DOTween.
/// Reflection failures on a single sequence (including a null array element) are ignored. The original Hide always runs
/// afterwards.
/// </para>
/// </summary>
/// <param name="__instance">The lantern set being hidden.</param>
[HarmonyPrefix]
[HarmonyPatch("Hide")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
+13 -1
View File
@@ -5,11 +5,19 @@ using UnityEngine;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Synchronizes mod localization with ADOFAI's in-game language settings.
/// Keeps the mod UI language (<see cref="LocalizationManager"/>) equal to the game language.
/// <para>
/// Patched members: <c>RDString.Setup()</c>, which initializes the game's string table and sets <c>RDString.language</c> from
/// <c>Persistence.language</c> (it returns immediately when already initialized); and <c>RDString.ChangeLanguage(SystemLanguage)</c>,
/// which stores the language in <c>Persistence.language</c>, clears the initialized flag and calls Setup again.
/// </para>
/// <para>Always active (independent of the render stage). Runs on the Unity main thread.</para>
/// </summary>
[HarmonyPatch(typeof(RDString))]
internal static class LanguageSyncPatch
{
/// <summary>Postfix of <c>RDString.ChangeLanguage</c>: applies the selected language to the mod UI.</summary>
/// <param name="language">Language passed to ChangeLanguage.</param>
[HarmonyPatch(nameof(RDString.ChangeLanguage))]
[HarmonyPostfix]
public static void OnChangeLanguage(SystemLanguage language)
@@ -17,6 +25,10 @@ internal static class LanguageSyncPatch
LocalizationManager.SetLanguage(language);
}
/// <summary>
/// Postfix of <c>RDString.Setup</c>: applies <c>RDString.language</c> (the language resolved by the game) to the mod UI.
/// Runs on every Setup call, including the early-return calls made when the string table is already initialized.
/// </summary>
[HarmonyPatch(nameof(RDString.Setup))]
[HarmonyPostfix]
public static void OnSetup()
+26
View File
@@ -3,9 +3,23 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Overrides two player settings read from <c>Persistence</c> during a render: the "skip intro" option and the planet colors.
/// </summary>
[HarmonyPatch(typeof(Persistence))]
public static class PersistencePatch
{
/// <summary>
/// Postfix of the <c>Persistence.skipIntroBehavior</c> getter (setting values Off / AfterFirstTry / Always).
/// <para>
/// <c>scrConductor.StartMusicCo</c> reads this setting when the song is scheduled and, for AfterFirstTry (after a death) or
/// Always (long intro, no checkpoint), sets <c>skipOffset</c>, which moves <c>dspTimeSong</c> back by the level offset and
/// starts the song part-way in. While <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene) and the song has not
/// started yet (<c>scrConductor.hasSongStarted</c> false), the getter returns Off, so <c>skipOffset</c> stays false and the
/// song always plays from its start. The mod's offline BGM mixing, spectrum and video background timing assume this.
/// </para>
/// </summary>
/// <param name="__result">Value returned by the getter; replaced with <c>SkipIntroBehavior.Off</c> under the conditions above.</param>
[HarmonyPostfix]
[HarmonyPatch("skipIntroBehavior", MethodType.Getter)]
// ReSharper disable once InconsistentNaming
@@ -19,6 +33,18 @@ public static class PersistencePatch
}
}
/// <summary>
/// Postfix of <c>Persistence.GetPlayerColor(bool red)</c>, which returns the player's saved planet color
/// (preset name or hex color from the general preferences; default red/blue otherwise). <c>PlanetRenderer</c> uses it to
/// color the fire (red) and ice (blue) planets.
/// <para>
/// Active from stage Load (render started, scene loading) until the stage returns to None. When the render configuration
/// sets <c>AdvancedConfig.firePlanetColor</c> (red) or <c>AdvancedConfig.icePlanetColor</c> (blue), that color replaces the
/// saved color; an unset (null) override keeps the game value. The saved preference itself is not changed.
/// </para>
/// </summary>
/// <param name="red">True for the fire (red) planet, false for the ice (blue) planet.</param>
/// <param name="__result">Color returned by the game; replaced by the configured override when set.</param>
[HarmonyPostfix]
[HarmonyPatch("GetPlayerColor")]
// ReSharper disable once InconsistentNaming
+19
View File
@@ -3,9 +3,21 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Applies the configured color of the third (green, "wind") planet used by multi-planet levels.
/// <para>
/// Patched members: <c>PlanetarySystem.Init()</c>, which creates the planets including <c>planetGreen</c> (default color
/// RGB 0.3, 0.7, 0.0); and <c>PlanetarySystem.ApplyMultiplanetColors()</c>, which recolors the extra planets
/// (green, yellow, purple, ...) from the red/blue planet colors (skipped by the game when <c>GCS.staticPlanetColors</c> is set).
/// Both postfixes reapply the override after the game has set its color.
/// </para>
/// <para>Active from stage Load until the stage returns to None. The fire/ice colors are handled by <see cref="PersistencePatch"/>.</para>
/// </summary>
[HarmonyPatch(typeof(PlanetarySystem))]
public static class PlanetarySystemPatch
{
/// <summary>Postfix of <c>PlanetarySystem.Init</c>: applies the wind planet color override to the new planets.</summary>
/// <param name="__instance">The planetary system that was initialized.</param>
[HarmonyPostfix]
[HarmonyPatch("Init")]
// ReSharper disable once InconsistentNaming
@@ -14,6 +26,8 @@ public static class PlanetarySystemPatch
ApplyWindPlanetColor(__instance);
}
/// <summary>Postfix of <c>PlanetarySystem.ApplyMultiplanetColors</c>: reapplies the wind planet color override.</summary>
/// <param name="__instance">The planetary system whose extra planet colors were updated.</param>
[HarmonyPostfix]
[HarmonyPatch("ApplyMultiplanetColors")]
// ReSharper disable once InconsistentNaming
@@ -22,6 +36,11 @@ public static class PlanetarySystemPatch
ApplyWindPlanetColor(__instance);
}
/// <summary>
/// Sets <c>planetGreen</c>'s renderer color to <c>AdvancedConfig.windPlanetColor</c> when a render is in progress
/// (stage != None) and the override is set. Does nothing when the override is null or the planet does not exist.
/// </summary>
/// <param name="planetarySystem">The planetary system to update.</param>
private static void ApplyWindPlanetColor(PlanetarySystem planetarySystem)
{
if (RenderManager.Instance == null || RenderManager.Instance.stage == RenderStage.None) return;
+20 -1
View File
@@ -6,10 +6,28 @@ using UnityEngine.UI;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Adds the "Render" button to the level editor toolbar.
/// <para>
/// Patched member: <c>scnEditor.Start()</c>, the editor scene setup (it wires the toolbar buttons, including
/// <c>buttonExit</c>). The postfix clones the exit button next to it and makes the clone start a render.
/// </para>
/// <para>
/// Always active except when the mod features are disabled (<see cref="RenderMod.IsFeatureDisabled"/>, set from the
/// driver warning dialog). Runs on the Unity main thread each time the editor scene starts.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scnEditor))]
// ReSharper disable once InconsistentNaming
public static class scnEditorPatch
{
/// <summary>
/// Postfix of <c>scnEditor.Start</c>: instantiates a copy of <c>buttonExit</c> under the same parent, named
/// <c>buttonRender</c>, whose click handler is <see cref="RenderManager.StartRender"/>, and replaces its icon with the
/// editor's MoveCamera event sprite (24 x 24 units). Logs a message and adds nothing when the editor or exit button is missing.
/// When the icon child or the sprite is not found, the button keeps the exit icon.
/// </summary>
/// <param name="__instance">The editor scene component.</param>
[HarmonyPostfix]
[HarmonyPatch("Start")]
// ReSharper disable once InconsistentNaming
@@ -38,8 +56,9 @@ public static class scnEditorPatch
var images = buttonObj.GetComponentsInChildren<Image>();
if (images.Length <= 1) return;
// images[0] is the background on the root, images[1] is the icon on the child.
// Sprite path: the icon of the editor's MoveCamera level event, loaded from the game's Resources.
// ReSharper disable once Unity.UnknownResource
// images[0] is the background on the root, images[1] is the icon on the child
var sprite = Resources.Load<Sprite>("LevelEditor/LevelEvents/MoveCamera");
if (sprite == null) return;
+37 -1
View File
@@ -6,10 +6,26 @@ using UnityEngine;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Patches of <c>scnGame</c> (custom level scene controller, also used by the editor's play mode): hides tiles and planets
/// for the "non-tile" render option and replaces the background video player during a render.
/// </summary>
[HarmonyPatch(typeof(scnGame))]
// ReSharper disable once InconsistentNaming
public static class scnGamePatch
{
/// <summary>
/// Postfix of <c>scnGame.Play(int seqID, bool remakeFloors)</c>, which starts (or restarts) play of the custom level:
/// it resets the conductor, camera and controller state for the given start tile.
/// <para>
/// Active from stage Load when <c>AdvancedConfig.NonTile</c> is enabled. Disables the renderers of every floor in
/// <c>scrLevelMaker.listFloors</c> (floor mesh, legacy sprite, icon, outline, top/bottom glow, multi-planet line,
/// hold mesh, dummy planets) and of every planet of every player (sprite mesh, core/tail/sparks particles, ring, glow,
/// face, samurai sprite), so only the background and decorations remain in the rendered video. The renderers are only
/// disabled; the objects keep running. Hold meshes re-enable themselves, so <c>RenderManager.OnLastUpdate</c> disables
/// them again every frame.
/// </para>
/// </summary>
[HarmonyPostfix]
[HarmonyPatch("Play")]
public static void ProcessFloors()
@@ -47,6 +63,7 @@ public static class scnGamePatch
}
return;
// Disables every renderer that draws one planet (fire, ice, extra planets and floor dummy planets).
void DisablePlanetRenderer(PlanetRenderer planet)
{
if (planet.sprite) DisableRenderer(planet.sprite.meshRenderer);
@@ -62,12 +79,29 @@ public static class scnGamePatch
foreach (var renderer in renderers) DisableRenderer(renderer);
}
// Sets Renderer.enabled = false; null or destroyed renderers are skipped (Unity null check).
void DisableRenderer(Renderer? renderer)
{
if (renderer) renderer.enabled = false;
}
}
/// <summary>
/// Prefix of <c>scnGame.UpdateVideo()</c>, which sets up the level's background video on the native Unity
/// <c>VideoPlayer</c> (<c>scnGame.videoBG</c>): it reads <c>miscSettings.bgVideo</c>, loads a <c>VideoClip</c> for internal
/// (Resources / Addressables) and bundle levels or sets <c>videoBG.url</c> to the file next to the custom level, then calls
/// <c>Stop()</c>, <c>Prepare()</c> and sets <c>isLooping</c> from <c>loopVideo</c>. The game calls it from
/// <c>scnGame.FinishCustomLevelLoading</c> and from <c>scnGame.ReloadAssets</c> when <c>force</c> is true.
/// <para>
/// While <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene), an external video file of a custom level is played by
/// <see cref="VideoBGPlayer"/> (FFmpeg decoder), which shows the frame for the render time of each frame. The native
/// <c>VideoPlayer</c> plays in real time and would drift from the offline render clock, so it is disabled for that case.
/// Internal and bundle levels (built-in <c>VideoClip</c> assets, not files) and a missing file use the original method.
/// </para>
/// <para>Outside a render, any <see cref="VideoBGPlayer"/> left on the video object is destroyed and the native player re-enabled.</para>
/// </summary>
/// <param name="__instance">The scene controller.</param>
/// <returns>True to run the original UpdateVideo; false when the mod handled the video (or there is none).</returns>
[HarmonyPrefix]
[HarmonyPatch("UpdateVideo")]
// ReSharper disable once InconsistentNaming
@@ -109,7 +143,8 @@ public static class scnGamePatch
return true; // Let the original print error
}
// Disable native VideoPlayer component (to prevent it from attempting real-time playback and sync errors)
// Camera for the CameraFarPlane render mode: the VideoPlayer's target camera, or the "BGStaticCam" scene object
// when none is assigned.
var targetCamera = __instance.videoBG.targetCamera;
if (!targetCamera)
{
@@ -120,6 +155,7 @@ public static class scnGamePatch
}
}
// Disable the native VideoPlayer so it does not play the video in real time alongside VideoBGPlayer.
__instance.videoBG.enabled = false;
// Add or retrieve our custom FFmpeg video background player
+22 -1
View File
@@ -3,16 +3,37 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Protects the camera render texture <c>scrCamera.camRT</c> when the custom frame rate effect is turned off while it is
/// already off.
/// <para>
/// Patched member: <c>scrCamera.SetCustomFrameRate(bool enable, float frame)</c> (level effect that updates the screen at a
/// lower frame rate). Enabling assigns a new RenderTexture to the camera quad material and refreshes it at <c>frame</c> fps.
/// Disabling calls <c>Release()</c> on the quad material's current <c>mainTexture</c> and assigns <c>camRT</c> back. When
/// the effect was never enabled, <c>mainTexture</c> is <c>camRT</c> itself, so the original releases the live camera
/// render texture.
/// </para>
/// <para>Always active (independent of the render stage).</para>
/// </summary>
[HarmonyPatch(typeof(scrCamera))]
// ReSharper disable once InconsistentNaming
public class scrCameraPatch
{
/// <summary>
/// Prefix of <c>scrCamera.SetCustomFrameRate</c>. For a disable request while <c>enableCustomFPS</c> is already false,
/// stores the arguments in <c>enableCustomFPS</c> / <c>frameRate</c> and skips the original (no texture is released).
/// Enable requests and disable requests while the effect is on run the original.
/// </summary>
/// <param name="__instance">The camera.</param>
/// <param name="enable">True to turn the custom frame rate on, false to turn it off.</param>
/// <param name="frame">Custom frame rate in frames per second.</param>
/// <returns>True to run the original method; false to skip it.</returns>
[HarmonyPrefix]
[HarmonyPatch("SetCustomFrameRate")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
private static bool FixCamRT(scrCamera __instance, bool enable, float frame)
{
// There is an issue that `mainTexture.Release` triggers even if `mainTexture` is `camRT`, so mod tries to fix it
// Disable request while the effect is off: the quad material's mainTexture is camRT, which the original would Release().
if (enable || __instance.enableCustomFPS != enable) return true;
__instance.enableCustomFPS = enable;
+47
View File
@@ -3,10 +3,25 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Keeps the game controller (<c>scrController</c>) in uninterrupted play during a render: no pause, no failure,
/// no scene change after the clear, and no clear texts.
/// <para>
/// All patches are active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene). RenderManager also sets
/// <c>noFail</c>, <c>noFailInfiniteMargin</c> and <c>disableCongratsMessage</c> on the controller when the scene is ready.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrController))]
// ReSharper disable once InconsistentNaming
public static class scrControllerPatch
{
/// <summary>
/// Prefix of <c>scrController.TogglePauseGame()</c> (pause menu toggle; sets <c>Time.timeScale</c> to 0 or 1 and shows the
/// pause menu; also called from the pause button, the pause key and <c>OnApplicationPauseCallback</c>). Skipped while
/// rendering, so the level cannot be paused. The skipped call returns
/// false (Harmony default for the unset bool result).
/// </summary>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("TogglePauseGame")]
public static bool PreventPause()
@@ -14,6 +29,12 @@ public static class scrControllerPatch
return !RenderManager.IsActive;
}
/// <summary>
/// Prefix of <c>scrController.PortalTravelAction(Portal)</c>, which leaves the level through a portal (next level,
/// level select, main menu, ...); the game calls it on entering the Won state for non-world levels and on input after a win.
/// Skipped while rendering, so the scene stays loaded for the delay after the level end.
/// </summary>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("PortalTravelAction")]
public static bool PreventPortalTravel()
@@ -21,6 +42,11 @@ public static class scrControllerPatch
return !RenderManager.IsActive;
}
/// <summary>
/// Prefix of <c>scrController.FailAction(...)</c>, which enters the Fail state: stops the song and all sounds, pauses the
/// VFX video and shows the failure texts. Skipped while rendering.
/// </summary>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("FailAction")]
public static bool PreventFailAction()
@@ -28,6 +54,11 @@ public static class scrControllerPatch
return !RenderManager.IsActive;
}
/// <summary>
/// Prefix of <c>scrController.Fail2Action()</c>, which moves from Fail to Fail2 (accuracy calculation, saving of the
/// best result for boss/custom boss levels, death counter). Skipped while rendering.
/// </summary>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("Fail2Action")]
public static bool PreventFail2Action()
@@ -35,6 +66,11 @@ public static class scrControllerPatch
return !RenderManager.IsActive;
}
/// <summary>
/// Prefix of <c>scrController.OnPlayerDied(...)</c>, which kills the remaining players and calls FailAction when no other
/// player is alive. Skipped while rendering.
/// </summary>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("OnPlayerDied")]
public static bool PreventPlayerDied()
@@ -42,6 +78,17 @@ public static class scrControllerPatch
return !RenderManager.IsActive;
}
/// <summary>
/// Postfix of <c>scrController.OnLandOnPortal(...)</c>, the win handler on the last tile: it sets and activates
/// <c>txtCongrats</c> / <c>txtAprilCongrats</c> / <c>txtAllStrictClear</c>, may show the detailed results and end-screen
/// lanterns, saves the result and changes the state to Won.
/// <para>
/// While rendering, clears and deactivates the three clear texts and disables the detailed results panel after the
/// original has run, so they do not appear in the rendered video. The Won state itself is kept: the default render end
/// condition (<c>RenderContext.EndCondition</c>) waits for it.
/// </para>
/// </summary>
/// <param name="__instance">The game controller.</param>
[HarmonyPostfix]
[HarmonyPatch("OnLandOnPortal")]
public static void PreventWinTextsPostfix(scrController __instance)
+15
View File
@@ -3,10 +3,25 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Prevents the end portal from being spawned on level tiles for the "non-tile" render option.
/// <para>
/// Patched member: <c>scrFloor.SpawnPortalParticles()</c>, which instantiates the animated portal prefab
/// (<c>scrPortalParticles</c>: glow, particles, cap and icon) as a child of the floor. The game calls it from
/// <c>scrLevelMaker</c>, <c>scrController</c> and <c>scrFloor</c> for the portal floor.
/// </para>
/// <para>
/// Active from stage Load when <c>AdvancedConfig.NonTile</c> is enabled, and only for floors that belong to
/// <c>scrLevelMaker.listFloors</c> (the level's tiles). See <see cref="scnGamePatch.ProcessFloors"/> for the other tile renderers.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrFloor))]
// ReSharper disable once InconsistentNaming
public class scrFloorPatch
{
/// <summary>Prefix of <c>scrFloor.SpawnPortalParticles</c>.</summary>
/// <param name="__instance">The floor that would receive the portal.</param>
/// <returns>True to run the original method (spawn the portal); false to skip it (non-tile render, level floor).</returns>
[HarmonyPrefix]
[HarmonyPatch("SpawnPortalParticles")]
// ReSharper disable once InconsistentNaming
+10
View File
@@ -3,10 +3,20 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Hides the judgement texts (e.g. Perfect, Early, Late) during a render unless the render configuration enables them.
/// <para>
/// Patched member: <c>scrHitTextManager.ShowHitText(HitMargin, scrPlanet, float, int?)</c>, which shows the hit margin
/// text at the planet for each hit (the game itself skips it for no-HUD modes and hidden margins).
/// </para>
/// <para>Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene) and <c>RenderConfig.ShowJudgementText</c> is false.</para>
/// </summary>
[HarmonyPatch(typeof(scrHitTextManager), "ShowHitText")]
// ReSharper disable once InconsistentNaming
public class scrHitTextManagerPatch
{
/// <summary>Prefix of <c>scrHitTextManager.ShowHitText</c>.</summary>
/// <returns>True to run the original method (not rendering, or judgement texts enabled); false to skip it.</returns>
public static bool Prefix()
{
return !RenderManager.IsActive || RenderManager.Instance is { Configuration: { ShowJudgementText: true } };
+15
View File
@@ -3,10 +3,25 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Keeps a render from writing custom level results to the player's save data.
/// <para>
/// Patched member: <c>scrMistakesManager.SaveCustom(string hash, bool wonLevel, float multiplier)</c>, which compares the
/// run with the stored completion, accuracy, speed trial and death records of the custom level (keyed by the level hash),
/// stores new bests through <c>Persistence</c> and returns an <c>EndLevelInfo</c>. The game calls it from
/// <c>scrController.OnLandOnPortal</c> (custom level win) and <c>Fail2Action</c> (custom boss level failure).
/// </para>
/// <para>
/// Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene); the run of a render is not stored as a player
/// record. The skipped call returns a default <c>EndLevelInfo</c>.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrMistakesManager))]
// ReSharper disable once InconsistentNaming
public static class scrMistakesManagerPatch
{
/// <summary>Prefix of <c>scrMistakesManager.SaveCustom</c>.</summary>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("SaveCustom")]
public static bool PreventSaveCustom()
+17
View File
@@ -3,10 +3,27 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Replaces the "any valid input" check with a render-controlled value, so player input cannot affect the level during a render.
/// <para>
/// Patched member: <c>scrPlayerManager.AnyValidInputWasTriggered()</c>, true when any player pressed a valid key this frame.
/// <c>scrController</c> uses it to leave the press-to-start wait, to restart after a failure (<c>Fail2_Update</c>) and to
/// leave the level after a win (<c>Won_Update</c>).
/// </para>
/// <para>
/// Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene). The result is true only while
/// <c>RenderManager.countdown</c> is 0 and <c>RenderManager.frameCount</c> is 0, that is, after the 3 second countdown in
/// <c>SetupPlay</c> has ended and reset the frame counter and before the next frame increments it; it is false otherwise.
/// <c>SetupPlay</c> also sets <c>scrController.levelWasSkipped</c>, which ends the press-to-start wait independently of input.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrPlayerManager))]
// ReSharper disable once InconsistentNaming
public static class scrPlayerManagerPatch
{
/// <summary>Prefix of <c>scrPlayerManager.AnyValidInputWasTriggered</c>.</summary>
/// <param name="__result">Return value used when the original is skipped (see class summary).</param>
/// <returns>True to run the original method (not rendering); false to skip it (rendering).</returns>
[HarmonyPrefix]
[HarmonyPatch("AnyValidInputWasTriggered")]
// ReSharper disable once InconsistentNaming
+16
View File
@@ -4,10 +4,26 @@ using UnityEngine;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Makes the end portal particle systems run on scaled game time during a render.
/// <para>
/// Patched member: <c>scrPortalParticles.Start()</c> (the animated portal spawned by <c>scrFloor.SpawnPortalParticles</c>;
/// Start pre-simulates its particle system for 1 second and plays it).
/// </para>
/// <para>
/// Active from stage Load. RenderManager sets <c>Time.captureDeltaTime</c> to 1 / RenderFPS, which advances scaled time by a
/// fixed step per frame; unscaled time is not affected by captureDeltaTime and follows real time. A particle system with
/// <c>main.useUnscaledTime</c> would therefore play at the real (slower or faster) frame rate of the offline render, so the
/// postfix sets it to false on every particle system under the portal (including inactive children).
/// Reference: https://docs.unity3d.com/ScriptReference/Time-captureDeltaTime.html
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrPortalParticles))]
// ReSharper disable once InconsistentNaming
public static class scrPortalParticlesPatch
{
/// <summary>Postfix of <c>scrPortalParticles.Start</c>: sets <c>useUnscaledTime = false</c> on all child particle systems.</summary>
/// <param name="__instance">The portal that started.</param>
[HarmonyPostfix]
[HarmonyPatch("Start")]
// ReSharper disable once InconsistentNaming
+16
View File
@@ -3,10 +3,26 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Hides the "press any key to begin" text of the first level start of a render.
/// <para>
/// Patched member: <c>scrPressToStart.ShowText()</c>, which sets the press/tap-to-begin text and shows the difficulty
/// selector (<c>scrUIController.ShowDifficultyContainer</c>). <c>scrController</c> calls it before waiting for the start input.
/// </para>
/// <para>
/// Active once per render: <c>RenderManager.skipPressToStart</c> is set when the render loads <c>scnGame</c> and cleared by
/// the first ShowText call (or when the render stops). Later ShowText calls run the original.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrPressToStart))]
// ReSharper disable once InconsistentNaming
public class scrPressToStartPatch
{
/// <summary>
/// Prefix of <c>scrPressToStart.ShowText</c>. When <c>skipPressToStart</c> is set, clears it, hides the difficulty
/// selector and skips the original.
/// </summary>
/// <returns>True to run the original method; false to skip it (first ShowText of a render).</returns>
[HarmonyPrefix]
[HarmonyPatch("ShowText")]
private static bool PreventShowText()
+21
View File
@@ -6,10 +6,31 @@ using UnityEngine.UI;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Replaces the game's status text (normally "Autoplay", "Auto tile" or "Debug Mode") with the render status.
/// <para>
/// Patched member: <c>scrShowIfDebug.Update()</c>, which each frame sets its UI <c>Text</c> (<c>txt</c>) to the autoplay /
/// auto-tile / debug label or hides it (no-HUD modes, recording mode), in <c>defaultColor</c> (green) or orange for auto tiles.
/// </para>
/// <para>
/// Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene). The postfix overwrites whatever the original set,
/// every frame, so the text is shown even in the game's no-HUD modes.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrShowIfDebug))]
// ReSharper disable once InconsistentNaming
public static class scrShowIfDebugPatch
{
/// <summary>
/// Postfix of <c>scrShowIfDebug.Update</c>. Shows, in the default (green) color:
/// <list type="bullet">
/// <item>"Render in Ns.." during the countdown (N = <c>RenderManager.countdown</c>, seconds), otherwise "Render";</item>
/// <item>after the end condition is met (<c>endFrame</c> != -1), a second line "Waiting for X.Xs.." with the remaining
/// part of <c>DelayAfterEnd</c> in render seconds ((frameCount - endFrame) / RenderFPS elapsed), clamped at 0.</item>
/// </list>
/// </summary>
/// <param name="___txt">Private field <c>txt</c>: the status Text component.</param>
/// <param name="___defaultColor">Private field <c>defaultColor</c>: the game's status text color.</param>
[HarmonyPostfix]
[HarmonyPatch("Update")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
+16
View File
@@ -3,10 +3,26 @@ using HarmonyLib;
namespace ADOFAIRenderer.Patch.Game;
/// <summary>
/// Hides the end-screen lanterns again right after the game shows them.
/// <para>
/// Patched member: <c>scrUIController.ShowEndscreenLanterns()</c>, which calls <c>EndscreenLanterns.Show</c> for each player's
/// lantern set (and fades the end-screen flash in co-op). <c>scrController.OnLandOnPortal</c> calls it on a win.
/// </para>
/// <para>
/// Active while <see cref="RenderManager.IsActive"/> (stage &gt;= OnScene). Works together with <see cref="EndscreenLanternsPatch"/>,
/// which already skips <c>EndscreenLanterns.Show</c> during a render.
/// </para>
/// </summary>
[HarmonyPatch(typeof(scrUIController))]
// ReSharper disable once InconsistentNaming
public class scrUIControllerPatch
{
/// <summary>
/// Postfix of <c>scrUIController.ShowEndscreenLanterns</c>: calls <c>HideEndscreenLanterns()</c>, which runs
/// <c>EndscreenLanterns.Hide</c> on every lantern set.
/// </summary>
/// <param name="__instance">The UI controller.</param>
[HarmonyPostfix]
[HarmonyPatch("ShowEndscreenLanterns")]
// ReSharper disable once InconsistentNaming