diff --git a/Celeste.Mod.mm/Mod/Entities/ActivateDreamBlocksTrigger.cs b/Celeste.Mod.mm/Mod/Entities/ActivateDreamBlocksTrigger.cs index 71a01b72f..dbbe56e8a 100644 --- a/Celeste.Mod.mm/Mod/Entities/ActivateDreamBlocksTrigger.cs +++ b/Celeste.Mod.mm/Mod/Entities/ActivateDreamBlocksTrigger.cs @@ -19,25 +19,25 @@ public override void OnEnter(Player player) { Level level = Scene as Level; if (activate && !level.Session.Inventory.DreamDash) { level.Session.Inventory.DreamDash = true; - foreach (DreamBlock dreamBlock in level.Tracker.GetEntities()) { + foreach (patch_DreamBlock dreamBlock in level.Tracker.GetEntities()) { if (rumble) { if (fastAnimation) - dreamBlock.Add(new Coroutine(((patch_DreamBlock) dreamBlock).FastActivate(), true)); + dreamBlock.Add(new Coroutine(dreamBlock.UpdateFastRoutine(), true)); else - dreamBlock.Add(new Coroutine(dreamBlock.Activate(), true)); + dreamBlock.Add(new Coroutine(dreamBlock.UpdateRoutine(), true)); } else - dreamBlock.ActivateNoRoutine(); + dreamBlock.UpdateNoRoutine(); } } else if (!activate && level.Session.Inventory.DreamDash) { level.Session.Inventory.DreamDash = false; foreach (DreamBlock dreamBlock in level.Tracker.GetEntities()) { if (rumble) { if (fastAnimation) - dreamBlock.Add(new Coroutine(((patch_DreamBlock) dreamBlock).FastDeactivate(), true)); + dreamBlock.Add(new Coroutine(((patch_DreamBlock) dreamBlock).UpdateFastRoutine(), true)); else - dreamBlock.Add(new Coroutine(((patch_DreamBlock) dreamBlock).Deactivate(), true)); + dreamBlock.Add(new Coroutine(((patch_DreamBlock) dreamBlock).UpdateRoutine(), true)); } else - ((patch_DreamBlock) dreamBlock).DeactivateNoRoutine(); + ((patch_DreamBlock) dreamBlock).UpdateNoRoutine(); } } } diff --git a/Celeste.Mod.mm/Patches/DreamBlock.cs b/Celeste.Mod.mm/Patches/DreamBlock.cs index 298222fd3..3e5de6930 100644 --- a/Celeste.Mod.mm/Patches/DreamBlock.cs +++ b/Celeste.Mod.mm/Patches/DreamBlock.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +#pragma warning disable CS0626 // extern +using Microsoft.Xna.Framework; using Mono.Cecil; using Mono.Cecil.Cil; using Monocle; @@ -8,14 +9,19 @@ using System.Collections; using System.Runtime.CompilerServices; using System.Linq; +using MonoMod.InlineRT; +using MonoMod.Utils; namespace Celeste { class patch_DreamBlock : DreamBlock { + static object DreamBlockPatch; internal Vector2 movementCounter { [MonoModLinkTo("Celeste.Platform", "get__movementCounter")] get; } + + private bool flagState; private bool playerHasDreamDash; private LightOcclude occlude; private float whiteHeight; @@ -23,6 +29,73 @@ internal Vector2 movementCounter { private Shaker shaker; private Vector2 shake; private int randomSeed = Calc.Random.Next(); + private string? flag; + + public bool DeactivatedIsSolid { get; set; } + + public string? Flag { + get => flag; + set { + if (Scene is not null) { + if (value is null && flag is not null) { + SceneAs().NewDreamBlockCounter--; + } + if (value is not null && flag is null) { + SceneAs().NewDreamBlockCounter++; + } + } + flag = value; + if (Scene is not null) { + CheckFlags(); + UpdateNoRoutine(); + } + } + } + + /// + /// determine if a dream block is activated. + /// you can add your custom state here by hooking it. + /// also change , + /// or it may not run correctly. + ///

+ /// this will not update visual state automatically. + /// if your custom state is changed, update it manually. + ///

+ /// anyone can add their own state, + /// so better to have a thing similar to to determine if you should update visual. + ///

+ /// as a reference, see how was implemented. + ///
+ public bool Activated { + [MethodImpl(MethodImplOptions.NoInlining)] + get => Flag is null ? SceneAs().Session.Inventory.DreamDash : flagState; + } + + /// + /// determine if a dream block can be dash through. + /// mainly used for some temp state that should not change visual state. + /// for example, for Tera Helper, if DreamBlock is Fairy type and Madeline is Dragon type, there will be no effect. + /// then this property returns false. + /// + public bool ActivatedPlus { + [MethodImpl(MethodImplOptions.NoInlining)] + get => Activated; + } + + public override void Removed(Scene scene) { + if (Flag is not null) { + SceneAs().NewDreamBlockCounter--; + } + base.Removed(scene); + } + + [MonoModIgnore] + [PatchDreamBlockAdded] + public override extern void Added(Scene scene); + + [MonoModIgnore] + [PatchDreamBlockUpdate] + public override extern void Update(); public patch_DreamBlock(EntityData data, Vector2 offset) : base(data, offset) { @@ -38,10 +111,92 @@ public void ctor(Vector2 position, float width, float height, Vector2? node, boo ctor(position, width, height, node, fastMoving, oneUse, false); } + public extern void orig_ctor(EntityData data, Vector2 offset); + + [MonoModConstructor] + public void ctor(EntityData data, Vector2 offset) { + orig_ctor(data, offset); + Flag = data.Attr("flag", null); + DeactivatedIsSolid = data.Bool("deactivatedIsSolid", false); + } + public void CheckFlags() { + bool fs = SceneAs().Session.GetFlag(Flag); + if (flagState != fs) { + flagState = fs; + UpdateNoRoutine(); + } + } + + /// + /// Aims to patch . has been ilhooked, so we can only patch it in this way. + /// + internal static bool Init(bool _, patch_DreamBlock self) { + if (self.Flag is not null) { + self.SceneAs().NewDreamBlockCounter++; + self.flagState = self.SceneAs().Session.GetFlag(self.Flag); + } + return self.Activated; + } + + public void UpdateVisual(bool routine, bool fast) { + if (routine) { + if (fast) { + Add(new Coroutine(UpdateFastRoutine())); + } else { + Add(new Coroutine(UpdateRoutine())); + } + } else { + UpdateRoutine(); + } + } + +#pragma warning disable CS0618 // obsolete + private static IEnumerator Empty() { + yield break; + } + public void UpdateNoRoutine() { + bool activated = Activated; + if (playerHasDreamDash != activated) { + if (activated) { + ActivateNoRoutine(); + } else { + DeactivateNoRoutine(); + } + } + } + public IEnumerator UpdateRoutine() { + bool activated = Activated; + if (playerHasDreamDash != activated) { + if (activated) { + return Activate(); + } else { + return Deactivate(); + } + } + return Empty(); + } + public IEnumerator UpdateFastRoutine() { + bool activated = Activated; + if (playerHasDreamDash != activated) { + if (activated) { + return Activate(); + } else { + return Deactivate(); + } + } + return Empty(); + } +#pragma warning restore CS0618 + [MonoModIgnore] [PatchDreamBlockSetup] public new extern void Setup(); + [PatchDreamBlockAddObsolete($"Use {nameof(UpdateNoRoutine)} instead")] + [MonoModIgnore] + public new extern void ActivateNoRoutine(); + + [Obsolete($"Use {nameof(UpdateNoRoutine)} instead")] public void DeactivateNoRoutine() { if (playerHasDreamDash) { playerHasDreamDash = false; @@ -64,6 +219,11 @@ public void DeactivateNoRoutine() { } } + [PatchDreamBlockAddObsolete($"Use {nameof(UpdateRoutine)} instead")] + [MonoModIgnore] + public new extern IEnumerator Activate(); + + [Obsolete($"Use {nameof(UpdateRoutine)} instead")] public IEnumerator Deactivate() { Level level = SceneAs(); yield return 1f; @@ -107,6 +267,7 @@ public IEnumerator Deactivate() { } } + [Obsolete($"Use {nameof(UpdateFastRoutine)} instead")] public IEnumerator FastDeactivate() { Level level = SceneAs(); yield return null; @@ -145,6 +306,7 @@ public IEnumerator FastDeactivate() { } } + [Obsolete($"Use {nameof(UpdateFastRoutine)} instead")] public IEnumerator FastActivate() { Level level = SceneAs(); yield return null; @@ -203,7 +365,7 @@ private Vector2 PutInside(Vector2 pos) { // Patch XNA/FNA jank in Tween.OnUpdate lambda [MonoModPatch("<>c__DisplayClass22_0")] class patch_AddedLambdas { - + [MonoModPatch("<>4__this")] private patch_DreamBlock _this = default; private Vector2 start = default, end = default; @@ -243,10 +405,51 @@ namespace MonoMod { /// same results). This fixes issue #556. /// [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchDreamBlockSetup))] - class PatchDreamBlockSetupAttribute : Attribute {} + class PatchDreamBlockSetupAttribute : Attribute { } + + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchDreamBlockAdded))] + class PatchDreamBlockAddedAttribute : Attribute { } + + /// + /// BetterFreezeFrames is il hooking it + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchDreamBlockUpdate))] + class PatchDreamBlockUpdateAttribute : Attribute { } + + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchDreamBlockAddObsolete))] + class PatchDreamBlockAddObsolete : Attribute { + public PatchDreamBlockAddObsolete(string v) { + Info = v; + } + + public string Info { get; set; } + } static partial class MonoModRules { + public static void PatchDreamBlockUpdate(ILContext context, CustomAttribute attrib) { + MethodDefinition m_patch_DreamBlock_UpdateHasDreamDash = MonoModRule.Modder.Module.GetType("Celeste.DreamBlock").FindMethod(nameof(Celeste.patch_DreamBlock.CheckFlags)); + ILCursor cursor = new(context); + cursor.EmitLdarg0(); + cursor.EmitCallvirt(m_patch_DreamBlock_UpdateHasDreamDash); + } + public static void PatchDreamBlockAddObsolete(ILContext context, CustomAttribute attrib) { + + var attr = new CustomAttribute(context.Import(typeof(ObsoleteAttribute).GetConstructor(new Type[] { typeof(string) }))); + + attr.ConstructorArguments.Add(attrib.ConstructorArguments[0]); + context.Method.CustomAttributes.Add(attr); + } + public static void PatchDreamBlockAdded(ILContext context, CustomAttribute attrib) { + ILCursor cursor = new(context); + TypeDefinition t_patch_DreamBlock = MonoModRule.Modder.Module.GetType("Celeste.DreamBlock"); + MethodDefinition m_patch_DreamBlock_Init = t_patch_DreamBlock.FindMethod(nameof(Celeste.patch_DreamBlock.Init)); + // this.playerHasDreamDash = base.SceneAs().Session.Inventory.DreamDash; + cursor.GotoNext(MoveType.AfterLabel, i => i.MatchStfld("Celeste.DreamBlock", "playerHasDreamDash")); + cursor.EmitLdarg0(); + cursor.EmitCall(m_patch_DreamBlock_Init); + } + public static void PatchDreamBlockSetup(ILContext context, CustomAttribute attrib) { // Patch instructions before the 'conv.i4' cast to use doubles instead of floats for (int i = 0; i < context.Instrs.Count; i++) { diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 89920d5c5..c4959302c 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -26,6 +26,9 @@ namespace Celeste { class patch_Level : Level { + public int NewDreamBlockCounter; + + public bool HasNewDreamBlock => NewDreamBlockCounter > 0; // We're effectively in GameLoader, but still need to "expose" private fields to our mod. private static EventInstance PauseSnapshot; public static EventInstance _PauseSnapshot => PauseSnapshot; @@ -177,19 +180,19 @@ void Unpause() { } Everest.Events.Level.Pause(this, startIndex, minimal, quickReset); - } - + } + /// /// Forcefully close the pause menu; resume from paused. - /// + /// public void Unpause() { - if (Paused) { - PauseMainMenuOpen = false; - if (Entities.FindFirst() is patch_TextMenu menu) - menu.CloseAndRun(Everest.SaveSettings(), null); - Paused = false; - Audio.Play("event:/ui/game/unpause"); - unpauseTimer = 0.15f; + if (Paused) { + PauseMainMenuOpen = false; + if (Entities.FindFirst() is patch_TextMenu menu) + menu.CloseAndRun(Everest.SaveSettings(), null); + Paused = false; + Audio.Play("event:/ui/game/unpause"); + unpauseTimer = 0.15f; } } @@ -823,7 +826,7 @@ public static void PatchLevelLoaderDecalCreation(ILContext context, CustomAttrib FieldDefinition f_DecalData_Rotation = t_DecalData.FindField("Rotation"); FieldDefinition f_DecalData_ColorHex = t_DecalData.FindField("ColorHex"); - FieldDefinition f_DecalData_Depth = t_DecalData.FindField("Depth"); + FieldDefinition f_DecalData_Depth = t_DecalData.FindField("Depth"); FieldDefinition f_Decal_DepthSetByPlacement = t_Decal.FindField("DepthSetByPlacement"); diff --git a/Celeste.Mod.mm/Patches/Player.cs b/Celeste.Mod.mm/Patches/Player.cs index eba189260..1f2f67d00 100644 --- a/Celeste.Mod.mm/Patches/Player.cs +++ b/Celeste.Mod.mm/Patches/Player.cs @@ -15,6 +15,8 @@ using MonoMod.Cil; using MonoMod.InlineRT; using MonoMod.Utils; +using Celeste; +using System.Linq; namespace Celeste { class patch_Player : Player { @@ -257,7 +259,7 @@ private Color GetTrailColor(bool wasDashB) { return wasDashB ? NormalBadelineHairColor : UsedBadelineHairColor; return wasDashB ? NormalHairColor : UsedHairColor; } - + /// /// Adds a new state to this player with the given behaviour, and returns the index of the new state. /// @@ -325,10 +327,71 @@ public override void SceneEnd(Scene scene) { [MonoModIgnore] [PatchPlayerApproachMaxMove] private extern int DummyUpdate(); - + [MonoModIgnore] [PatchPlayerApproachMaxMove] private extern int NormalUpdate(); + + [MonoModIgnore] + [PatchPlayerDreamDashUpdate] + private extern int DreamDashUpdate(); + + [MonoModPatch("d__427")] + class patch_DashCoroutine { + [PatchPlayerDashCoroutine] + [MonoModIgnore] + private extern bool MoveNext(); + } + internal static class DashCoroutineHelper { + public static bool CollideCheck(Entity self, Vector2 at) { + if (!self.SceneAs().HasNewDreamBlock) { + return self.CollideCheck(at); + } + return Collide.Check(self, self.Scene.Tracker.GetEntities().Cast().Where(x => x.ActivatedPlus), at); + } + } + + [MonoModIgnore] + [PatchPlayerDreamDashCheck] + private extern bool DreamDashCheck(Vector2 dir); + + internal static class DreamDashCheckHelper { + public static patch_DreamBlock CollideFirst(Entity self, Vector2 at) { + if (!self.SceneAs().HasNewDreamBlock) { + return self.CollideFirst(at); + } + return Collide.First(self, self.SceneAs().Tracker.GetEntities().Cast().Where(x => x.ActivatedPlus), at) as patch_DreamBlock; + } + public static bool CollideCheck(Entity self, Vector2 at) { + if (!self.SceneAs().HasNewDreamBlock) { + return self.CollideCheck(at); + } + Vector2 position = self.Position; + self.Position = at; + bool result = _CollideCheck(self); + self.Position = position; + return result; + } + static bool _CollideCheck(Entity self) { + // dream block can be untracked, but this should never have happened + foreach (Entity entity in self.Scene.Tracker.Entities[typeof(Solid)]) { + if ((entity is not patch_DreamBlock db || !db.ActivatedPlus) && Collide.Check(self, entity)) { + return true; + } + } + return false; + } + } + + [MonoModIgnore] + [PatchPlayerDreamDashUpdate] + private extern bool DreamDashUpdate(Vector2 dir); + + internal static class DreamDashUpdateHelper { + public static patch_DreamBlock CollideFirst(Entity self) { + return Collide.First(self, self.SceneAs().Tracker.GetEntities().Cast().Where(x => x.ActivatedPlus || !x.DeactivatedIsSolid)) as patch_DreamBlock; + } + } } public static class PlayerExt { @@ -355,6 +418,18 @@ namespace MonoMod { [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerOrigUpdate))] class PatchPlayerOrigUpdateAttribute : Attribute { } + /// + /// Patch the DreamDashCheck method in Player instead of reimplementing it in Everest. + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerDreamDashCheck))] + class PatchPlayerDreamDashCheckAttribute : Attribute { } + + /// + /// Patch the DashCoroutine method in Player instead of reimplementing it in Everest. + /// + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerDashCoroutine))] + class PatchPlayerDashCoroutineAttribute : Attribute { } + /// /// Patches the method to only set the player Speed.X if not in the RedDash state. /// @@ -391,7 +466,7 @@ class PatchPlayerOrigWallJumpAttribute : Attribute { } /// [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerCtor))] class PatchPlayerCtorAttribute : Attribute { } - + /// /// Patches the method to fix puffer boosts breaking on respawn. /// @@ -404,8 +479,74 @@ class PatchPlayerExplodeLaunchAttribute : Attribute { } [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerApproachMaxMove))] class PatchPlayerApproachMaxMoveAttribute : Attribute { } + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerDreamDashUpdate))] + class PatchPlayerDreamDashUpdateAttribute : Attribute { } + static partial class MonoModRules { + public static void PatchPlayerDreamDashCheck(ILContext context, CustomAttribute attrib) { + TypeDefinition t_patch_Level = MonoModRule.Modder.Module.GetType("Celeste.Level"); + TypeDefinition t_Entity = MonoModRule.Modder.Module.GetType("Monocle.Entity"); + TypeDefinition t_patch_Player_DreamDashCheckHelper = MonoModRule.Modder.Module.GetType($"Celeste.Player/{nameof(patch_Player.DreamDashCheckHelper)}"); + PropertyDefinition p_Entity_Scene = t_Entity.FindProperty("Scene"); + PropertyDefinition p_patch_Level_HasNewDreamBlock = t_patch_Level.FindProperty(nameof(patch_Level.HasNewDreamBlock)); + MethodDefinition m_patch_Player_DreamDashCheckHelper_CollideFirst = t_patch_Player_DreamDashCheckHelper.FindMethod(nameof(patch_Player.DreamDashCheckHelper.CollideFirst)); + MethodDefinition m_patch_Player_DreamDashCheckHelper_CollideCheck = t_patch_Player_DreamDashCheckHelper.FindMethod(nameof(patch_Player.DreamDashCheckHelper.CollideCheck)); + + ILCursor cursor = new ILCursor(context); + // if (this.Inventory.DreamDash && + cursor.GotoNext(MoveType.After, instr => instr.MatchLdfld("Celeste.PlayerInventory", "DreamDash")); + cursor.EmitLdarg0(); + cursor.EmitCallvirt(p_Entity_Scene.GetMethod); + cursor.EmitCastclass(t_patch_Level); + cursor.EmitCallvirt(p_patch_Level_HasNewDreamBlock.GetMethod); + cursor.EmitOr(); + + // DreamBlock dreamBlock = base.CollideFirst(this.Position + dir); + cursor.GotoNext(MoveType.Before, instr => instr.MatchCallOrCallvirt("Monocle.Entity", "CollideFirst")); + cursor.Remove(); + cursor.EmitCall(m_patch_Player_DreamDashCheckHelper_CollideFirst); + + // if (base.CollideCheck(...)) + while (cursor.TryGotoNext(MoveType.Before, instr => instr.MatchCallOrCallvirt("Monocle.Entity", "CollideCheck"))) { + cursor.Remove(); + cursor.EmitCall(m_patch_Player_DreamDashCheckHelper_CollideCheck); + } + } + public static void PatchPlayerDashCoroutine(ILContext context, CustomAttribute attrib) { + TypeDefinition t_patch_Level = MonoModRule.Modder.Module.GetType("Celeste.Level"); + TypeDefinition t_Entity = MonoModRule.Modder.Module.GetType("Monocle.Entity"); + TypeDefinition t_patch_Player_DashCoroutineHelper = MonoModRule.Modder.Module.GetType($"Celeste.Player/{nameof(patch_Player.DashCoroutineHelper)}"); + PropertyDefinition p_Entity_Scene = t_Entity.FindProperty("Scene"); + PropertyDefinition p_patch_Level_HasNewDreamBlock = t_patch_Level.FindProperty(nameof(patch_Level.HasNewDreamBlock)); + MethodDefinition m_patch_Player_DashCoroutineHelper_CollideCheck = t_patch_Player_DashCoroutineHelper.FindMethod(nameof(patch_Player.DashCoroutineHelper.CollideCheck)); + + // this.Inventory.DreamDash || !this.CollideCheck(this.Position + Vector2.UnitY) + ILCursor cursor = new ILCursor(context); + + cursor.GotoNext(MoveType.After, instr => instr.MatchLdfld("Celeste.PlayerInventory", "DreamDash")); + cursor.EmitLdloc1(); + cursor.EmitCallvirt(p_Entity_Scene.GetMethod); + cursor.EmitCastclass(t_patch_Level); + cursor.EmitCallvirt(p_patch_Level_HasNewDreamBlock.GetMethod); + cursor.EmitOr(); + + cursor.GotoNext(MoveType.Before, instr => instr.MatchCallOrCallvirt("Monocle.Entity", "CollideCheck")); + cursor.Remove(); + cursor.EmitCall(m_patch_Player_DashCoroutineHelper_CollideCheck); + } + public static void PatchPlayerDreamDashUpdate(ILContext context, CustomAttribute attrib) { + TypeDefinition t_patch_Player_DreamDashUpdateHelper = MonoModRule.Modder.Module.GetType($"Celeste.Player/{nameof(patch_Player.DreamDashUpdateHelper)}"); + MethodDefinition m_patch_Player_DreamDashUpdateHelper_CollideCheck = t_patch_Player_DreamDashUpdateHelper.FindMethod(nameof(patch_Player.DreamDashUpdateHelper.CollideFirst)); + + // DreamBlock dreamBlock = base.CollideFirst(); + ILCursor cursor = new ILCursor(context); + + cursor.GotoNext(MoveType.Before, instr => instr.MatchCallOrCallvirt("Monocle.Entity", "CollideFirst")); + cursor.Remove(); + cursor.EmitCall(m_patch_Player_DreamDashUpdateHelper_CollideCheck); + } + public static void PatchPlayerOrigUpdate(ILContext context, CustomAttribute attrib) { MethodDefinition m_IsOverWater = context.Method.DeclaringType.FindMethod("System.Boolean _IsOverWater()");