From 642e90539117fec95713d585e1a93ccb241ae2e8 Mon Sep 17 00:00:00 2001 From: microlith57 Date: Tue, 31 Mar 2026 23:14:36 +1300 Subject: [PATCH 1/2] implement ISpeed interface --- Celeste.Mod.mm/Mod/Entities/ISpeed.cs | 11 +++++++++++ Celeste.Mod.mm/Patches/Glider.cs | 7 +++++-- Celeste.Mod.mm/Patches/Holdable.cs | 4 +++- Celeste.Mod.mm/Patches/Player.cs | 5 +++-- Celeste.Mod.mm/Patches/Seeker.cs | 4 +++- Celeste.Mod.mm/Patches/Solid.cs | 21 +++++++++++++++------ Celeste.Mod.mm/Patches/TheoCrystal.cs | 7 +++++-- 7 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 Celeste.Mod.mm/Mod/Entities/ISpeed.cs diff --git a/Celeste.Mod.mm/Mod/Entities/ISpeed.cs b/Celeste.Mod.mm/Mod/Entities/ISpeed.cs new file mode 100644 index 000000000..e703dfd09 --- /dev/null +++ b/Celeste.Mod.mm/Mod/Entities/ISpeed.cs @@ -0,0 +1,11 @@ +using Microsoft.Xna.Framework; + +namespace Celeste.Mod { + /// + /// + public interface ISpeed { + /// + /// + public Vector2 Speed { get; set; } + } +} diff --git a/Celeste.Mod.mm/Patches/Glider.cs b/Celeste.Mod.mm/Patches/Glider.cs index 4da373834..935a4f828 100644 --- a/Celeste.Mod.mm/Patches/Glider.cs +++ b/Celeste.Mod.mm/Patches/Glider.cs @@ -2,13 +2,16 @@ #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value +using Celeste.Mod; using Microsoft.Xna.Framework; using MonoMod; namespace Celeste { - class patch_Glider : Glider { + class patch_Glider : Glider, ISpeed { public patch_Holdable Hold; // avoids extra cast + Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } + public patch_Glider(Vector2 position, bool bubble, bool tutorial) : base(position, bubble, tutorial) { } @@ -21,4 +24,4 @@ public void ctor(Vector2 position, bool bubble, bool tutorial) { Hold.SpeedSetter = (speed) => { Speed = speed; }; } } -} \ No newline at end of file +} diff --git a/Celeste.Mod.mm/Patches/Holdable.cs b/Celeste.Mod.mm/Patches/Holdable.cs index 481debb18..1133a1f22 100644 --- a/Celeste.Mod.mm/Patches/Holdable.cs +++ b/Celeste.Mod.mm/Patches/Holdable.cs @@ -3,10 +3,12 @@ using Microsoft.Xna.Framework; using MonoMod; using System; +using Celeste.Mod; namespace Celeste { - class patch_Holdable : Holdable { + class patch_Holdable : Holdable, ISpeed { public Action SpeedSetter; + public Vector2 Speed { get => GetSpeed(); set => SetSpeed(value); } [MonoModLinkTo("Celeste.Holdable", "System.Void .ctor(System.Single)")] [MonoModForceCall] diff --git a/Celeste.Mod.mm/Patches/Player.cs b/Celeste.Mod.mm/Patches/Player.cs index 8d8d1d2bc..e0062a8e9 100644 --- a/Celeste.Mod.mm/Patches/Player.cs +++ b/Celeste.Mod.mm/Patches/Player.cs @@ -20,7 +20,7 @@ using _Player = Celeste.Player; namespace Celeste { - class patch_Player : Player { + class patch_Player : Player, ISpeed { // We're effectively in Player, but still need to "expose" private fields to our mod. private bool wasDashB; @@ -43,7 +43,6 @@ class patch_Player : Player { } } - public bool IsIntroState { get { int state = StateMachine.State; @@ -51,6 +50,8 @@ public bool IsIntroState { } } + Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } + public patch_Player(Vector2 position, PlayerSpriteMode spriteMode) : base(position, spriteMode) { // no-op. MonoMod ignores this - we only need this to make the compiler shut up. diff --git a/Celeste.Mod.mm/Patches/Seeker.cs b/Celeste.Mod.mm/Patches/Seeker.cs index 777f793e8..17cc4bddd 100644 --- a/Celeste.Mod.mm/Patches/Seeker.cs +++ b/Celeste.Mod.mm/Patches/Seeker.cs @@ -11,11 +11,13 @@ using _Seeker = Celeste.Seeker; namespace Celeste { - public class patch_Seeker : Seeker { + public class patch_Seeker : Seeker, ISpeed { // We're effectively in Seeker, but still need to "expose" private fields to our mod. private StateMachine State; + Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } + // no-op - only here to make public patch_Seeker(EntityData data, Vector2 offset) : base(data, offset) { diff --git a/Celeste.Mod.mm/Patches/Solid.cs b/Celeste.Mod.mm/Patches/Solid.cs index 53c3d1502..35c0e851a 100644 --- a/Celeste.Mod.mm/Patches/Solid.cs +++ b/Celeste.Mod.mm/Patches/Solid.cs @@ -1,4 +1,6 @@ -using Mono.Cecil; +using Celeste.Mod; +using Microsoft.Xna.Framework; +using Mono.Cecil; using Mono.Cecil.Cil; using Monocle; using MonoMod; @@ -8,22 +10,29 @@ using System; namespace Celeste { - internal class patch_Solid { + internal class patch_Solid : Solid, ISpeed { + + Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } + + public patch_Solid(Vector2 position, float width, float height, bool safe) : base(position, width, height, safe) { + // no-op. MonoMod ignores this - we only need this to make the compiler shut up. + } + [MonoModIgnore] [PatchSolidAwake] - public extern void Awake(Scene scene); + public extern new void Awake(Scene scene); [MonoModIgnore] [ForceNoInlining] - public extern bool HasPlayerClimbing(); + public extern new bool HasPlayerClimbing(); [MonoModIgnore] [ForceNoInlining] - public extern bool HasPlayerOnTop(); + public extern new bool HasPlayerOnTop(); [MonoModIgnore] [ForceNoInlining] - public extern bool HasPlayerRider(); + public extern new bool HasPlayerRider(); } } diff --git a/Celeste.Mod.mm/Patches/TheoCrystal.cs b/Celeste.Mod.mm/Patches/TheoCrystal.cs index 78c3765b4..d9e566ecd 100644 --- a/Celeste.Mod.mm/Patches/TheoCrystal.cs +++ b/Celeste.Mod.mm/Patches/TheoCrystal.cs @@ -2,13 +2,16 @@ #pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value +using Celeste.Mod; using Microsoft.Xna.Framework; using MonoMod; namespace Celeste { - class patch_TheoCrystal : TheoCrystal { + class patch_TheoCrystal : TheoCrystal, ISpeed { public patch_Holdable Hold; // avoids extra cast + Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } + public patch_TheoCrystal(EntityData data, Vector2 offset) : base(data, offset) { } @@ -21,4 +24,4 @@ public void ctor(Vector2 position) { Hold.SpeedSetter = (speed) => { Speed = speed; }; } } -} \ No newline at end of file +} From 73bd50e44e750b7daca0edd8c87d7d86e987a12c Mon Sep 17 00:00:00 2001 From: microlith57 Date: Thu, 16 Apr 2026 19:11:00 +1200 Subject: [PATCH 2/2] monomod shenanigans --- Celeste.Mod.mm/Mod/Entities/ISpeed.cs | 22 ++++++++++++++++++++++ Celeste.Mod.mm/MonoModRules.cs | 13 +++++++++++++ Celeste.Mod.mm/Patches/Glider.cs | 2 ++ Celeste.Mod.mm/Patches/Holdable.cs | 3 +++ Celeste.Mod.mm/Patches/MoveBlock.cs | 9 +++++++++ Celeste.Mod.mm/Patches/Player.cs | 2 ++ Celeste.Mod.mm/Patches/Seeker.cs | 2 ++ Celeste.Mod.mm/Patches/Solid.cs | 5 +++-- Celeste.Mod.mm/Patches/TheoCrystal.cs | 2 ++ 9 files changed, 58 insertions(+), 2 deletions(-) diff --git a/Celeste.Mod.mm/Mod/Entities/ISpeed.cs b/Celeste.Mod.mm/Mod/Entities/ISpeed.cs index e703dfd09..b0d46a2f2 100644 --- a/Celeste.Mod.mm/Mod/Entities/ISpeed.cs +++ b/Celeste.Mod.mm/Mod/Entities/ISpeed.cs @@ -1,4 +1,7 @@ +using System; using Microsoft.Xna.Framework; +using Mono.Cecil; +using MonoMod.InlineRT; namespace Celeste.Mod { /// @@ -9,3 +12,22 @@ public interface ISpeed { public Vector2 Speed { get; set; } } } + +namespace MonoMod { + + /// + /// Patch the given class to tack on the ISpeed interface + /// + [MonoModCustomAttribute(nameof(MonoModRules.PatchSpeedInterface))] + class PatchSpeedInterfaceAttribute : Attribute { } + + static partial class MonoModRules { + + public static void PatchSpeedInterface(ICustomAttributeProvider provider, CustomAttribute attrib) { + InterfaceImplementation i_ISpeed = new InterfaceImplementation(MonoModRule.Modder.FindType("Celeste.Mod.ISpeed")); + + ((TypeDefinition) provider).Interfaces.Add(i_ISpeed); + } + + } +} diff --git a/Celeste.Mod.mm/MonoModRules.cs b/Celeste.Mod.mm/MonoModRules.cs index d8f3bf84a..41ff5c83e 100644 --- a/Celeste.Mod.mm/MonoModRules.cs +++ b/Celeste.Mod.mm/MonoModRules.cs @@ -8,6 +8,7 @@ using System.Reflection; using ICustomAttributeProvider = Mono.Cecil.ICustomAttributeProvider; using MethodAttributes = Mono.Cecil.MethodAttributes; +using PropertyAttributes = Mono.Cecil.PropertyAttributes; namespace MonoMod { #region Helper Patch Attributes @@ -23,6 +24,12 @@ class MakeEntryPointAttribute : Attribute { } [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchInterface))] class PatchInterfaceAttribute : Attribute { } + /// + /// Helper for patching properties force-implemented by an interface + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchInterfaceProperty))] + class PatchInterfacePropertyAttribute : Attribute { } + /// /// Forcibly changes a given member's name. /// @@ -228,6 +235,12 @@ public static void PatchInterface(MethodDefinition method, CustomAttribute attri method.Attributes |= flags; } + public static void PatchInterfaceProperty(PropertyDefinition property, CustomAttribute attrib) { + MethodAttributes flags = MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.NewSlot; + if (property.GetMethod is {} @get) @get.Attributes |= flags; + if (property.SetMethod is {} @set) @set.Attributes |= flags; + } + public static void ForceName(ICustomAttributeProvider cap, CustomAttribute attrib) { if (cap is IMemberDefinition member) member.Name = (string) attrib.ConstructorArguments[0].Value; diff --git a/Celeste.Mod.mm/Patches/Glider.cs b/Celeste.Mod.mm/Patches/Glider.cs index 935a4f828..ae8d9333b 100644 --- a/Celeste.Mod.mm/Patches/Glider.cs +++ b/Celeste.Mod.mm/Patches/Glider.cs @@ -7,9 +7,11 @@ using MonoMod; namespace Celeste { + [PatchSpeedInterface] class patch_Glider : Glider, ISpeed { public patch_Holdable Hold; // avoids extra cast + [PatchInterfaceProperty] Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } public patch_Glider(Vector2 position, bool bubble, bool tutorial) diff --git a/Celeste.Mod.mm/Patches/Holdable.cs b/Celeste.Mod.mm/Patches/Holdable.cs index 1133a1f22..898f4dcf1 100644 --- a/Celeste.Mod.mm/Patches/Holdable.cs +++ b/Celeste.Mod.mm/Patches/Holdable.cs @@ -6,8 +6,11 @@ using Celeste.Mod; namespace Celeste { + [PatchSpeedInterface] class patch_Holdable : Holdable, ISpeed { public Action SpeedSetter; + + [PatchInterfaceProperty] public Vector2 Speed { get => GetSpeed(); set => SetSpeed(value); } [MonoModLinkTo("Celeste.Holdable", "System.Void .ctor(System.Single)")] diff --git a/Celeste.Mod.mm/Patches/MoveBlock.cs b/Celeste.Mod.mm/Patches/MoveBlock.cs index 237c67563..d254cf2b1 100644 --- a/Celeste.Mod.mm/Patches/MoveBlock.cs +++ b/Celeste.Mod.mm/Patches/MoveBlock.cs @@ -7,8 +7,10 @@ using MonoMod.Cil; using MonoMod.InlineRT; using MonoMod.Utils; +using Celeste.Mod; namespace Celeste { + [PatchSpeedInterface] class patch_MoveBlock : MoveBlock { public patch_MoveBlock(Vector2 position, int width, int height, MoveBlock.Directions direction, bool canSteer, bool fast) @@ -19,6 +21,13 @@ public patch_MoveBlock(Vector2 position, int width, int height, MoveBlock.Direct [MonoModIgnore] [PatchMoveBlockController] private extern IEnumerator Controller(); + + class patch_Debris : ISpeed { + private Vector2 speed; + + [PatchInterfaceProperty] + Vector2 ISpeed.Speed { get => speed; set => speed = value; } + } } } diff --git a/Celeste.Mod.mm/Patches/Player.cs b/Celeste.Mod.mm/Patches/Player.cs index e0062a8e9..327d0f229 100644 --- a/Celeste.Mod.mm/Patches/Player.cs +++ b/Celeste.Mod.mm/Patches/Player.cs @@ -20,6 +20,7 @@ using _Player = Celeste.Player; namespace Celeste { + [PatchSpeedInterface] class patch_Player : Player, ISpeed { // We're effectively in Player, but still need to "expose" private fields to our mod. @@ -50,6 +51,7 @@ public bool IsIntroState { } } + [PatchInterfaceProperty] Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } public patch_Player(Vector2 position, PlayerSpriteMode spriteMode) diff --git a/Celeste.Mod.mm/Patches/Seeker.cs b/Celeste.Mod.mm/Patches/Seeker.cs index 17cc4bddd..2a3b15a65 100644 --- a/Celeste.Mod.mm/Patches/Seeker.cs +++ b/Celeste.Mod.mm/Patches/Seeker.cs @@ -11,11 +11,13 @@ using _Seeker = Celeste.Seeker; namespace Celeste { + [PatchSpeedInterface] public class patch_Seeker : Seeker, ISpeed { // We're effectively in Seeker, but still need to "expose" private fields to our mod. private StateMachine State; + [PatchInterfaceProperty] Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } // no-op - only here to make diff --git a/Celeste.Mod.mm/Patches/Solid.cs b/Celeste.Mod.mm/Patches/Solid.cs index 35c0e851a..d22a13f6a 100644 --- a/Celeste.Mod.mm/Patches/Solid.cs +++ b/Celeste.Mod.mm/Patches/Solid.cs @@ -10,10 +10,11 @@ using System; namespace Celeste { + [PatchSpeedInterface] internal class patch_Solid : Solid, ISpeed { - + [PatchInterfaceProperty] Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } - + public patch_Solid(Vector2 position, float width, float height, bool safe) : base(position, width, height, safe) { // no-op. MonoMod ignores this - we only need this to make the compiler shut up. } diff --git a/Celeste.Mod.mm/Patches/TheoCrystal.cs b/Celeste.Mod.mm/Patches/TheoCrystal.cs index d9e566ecd..d82ba9289 100644 --- a/Celeste.Mod.mm/Patches/TheoCrystal.cs +++ b/Celeste.Mod.mm/Patches/TheoCrystal.cs @@ -7,9 +7,11 @@ using MonoMod; namespace Celeste { + [PatchSpeedInterface] class patch_TheoCrystal : TheoCrystal, ISpeed { public patch_Holdable Hold; // avoids extra cast + [PatchInterfaceProperty] Vector2 ISpeed.Speed { get => Speed; set => Speed = value; } public patch_TheoCrystal(EntityData data, Vector2 offset)