@@ -22,12 +22,17 @@ class UpsideDownJumpThru : JumpThru {
2222
2323 public static void Load ( ) {
2424 using ( new DetourContext { Before = { "*" } } ) { // these don't always call the orig methods, better apply them first.
25+ // fix general actor/platform behavior to make them comply with jumpthrus.
2526 On . Celeste . Actor . MoveVExact += onActorMoveVExact ;
2627 On . Celeste . Platform . MoveVExactCollideSolids += onPlatformMoveVExactCollideSolids ;
2728 }
2829
30+ // fix player specific behavior allowing them to go through upside-down jumpthrus.
2931 On . Celeste . Player . OnCollideV += onPlayerOnCollideV ;
3032
33+ // block player if they try to climb past an upside-down jumpthru.
34+ IL . Celeste . Player . ClimbUpdate += patchPlayerClimbUpdate ;
35+
3136 // ignore upside-down jumpthrus in select places.
3237 playerOrigUpdateHook = new ILHook ( typeof ( Player ) . GetMethod ( "orig_Update" ) , filterOutJumpThrusFromCollideChecks ) ;
3338 IL . Celeste . Player . DashUpdate += filterOutJumpThrusFromCollideChecks ;
@@ -39,6 +44,7 @@ public static void Unload() {
3944 On . Celeste . Platform . MoveVExactCollideSolids -= onPlatformMoveVExactCollideSolids ;
4045
4146 On . Celeste . Player . OnCollideV -= onPlayerOnCollideV ;
47+ IL . Celeste . Player . ClimbUpdate -= patchPlayerClimbUpdate ;
4248
4349 if ( playerOrigUpdateHook != null )
4450 playerOrigUpdateHook . Dispose ( ) ;
@@ -78,7 +84,7 @@ private static bool onActorMoveVExact(On.Celeste.Actor.orig_MoveVExact orig, Act
7884 }
7985 }
8086
81- if ( didCollide ) {
87+ if ( didCollide ) {
8288 Vector2 movementCounter = ( Vector2 ) actorMovementCounter . GetValue ( self ) ;
8389 movementCounter . Y = 0f ;
8490 if ( onCollide != null ) {
@@ -184,7 +190,7 @@ private static void filterOutJumpThrusFromCollideChecks(ILContext il) {
184190 case "T Monocle.Entity::CollideFirstOutside<Celeste.JumpThru>(Microsoft.Xna.Framework.Vector2)" :
185191 Logger . Log ( "SpringCollab2020/UpsideDownJumpThru" , $ "Patching CollideFirstOutside at { cursor . Index } in IL for { il . Method . Name } ") ;
186192 cursor . Index ++ ;
187-
193+
188194 // nullify if mod jumpthru.
189195 cursor . EmitDelegate < Func < JumpThru , JumpThru > > ( jumpThru => {
190196 if ( jumpThru ? . GetType ( ) == typeof ( UpsideDownJumpThru ) )
@@ -218,8 +224,8 @@ private static void filterOutJumpThrusFromCollideChecks(ILContext il) {
218224
219225 // remove all mod jumpthrus from the returned list.
220226 cursor . EmitDelegate < Func < List < Entity > , List < Entity > > > ( matches => {
221- for ( int i = 0 ; i < matches . Count ; i ++ ) {
222- if ( matches [ i ] . GetType ( ) == typeof ( UpsideDownJumpThru ) ) {
227+ for ( int i = 0 ; i < matches . Count ; i ++ ) {
228+ if ( matches [ i ] . GetType ( ) == typeof ( UpsideDownJumpThru ) ) {
223229 matches . RemoveAt ( i ) ;
224230 i -- ;
225231 }
@@ -234,11 +240,48 @@ private static void filterOutJumpThrusFromCollideChecks(ILContext il) {
234240 }
235241 }
236242
243+ private static void patchPlayerClimbUpdate ( ILContext il ) {
244+ ILCursor cursor = new ILCursor ( il ) ;
245+
246+ // in decompiled code, we want to get ourselves just before the last occurrence of "if (climbNoMoveTimer <= 0f)".
247+ while ( cursor . TryGotoNext (
248+ instr => instr . MatchStfld < Vector2 > ( "Y" ) ,
249+ instr => instr . MatchLdarg ( 0 ) ,
250+ instr => instr . MatchLdfld < Player > ( "climbNoMoveTimer" ) ,
251+ instr => instr . MatchLdcR4 ( 0f ) ) ) {
252+
253+ cursor . Index += 2 ;
254+
255+ FieldInfo f_lastClimbMove = typeof ( Player ) . GetField ( "lastClimbMove" , BindingFlags . NonPublic | BindingFlags . Instance ) ;
256+
257+ Logger . Log ( "SpringCollab2020/UpsideDownJumpThru" , $ "Injecting collide check to block climbing at { cursor . Index } in IL for { il . Method . Name } ") ;
258+
259+ cursor . Emit ( OpCodes . Ldarg_0 ) ;
260+ cursor . Emit ( OpCodes . Ldarg_0 ) ;
261+ cursor . Emit ( OpCodes . Ldfld , f_lastClimbMove ) ;
262+
263+ // if climbing is blocked by an upside down jumpthru, cancel the climb (lastClimbMove = 0 and Speed.Y = 0).
264+ // injecting that at that point in the method allows it to drain stamina as if the player was not climbing.
265+ cursor . EmitDelegate < Func < Player , int , int > > ( ( self , lastClimbMove ) => {
266+ if ( Input . MoveY . Value == - 1 && self . CollideCheck < UpsideDownJumpThru > ( self . Position - Vector2 . UnitY ) ) {
267+ self . Speed . Y = 0 ;
268+ return 0 ;
269+ }
270+ return lastClimbMove ;
271+ } ) ;
272+
273+ cursor . Emit ( OpCodes . Stfld , f_lastClimbMove ) ;
274+ cursor . Emit ( OpCodes . Ldarg_0 ) ;
275+ }
276+ }
277+
278+
279+
237280
238281 private int columns ;
239282 private string overrideTexture ;
240283
241- public UpsideDownJumpThru ( EntityData data , Vector2 offset )
284+ public UpsideDownJumpThru ( EntityData data , Vector2 offset )
242285 : base ( data . Position + offset , data . Width , false ) {
243286
244287 columns = data . Width / 8 ;
0 commit comments