Files
ADOFAIRenderer/Patch/Game/EndscreenLanternsPatch.cs
2026-10-15 16:17:13 +09:00

106 lines
4.8 KiB
C#

using System.Diagnostics.CodeAnalysis;
using ADOFAIRenderer.Core;
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")]
public static bool ForceHide(EndscreenLanterns __instance, ref int __result)
{
if (!RenderManager.IsActive) return true;
if (__instance != null)
{
__instance.Hide();
}
__result = 0; // Return 0 lanterns shown
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")]
public static void PrefixHide(EndscreenLanterns __instance)
{
if (__instance == null) return;
if (!RenderManager.IsActive) return;
var traverse = Traverse.Create(__instance);
// Kill lampSequences (private Sequence[] lampSequences) via reflection
var lampSequencesField = traverse.Field("lampSequences");
var lampSequencesObj = lampSequencesField.GetValue();
if (lampSequencesObj is System.Array lampSequencesArray)
{
for (var i = 0; i < lampSequencesArray.Length; i++)
{
var item = lampSequencesArray.GetValue(i);
try
{
var killMethod = item.GetType().GetMethod("Kill", new[] { typeof(bool) });
killMethod?.Invoke(item, new object[] { false });
}
catch
{
// Ignore reflection errors
}
lampSequencesArray.SetValue(null, i);
}
}
// Kill chainSequence (private Sequence chainSequence) via reflection
var chainSequenceField = traverse.Field("chainSequence");
var chainSequenceObj = chainSequenceField.GetValue();
if (chainSequenceObj == null) return;
try
{
var killMethod = chainSequenceObj.GetType().GetMethod("Kill", new[] { typeof(bool) });
killMethod?.Invoke(chainSequenceObj, new object[] { false });
}
catch
{
// Ignore reflection errors
}
chainSequenceField.SetValue(null);
}
}