From bab055892745158a694bcc9aa2f20710dedcc9b8 Mon Sep 17 00:00:00 2001 From: aonkeeper4 Date: Thu, 12 Feb 2026 22:52:32 +0000 Subject: [PATCH 1/8] add `TagsExt.FreezeUpdate` --- Celeste.Mod.mm/Mod/Everest/Everest.Events.cs | 15 ++++++++ Celeste.Mod.mm/Patches/Level.cs | 40 ++++++++++++++++++++ Celeste.Mod.mm/Patches/Monocle/Engine.cs | 13 +++++++ Celeste.Mod.mm/Patches/Monocle/Scene.cs | 20 +++++++++- Celeste.Mod.mm/Patches/Tags.cs | 5 +++ 5 files changed, 92 insertions(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs index adc5dc5ab..c6f15ceca 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs @@ -235,6 +235,21 @@ internal static void BeforeUpdate(_Level level) internal static void AfterUpdate(_Level level) => OnAfterUpdate?.Invoke(level); + + /// + /// Called at the very beginning of . + /// + public static event Action<_Level> OnBeforeFreezeUpdate; + internal static void BeforeFreezeUpdate(_Level level) + => OnBeforeFreezeUpdate?.Invoke(level); + + /// + /// Called at the very end of . + /// + public static event Action<_Level> OnAfterFreezeUpdate; + + internal static void AfterFreezeUpdate(_Level level) + => OnAfterFreezeUpdate?.Invoke(level); } public static class Session { diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index b32cfaf6e..8148ceb2e 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -103,6 +103,24 @@ public static void RegisterLoadOverride(Level level, LoadOverride loadOverride) [PatchLevelUpdate] // ... except for manually manipulating the method via MonoModRules public extern new void Update(); + // ... this is gonna be fun + [MonoModIgnore] // don't put this in `Level` when we're done + internal extern static void base_FreezeUpdate(); // dummy method, will be replaced with the actual `base.FreezeUpdate` call in the il patch + [PatchLevelFreezeUpdate] // add the `virtual` flag to this method so it overrides the one in `patch_Scene` properly and calls `base.FreezeUpdate` + // todo: does there need to be anything else in here? + public void FreezeUpdate() { + Everest.Events.Level.BeforeFreezeUpdate(this); + + base_FreezeUpdate(); + + foreach (PostUpdateHook component in Tracker.GetComponents()) { + if (component.Entity.Active && component.Entity.TagCheck(TagsExt.FreezeUpdate)) { + component.OnPostUpdate(); + } + } + Everest.Events.Level.AfterFreezeUpdate(this); + } + /// /// Flash the screen a solid color. Respects the user's advanced photosensitivity settings. /// @@ -716,6 +734,12 @@ class PatchLevelLoaderDecalCreationAttribute : Attribute { } /// [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelUpdate))] class PatchLevelUpdateAttribute : Attribute { } + + /// + /// Patch our to be marked as virtual so it actually overrides the base method. + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelFreezeUpdate))] + class PatchLevelFreezeUpdateAttribute : Attribute { } /// /// Patch the Godzilla-sized level rendering method instead of reimplementing it in Everest. @@ -1041,6 +1065,22 @@ ldc.i4.s 9 } } + public static void PatchLevelFreezeUpdate(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_FreezeUpdate = t_Scene.FindMethod("FreezeUpdate"); + + ILCursor cursor = new ILCursor(context); + + // replace the dummy method with the actual call to `base.FreezeUpdate` + cursor.GotoNext(MoveType.Before, instr => instr.MatchCall("Celeste.Level", "base_FreezeUpdate")); + cursor.Remove(); + cursor.EmitLdarg0(); + cursor.EmitCall(m_Scene_FreezeUpdate); + } + 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..a5feb63b1 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Engine.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Engine.cs @@ -208,6 +208,8 @@ public static void PatchEngineUpdate(ILContext context, CustomAttribute attrib) MethodReference m_GetTimeRateComponentMultiplier = t_Engine.FindMethod("GetTimeRateComponentMultiplier"); VariableDefinition v_componentTimeRate = new(context.Import(typeof(float))); context.Method.Body.Variables.Add(v_componentTimeRate); + TypeDefinition t_Scene = f_scene.FieldType.Resolve(); + MethodReference m_Scene_FreezeUpdate = t_Scene.FindMethod("FreezeUpdate"); ILCursor cursor = new ILCursor(context); cursor.GotoNext( @@ -234,6 +236,17 @@ public static void PatchEngineUpdate(ILContext context, CustomAttribute attrib) instr => instr.MatchMul()); cursor.EmitLdloc(v_componentTimeRate); cursor.EmitMul(); + + 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_FreezeUpdate); } 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..30e0a6fec 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; @@ -48,5 +49,22 @@ public IEnumerable FindEntitiesWithSid(string sid) { public new virtual void AfterUpdate() { Interlocked.Exchange(ref OnEndOfFrame, null)?.Invoke(); } + + /// + /// Called during freeze frames instead of the normal . + /// + // todo: allow some renderers to update? + public virtual void FreezeUpdate() { + if (!Paused) { + foreach (patch_Entity entity in this[TagsExt.FreezeUpdate]) { + entity._PreUpdate(); + if (entity.Active) + { + entity.Update(); + } + entity._PostUpdate(); + } + } + } } } diff --git a/Celeste.Mod.mm/Patches/Tags.cs b/Celeste.Mod.mm/Patches/Tags.cs index 5163ef52b..229a63e8a 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.FreezeUpdate = new BitTag("freezeUpdate"); } } @@ -20,5 +21,9 @@ public static class TagsExt { /// public static BitTag SubHUD; + /// + /// Tag to be used for entities and renderers that should update during freeze frames. + /// + public static BitTag FreezeUpdate; } } From fefb4abcc76c3d7b7903275c87cd1d4c4013242b Mon Sep 17 00:00:00 2001 From: aonkeeper4 Date: Thu, 12 Feb 2026 22:58:00 +0000 Subject: [PATCH 2/8] reword tag description --- Celeste.Mod.mm/Patches/Tags.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Patches/Tags.cs b/Celeste.Mod.mm/Patches/Tags.cs index 229a63e8a..a9d62ff79 100644 --- a/Celeste.Mod.mm/Patches/Tags.cs +++ b/Celeste.Mod.mm/Patches/Tags.cs @@ -22,7 +22,7 @@ public static class TagsExt { public static BitTag SubHUD; /// - /// Tag to be used for entities and renderers that should update during freeze frames. + /// Tag to be used for entities that should update during freeze frames. /// public static BitTag FreezeUpdate; } From 4f9901f3ab311dede1ee3b6b8deac3c4d86cee8b Mon Sep 17 00:00:00 2001 From: aonkeeper4 Date: Thu, 12 Feb 2026 23:03:09 +0000 Subject: [PATCH 3/8] cleanup --- Celeste.Mod.mm/Mod/Everest/Everest.Events.cs | 1 - Celeste.Mod.mm/Patches/Level.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs index c6f15ceca..85cc44d4b 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs @@ -247,7 +247,6 @@ internal static void BeforeFreezeUpdate(_Level level) /// Called at the very end of . /// public static event Action<_Level> OnAfterFreezeUpdate; - internal static void AfterFreezeUpdate(_Level level) => OnAfterFreezeUpdate?.Invoke(level); } diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 8148ceb2e..8eecfbc03 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -105,7 +105,7 @@ public static void RegisterLoadOverride(Level level, LoadOverride loadOverride) // ... this is gonna be fun [MonoModIgnore] // don't put this in `Level` when we're done - internal extern static void base_FreezeUpdate(); // dummy method, will be replaced with the actual `base.FreezeUpdate` call in the il patch + internal extern static void base_FreezeUpdate(); // dummy method, will be replaced with the actual `base.FreezeUpdate` call in the IL patch [PatchLevelFreezeUpdate] // add the `virtual` flag to this method so it overrides the one in `patch_Scene` properly and calls `base.FreezeUpdate` // todo: does there need to be anything else in here? public void FreezeUpdate() { @@ -736,7 +736,7 @@ class PatchLevelLoaderDecalCreationAttribute : Attribute { } class PatchLevelUpdateAttribute : Attribute { } /// - /// Patch our to be marked as virtual so it actually overrides the base method. + /// Patch our to be marked as virtual so it actually overrides the base method, and to actually call the base method instead of the dummy. /// [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelFreezeUpdate))] class PatchLevelFreezeUpdateAttribute : Attribute { } From b885b8343495736bc2df9c7fa7a7fa903f2b2c2b Mon Sep 17 00:00:00 2001 From: aonkeeper4 Date: Thu, 12 Feb 2026 23:25:26 +0000 Subject: [PATCH 4/8] rename all `FreezeUpdate`s to `FreezeFrameUpdate` to be distinct from `Tags.FrozenUpdate` --- Celeste.Mod.mm/Mod/Everest/Everest.Events.cs | 16 +++++------ Celeste.Mod.mm/Patches/Level.cs | 30 ++++++++++---------- Celeste.Mod.mm/Patches/Monocle/Engine.cs | 4 +-- Celeste.Mod.mm/Patches/Monocle/Scene.cs | 4 +-- Celeste.Mod.mm/Patches/Tags.cs | 4 +-- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs index 85cc44d4b..9617f3e4f 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs @@ -237,18 +237,18 @@ internal static void AfterUpdate(_Level level) => OnAfterUpdate?.Invoke(level); /// - /// Called at the very beginning of . + /// Called at the very beginning of . /// - public static event Action<_Level> OnBeforeFreezeUpdate; - internal static void BeforeFreezeUpdate(_Level level) - => OnBeforeFreezeUpdate?.Invoke(level); + public static event Action<_Level> OnBeforeFreezeFrameUpdate; + internal static void BeforeFreezeFrameUpdate(_Level level) + => OnBeforeFreezeFrameUpdate?.Invoke(level); /// - /// Called at the very end of . + /// Called at the very end of . /// - public static event Action<_Level> OnAfterFreezeUpdate; - internal static void AfterFreezeUpdate(_Level level) - => OnAfterFreezeUpdate?.Invoke(level); + public static event Action<_Level> OnAfterFreezeFrameUpdate; + internal static void AfterFreezeFrameUpdate(_Level level) + => OnAfterFreezeFrameUpdate?.Invoke(level); } public static class Session { diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 8eecfbc03..b0c5c1b05 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -105,20 +105,20 @@ public static void RegisterLoadOverride(Level level, LoadOverride loadOverride) // ... this is gonna be fun [MonoModIgnore] // don't put this in `Level` when we're done - internal extern static void base_FreezeUpdate(); // dummy method, will be replaced with the actual `base.FreezeUpdate` call in the IL patch - [PatchLevelFreezeUpdate] // add the `virtual` flag to this method so it overrides the one in `patch_Scene` properly and calls `base.FreezeUpdate` + internal extern static 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` // todo: does there need to be anything else in here? - public void FreezeUpdate() { - Everest.Events.Level.BeforeFreezeUpdate(this); + public void FreezeFrameUpdate() { + Everest.Events.Level.BeforeFreezeFrameUpdate(this); - base_FreezeUpdate(); + base_FreezeFrameUpdate(); foreach (PostUpdateHook component in Tracker.GetComponents()) { - if (component.Entity.Active && component.Entity.TagCheck(TagsExt.FreezeUpdate)) { + if (component.Entity.Active && component.Entity.TagCheck(TagsExt.FreezeFrameUpdate)) { component.OnPostUpdate(); } } - Everest.Events.Level.AfterFreezeUpdate(this); + Everest.Events.Level.AfterFreezeFrameUpdate(this); } /// @@ -736,10 +736,10 @@ class PatchLevelLoaderDecalCreationAttribute : Attribute { } class PatchLevelUpdateAttribute : Attribute { } /// - /// Patch our to be marked as virtual so it actually overrides the base method, and to actually call the base method instead of the dummy. + /// Patch our to be marked as virtual so it actually overrides the base method, and to actually call the base method instead of the dummy. /// - [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelFreezeUpdate))] - class PatchLevelFreezeUpdateAttribute : Attribute { } + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelFreezeFrameUpdate))] + class PatchLevelFreezeFrameUpdateAttribute : Attribute { } /// /// Patch the Godzilla-sized level rendering method instead of reimplementing it in Everest. @@ -1065,20 +1065,20 @@ ldc.i4.s 9 } } - public static void PatchLevelFreezeUpdate(ILContext context, CustomAttribute attrib) { + 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_FreezeUpdate = t_Scene.FindMethod("FreezeUpdate"); + MethodReference m_Scene_FreezeFrameUpdate = t_Scene.FindMethod("FreezeFrameUpdate"); ILCursor cursor = new ILCursor(context); - // replace the dummy method with the actual call to `base.FreezeUpdate` - cursor.GotoNext(MoveType.Before, instr => instr.MatchCall("Celeste.Level", "base_FreezeUpdate")); + // 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_FreezeUpdate); + cursor.EmitCall(m_Scene_FreezeFrameUpdate); } public static void PatchLevelRender(ILContext context, CustomAttribute attrib) { diff --git a/Celeste.Mod.mm/Patches/Monocle/Engine.cs b/Celeste.Mod.mm/Patches/Monocle/Engine.cs index a5feb63b1..76b4967a4 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Engine.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Engine.cs @@ -209,7 +209,7 @@ public static void PatchEngineUpdate(ILContext context, CustomAttribute attrib) VariableDefinition v_componentTimeRate = new(context.Import(typeof(float))); context.Method.Body.Variables.Add(v_componentTimeRate); TypeDefinition t_Scene = f_scene.FieldType.Resolve(); - MethodReference m_Scene_FreezeUpdate = t_Scene.FindMethod("FreezeUpdate"); + MethodReference m_Scene_FreezeFrameUpdate = t_Scene.FindMethod("FreezeFrameUpdate"); ILCursor cursor = new ILCursor(context); cursor.GotoNext( @@ -246,7 +246,7 @@ public static void PatchEngineUpdate(ILContext context, CustomAttribute attrib) instr => instr.MatchStsfld("Monocle.Engine", "FreezeTimer")); cursor.EmitLdarg0(); cursor.EmitLdfld(f_scene); - cursor.EmitCallvirt(m_Scene_FreezeUpdate); + 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 30e0a6fec..9266202a8 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Scene.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Scene.cs @@ -54,9 +54,9 @@ public IEnumerable FindEntitiesWithSid(string sid) { /// Called during freeze frames instead of the normal . /// // todo: allow some renderers to update? - public virtual void FreezeUpdate() { + public virtual void FreezeFrameUpdate() { if (!Paused) { - foreach (patch_Entity entity in this[TagsExt.FreezeUpdate]) { + foreach (patch_Entity entity in this[TagsExt.FreezeFrameUpdate]) { entity._PreUpdate(); if (entity.Active) { diff --git a/Celeste.Mod.mm/Patches/Tags.cs b/Celeste.Mod.mm/Patches/Tags.cs index a9d62ff79..c6a8db2a7 100644 --- a/Celeste.Mod.mm/Patches/Tags.cs +++ b/Celeste.Mod.mm/Patches/Tags.cs @@ -10,7 +10,7 @@ class patch_Tags { public static void Initialize() { orig_Initialize(); TagsExt.SubHUD = new BitTag("subHUD"); - TagsExt.FreezeUpdate = new BitTag("freezeUpdate"); + TagsExt.FreezeFrameUpdate = new BitTag("freezeFrameUpdate"); } } @@ -24,6 +24,6 @@ public static class TagsExt { /// /// Tag to be used for entities that should update during freeze frames. /// - public static BitTag FreezeUpdate; + public static BitTag FreezeFrameUpdate; } } From 16032e585bf739ed25bf1587ed63fd75b8d7cf1c Mon Sep 17 00:00:00 2001 From: aonkeeper4 Date: Fri, 20 Mar 2026 15:26:59 +0000 Subject: [PATCH 5/8] migrate events --- Celeste.Mod.mm/Patches/Level.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 62906c5e1..713e20392 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -815,6 +815,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); } } } From 52bc1984bb6364e6cbd84df5741ea7a8909e2261 Mon Sep 17 00:00:00 2001 From: aon Date: Fri, 17 Apr 2026 08:14:17 +0100 Subject: [PATCH 6/8] fix xmldoc --- Celeste.Mod.mm/Patches/Level.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 713e20392..1df0ae4db 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -816,7 +816,7 @@ internal static void BeforeUpdate(_Level level) internal static void AfterUpdate(_Level level) => OnAfterUpdate?.Invoke(level); - // + /// /// Called at the very beginning of . /// public static event Action<_Level> OnBeforeFreezeFrameUpdate; From 105b62eb29619806389db9645798df712da6df5b Mon Sep 17 00:00:00 2001 From: aonkeeper4 Date: Wed, 24 Jun 2026 16:14:22 +0100 Subject: [PATCH 7/8] apply changes from review --- Celeste.Mod.mm/Patches/Level.cs | 46 ++++++++++++++++++++---- Celeste.Mod.mm/Patches/Monocle/Engine.cs | 6 ++-- Celeste.Mod.mm/Patches/Monocle/Scene.cs | 9 ++--- Celeste.Mod.mm/Patches/Tags.cs | 5 ++- 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 1df0ae4db..187513085 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -106,21 +106,55 @@ public static void RegisterLoadOverride(Level level, LoadOverride loadOverride) [PatchLevelUpdate] // ... except for manually manipulating the method via MonoModRules public extern new void Update(); - // ... this is gonna be fun [MonoModIgnore] // don't put this in `Level` when we're done - internal extern static void base_FreezeFrameUpdate(); // dummy method, will be replaced with the actual `base.FreezeFrameUpdate` call in the IL patch + 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` - // todo: does there need to be anything else in here? public void FreezeFrameUpdate() { Everest.Events.Level.BeforeFreezeFrameUpdate(this); - - base_FreezeFrameUpdate(); - + + // 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); } diff --git a/Celeste.Mod.mm/Patches/Monocle/Engine.cs b/Celeste.Mod.mm/Patches/Monocle/Engine.cs index 76b4967a4..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,15 +202,15 @@ 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); - TypeDefinition t_Scene = f_scene.FieldType.Resolve(); - MethodReference m_Scene_FreezeFrameUpdate = t_Scene.FindMethod("FreezeFrameUpdate"); ILCursor cursor = new ILCursor(context); cursor.GotoNext( @@ -237,6 +238,7 @@ public static void PatchEngineUpdate(ILContext context, CustomAttribute attrib) 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"), diff --git a/Celeste.Mod.mm/Patches/Monocle/Scene.cs b/Celeste.Mod.mm/Patches/Monocle/Scene.cs index 9266202a8..5f115d25a 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Scene.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Scene.cs @@ -24,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. /// @@ -53,16 +52,12 @@ public IEnumerable FindEntitiesWithSid(string sid) { /// /// Called during freeze frames instead of the normal . /// - // todo: allow some renderers to update? public virtual void FreezeFrameUpdate() { if (!Paused) { foreach (patch_Entity entity in this[TagsExt.FreezeFrameUpdate]) { - entity._PreUpdate(); - if (entity.Active) - { + if (entity.Active) { entity.Update(); } - entity._PostUpdate(); } } } diff --git a/Celeste.Mod.mm/Patches/Tags.cs b/Celeste.Mod.mm/Patches/Tags.cs index c6a8db2a7..654cb4762 100644 --- a/Celeste.Mod.mm/Patches/Tags.cs +++ b/Celeste.Mod.mm/Patches/Tags.cs @@ -22,8 +22,11 @@ public static class TagsExt { public static BitTag SubHUD; /// - /// Tag to be used for entities that should update during freeze frames. + /// 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; + } } From e71444838251f897c98bfd1dcdc5d744f3213a9f Mon Sep 17 00:00:00 2001 From: aonkeeper4 Date: Wed, 24 Jun 2026 16:55:15 +0100 Subject: [PATCH 8/8] update docs --- Celeste.Mod.mm/Patches/Level.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 187513085..76ce26845 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -888,7 +888,7 @@ class PatchLevelLoaderDecalCreationAttribute : Attribute { } class PatchLevelUpdateAttribute : Attribute { } /// - /// Patch our to be marked as virtual so it actually overrides the base method, and to actually call the base method instead of the dummy. + /// 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 { }