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