|
| 1 | +using Celeste.Mod.Entities; |
| 2 | +using Microsoft.Xna.Framework; |
| 3 | +using Mono.Cecil; |
| 4 | +using Mono.Cecil.Cil; |
| 5 | +using Monocle; |
| 6 | +using MonoMod.Cil; |
| 7 | +using MonoMod.RuntimeDetour; |
| 8 | +using System; |
| 9 | +using System.Reflection; |
| 10 | + |
| 11 | +namespace Celeste.Mod.SpringCollab2020.Entities { |
| 12 | + [CustomEntity("SpringCollab2020/SidewaysJumpThru")] |
| 13 | + [Tracked] |
| 14 | + class SidewaysJumpThru : Entity { |
| 15 | + |
| 16 | + private static FieldInfo actorMovementCounter = typeof(Actor).GetField("movementCounter", BindingFlags.Instance | BindingFlags.NonPublic); |
| 17 | + |
| 18 | + public static void Load() { |
| 19 | + // implement the basic collision between actors/platforms and sideways jumpthrus. |
| 20 | + using (new DetourContext { Before = { "*" } }) { // these don't always call the orig methods, better apply them first. |
| 21 | + On.Celeste.Actor.MoveHExact += onActorMoveHExact; |
| 22 | + On.Celeste.Platform.MoveHExactCollideSolids += onPlatformMoveHExactCollideSolids; |
| 23 | + } |
| 24 | + |
| 25 | + // block "climb hopping" on top of sideways jumpthrus, because this just looks weird. |
| 26 | + On.Celeste.Player.ClimbHopBlockedCheck += onPlayerClimbHopBlockedCheck; |
| 27 | + |
| 28 | + // mod collide checks to include sideways jumpthrus, so that the player behaves with them like with walls. |
| 29 | + IL.Celeste.Player.WallJumpCheck += modCollideChecks; // allow player to walljump off them |
| 30 | + IL.Celeste.Player.ClimbCheck += modCollideChecks; // allow player to climb on them |
| 31 | + IL.Celeste.Player.ClimbBegin += modCollideChecks; // if not applied, the player will clip through jumpthrus if trying to climb on them |
| 32 | + IL.Celeste.Player.ClimbUpdate += modCollideChecks; // when climbing, jumpthrus are handled like walls |
| 33 | + IL.Celeste.Player.SlipCheck += modCollideChecks; // make climbing on jumpthrus not slippery |
| 34 | + IL.Celeste.Player.UpdateSprite += modCollideChecks; // have the push animation when Madeline runs against a jumpthru for example |
| 35 | + IL.Celeste.Player.NormalUpdate += modCollideChecks; // get the wall slide effect |
| 36 | + IL.Celeste.Player.OnCollideH += modCollideChecks; // handle dashes against jumpthrus properly, without "shifting" down |
| 37 | + |
| 38 | + // one extra hook that kills the player momentum when hitting a jumpthru so that they don't get "stuck" on them. |
| 39 | + On.Celeste.Player.NormalUpdate += onPlayerNormalUpdate; |
| 40 | + } |
| 41 | + |
| 42 | + public static void Unload() { |
| 43 | + On.Celeste.Actor.MoveHExact -= onActorMoveHExact; |
| 44 | + On.Celeste.Platform.MoveHExactCollideSolids -= onPlatformMoveHExactCollideSolids; |
| 45 | + |
| 46 | + On.Celeste.Player.ClimbHopBlockedCheck -= onPlayerClimbHopBlockedCheck; |
| 47 | + |
| 48 | + IL.Celeste.Player.WallJumpCheck -= modCollideChecks; |
| 49 | + IL.Celeste.Player.ClimbCheck -= modCollideChecks; |
| 50 | + IL.Celeste.Player.ClimbBegin -= modCollideChecks; |
| 51 | + IL.Celeste.Player.ClimbUpdate -= modCollideChecks; |
| 52 | + IL.Celeste.Player.SlipCheck -= modCollideChecks; |
| 53 | + IL.Celeste.Player.UpdateSprite -= modCollideChecks; |
| 54 | + IL.Celeste.Player.NormalUpdate -= modCollideChecks; |
| 55 | + IL.Celeste.Player.OnCollideH -= modCollideChecks; |
| 56 | + |
| 57 | + On.Celeste.Player.NormalUpdate -= onPlayerNormalUpdate; |
| 58 | + } |
| 59 | + |
| 60 | + private static bool onActorMoveHExact(On.Celeste.Actor.orig_MoveHExact orig, Actor self, int moveH, Collision onCollide, Solid pusher) { |
| 61 | + // fall back to vanilla if no sideways jumpthru is in the room. |
| 62 | + if (self.SceneAs<Level>().Tracker.CountEntities<SidewaysJumpThru>() == 0) |
| 63 | + return orig(self, moveH, onCollide, pusher); |
| 64 | + |
| 65 | + Vector2 targetPosition = self.Position + Vector2.UnitX * moveH; |
| 66 | + int moveDirection = Math.Sign(moveH); |
| 67 | + int moveAmount = 0; |
| 68 | + bool movingLeftToRight = moveH > 0; |
| 69 | + while (moveH != 0) { |
| 70 | + bool didCollide = false; |
| 71 | + |
| 72 | + // check if colliding with a solid |
| 73 | + Solid solid = self.CollideFirst<Solid>(self.Position + Vector2.UnitX * moveDirection); |
| 74 | + if (solid != null) { |
| 75 | + didCollide = true; |
| 76 | + } else { |
| 77 | + // check if colliding with a sideways jumpthru |
| 78 | + SidewaysJumpThru jumpThru = self.CollideFirstOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * moveDirection); |
| 79 | + if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) { |
| 80 | + // there is a sideways jump-thru and we are moving in the opposite direction => collision |
| 81 | + didCollide = true; |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + if (didCollide) { |
| 86 | + Vector2 movementCounter = (Vector2) actorMovementCounter.GetValue(self); |
| 87 | + movementCounter.X = 0f; |
| 88 | + onCollide?.Invoke(new CollisionData { |
| 89 | + Direction = Vector2.UnitX * moveDirection, |
| 90 | + Moved = Vector2.UnitX * moveAmount, |
| 91 | + TargetPosition = targetPosition, |
| 92 | + Hit = solid, |
| 93 | + Pusher = pusher |
| 94 | + }); |
| 95 | + return true; |
| 96 | + } |
| 97 | + |
| 98 | + // continue moving |
| 99 | + moveAmount += moveDirection; |
| 100 | + moveH -= moveDirection; |
| 101 | + self.X += moveDirection; |
| 102 | + } |
| 103 | + return false; |
| 104 | + } |
| 105 | + |
| 106 | + private static bool onPlatformMoveHExactCollideSolids(On.Celeste.Platform.orig_MoveHExactCollideSolids orig, Platform self, |
| 107 | + int moveH, bool thruDashBlocks, Action<Vector2, Vector2, Platform> onCollide) { |
| 108 | + // fall back to vanilla if no sideways jumpthru is in the room. |
| 109 | + if (self.SceneAs<Level>().Tracker.CountEntities<SidewaysJumpThru>() == 0) |
| 110 | + return orig(self, moveH, thruDashBlocks, onCollide); |
| 111 | + |
| 112 | + float x = self.X; |
| 113 | + int moveDirection = Math.Sign(moveH); |
| 114 | + int moveAmount = 0; |
| 115 | + Solid solid = null; |
| 116 | + bool movingLeftToRight = moveH > 0; |
| 117 | + bool collidedWithJumpthru = false; |
| 118 | + while (moveH != 0) { |
| 119 | + if (thruDashBlocks) { |
| 120 | + // check if we have dash blocks to break on our way. |
| 121 | + foreach (DashBlock entity in self.Scene.Tracker.GetEntities<DashBlock>()) { |
| 122 | + if (self.CollideCheck(entity, self.Position + Vector2.UnitX * moveDirection)) { |
| 123 | + entity.Break(self.Center, Vector2.UnitX * moveDirection, true, true); |
| 124 | + self.SceneAs<Level>().Shake(0.2f); |
| 125 | + Input.Rumble(RumbleStrength.Medium, RumbleLength.Medium); |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + // check for collision with a solid |
| 131 | + solid = self.CollideFirst<Solid>(self.Position + Vector2.UnitX * moveDirection); |
| 132 | + |
| 133 | + // check for collision with a sideways jumpthru |
| 134 | + SidewaysJumpThru jumpThru = self.CollideFirstOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * moveDirection); |
| 135 | + if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) { |
| 136 | + // there is a sideways jump-thru and we are moving in the opposite direction => collision |
| 137 | + collidedWithJumpthru = true; |
| 138 | + } |
| 139 | + |
| 140 | + if (solid != null || collidedWithJumpthru) { |
| 141 | + break; |
| 142 | + } |
| 143 | + |
| 144 | + // continue moving |
| 145 | + moveAmount += moveDirection; |
| 146 | + moveH -= moveDirection; |
| 147 | + self.X += moveDirection; |
| 148 | + } |
| 149 | + |
| 150 | + // actually move and call the collision callback if any |
| 151 | + self.X = x; |
| 152 | + self.MoveHExact(moveAmount); |
| 153 | + if (solid != null && onCollide != null) { |
| 154 | + onCollide(Vector2.UnitX * moveDirection, Vector2.UnitX * moveAmount, solid); |
| 155 | + } |
| 156 | + return solid != null || collidedWithJumpthru; |
| 157 | + } |
| 158 | + |
| 159 | + private static bool onPlayerClimbHopBlockedCheck(On.Celeste.Player.orig_ClimbHopBlockedCheck orig, Player self) { |
| 160 | + bool vanillaCheck = orig(self); |
| 161 | + if (vanillaCheck) |
| 162 | + return vanillaCheck; |
| 163 | + |
| 164 | + // block climb hops on jumpthrus because those look weird |
| 165 | + return self.CollideCheckOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * (int) self.Facing); |
| 166 | + } |
| 167 | + |
| 168 | + private static void modCollideChecks(ILContext il) { |
| 169 | + ILCursor cursor = new ILCursor(il); |
| 170 | + |
| 171 | + while (cursor.Next != null) { |
| 172 | + Instruction next = cursor.Next; |
| 173 | + |
| 174 | + // we want to replace all CollideChecks with solids here. |
| 175 | + if (next.OpCode == OpCodes.Call && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Entity::CollideCheck<Celeste.Solid>(Microsoft.Xna.Framework.Vector2)") { |
| 176 | + Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Entity.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}"); |
| 177 | + |
| 178 | + cursor.Remove(); |
| 179 | + cursor.EmitDelegate<Func<Entity, Vector2, bool>>((self, checkAtPosition) => { |
| 180 | + // we still want to check for solids... |
| 181 | + if (self.CollideCheck<Solid>(checkAtPosition)) |
| 182 | + return true; |
| 183 | + |
| 184 | + // if we are not checking a side, this certainly has nothing to do with jumpthrus. |
| 185 | + if (self.Position.X == checkAtPosition.X) |
| 186 | + return false; |
| 187 | + |
| 188 | + // our entity also collides if this is with a jumpthru and we are colliding with the solid side of it. |
| 189 | + // we are in this case if the jumpthru is left to right (the "solid" side of it is the right one) |
| 190 | + // and we are checking the collision on the left side of the player for example. |
| 191 | + bool collideOnLeftSideOfPlayer = (self.Position.X > checkAtPosition.X); |
| 192 | + SidewaysJumpThru jumpthru = self.CollideFirstOutside<SidewaysJumpThru>(checkAtPosition); |
| 193 | + return jumpthru != null && self is Player player && (jumpthru.AllowLeftToRight == collideOnLeftSideOfPlayer); |
| 194 | + }); |
| 195 | + } |
| 196 | + |
| 197 | + if (next.OpCode == OpCodes.Callvirt && (next.Operand as MethodReference)?.FullName == "System.Boolean Monocle.Scene::CollideCheck<Celeste.Solid>(Microsoft.Xna.Framework.Vector2)") { |
| 198 | + Logger.Log("SpringCollab2020/SidewaysJumpThru", $"Patching Scene.CollideCheck to include sideways jumpthrus at {cursor.Index} in IL for {il.Method.Name}"); |
| 199 | + |
| 200 | + cursor.Remove(); |
| 201 | + cursor.EmitDelegate<Func<Scene, Vector2, bool>>((self, vector) => self.CollideCheck<Solid>(vector) || self.CollideCheck<SidewaysJumpThru>(vector)); |
| 202 | + } |
| 203 | + |
| 204 | + cursor.Index++; |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + private static int onPlayerNormalUpdate(On.Celeste.Player.orig_NormalUpdate orig, Player self) { |
| 209 | + int result = orig(self); |
| 210 | + |
| 211 | + // kill speed if player is going towards a jumpthru. |
| 212 | + if (self.Speed.X != 0) { |
| 213 | + bool movingLeftToRight = self.Speed.X > 0; |
| 214 | + SidewaysJumpThru jumpThru = self.CollideFirstOutside<SidewaysJumpThru>(self.Position + Vector2.UnitX * Math.Sign(self.Speed.X)); |
| 215 | + if (jumpThru != null && jumpThru.AllowLeftToRight != movingLeftToRight) { |
| 216 | + self.Speed.X = 0; |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + return result; |
| 221 | + } |
| 222 | + |
| 223 | + |
| 224 | + |
| 225 | + |
| 226 | + private int lines; |
| 227 | + private string overrideTexture; |
| 228 | + |
| 229 | + public bool AllowLeftToRight; |
| 230 | + |
| 231 | + public SidewaysJumpThru(Vector2 position, int height, bool allowLeftToRight, string overrideTexture) |
| 232 | + : base(position) { |
| 233 | + |
| 234 | + lines = height / 8; |
| 235 | + AllowLeftToRight = allowLeftToRight; |
| 236 | + Depth = -60; |
| 237 | + this.overrideTexture = overrideTexture; |
| 238 | + |
| 239 | + float hitboxOffset = 0f; |
| 240 | + if (AllowLeftToRight) |
| 241 | + hitboxOffset = 3f; |
| 242 | + |
| 243 | + Collider = new Hitbox(5f, height, hitboxOffset, 0); |
| 244 | + } |
| 245 | + |
| 246 | + public SidewaysJumpThru(EntityData data, Vector2 offset) |
| 247 | + : this(data.Position + offset, data.Height, data.Bool("allowLeftToRight"), data.Attr("texture", "default")) { |
| 248 | + } |
| 249 | + |
| 250 | + public override void Awake(Scene scene) { |
| 251 | + AreaData areaData = AreaData.Get(scene); |
| 252 | + string jumpthru = areaData.Jumpthru; |
| 253 | + if (!string.IsNullOrEmpty(overrideTexture) && !overrideTexture.Equals("default")) { |
| 254 | + jumpthru = overrideTexture; |
| 255 | + } |
| 256 | + |
| 257 | + MTexture mTexture = GFX.Game["objects/jumpthru/" + jumpthru]; |
| 258 | + int num = mTexture.Width / 8; |
| 259 | + for (int i = 0; i < lines; i++) { |
| 260 | + int xTilePosition; |
| 261 | + int yTilePosition; |
| 262 | + if (i == 0) { |
| 263 | + xTilePosition = 0; |
| 264 | + yTilePosition = ((!CollideCheck<Solid>(Position + new Vector2(0f, -1f))) ? 1 : 0); |
| 265 | + } else if (i == lines - 1) { |
| 266 | + xTilePosition = num - 1; |
| 267 | + yTilePosition = ((!CollideCheck<Solid>(Position + new Vector2(0f, 1f))) ? 1 : 0); |
| 268 | + } else { |
| 269 | + xTilePosition = 1 + Calc.Random.Next(num - 2); |
| 270 | + yTilePosition = Calc.Random.Choose(0, 1); |
| 271 | + } |
| 272 | + Image image = new Image(mTexture.GetSubtexture(xTilePosition * 8, yTilePosition * 8, 8, 8)); |
| 273 | + image.Y = i * 8; |
| 274 | + image.Rotation = (float) (Math.PI / 2); |
| 275 | + |
| 276 | + if (AllowLeftToRight) |
| 277 | + image.X = 8; |
| 278 | + else |
| 279 | + image.Scale.Y = -1; |
| 280 | + |
| 281 | + Add(image); |
| 282 | + } |
| 283 | + } |
| 284 | + } |
| 285 | +} |
0 commit comments