diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 4c5aeaf43..76ce26845 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -106,6 +106,58 @@ public static void RegisterLoadOverride(Level level, LoadOverride loadOverride) [PatchLevelUpdate] // ... except for manually manipulating the method via MonoModRules public extern new void Update(); + [MonoModIgnore] // don't put this in `Level` when we're done + internal static extern void base_FreezeFrameUpdate(); // dummy method, will be replaced with the actual `base.FreezeFrameUpdate` call in the IL patch + [PatchLevelFreezeFrameUpdate] // add the `virtual` flag to this method so it overrides the one in `patch_Scene` properly and calls `base.FreezeFrameUpdate` + public void FreezeFrameUpdate() { + Everest.Events.Level.BeforeFreezeFrameUpdate(this); + + // same logic as in `Level.Update` so entities with `TagsExt.FreezeFrameUpdate` and other "special update tags" are handled correctly + if (FrozenOrPaused) { + bool disabled = MInput.Disabled; + MInput.Disabled = false; + + if (!Paused) { + foreach (Entity entity in base[Tags.FrozenUpdate]) { + if (entity.Active && entity.TagCheck(TagsExt.FreezeFrameUpdate)) { + entity.Update(); + } + } + } + foreach (Entity entity in base[Tags.PauseUpdate]) { + if (entity.Active && entity.TagCheck(TagsExt.FreezeFrameUpdate)) { + entity.Update(); + } + } + + MInput.Disabled = disabled; + } else if (!Transitioning) { + if (RetryPlayerCorpse == null) { + base_FreezeFrameUpdate(); + } else { + foreach (Entity entity in base[Tags.PauseUpdate]) { + if (entity.Active && entity.TagCheck(TagsExt.FreezeFrameUpdate)) { + entity.Update(); + } + } + } + } else { + foreach (Entity entity in base[Tags.TransitionUpdate]) { + if (entity.TagCheck(TagsExt.FreezeFrameUpdate)) { + entity.Update(); + } + } + } + + foreach (PostUpdateHook component in Tracker.GetComponents()) { + if (component.Entity.Active && component.Entity.TagCheck(TagsExt.FreezeFrameUpdate)) { + component.OnPostUpdate(); + } + } + + Everest.Events.Level.AfterFreezeFrameUpdate(this); + } + /// /// Flash the screen a solid color. Respects the user's advanced photosensitivity settings. /// @@ -797,6 +849,20 @@ internal static void BeforeUpdate(_Level level) public static event Action<_Level> OnAfterUpdate; internal static void AfterUpdate(_Level level) => OnAfterUpdate?.Invoke(level); + + /// + /// Called at the very beginning of . + /// + public static event Action<_Level> OnBeforeFreezeFrameUpdate; + internal static void BeforeFreezeFrameUpdate(_Level level) + => OnBeforeFreezeFrameUpdate?.Invoke(level); + + /// + /// Called at the very end of . + /// + public static event Action<_Level> OnAfterFreezeFrameUpdate; + internal static void AfterFreezeFrameUpdate(_Level level) + => OnAfterFreezeFrameUpdate?.Invoke(level); } } } @@ -820,6 +886,12 @@ class PatchLevelLoaderDecalCreationAttribute : Attribute { } /// [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelUpdate))] class PatchLevelUpdateAttribute : Attribute { } + + /// + /// Patch our to be marked as virtual so it overrides the base method, and to call the base method instead of the dummy. + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelFreezeFrameUpdate))] + class PatchLevelFreezeFrameUpdateAttribute : Attribute { } /// /// Patch the Godzilla-sized level rendering method instead of reimplementing it in Everest. @@ -1165,6 +1237,22 @@ ldc.i4.s 9 } } + public static void PatchLevelFreezeFrameUpdate(ILContext context, CustomAttribute attrib) { + // mark the method as `virtual` so it registers as an override + context.Method.IsVirtual = true; + + TypeDefinition t_Scene = context.Method.DeclaringType.BaseType.Resolve(); + MethodReference m_Scene_FreezeFrameUpdate = t_Scene.FindMethod("FreezeFrameUpdate"); + + ILCursor cursor = new ILCursor(context); + + // replace the dummy method with the actual call to `base.FreezeFrameUpdate` + cursor.GotoNext(MoveType.Before, instr => instr.MatchCall("Celeste.Level", "base_FreezeFrameUpdate")); + cursor.Remove(); + cursor.EmitLdarg0(); + cursor.EmitCall(m_Scene_FreezeFrameUpdate); + } + public static void PatchLevelRender(ILContext context, CustomAttribute attrib) { FieldDefinition f_SubHudRenderer = context.Method.DeclaringType.FindField("SubHudRenderer"); diff --git a/Celeste.Mod.mm/Patches/Monocle/Engine.cs b/Celeste.Mod.mm/Patches/Monocle/Engine.cs index 49f4a2f31..7059229d3 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Engine.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Engine.cs @@ -6,6 +6,7 @@ using Mono.Cecil.Cil; using MonoMod; using MonoMod.Cil; +using MonoMod.InlineRT; using MonoMod.Utils; using System; using System.Linq; @@ -201,11 +202,13 @@ class PatchEngineCctorAttribute : Attribute { } static partial class MonoModRules { public static void PatchEngineUpdate(ILContext context, CustomAttribute attrib) { TypeDefinition t_Engine = context.Method.DeclaringType; + TypeDefinition t_Scene = MonoModRule.Modder.FindType("Monocle.Scene").Resolve(); FieldReference f_scene = t_Engine.FindField("scene"); FieldReference sf_TimeRate = t_Engine.FindField("TimeRate"); FieldReference sf_TimeRateB = t_Engine.FindField("TimeRateB"); FieldReference sf_EffectiveTimeRate = t_Engine.FindField("EffectiveTimeRate"); MethodReference m_GetTimeRateComponentMultiplier = t_Engine.FindMethod("GetTimeRateComponentMultiplier"); + MethodReference m_Scene_FreezeFrameUpdate = t_Scene.FindMethod("FreezeFrameUpdate"); VariableDefinition v_componentTimeRate = new(context.Import(typeof(float))); context.Method.Body.Variables.Add(v_componentTimeRate); @@ -234,6 +237,18 @@ public static void PatchEngineUpdate(ILContext context, CustomAttribute attrib) instr => instr.MatchMul()); cursor.EmitLdloc(v_componentTimeRate); cursor.EmitMul(); + + // call the current scene's `FreezeFrameUpdate` before `FreezeTimer` is updated + cursor.GotoNext(MoveType.Before, + instr => instr.MatchLdsfld("Monocle.Engine", "FreezeTimer"), + instr => instr.MatchCall("Monocle.Engine", "get_RawDeltaTime"), + instr => instr.MatchSub(), + instr => instr.MatchLdcR4(0f), + instr => instr.MatchCall("System.Math", "Max"), + instr => instr.MatchStsfld("Monocle.Engine", "FreezeTimer")); + cursor.EmitLdarg0(); + cursor.EmitLdfld(f_scene); + cursor.EmitCallvirt(m_Scene_FreezeFrameUpdate); } public static void PatchEngineCctor(ILContext context, CustomAttribute attrib) { diff --git a/Celeste.Mod.mm/Patches/Monocle/Scene.cs b/Celeste.Mod.mm/Patches/Monocle/Scene.cs index 1dbd4c382..5f115d25a 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Scene.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Scene.cs @@ -1,4 +1,5 @@ -using Celeste.Mod.Registry; +using Celeste; +using Celeste.Mod.Registry; using MonoMod; using System; using System.Collections.Generic; @@ -23,8 +24,7 @@ class patch_Scene : Scene { public new bool OnInterval(float interval, float offset) { return Math.Floor(((double) TimeActive - offset - Engine.DeltaTime) / interval) < Math.Floor(((double) TimeActive - offset) / interval); } - - + /// /// Finds all entities created from an EntityData with the specified SID, using the Tracker if possible. /// @@ -48,5 +48,18 @@ public IEnumerable FindEntitiesWithSid(string sid) { public new virtual void AfterUpdate() { Interlocked.Exchange(ref OnEndOfFrame, null)?.Invoke(); } + + /// + /// Called during freeze frames instead of the normal . + /// + public virtual void FreezeFrameUpdate() { + if (!Paused) { + foreach (patch_Entity entity in this[TagsExt.FreezeFrameUpdate]) { + if (entity.Active) { + entity.Update(); + } + } + } + } } } diff --git a/Celeste.Mod.mm/Patches/Tags.cs b/Celeste.Mod.mm/Patches/Tags.cs index 5163ef52b..654cb4762 100644 --- a/Celeste.Mod.mm/Patches/Tags.cs +++ b/Celeste.Mod.mm/Patches/Tags.cs @@ -10,6 +10,7 @@ class patch_Tags { public static void Initialize() { orig_Initialize(); TagsExt.SubHUD = new BitTag("subHUD"); + TagsExt.FreezeFrameUpdate = new BitTag("freezeFrameUpdate"); } } @@ -20,5 +21,12 @@ public static class TagsExt { /// public static BitTag SubHUD; + /// + /// Tag to be used for entities that should update during freeze frames.
+ /// If in a , is also required for this entity to update during freeze frames when the level is Paused, + /// is required to update during a transition, and is required to update when the level is Frozen. + ///
+ public static BitTag FreezeFrameUpdate; + } }