Skip to content

Commit 05336cc

Browse files
committed
add disable auto camera offset controller
1 parent b78dc90 commit 05336cc

File tree

5 files changed

+143
-0
lines changed

5 files changed

+143
-0
lines changed
392 Bytes
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
local controller = {}
2+
3+
controller.name = "CommunalHelper/DisableAutoCameraOffsetController"
4+
controller.texture = "objects/CommunalHelper/disableAutoCameraOffsetController/icon"
5+
controller.placements = {
6+
name = "controller",
7+
data = {
8+
flag = ""
9+
}
10+
}
11+
12+
return controller

Loenn/lang/en_gb.lang

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mods.CommunalHelper.name=Communal Helper
22

3+
# ---------------------- ENTITIES --------------------------
4+
35
# Dream Refill
46
entities.CommunalHelper/DreamRefill.placements.name.dream_refill=Dream Refill
57
entities.CommunalHelper/DreamRefill.placements.name.double_dream_refill=Dream Refill (Two Dashes)
@@ -994,6 +996,9 @@ entities.CommunalHelper/DreamTunnelBlocker.placements.name.normal=Dream Tunnel B
994996
entities.CommunalHelper/DreamTunnelBlocker.attributes.description.blockDreamTunnelDashes=Whether to prevent dream tunneling when dashing into a solid covered by this entity.
995997
entities.CommunalHelper/DreamTunnelBlocker.attributes.description.blockDreamDashes=Whether to prevent dream dashing when dashing into a dream block covered by this entity.
996998

999+
entities.CommunalHelper/DisableAutoCameraOffsetController.placements.name.controller=Disable Auto Camera Offset Controller
1000+
entities.CommunalHelper/DisableAutoCameraOffsetController.attributes.description.flag=The flag this controller should use to determine when to disable the automatic camera offset that some player states impart.\nLeave empty for this controller to be active always.
1001+
9971002
# ---------------------- TRIGGERS --------------------------
9981003

9991004
# [Strawberry Jam] Show Hitbox Trigger
@@ -1115,6 +1120,8 @@ triggers.CommunalHelper/SolarElevatorLevelTrigger.placements.name.trigger=Set So
11151120
triggers.CommunalHelper/SolarElevatorLevelTrigger.attributes.description.elevatorID=The entity ID of the elevator to modify in this room. (integer)
11161121
triggers.CommunalHelper/SolarElevatorLevelTrigger.attributes.description.position=Determines which of the two floors the elevator is sent to.\n- Closest: the one closest to the player when entering the trigger.\n- Bottom: always the bottom floor.\n- Top: always the top floor.
11171122

1123+
# ---------------------- STYLEGROUNDS --------------------------
1124+
11181125
# Cloudscape
11191126
style.effects.CommunalHelper/Cloudscape.name=Cloudscape
11201127
style.effects.CommunalHelper/Cloudscape.description.seed=The RNG seed that this cloudscape instance will be generated with.

src/CommunalHelperModule.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ public override void Load()
119119
AffectSpriteTrigger.Load();
120120

121121
MelvinTargetable.Load();
122+
123+
DisableAutoCameraOffsetController.Load();
122124

123125
#region Imports
124126

@@ -217,6 +219,8 @@ public override void Unload()
217219
AffectSpriteTrigger.Unload();
218220

219221
MelvinTargetable.Unload();
222+
223+
DisableAutoCameraOffsetController.Unload();
220224

221225
LaserEmitter.Unload();
222226
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using Celeste.Mod.Helpers;
2+
using Mono.Cecil.Cil;
3+
using MonoMod.Cil;
4+
using MonoMod.RuntimeDetour;
5+
using System.Reflection;
6+
7+
namespace Celeste.Mod.CommunalHelper.Entities;
8+
9+
[CustomEntity("CommunalHelper/DisableAutoCameraOffsetController")]
10+
[Tracked]
11+
public class DisableAutoCameraOffsetController(EntityData data, Vector2 offset) : Entity(data.Position + offset)
12+
{
13+
private readonly string flag = data.Attr("flag");
14+
15+
private bool IsActive()
16+
=> string.IsNullOrEmpty(flag) || SceneAs<Level>().Session.GetFlag(flag);
17+
18+
#region Hooks
19+
20+
private static ILHook ilHook_Player_get_CameraTarget;
21+
22+
internal static void Load()
23+
{
24+
ilHook_Player_get_CameraTarget = new ILHook(typeof(Player).GetMethod("get_CameraTarget", BindingFlags.Public | BindingFlags.Instance)!, Player_get_CameraTarget);
25+
}
26+
27+
internal static void Unload()
28+
{
29+
ilHook_Player_get_CameraTarget.Dispose();
30+
ilHook_Player_get_CameraTarget = null;
31+
}
32+
33+
private static void Player_get_CameraTarget(ILContext il)
34+
{
35+
ILCursor cursor = new(il);
36+
37+
// add check for controller being active to the check for StReflectionFall
38+
/*
39+
* IL_0062: ldarg.0
40+
* IL_0063: ldfld class Monocle.StateMachine Celeste.Player::StateMachine
41+
* IL_0068: callvirt instance int32 Monocle.StateMachine::get_State()
42+
* IL_0032: ldc.i4.s 18
43+
* IL_0034: beq.s IL_0062
44+
*/
45+
if (!cursor.TryGotoNextBestFit(MoveType.Before,
46+
instr => instr.MatchLdarg(0),
47+
instr => instr.MatchLdfld<Player>("StateMachine"),
48+
instr => instr.MatchCallvirt<StateMachine>("get_State"),
49+
instr => instr.MatchLdcI4(Player.StReflectionFall),
50+
instr => instr.MatchBeq(out _)))
51+
return;
52+
53+
ILCursor cameraOffsetLabelCursor = cursor.Clone();
54+
ILLabel setCameraOffset = cameraOffsetLabelCursor.DefineLabel();
55+
56+
/*
57+
* IL_0036: ldloc.1
58+
* IL_0037: ldarg.0
59+
* IL_0038: ldfld class Celeste.Level Celeste.Player::level
60+
* IL_003d: ldflda valuetype [FNA]Microsoft.Xna.Framework.Vector2 Celeste.Level::CameraOffset
61+
*/
62+
if (!cameraOffsetLabelCursor.TryGotoNextBestFit(MoveType.Before,
63+
instr => instr.MatchLdloc(1),
64+
instr => instr.MatchLdarg(0),
65+
instr => instr.MatchLdfld<Player>("level"),
66+
instr => instr.MatchLdflda<Level>("CameraOffset")))
67+
return;
68+
69+
cameraOffsetLabelCursor.MarkLabel(setCameraOffset);
70+
71+
cursor.Emit(OpCodes.Ldarg_0);
72+
cursor.EmitDelegate(IsControllerActive);
73+
cursor.Emit(OpCodes.Brtrue, setCameraOffset);
74+
75+
// skip all the state-specific camera offsets if the controller is active
76+
/*
77+
* IL_0062: ldarg.0
78+
* IL_0063: ldfld class Monocle.StateMachine Celeste.Player::StateMachine
79+
* IL_0068: callvirt instance int32 Monocle.StateMachine::get_State()
80+
* IL_006d: ldc.i4.s 19
81+
* IL_006f: bne.un.s IL_00ae
82+
*/
83+
if (!cursor.TryGotoNextBestFit(MoveType.Before,
84+
instr => instr.MatchLdarg(0),
85+
instr => instr.MatchLdfld<Player>("StateMachine"),
86+
instr => instr.MatchCallvirt<StateMachine>("get_State"),
87+
instr => instr.MatchLdcI4(Player.StStarFly),
88+
instr => instr.MatchBneUn(out _)))
89+
return;
90+
91+
ILCursor skipStateOffsetLabelCursor = cursor.Clone();
92+
ILLabel skipStateOffset = skipStateOffsetLabelCursor.DefineLabel();
93+
94+
/*
95+
* IL_013c: ldarg.0
96+
* IL_013d: ldflda valuetype [FNA]Microsoft.Xna.Framework.Vector2 Celeste.Player::CameraAnchorLerp
97+
* IL_0142: call instance float32 [FNA]Microsoft.Xna.Framework.Vector2::Length()
98+
* IL_0147: ldc.r4 0.0
99+
* IL_014c: ble.un IL_024d
100+
*/
101+
if (!skipStateOffsetLabelCursor.TryGotoNextBestFit(MoveType.Before,
102+
instr => instr.MatchLdarg(0),
103+
instr => instr.MatchLdflda<Player>("CameraAnchorLerp"),
104+
instr => instr.MatchCall<Vector2>("Length"),
105+
instr => instr.MatchLdcR4(0f),
106+
instr => instr.MatchBleUn(out _)))
107+
return;
108+
109+
skipStateOffsetLabelCursor.MarkLabel(skipStateOffset);
110+
111+
cursor.Emit(OpCodes.Ldarg_0);
112+
cursor.EmitDelegate(IsControllerActive);
113+
cursor.Emit(OpCodes.Brtrue, skipStateOffset);
114+
}
115+
116+
private static bool IsControllerActive(Player player)
117+
=> player.Scene?.Tracker?.GetEntity<DisableAutoCameraOffsetController>() is { } controller && controller.IsActive();
118+
119+
#endregion
120+
}

0 commit comments

Comments
 (0)