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