From 4c4e8031dfa310cb3cd732fe507377ac7bbf360b Mon Sep 17 00:00:00 2001 From: SilverDorian46 <86711559+SilverDorian46@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:00:42 +0400 Subject: [PATCH 1/5] Implement CustomWipe attribute --- .../Mod/Entities/CustomWipeAttribute.cs | 28 +++++++++++ Celeste.Mod.mm/Mod/Everest/Everest.Events.cs | 17 +++++++ Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs | 47 +++++++++++++++++++ Celeste.Mod.mm/Mod/Meta/MapMeta.cs | 14 ++++-- Celeste.Mod.mm/Patches/AreaData.cs | 2 + 5 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs diff --git a/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs b/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs new file mode 100644 index 000000000..449b089e8 --- /dev/null +++ b/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs @@ -0,0 +1,28 @@ +using System; + +namespace Celeste.Mod.Entities; + +/// +/// Mark this renderer as a custom with an identifier. +///
+/// This Screen Wipe will be applied if the map's Wipe metadata has a matching value. +///
+/// If there is no match, then the full type name of the Screen Wipe is checked for. +///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class CustomWipeAttribute : Attribute { + + /// + /// A list of unique identifiers for this Screen Wipe. + /// + public string[] IDs; + + /// + /// Mark this renderer as a custom with an identifier.
+ /// If there is no match, then the full type name of the Screen Wipe is checked for. + ///
+ /// A list of unique identifiers for this Screen Wipe. + public CustomWipeAttribute(params string[] ids) { + IDs = ids; + } +} diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs index adc5dc5ab..755cd544d 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs @@ -14,6 +14,7 @@ using _Seeker = Celeste.Seeker; using _AngryOshiro = Celeste.AngryOshiro; using _SubHudRenderer = Celeste.Mod.UI.SubHudRenderer; +using _AreaData = Celeste.AreaData; using Monocle; namespace Celeste.Mod { @@ -404,6 +405,22 @@ public static class SubHudRenderer { internal static void BeforeRender(_SubHudRenderer renderer, Scene scene) => OnBeforeRender?.Invoke(renderer, scene); } + + public static class MapMeta { + public delegate Action ApplyWipeHandler(_AreaData area, string wipe); + public static event ApplyWipeHandler OnApplyWipe; + internal static Action ApplyWipe(_AreaData area, string wipe) { + if (OnApplyWipe is null) + return null; + + foreach (ApplyWipeHandler handler in OnApplyWipe.GetInvocationList()) { + if (handler(area, wipe) is { } wipeLoader) + return wipeLoader; + } + + return null; + } + } } } } diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs index 0271ed89c..880789c2d 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs @@ -822,6 +822,53 @@ internal static void ProcessAssembly(EverestModuleMetadata meta, Assembly asm, T } } + // Search for all Screen Wipes marked with the CustomWipeAttribute. + foreach (CustomWipeAttribute attrib in type.GetCustomAttributes()) { + foreach (string idFull in attrib.IDs) { + string id; + string genName; + string[] split = idFull.Split('='); + + if (split.Length == 1) { + id = split[0]; + genName = "Load"; + } else if (split.Length == 2) { + id = split[0]; + genName = split[1]; + } else { + Logger.Warn("core", $"Invalid number of custom wipe ID elements: {idFull} ({type.FullName})"); + continue; + } + + id = id.Trim(); + genName = genName.Trim(); + + Action loader = null; + + ConstructorInfo ctor; + MethodInfo gen; + + gen = type.GetMethod(genName, new Type[] { typeof(Scene), typeof(bool), typeof(Action) }); + if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(ScreenWipe))) { + loader = (scene, wipeIn, onComplete) => gen.Invoke(null, new object[] { scene, wipeIn, onComplete }); + goto RegisterWipeLoader; + } + + ctor = type.GetConstructor(new Type[] { typeof(Scene), typeof(bool), typeof(Action) }); + if (ctor != null) { + loader = (scene, wipeIn, onComplete) => ctor.Invoke(new object[] { scene, wipeIn, onComplete }); + goto RegisterWipeLoader; + } + + RegisterWipeLoader: + if (loader == null) { + Logger.Warn("core", $"Found custom wipe without suitable constructor / {genName}(Scene, bool, Action): {id} ({type.FullName})"); + continue; + } + patch_AreaData.WipeLoaders[id] = loader; + } + } + // we already are in the overworld. Register new Ouis real quick! if (Engine.Instance != null && Engine.Scene is Overworld overworld && typeof(Oui).IsAssignableFrom(type) && !type.IsAbstract) { Logger.Verbose("core", $"Instantiating UI from {meta}: {type.FullName}"); diff --git a/Celeste.Mod.mm/Mod/Meta/MapMeta.cs b/Celeste.Mod.mm/Mod/Meta/MapMeta.cs index 6614d80ee..eed9cfed9 100644 --- a/Celeste.Mod.mm/Mod/Meta/MapMeta.cs +++ b/Celeste.Mod.mm/Mod/Meta/MapMeta.cs @@ -161,10 +161,16 @@ public void ApplyTo(patch_AreaData area) { area.ColorGrade = ColorGrade; if (!string.IsNullOrEmpty(Wipe)) { - Type type = Assembly.GetEntryAssembly().GetType(Wipe); - ConstructorInfo ctor = type?.GetConstructor(new Type[] { typeof(Scene), typeof(bool), typeof(Action) }); - if (type != null && ctor != null) { - area.Wipe = (scene, wipeIn, onComplete) => ctor.Invoke(new object[] { scene, wipeIn, onComplete }); + string wipeStr = Wipe; + if (Everest.Events.MapMeta.ApplyWipe(area, wipeStr) is { } wipeLoader + || (patch_AreaData.WipeLoaders.TryGetValue(wipeStr, out wipeLoader) && wipeLoader is not null)) + area.Wipe = wipeLoader; + else { + Type type = Assembly.GetEntryAssembly().GetType(wipeStr); + ConstructorInfo ctor = type?.GetConstructor(new Type[] { typeof(Scene), typeof(bool), typeof(Action) }); + if (type != null && ctor != null) { + area.Wipe = (scene, wipeIn, onComplete) => ctor.Invoke(new object[] { scene, wipeIn, onComplete }); + } } } diff --git a/Celeste.Mod.mm/Patches/AreaData.cs b/Celeste.Mod.mm/Patches/AreaData.cs index df8c3ff0e..b5c948e49 100644 --- a/Celeste.Mod.mm/Patches/AreaData.cs +++ b/Celeste.Mod.mm/Patches/AreaData.cs @@ -14,6 +14,8 @@ namespace Celeste { public class patch_AreaData : AreaData { + public static readonly Dictionary> WipeLoaders = new(); + #pragma warning disable CS0108 // Hides inherited member // Required to reference this class in other files From ea2f04dffdcefe57d2df32434684f2465e73b081 Mon Sep 17 00:00:00 2001 From: SilverDorian46 <86711559+SilverDorian46@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:19:34 +0400 Subject: [PATCH 2/5] Rename "ApplyWipe" to "ParseWipe" and remove AreaData parameter from event --- Celeste.Mod.mm/Mod/Everest/Everest.Events.cs | 13 ++++++------- Celeste.Mod.mm/Mod/Meta/MapMeta.cs | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs index 755cd544d..2ebc645e6 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs @@ -14,7 +14,6 @@ using _Seeker = Celeste.Seeker; using _AngryOshiro = Celeste.AngryOshiro; using _SubHudRenderer = Celeste.Mod.UI.SubHudRenderer; -using _AreaData = Celeste.AreaData; using Monocle; namespace Celeste.Mod { @@ -407,14 +406,14 @@ internal static void BeforeRender(_SubHudRenderer renderer, Scene scene) } public static class MapMeta { - public delegate Action ApplyWipeHandler(_AreaData area, string wipe); - public static event ApplyWipeHandler OnApplyWipe; - internal static Action ApplyWipe(_AreaData area, string wipe) { - if (OnApplyWipe is null) + public delegate Action ParseWipeHandler(string wipe); + public static event ParseWipeHandler OnParseWipe; + internal static Action ParseWipe(string wipe) { + if (OnParseWipe is null) return null; - foreach (ApplyWipeHandler handler in OnApplyWipe.GetInvocationList()) { - if (handler(area, wipe) is { } wipeLoader) + foreach (ParseWipeHandler handler in OnParseWipe.GetInvocationList()) { + if (handler(wipe) is { } wipeLoader) return wipeLoader; } diff --git a/Celeste.Mod.mm/Mod/Meta/MapMeta.cs b/Celeste.Mod.mm/Mod/Meta/MapMeta.cs index eed9cfed9..2a9933d7c 100644 --- a/Celeste.Mod.mm/Mod/Meta/MapMeta.cs +++ b/Celeste.Mod.mm/Mod/Meta/MapMeta.cs @@ -162,7 +162,7 @@ public void ApplyTo(patch_AreaData area) { if (!string.IsNullOrEmpty(Wipe)) { string wipeStr = Wipe; - if (Everest.Events.MapMeta.ApplyWipe(area, wipeStr) is { } wipeLoader + if (Everest.Events.MapMeta.ParseWipe(wipeStr) is { } wipeLoader || (patch_AreaData.WipeLoaders.TryGetValue(wipeStr, out wipeLoader) && wipeLoader is not null)) area.Wipe = wipeLoader; else { From 78431cddf222fac8d07a0c3f1a6f3a847f7cefdd Mon Sep 17 00:00:00 2001 From: SilverDorian46 <86711559+SilverDorian46@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:36:31 +0400 Subject: [PATCH 3/5] Split parse wipe condition with an `else if` for readability --- Celeste.Mod.mm/Mod/Meta/MapMeta.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Meta/MapMeta.cs b/Celeste.Mod.mm/Mod/Meta/MapMeta.cs index 2a9933d7c..46f63aa1f 100644 --- a/Celeste.Mod.mm/Mod/Meta/MapMeta.cs +++ b/Celeste.Mod.mm/Mod/Meta/MapMeta.cs @@ -162,8 +162,9 @@ public void ApplyTo(patch_AreaData area) { if (!string.IsNullOrEmpty(Wipe)) { string wipeStr = Wipe; - if (Everest.Events.MapMeta.ParseWipe(wipeStr) is { } wipeLoader - || (patch_AreaData.WipeLoaders.TryGetValue(wipeStr, out wipeLoader) && wipeLoader is not null)) + if (Everest.Events.MapMeta.ParseWipe(wipeStr) is { } wipeLoader) + area.Wipe = wipeLoader; + else if (patch_AreaData.WipeLoaders.TryGetValue(wipeStr, out wipeLoader) && wipeLoader is not null) area.Wipe = wipeLoader; else { Type type = Assembly.GetEntryAssembly().GetType(wipeStr); From 856e02c803d45a778d3426094c4c359dbda458d0 Mon Sep 17 00:00:00 2001 From: SilverDorian46 <86711559+SilverDorian46@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:38:43 +0400 Subject: [PATCH 4/5] Specify the pattern in the summary for the attribute's ID --- Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs b/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs index 449b089e8..3df44467b 100644 --- a/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs +++ b/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs @@ -13,7 +13,8 @@ namespace Celeste.Mod.Entities; public class CustomWipeAttribute : Attribute { /// - /// A list of unique identifiers for this Screen Wipe. + /// A list of unique identifiers for this Screen Wipe.
+ /// Follows the pattern "ID [= LoadMethodName]" ///
public string[] IDs; @@ -21,7 +22,10 @@ public class CustomWipeAttribute : Attribute { /// Mark this renderer as a custom with an identifier.
/// If there is no match, then the full type name of the Screen Wipe is checked for. /// - /// A list of unique identifiers for this Screen Wipe. + /// + /// A list of unique identifiers for this Screen Wipe.
+ /// Follows the pattern "ID [= LoadMethodName]" + /// public CustomWipeAttribute(params string[] ids) { IDs = ids; } From 95083bb1bc154ac7e50dcb188d7274aa0d6210a7 Mon Sep 17 00:00:00 2001 From: SilverDorian46 <86711559+SilverDorian46@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:53:04 +0400 Subject: [PATCH 5/5] Specify ID pattern in the summary for every Custom____Attribute --- Celeste.Mod.mm/Mod/Backdrops/CustomBackdropAttribute.cs | 8 ++++++-- Celeste.Mod.mm/Mod/Entities/CustomEntityAttribute.cs | 8 ++++++-- Celeste.Mod.mm/Mod/Entities/CustomEventAttribute.cs | 8 ++++++-- Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs | 4 ++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Backdrops/CustomBackdropAttribute.cs b/Celeste.Mod.mm/Mod/Backdrops/CustomBackdropAttribute.cs index 79be0fbdb..5e7af635e 100644 --- a/Celeste.Mod.mm/Mod/Backdrops/CustomBackdropAttribute.cs +++ b/Celeste.Mod.mm/Mod/Backdrops/CustomBackdropAttribute.cs @@ -9,14 +9,18 @@ namespace Celeste.Mod.Backdrops { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class CustomBackdropAttribute : Attribute { /// - /// A list of unique identifiers for this Backdrop. + /// A list of unique identifiers for this Backdrop.
+ /// Follows the pattern "ID [= LoadMethodName]". ///
public string[] IDs { get; } /// /// Marks this backdrop as a Custom . /// - /// A list of unique identifiers for this Backdrop. + /// + /// A list of unique identifiers for this Backdrop.
+ /// Follows the pattern "ID [= LoadMethodName]". + /// public CustomBackdropAttribute(params string[] ids) { IDs = ids; } diff --git a/Celeste.Mod.mm/Mod/Entities/CustomEntityAttribute.cs b/Celeste.Mod.mm/Mod/Entities/CustomEntityAttribute.cs index af29475e5..bed6419e2 100644 --- a/Celeste.Mod.mm/Mod/Entities/CustomEntityAttribute.cs +++ b/Celeste.Mod.mm/Mod/Entities/CustomEntityAttribute.cs @@ -13,14 +13,18 @@ namespace Celeste.Mod.Entities { public class CustomEntityAttribute : Attribute { /// - /// A list of unique identifiers for this Entity. + /// A list of unique identifiers for this Entity.
+ /// Follows the pattern "ID [= LoadMethodName]". ///
public string[] IDs; /// /// Mark this entity as a Custom or . /// - /// A list of unique identifiers for this Entity. + /// + /// A list of unique identifiers for this Entity.
+ /// Follows the pattern "ID [= LoadMethodName]". + /// public CustomEntityAttribute(params string[] ids) { IDs = ids; } diff --git a/Celeste.Mod.mm/Mod/Entities/CustomEventAttribute.cs b/Celeste.Mod.mm/Mod/Entities/CustomEventAttribute.cs index 925743673..80b6d7b54 100644 --- a/Celeste.Mod.mm/Mod/Entities/CustomEventAttribute.cs +++ b/Celeste.Mod.mm/Mod/Entities/CustomEventAttribute.cs @@ -13,14 +13,18 @@ namespace Celeste.Mod.Entities { public class CustomEventAttribute : Attribute { /// - /// A list of unique identifiers for this Event. + /// A list of unique identifiers for this Event.
+ /// Follows the pattern "ID [= LoadMethodName]". ///
public string[] IDs; /// /// Mark this entity as a Custom or other Event . /// - /// A list of unique identifiers for this Event. + /// + /// A list of unique identifiers for this Event.
+ /// Follows the pattern "ID [= LoadMethodName]". + /// public CustomEventAttribute(params string[] ids) { IDs = ids; } diff --git a/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs b/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs index 3df44467b..e77ebad3c 100644 --- a/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs +++ b/Celeste.Mod.mm/Mod/Entities/CustomWipeAttribute.cs @@ -14,7 +14,7 @@ public class CustomWipeAttribute : Attribute { /// /// A list of unique identifiers for this Screen Wipe.
- /// Follows the pattern "ID [= LoadMethodName]" + /// Follows the pattern "ID [= LoadMethodName]". ///
public string[] IDs; @@ -24,7 +24,7 @@ public class CustomWipeAttribute : Attribute { /// /// /// A list of unique identifiers for this Screen Wipe.
- /// Follows the pattern "ID [= LoadMethodName]" + /// Follows the pattern "ID [= LoadMethodName]". /// public CustomWipeAttribute(params string[] ids) { IDs = ids;