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;