|
4 | 4 | using MonoMod.Cil; |
5 | 5 | using MonoMod.RuntimeDetour; |
6 | 6 | using MonoMod.Utils; |
| 7 | +using On.Celeste.Mod; |
7 | 8 | using System.Linq; |
8 | 9 | using System.Reflection; |
9 | 10 |
|
@@ -126,7 +127,7 @@ public static void Load() |
126 | 127 |
|
127 | 128 | IL.Celeste.FakeWall.Update += State_DreamDashNotEqual; |
128 | 129 | IL.Celeste.Spring.OnCollide += State_DreamDashEqual; |
129 | | - IL.Celeste.Solid.Update += State_DreamDashNotEqual_And; |
| 130 | + IL.Celeste.Solid.Update += State_DreamDashNotEqual; |
130 | 131 | } |
131 | 132 |
|
132 | 133 | public static void Unload() |
@@ -159,7 +160,7 @@ public static void Unload() |
159 | 160 |
|
160 | 161 | IL.Celeste.FakeWall.Update -= State_DreamDashNotEqual; |
161 | 162 | IL.Celeste.Spring.OnCollide -= State_DreamDashEqual; |
162 | | - IL.Celeste.Solid.Update -= State_DreamDashNotEqual_And; |
| 163 | + IL.Celeste.Solid.Update -= State_DreamDashNotEqual; |
163 | 164 | } |
164 | 165 |
|
165 | 166 | public static void InitializeParticles() |
@@ -365,22 +366,22 @@ private static void Player_IsRiding_JumpThru(ILContext il) |
365 | 366 | if (il.Instrs[0].OpCode == OpCodes.Nop) |
366 | 367 | State_DreamDashEqual(il); |
367 | 368 | else |
368 | | - State_DreamDashNotEqual_And(il); |
| 369 | + State_DreamDashNotEqual(il); |
369 | 370 | } |
370 | 371 |
|
371 | 372 | private static void Player_BeforeUpTransition(ILContext il) |
372 | 373 | { |
373 | 374 | ILCursor cursor = new(il); |
374 | 375 |
|
375 | 376 | CheckState(cursor, Player.StRedDash, false); |
376 | | - CheckState(cursor, Player.StRedDash, false, true); |
| 377 | + CheckState(cursor, Player.StRedDash, false); |
377 | 378 | } |
378 | 379 |
|
379 | 380 | private static void Player_BeforeDownTransition(ILContext il) |
380 | 381 | { |
381 | 382 | ILCursor cursor = new(il); |
382 | 383 |
|
383 | | - CheckState(cursor, Player.StRedDash, false, true); |
| 384 | + CheckState(cursor, Player.StRedDash, false); |
384 | 385 | } |
385 | 386 |
|
386 | 387 | private static void Player_TransitionTo(ILContext il) |
@@ -429,74 +430,106 @@ private static void NaiveMoveTowardsY(Player player, float targetY, float maxAmo |
429 | 430 | player.NaiveMove(Vector2.UnitY * moveY); |
430 | 431 | } |
431 | 432 |
|
432 | | - // Patch any method that checks the player's State |
| 433 | + // Utilities to patch any method that checks the player's State |
433 | 434 | /// <summary> |
434 | | - /// Use if decompilation says <c>State==9</c> and NOT followed by <c>&&</c>. |
| 435 | + /// Use if decompilation says <c>State==9</c>. |
435 | 436 | /// </summary> |
436 | 437 | private static readonly ILContext.Manipulator State_DreamDashEqual = il => CheckState(new ILCursor(il), Player.StDreamDash, true); |
437 | 438 | /// <summary> |
438 | | - /// Use if decompilation says <c>State!=9</c> and NOT followed by <c>&&</c>. |
| 439 | + /// Use if decompilation says <c>State!=9</c>. |
439 | 440 | /// </summary> |
440 | 441 | private static readonly ILContext.Manipulator State_DreamDashNotEqual = il => CheckState(new ILCursor(il), Player.StDreamDash, false); |
441 | | - /// <summary> |
442 | | - /// Use if decompilation says <c>State==9</c> and IS followed by <c>&&</c>. |
443 | | - /// </summary> |
444 | | - private static readonly ILContext.Manipulator State_DreamDashEqual_And = il => CheckState(new ILCursor(il), Player.StDreamDash, true, true); |
445 | | - /// <summary> |
446 | | - /// Use if decompilation says <c>State!=9</c> and IS followed by <c>&&</c>. |
447 | | - /// </summary> |
448 | | - private static readonly ILContext.Manipulator State_DreamDashNotEqual_And = il => CheckState(new ILCursor(il), Player.StDreamDash, false, true); |
| 442 | + |
449 | 443 | /// <summary> |
450 | 444 | /// Patch any method that checks the player's state. |
451 | 445 | /// </summary> |
452 | 446 | /// <remarks>Checks for <c>ldc.i4.s <state></c></remarks> |
453 | 447 | /// <param name="cursor">The ILCursor to use</param> |
454 | 448 | /// <param name="state">The state to check for</param> |
455 | 449 | /// <param name="equal">Whether the decompilation says <c>State == <state></c></param> |
456 | | - /// <param name="and">Whether the check is followed by <c>&&</c></param> |
457 | | - private static void CheckState(ILCursor cursor, int state, bool equal, bool and = false) |
| 450 | + private static void CheckState(ILCursor cursor, int state, bool equal) |
458 | 451 | { |
459 | | - if (cursor.TryGotoNext(instr => instr.MatchLdcI4(state) && |
460 | | - instr.Previous != null && instr.Previous.MatchCallvirt<StateMachine>("get_State"))) |
| 452 | + // essentially, we want to perform these conversions: |
| 453 | + // `player.StateMachine.State == state` -> `player.StateMachine.State == state || player.StateMachine.State == St.DreamTunnelDash` |
| 454 | + // `player.StateMachine.State != state` -> `player.StateMachine.State != state && player.StateMachine.State != St.DreamTunnelDash` |
| 455 | + |
| 456 | + // variables to grab stuff later |
| 457 | + Instruction afterMatch = null; |
| 458 | + |
| 459 | + bool matchedBeqOrBne = false, matchedCeq = false; |
| 460 | + ILLabel failedCheck = null; |
| 461 | + Instruction ceqInstr = null; |
| 462 | + |
| 463 | + if (!cursor.TryGotoNext(MoveType.AfterLabel, |
| 464 | + instr => instr.MatchLdfld<Player>("StateMachine"), |
| 465 | + instr => instr.MatchCallvirt<StateMachine>("get_State"), |
| 466 | + instr => instr.MatchLdcI4(state), |
| 467 | + instr => |
| 468 | + { |
| 469 | + // we grab a lot of stuff here: the instruction directly after this match, whether we matched a beq/bne.un or a ceq, |
| 470 | + // the "fail state" label of the beq/bne.un (if we matched one of those) and the actual ceq instruction (if we matched that). |
| 471 | + |
| 472 | + afterMatch = instr.Next; |
| 473 | + // equality checks usually use bne.un (for ==) or beq (for !=) to branch past the block of the if statement if the values don't match |
| 474 | + matchedBeqOrBne = equal ? instr.MatchBneUn(out failedCheck) : instr.MatchBeq(out failedCheck); |
| 475 | + matchedCeq = (ceqInstr = instr).MatchCeq(); |
| 476 | + return matchedBeqOrBne || matchedCeq; |
| 477 | + })) |
| 478 | + return; |
| 479 | + |
| 480 | + // beq and bne.un work with labels, whereas ceq just leves a bool so we need to deal with them differently |
| 481 | + if (matchedBeqOrBne) |
461 | 482 | { |
462 | | - Instruction idx = cursor.Next; |
463 | | - // Duplicate the Player State |
| 483 | + // labels for cleaning up duplicate player on stack |
| 484 | + ILLabel cleanUpPlayer = cursor.DefineLabel(), pastCleanUpPlayer = cursor.DefineLabel(); |
| 485 | + |
| 486 | + // duplicate player on stack |
464 | 487 | cursor.Emit(OpCodes.Dup); |
465 | | - // Check whether the state matches St.DreamTunnelDash AND we want them to match |
466 | | - cursor.EmitDelegate<Func<int, bool>>(st => st == St.DreamTunnelDash == equal ^ and); |
467 | | - // If not, skip the rest of the emitted instructions |
468 | | - cursor.Emit(OpCodes.Brfalse_S, cursor.Next); |
469 | | - |
470 | | - // Else |
471 | | - // Duplicated Player State value will be unused, so it must be trashed |
| 488 | + // check if player is dream tunnel dashing and if so, short-circuit (while also cleaning up the other, now unnecessary duplicate player) |
| 489 | + cursor.EmitDelegate<Func<Player, bool>>(player => player.StateMachine.State == St.DreamTunnelDash); |
| 490 | + cursor.Emit(OpCodes.Brtrue, cleanUpPlayer); |
| 491 | + // else, continue with check as normal |
| 492 | + |
| 493 | + // where we short-circuit to depends on whether we check for equality or not. |
| 494 | + // for equality, we should short-circuit to the "block of the if statement" (past our current condition, whether it be the actual block or another condition), |
| 495 | + // since our desired behaviour is `player.StateMachine.State == state || player.StateMachine.State == St.DreamTunnelDash` and the first part of that or has been satisfied, |
| 496 | + // just like how `if (true || condition()) { ... }` should immediately skip checking `condition()` (since `true` or anything is `true`) and run the block inside the if. |
| 497 | + // for inequality, it's the other way around: we should short-circuit immediately past the rest of the if statement, since our desired behaviour will be |
| 498 | + // `player.StateMachine.State != state && player.StateMachine.State != St.DreamTunnelDash` and we know the first condition of that and is false, just like how |
| 499 | + // `if (false && condition()) { ... }` should immediately skip checking `condition()` (since `false` and anything is `false`) and never run the block inside the if. |
| 500 | + // our `afterMatch` label points to after our current condition, and our `failedCheck` label points to after the rest of the statement. |
| 501 | + cursor.Goto(equal ? afterMatch : failedCheck.Target); |
| 502 | + // extra player cleanup, we branch over it in normal behaviour but jump into it if needed (see above) |
| 503 | + cursor.Emit(OpCodes.Br, pastCleanUpPlayer); |
472 | 504 | cursor.Emit(OpCodes.Pop); |
473 | | - |
474 | | - // Retrieve the next break instruction that checks equality |
475 | | - Instruction breakInstr = cursor.Clone().GotoNext(instr => instr.Match(OpCodes.Beq_S) || instr.Match(OpCodes.Bne_Un_S) || instr.Match(OpCodes.Ceq)).Next; |
476 | | - |
477 | | - // For SteamFNA, if there is a check for equality just break to after it after pushing the appropriate value to the stack |
478 | | - if (breakInstr.OpCode == OpCodes.Ceq) |
479 | | - { |
480 | | - cursor.Emit(equal ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); |
481 | | - cursor.Emit(OpCodes.Br_S, breakInstr.Next); |
482 | | - } |
483 | | - // If our intended behaviour matches what the break instruction is checking for, break to its target |
484 | | - else if (breakInstr.OpCode == OpCodes.Beq_S == equal ^ and) |
485 | | - cursor.Emit(OpCodes.Br_S, breakInstr.Operand); |
486 | | - // Otherwise, break to after the break instruction (skip it) |
487 | | - else |
488 | | - cursor.Emit(OpCodes.Br_S, breakInstr.Next); |
489 | | - |
490 | | - cursor.Goto(idx, MoveType.After); |
| 505 | + cursor.MarkLabel(pastCleanUpPlayer); |
| 506 | + cursor.Index--; |
| 507 | + cursor.MarkLabel(cleanUpPlayer); |
491 | 508 | } |
| 509 | + else if (matchedCeq) |
| 510 | + { |
| 511 | + // duplicate player on stack |
| 512 | + cursor.Emit(OpCodes.Dup); |
| 513 | + // go to the ceq instruction and modify the value it returns |
| 514 | + cursor.Goto(ceqInstr, MoveType.After); |
| 515 | + // our desired value for what the ceq instruction returns depends on whether we check equality or not. but we don't control that, the brtrue/brfalse after the ceq does. |
| 516 | + // to solve this, our desired conditions can be shown equivalent to `player.StateMachine.State == state || player.StateMachine.State == St.DreamTunnelDash` (for equality) and |
| 517 | + // `!(player.StateMachine.State == state || player.StateMachine.State == St.DreamTunnelDash)` (for inequality). notice how the desired condition for inequality is simply |
| 518 | + // the inverse of the one for equality. this is great, because the brtrue/brfalse after the ceq will do the required inversion (or lack thereof) for the check, and all |
| 519 | + // we need to do is calculate the thing on the inside. conveniently, the ceq is already checking for the left term (so that is its return value), and we just need to or it with the right. |
| 520 | + cursor.EmitDelegate<Func<Player, bool, bool>>((player, orig) => orig || player.StateMachine.State == St.DreamTunnelDash); |
| 521 | + } |
| 522 | + |
| 523 | + // go to after the current match to continue with the il hook (no infinite loops!) |
| 524 | + cursor.Goto(afterMatch, MoveType.After); |
492 | 525 | } |
493 | 526 |
|
494 | 527 | private static void Player_orig_Update(ILContext il) |
495 | 528 | { |
496 | 529 | ILCursor cursor = new(il); |
497 | 530 | CheckState(cursor, Player.StDreamDash, true); |
498 | | - CheckState(cursor, Player.StDreamDash, false, true); |
499 | | - CheckState(cursor, Player.StDreamDash, false, true); |
| 531 | + CheckState(cursor, Player.StDreamDash, false); |
| 532 | + CheckState(cursor, Player.StDreamDash, false); |
500 | 533 | // Not used because we DO want to enforce Level bounds. |
501 | 534 | //Check_State_DreamDash(cursor, false, true); |
502 | 535 | } |
|
0 commit comments