Skip to content

Commit d102b25

Browse files
committed
Overhaul state check util to actually make sense
1 parent 434d5c9 commit d102b25

File tree

1 file changed

+82
-49
lines changed

1 file changed

+82
-49
lines changed

src/DashStates/DreamTunnelDash/DreamTunnelDash.cs

Lines changed: 82 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using MonoMod.Cil;
55
using MonoMod.RuntimeDetour;
66
using MonoMod.Utils;
7+
using On.Celeste.Mod;
78
using System.Linq;
89
using System.Reflection;
910

@@ -126,7 +127,7 @@ public static void Load()
126127

127128
IL.Celeste.FakeWall.Update += State_DreamDashNotEqual;
128129
IL.Celeste.Spring.OnCollide += State_DreamDashEqual;
129-
IL.Celeste.Solid.Update += State_DreamDashNotEqual_And;
130+
IL.Celeste.Solid.Update += State_DreamDashNotEqual;
130131
}
131132

132133
public static void Unload()
@@ -159,7 +160,7 @@ public static void Unload()
159160

160161
IL.Celeste.FakeWall.Update -= State_DreamDashNotEqual;
161162
IL.Celeste.Spring.OnCollide -= State_DreamDashEqual;
162-
IL.Celeste.Solid.Update -= State_DreamDashNotEqual_And;
163+
IL.Celeste.Solid.Update -= State_DreamDashNotEqual;
163164
}
164165

165166
public static void InitializeParticles()
@@ -365,22 +366,22 @@ private static void Player_IsRiding_JumpThru(ILContext il)
365366
if (il.Instrs[0].OpCode == OpCodes.Nop)
366367
State_DreamDashEqual(il);
367368
else
368-
State_DreamDashNotEqual_And(il);
369+
State_DreamDashNotEqual(il);
369370
}
370371

371372
private static void Player_BeforeUpTransition(ILContext il)
372373
{
373374
ILCursor cursor = new(il);
374375

375376
CheckState(cursor, Player.StRedDash, false);
376-
CheckState(cursor, Player.StRedDash, false, true);
377+
CheckState(cursor, Player.StRedDash, false);
377378
}
378379

379380
private static void Player_BeforeDownTransition(ILContext il)
380381
{
381382
ILCursor cursor = new(il);
382383

383-
CheckState(cursor, Player.StRedDash, false, true);
384+
CheckState(cursor, Player.StRedDash, false);
384385
}
385386

386387
private static void Player_TransitionTo(ILContext il)
@@ -429,74 +430,106 @@ private static void NaiveMoveTowardsY(Player player, float targetY, float maxAmo
429430
player.NaiveMove(Vector2.UnitY * moveY);
430431
}
431432

432-
// Patch any method that checks the player's State
433+
// Utilities to patch any method that checks the player's State
433434
/// <summary>
434-
/// Use if decompilation says <c>State==9</c> and NOT followed by <c>&amp;&amp;</c>.
435+
/// Use if decompilation says <c>State==9</c>.
435436
/// </summary>
436437
private static readonly ILContext.Manipulator State_DreamDashEqual = il => CheckState(new ILCursor(il), Player.StDreamDash, true);
437438
/// <summary>
438-
/// Use if decompilation says <c>State!=9</c> and NOT followed by <c>&amp;&amp;</c>.
439+
/// Use if decompilation says <c>State!=9</c>.
439440
/// </summary>
440441
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>&amp;&amp;</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>&amp;&amp;</c>.
447-
/// </summary>
448-
private static readonly ILContext.Manipulator State_DreamDashNotEqual_And = il => CheckState(new ILCursor(il), Player.StDreamDash, false, true);
442+
449443
/// <summary>
450444
/// Patch any method that checks the player's state.
451445
/// </summary>
452446
/// <remarks>Checks for <c>ldc.i4.s &lt;state&gt;</c></remarks>
453447
/// <param name="cursor">The ILCursor to use</param>
454448
/// <param name="state">The state to check for</param>
455449
/// <param name="equal">Whether the decompilation says <c>State == &lt;state&gt;</c></param>
456-
/// <param name="and">Whether the check is followed by <c>&amp;&amp;</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)
458451
{
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)
461482
{
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
464487
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);
472504
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);
491508
}
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);
492525
}
493526

494527
private static void Player_orig_Update(ILContext il)
495528
{
496529
ILCursor cursor = new(il);
497530
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);
500533
// Not used because we DO want to enforce Level bounds.
501534
//Check_State_DreamDash(cursor, false, true);
502535
}

0 commit comments

Comments
 (0)