From 3223e83eea98f57708a3412af8c0fcee1c13a4bd Mon Sep 17 00:00:00 2001 From: horizon_wings Date: Tue, 25 Jul 2023 09:31:40 -0700 Subject: [PATCH 1/5] Allow merged metadata Sprites.xml to override sprite justification/origin/position --- Celeste.Mod.mm/Patches/LevelLoader.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Celeste.Mod.mm/Patches/LevelLoader.cs b/Celeste.Mod.mm/Patches/LevelLoader.cs index 3b895dad1..82fcf2a53 100644 --- a/Celeste.Mod.mm/Patches/LevelLoader.cs +++ b/Celeste.Mod.mm/Patches/LevelLoader.cs @@ -124,6 +124,10 @@ public void ctor(Session session, Vector2? startPosition = default) { SpriteData valueMod = kvpBank.Value; if (bankOrig.SpriteData.TryGetValue(key, out SpriteData valueOrig)) { + valueOrig.Sprite.Justify = valueMod.Sprite.Justify; + valueOrig.Sprite.Origin = valueMod.Sprite.Origin; + valueOrig.Sprite.Position = valueMod.Sprite.Position; + IDictionary animsOrig = valueOrig.Sprite.GetAnimations(); IDictionary animsMod = valueMod.Sprite.GetAnimations(); foreach (DictionaryEntry kvpAnim in animsMod) { From 12d7ec2702ee4603c96d342bb2250a02f8b1c652 Mon Sep 17 00:00:00 2001 From: horizon_wings Date: Thu, 27 Jul 2023 20:52:24 -0700 Subject: [PATCH 2/5] Instead of overriding always, only override if a value is explicitly defined --- Celeste.Mod.mm/Patches/LevelLoader.cs | 47 +++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/Celeste.Mod.mm/Patches/LevelLoader.cs b/Celeste.Mod.mm/Patches/LevelLoader.cs index 82fcf2a53..090691469 100644 --- a/Celeste.Mod.mm/Patches/LevelLoader.cs +++ b/Celeste.Mod.mm/Patches/LevelLoader.cs @@ -124,9 +124,50 @@ public void ctor(Session session, Vector2? startPosition = default) { SpriteData valueMod = kvpBank.Value; if (bankOrig.SpriteData.TryGetValue(key, out SpriteData valueOrig)) { - valueOrig.Sprite.Justify = valueMod.Sprite.Justify; - valueOrig.Sprite.Origin = valueMod.Sprite.Origin; - valueOrig.Sprite.Position = valueMod.Sprite.Position; + + // in order to allow map metadata Sprites.xml to override sprite origin and position, we + // need to manually copy the property from the map metadata sprites onto the main spritebank + // (done only if the overriding Sprites.xml specifies a value for that property) + + bool foundOrigin = false; // Center, Justify, Origin are all ways to specify the origin + bool foundPosition = false; + + // iterate through the sources list, starting from the end (the most recently added source + // setting a particular type of property is used) + for (int i = valueMod.Sources.Count - 1; i >= 0; i--) { + XmlElement xml = valueMod.Sources[i].XML; + + if (xml != null) { + // based on SpriteData.Add() + if (!foundOrigin) { + if (xml.HasChild("Center")) { + valueOrig.Sprite.CenterOrigin(); + valueOrig.Sprite.Justify = new Vector2(0.5f, 0.5f); + foundOrigin = true; + } + else if (xml.HasChild("Justify")) { + valueOrig.Sprite.JustifyOrigin(xml.ChildPosition("Justify")); + valueOrig.Sprite.Justify = xml.ChildPosition("Justify"); + foundOrigin = true; + } + else if (xml.HasChild("Origin")) { + valueOrig.Sprite.Origin = xml.ChildPosition("Origin"); + foundOrigin = true; + } + } + + if (!foundPosition) { + if (xml.HasChild("Position")) { + valueOrig.Sprite.Position = xml.ChildPosition("Position"); + foundPosition = true; + } + } + + if (foundOrigin && foundPosition) { + break; + } + } + } IDictionary animsOrig = valueOrig.Sprite.GetAnimations(); IDictionary animsMod = valueMod.Sprite.GetAnimations(); From 211891ce39f5deb3896a583b3e049960571d5b04 Mon Sep 17 00:00:00 2001 From: nhruo Date: Wed, 8 Nov 2023 10:06:06 +0200 Subject: [PATCH 3/5] fixed off by one error in WrappingLinearSearch --- Celeste.Mod.mm/Mod/UI/OuiModToggler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs b/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs index 637220e11..0333bc1de 100755 --- a/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs +++ b/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs @@ -379,7 +379,7 @@ private static bool WrappingLinearSearch(List items, Func predica } nextModIndex = startIndex; - return false; + return predicate(items[nextModIndex]); } private void AddSearchBox(TextMenu menu) { From db8205e37cb4ed49aace2fde2e6d5366b8de35a1 Mon Sep 17 00:00:00 2001 From: nhruo Date: Wed, 8 Nov 2023 11:50:30 +0200 Subject: [PATCH 4/5] added shift enter to search in reverse --- Celeste.Mod.mm/Mod/UI/OuiModToggler.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs b/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs index 0333bc1de..a23674715 100755 --- a/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs +++ b/Celeste.Mod.mm/Mod/UI/OuiModToggler.cs @@ -1,5 +1,6 @@ using Ionic.Zip; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; using Monocle; using System; using System.Collections; @@ -437,7 +438,14 @@ void exitSearch(TextMenuExt.TextBox textBox) { textBox.OnTextInputCharActions['\t'] = searchNextMod(false); textBox.OnTextInputCharActions['\n'] = (_) => { }; - textBox.OnTextInputCharActions['\r'] = searchNextMod(false); + textBox.OnTextInputCharActions['\r'] = (textBox) => { + if (MInput.Keyboard.CurrentState.IsKeyDown(Keys.LeftShift) + || MInput.Keyboard.CurrentState.IsKeyDown(Keys.RightShift)) { + searchNextMod(true)(textBox); + } else { + searchNextMod(false)(textBox); + } + }; textBox.OnTextInputCharActions['\b'] = (textBox) => { if (textBox.DeleteCharacter()) { Audio.Play(SFX.ui_main_rename_entry_backspace); From e3440dd539502f62071ba725d68fbc19286305ab Mon Sep 17 00:00:00 2001 From: nhruo123 <32033791+nhruo123@users.noreply.github.com> Date: Wed, 8 Nov 2023 22:49:46 +0200 Subject: [PATCH 5/5] Fixed Theo up transition crash (#663) Co-authored-by: Maddie <52103563+maddie480@users.noreply.github.com> --- Celeste.Mod.mm/Patches/Level.cs | 39 +++++++++++++++++ Celeste.Mod.mm/Patches/Player.cs | 17 +++++++- Celeste.Mod.mm/Patches/TheoCrystal.cs | 63 ++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index fc295cd8b..5ede78009 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -36,6 +36,8 @@ class patch_Level : Level { public static int SkipScreenWipes; public static bool ShouldAutoPause = false; + public Vector2 TransitionDirection { get; private set; } = new(); + public delegate Entity EntityLoader(Level level, LevelData levelData, Vector2 offset, EntityData entityData); public static readonly Dictionary EntityLoaders = new Dictionary(); @@ -69,6 +71,10 @@ class patch_Level : Level { [PatchLevelUpdate] // ... except for manually manipulating the method via MonoModRules public extern new void Update(); + [MonoModIgnore] // We don't want to change anything about the method... + [PatchLevelEnforceBounds] // ... except for manually manipulating the method via MonoModRules + public extern new void EnforceBounds(Player player); + [MonoModReplace] public new void RegisterAreaComplete() { bool completed = Completed; @@ -144,6 +150,7 @@ void Unpause() { public extern void orig_TransitionTo(LevelData next, Vector2 direction); public new void TransitionTo(LevelData next, Vector2 direction) { + TransitionDirection = direction; orig_TransitionTo(next, direction); Everest.Events.Level.TransitionTo(this, next, direction); } @@ -193,6 +200,7 @@ private IEnumerator TransitionRoutine(LevelData next, Vector2 direction) { yield return orig.Current; } + TransitionDirection = new(); } [PatchLevelLoader] // Manually manipulate the method via MonoModRules @@ -510,6 +518,14 @@ private bool CheckForErrors() { return errorPresent; } + + private static void BlockUpTransitionsWithoutHoldable(TheoCrystal entity, Player player, Rectangle bounds) { + // we stay consistent with base game, so we let the player transition with any holdable item and not just TheoCrystal + if (entity != null && player.Top < (bounds.Top + 1) && (!player.Holding?.IsHeld ?? true)) { + player.Top = bounds.Top + 1; + player.OnBoundsV(); + } + } } public static class LevelExt { @@ -563,6 +579,12 @@ class PatchTransitionRoutineAttribute : Attribute { } [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelCanPause))] class PatchLevelCanPauseAttribute : Attribute { } + /// + /// A patch for the EnforceBounds method that checks if the player can transition up with Theo in the room. + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchLevelEnforceBounds))] + class PatchLevelEnforceBoundsAttribute : Attribute { } + static partial class MonoModRules { public static void PatchLevelLoader(ILContext context, CustomAttribute attrib) { @@ -780,5 +802,22 @@ public static void PatchLevelCanPause(ILContext il, CustomAttribute attrib) { c.Emit(OpCodes.Ldc_I4_0); } + + public static void PatchLevelEnforceBounds(MethodDefinition method, CustomAttribute attrib) { + + MethodDefinition m_BlockUpTransitionsWithoutHoldable = method.DeclaringType.FindMethod("BlockUpTransitionsWithoutHoldable"); + + new ILContext(method).Invoke(il => { + ILCursor cursor = new(il); + + cursor.GotoNext(MoveType.After, + instr => instr.MatchCallvirt("Monocle.Tracker", "GetEntity"), + instr => instr.MatchStloc(2)); + cursor.Emit(OpCodes.Ldloc_2); + cursor.Emit(OpCodes.Ldarg_1); + cursor.Emit(OpCodes.Ldloc_0); + cursor.Emit(OpCodes.Call, m_BlockUpTransitionsWithoutHoldable); + }); + } } } diff --git a/Celeste.Mod.mm/Patches/Player.cs b/Celeste.Mod.mm/Patches/Player.cs index 91c16e5e3..b4f037125 100644 --- a/Celeste.Mod.mm/Patches/Player.cs +++ b/Celeste.Mod.mm/Patches/Player.cs @@ -23,6 +23,7 @@ class patch_Player : Player { private bool wasDashB; private HashSet triggersInside; private List temp; + private readonly Hitbox normalHitbox; private static int diedInGBJ = 0; private int framesAlive; @@ -268,6 +269,20 @@ public override void SceneEnd(Scene scene) { [MonoModIgnore] [PatchPlayerStarFlyReturnToNormalHitbox] private extern void StarFlyReturnToNormalHitbox(); + + + public extern bool orig_get_CanUnDuck(); + + public bool get_CanUnDuck() { + bool origCanUnDuck = orig_get_CanUnDuck(); + bool theoBlockingUpTransition = false; + + if (origCanUnDuck && level.Tracker.GetEntity() != null && (!Holding?.IsHeld ?? true)) { + theoBlockingUpTransition = normalHitbox.Top + Position.Y < level.Bounds.Top + 1; + } + + return origCanUnDuck && !theoBlockingUpTransition; + } } public static class PlayerExt { @@ -329,7 +344,7 @@ class PatchPlayerOrigWallJumpAttribute : Attribute { } /// [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerCtor))] class PatchPlayerCtorAttribute : Attribute { } - + /// /// Patches the method to fix puffer boosts breaking on respawn. /// diff --git a/Celeste.Mod.mm/Patches/TheoCrystal.cs b/Celeste.Mod.mm/Patches/TheoCrystal.cs index 78c3765b4..bf10dea5d 100644 --- a/Celeste.Mod.mm/Patches/TheoCrystal.cs +++ b/Celeste.Mod.mm/Patches/TheoCrystal.cs @@ -3,13 +3,21 @@ #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value using Microsoft.Xna.Framework; +using Mono.Cecil; +using Mono.Cecil.Cil; using MonoMod; +using MonoMod.Cil; +using MonoMod.Utils; +using System; namespace Celeste { class patch_TheoCrystal : TheoCrystal { public patch_Holdable Hold; // avoids extra cast - public patch_TheoCrystal(EntityData data, Vector2 offset) + // we need to expose that vanilla private field to our patch class. + private patch_Level Level; + + public patch_TheoCrystal(EntityData data, Vector2 offset) : base(data, offset) { } @@ -20,5 +28,56 @@ public void ctor(Vector2 position) { orig_ctor(position); Hold.SpeedSetter = (speed) => { Speed = speed; }; } + + [MonoModIgnore] + [PatchTheoCrystalUpdate] + public extern new void Update(); + + private static bool IsPlayerHoldingItemAndTransitioningUp(TheoCrystal theoCrystal) { + bool isPlayerHoldingItem = false; + bool isUpTransition = false; + + patch_Level level = ((patch_TheoCrystal) theoCrystal).Level; + if (level.Tracker.GetEntity() is Player player) { + isPlayerHoldingItem = player.Holding?.IsHeld ?? false; + isUpTransition = level.Transitioning && level.TransitionDirection == -Vector2.UnitY; + } + + return isPlayerHoldingItem && isUpTransition; + } + } +} + +namespace MonoMod { + + /// + /// A patch for the Update method that keeps the player alive on up transition and item held + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchTheoCrystalUpdate))] + class PatchTheoCrystalUpdateAttribute : Attribute { } + + static partial class MonoModRules { + public static void PatchTheoCrystalUpdate(MethodDefinition method, CustomAttribute attrib) { + + MethodDefinition m_IsPlayerHoldingItemAndTransitioningUp = method.DeclaringType.FindMethod("IsPlayerHoldingItemAndTransitioningUp"); + ILLabel afterDieLabel = null; + + new ILContext(method).Invoke(il => { + ILCursor curser = new(il); + curser.GotoNext(MoveType.After, + instr => instr.MatchCall("Microsoft.Xna.Framework.Rectangle", "get_Bottom"), + instr => instr.MatchConvR4(), + instr => instr.MatchBleUn(out afterDieLabel), + instr => instr.MatchLdarg(0), + instr => instr.MatchCallvirt("Celeste.TheoCrystal", "Die")); + + curser.Index -= 2; + + curser.Emit(OpCodes.Ldarg_0); + curser.Emit(OpCodes.Call, m_IsPlayerHoldingItemAndTransitioningUp); + curser.Emit(OpCodes.Brtrue, afterDieLabel); + }); + } } -} \ No newline at end of file +} +