Skip to content

Commit

Permalink
Merge branch 'dev' into core
Browse files Browse the repository at this point in the history
  • Loading branch information
maddie480 committed Oct 19, 2023
2 parents 575c794 + fda4a9c commit efdca51
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 10 deletions.
7 changes: 6 additions & 1 deletion Celeste.Mod.mm/Mod/Core/CoreModuleSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,13 @@ public string CurrentBranch {
set => _CurrentBranch = value is "dev" or "beta" or "stable" ? "updater_src_" + value : value; // branch names were changed at some point
}

private Dictionary<string, LogLevel> _LogLevels = new Dictionary<string, LogLevel>();

[SettingIgnore]
public Dictionary<string, LogLevel> LogLevels { get; set; } = new Dictionary<string, LogLevel>();
public Dictionary<string, LogLevel> LogLevels {
get => _LogLevels;
set => _LogLevels = value ?? new Dictionary<string, LogLevel>();
}

[SettingSubHeader("MODOPTIONS_COREMODULE_MENUNAV_SUBHEADER")]
[SettingInGame(false)]
Expand Down
6 changes: 3 additions & 3 deletions Celeste.Mod.mm/Mod/Everest/ButtonBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ public class ButtonBinding {

public List<Buttons> Buttons {
get => Binding.Controller;
set => Binding.Controller = value;
set => Binding.Controller = value ?? new List<Buttons>();
}

public List<Keys> Keys {
get => Binding.Keyboard;
set => Binding.Keyboard = value;
set => Binding.Keyboard = value ?? new List<Keys>();
}

public List<patch_MInput.patch_MouseData.MouseButtons> MouseButtons {
get => ((patch_Binding) Binding).Mouse;
set => ((patch_Binding) Binding).Mouse = value;
set => ((patch_Binding) Binding).Mouse = value ?? new List<patch_MInput.patch_MouseData.MouseButtons>();
}

private Binding _Binding;
Expand Down
18 changes: 18 additions & 0 deletions Celeste.Mod.mm/Mod/Everest/Everest.Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using _OuiJournal = Celeste.OuiJournal;
using _OuiMainMenu = Celeste.OuiMainMenu;
using _Player = Celeste.Player;
using _Seeker = Celeste.Seeker;
using _AngryOshiro = Celeste.AngryOshiro;

namespace Celeste.Mod {
public static partial class Everest {
Expand Down Expand Up @@ -184,6 +186,22 @@ internal static void Spawn(_Player player)
public static event Action<_Player> OnDie;
internal static void Die(_Player player)
=> OnDie?.Invoke(player);

public static event Action<_Player> OnRegisterStates;
internal static void RegisterStates(_Player player)
=> OnRegisterStates?.Invoke(player);
}

public static class Seeker {
public static event Action<_Seeker> OnRegisterStates;
internal static void RegisterStates(_Seeker seeker)
=> OnRegisterStates?.Invoke(seeker);
}

public static class AngryOshiro {
public static event Action<_AngryOshiro> OnRegisterStates;
internal static void RegisterStates(_AngryOshiro oshiro)
=> OnRegisterStates?.Invoke(oshiro);
}

public static class Input {
Expand Down
53 changes: 53 additions & 0 deletions Celeste.Mod.mm/Patches/AngryOshiro.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null

using Celeste.Mod;
using Microsoft.Xna.Framework;
using Monocle;
using MonoMod;
using System;
using System.Collections;

namespace Celeste {
public class patch_AngryOshiro : AngryOshiro {

// We're effectively in AngryOshiro, but still need to "expose" private fields to our mod.
private StateMachine state;

public patch_AngryOshiro(EntityData data, Vector2 offset)
: base(data, offset) {
// no-op. MonoMod ignores this - we only need this to make the compiler shut up.
}

public extern void orig_ctor(Vector2 position, bool fromCutscene);
[MonoModConstructor]
public void ctor(Vector2 position, bool fromCutscene) {
orig_ctor(position, fromCutscene);

// setup vanilla state names
((patch_StateMachine) state).SetStateName(0, "Chase");
((patch_StateMachine) state).SetStateName(1, "ChargeUp");
((patch_StateMachine) state).SetStateName(2, "Attack");
((patch_StateMachine) state).SetStateName(3, "Dummy");
((patch_StateMachine) state).SetStateName(4, "Waiting");
((patch_StateMachine) state).SetStateName(5, "Hurt");
// then allow mods to register new ones
Everest.Events.AngryOshiro.RegisterStates(this);
}

/// <summary>
/// Adds a new state to this oshiro with the given behaviour, and returns the index of the new state.
///
/// States should always be added during the <c>Events.AngryOshiro.OnRegisterStates</c> event.
/// </summary>
/// <param name="name">The name of this state, for display purposes by mods only.</param>
/// <param name="onUpdate">A function to run every frame during this state, returning the index of the state that should be switched to next frame.</param>
/// <param name="coroutine">A function that creates a coroutine to run when this state is switched to.</param>
/// <param name="begin">An action to run when this state is switched to.</param>
/// <param name="end">An action to run when this state ends.</param>
/// <returns>The index of the new state.</returns>
public int AddState(string name, Func<AngryOshiro, int> onUpdate, Func<AngryOshiro, IEnumerator> coroutine = null, Action<AngryOshiro> begin = null, Action<AngryOshiro> end = null){
return ((patch_StateMachine)state).AddState(name, onUpdate, coroutine, begin, end);
}
}
}
31 changes: 30 additions & 1 deletion Celeste.Mod.mm/Patches/Monocle/EntityList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ internal void ClearEntities() {

[MonoModIgnore]
[PatchEntityListUpdateLists]
internal extern void UpdateLists();
public extern void UpdateLists();

[MonoModIgnore]
[PatchEntityListAdd]
public extern void Add(Entity entity);
}

public static class EntityListExt {

/// <summary>
Expand All @@ -59,6 +64,13 @@ class PatchEntityListUpdateAttribute : Attribute { }
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchEntityListUpdateLists))]
class PatchEntityListUpdateListsAttribute : Attribute { }

/// <summary>
/// Make Add(entity) method to crash when entity is null
/// so modders can catch bugs in time
/// </summary>
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchEntityListAdd))]
class PatchEntityListAddAttribute : Attribute { }

static partial class MonoModRules {

public static void PatchEntityListUpdate(ILContext context, CustomAttribute attrib) {
Expand Down Expand Up @@ -113,5 +125,22 @@ public static void PatchEntityListUpdateLists(ILContext context, CustomAttribute
cursor.Next.Operand = hashRemoveOperand;
}

public static void PatchEntityListAdd(ILContext context, CustomAttribute attrib) {
// insert the following code at the beginning of the method
// if (entity == null) throw new ArgumentNullException("entity")

TypeDefinition t_ArgumentNullException = MonoModRule.Modder.FindType("System.ArgumentNullException").Resolve();
MethodReference ctor_ArgumentNullException = MonoModRule.Modder.Module.ImportReference(t_ArgumentNullException.FindMethod("System.Void .ctor(System.String)"));

ILCursor cursor = new ILCursor(context);
ILLabel label = cursor.DefineLabel();
cursor.Emit(OpCodes.Ldarg_1)
.Emit(OpCodes.Brtrue_S, label)
.Emit(OpCodes.Ldstr, "entity")
.Emit(OpCodes.Newobj, ctor_ArgumentNullException)
.Emit(OpCodes.Throw)
.MarkLabel(label);
}

}
}
118 changes: 118 additions & 0 deletions Celeste.Mod.mm/Patches/Monocle/StateMachine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it

using MonoMod;
using System;
using System.Collections;

namespace Monocle {
public class patch_StateMachine : StateMachine {

// We're effectively in StateMachine, but still need to "expose" private fields to our mod.
private Action[] begins;
private Func<int>[] updates;
private Action[] ends;
private Func<IEnumerator>[] coroutines;

// Keep track of state's names.
private string[] names;

private extern void orig_ctor(int maxStates = 10);
[MonoModConstructor]
public void ctor(int maxStates = 10) {
orig_ctor(maxStates);
names = new string[maxStates];
}

private int Expand() {
int nextIdx = begins.Length;

int newLength = begins.Length + 1;
Array.Resize(ref begins, newLength);
Array.Resize(ref updates, newLength);
Array.Resize(ref ends, newLength);
Array.Resize(ref coroutines, newLength);
Array.Resize(ref names, newLength);

return nextIdx;
}

/// <summary>
/// Adds a new state to this state machine with the given behaviour, and returns the index of the new state.
/// </summary>
/// <param name="name">The name of this state, for display purposes by mods only.</param>
/// <param name="onUpdate">A function to run every frame during this state, returning the index of the state that should be switched to next frame.</param>
/// <param name="coroutine">A function that creates a coroutine to run when this state is switched to.</param>
/// <param name="begin">An action to run when this state is switched to.</param>
/// <param name="end">An action to run when this state ends.</param>
/// <returns>The index of the new state.</returns>
public int AddState(string name, Func<int> onUpdate, Func<IEnumerator> coroutine = null, Action begin = null, Action end = null){
int nextIdx = Expand();
SetCallbacks(nextIdx, onUpdate, coroutine, begin, end);
names[nextIdx] = name;
return nextIdx;
}

/// <summary>
/// Adds a new state to this state machine with the given behaviour, providing access to the entity running this state machine.
///
/// It's preferable to use the <c>AddState</c> methods provided in <c>Player</c>, <c>AngryOshiro</c>, and <c>Seeker</c> than to use this directly, as
/// they ensure the correct type is used. If the entity has the wrong type, then these functions are given <c>null</c>.
/// </summary>
/// <param name="name">The name of this state, for display purposes by mods only.</param>
/// <param name="onUpdate">A function to run every frame during this state, returning the index of the state that should be switched to next frame.</param>
/// <param name="coroutine">A function that creates a coroutine to run when this state is switched to.</param>
/// <param name="begin">An action to run when this state is switched to.</param>
/// <param name="end">An action to run when this state ends.</param>
/// <typeparam name="E">The type of the entity that these functions run on. If the entity has the wrong type, the functions are given <c>null</c>.</typeparam>
/// <returns>The index of the new state.</returns>
public int AddState<E>(string name, Func<E, int> onUpdate, Func<E, IEnumerator> coroutine = null, Action<E> begin = null, Action<E> end = null)
where E : Entity
{
int nextIdx = Expand();
SetCallbacks(nextIdx, () => onUpdate(Entity as E),
coroutine == null ? null : () => coroutine(Entity as E),
begin == null ? null : () => begin(Entity as E),
end == null ? null : () => end(Entity as E));
names[nextIdx] = name;
return nextIdx;
}

// Mods that expand the state machine manually won't increase the size of `names`, so we need to bounds-check
// accesses ourselves, both against `begins` ("is it an actual valid state") and `names` ("does it have a coherent name")
private void CheckBounds(int state) {
if (!(state < begins.Length && state >= 0))
throw new IndexOutOfRangeException($"State {state} is out of range, maximum is {begins.Length}.");
}

/// <summary>
/// Returns the name of the state with the given index, which defaults to the index in string form.
/// These names are for display purposes by mods only.
/// </summary>
/// <param name="state">The index of the state.</param>
/// <returns>The display name of that state.</returns>
public string GetStateName(int state) {
CheckBounds(state);
return (state < names.Length ? names[state] : null) ?? state.ToString();
}

/// <summary>
/// Sets the name of the state with the given index.
/// These names are for display purposes by mods only.
/// </summary>
/// <param name="state">The index of the state.</param>
/// <param name="name">The new display name it should use.</param>
public void SetStateName(int state, string name) {
CheckBounds(state);
if (state >= names.Length)
Array.Resize(ref names, state + 1);
names[state] = name;
}

/// <summary>
/// Returns the name of the current state, which defaults to the index in string form.
/// These names are for display purposes by mods only.
/// </summary>
/// <returns>The display name of the current state.</returns>
public string GetCurrentStateName() => GetStateName(State);
}
}
69 changes: 64 additions & 5 deletions Celeste.Mod.mm/Patches/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,41 @@ public patch_Player(Vector2 position, PlayerSpriteMode spriteMode)

[MonoModIgnore]
[MonoModConstructor]
[PatchPlayerCtorOnFrameChange]
[PatchPlayerCtor]
public extern void ctor(Vector2 position, PlayerSpriteMode spriteMode);

private void PostCtor() {
// setup vanilla state names
((patch_StateMachine) StateMachine).SetStateName(StNormal, "Normal");
((patch_StateMachine) StateMachine).SetStateName(StClimb, "Climb");
((patch_StateMachine) StateMachine).SetStateName(StDash, "Dash");
((patch_StateMachine) StateMachine).SetStateName(StSwim, "Swim");
((patch_StateMachine) StateMachine).SetStateName(StBoost, "Boost");
((patch_StateMachine) StateMachine).SetStateName(StRedDash, "RedDash");
((patch_StateMachine) StateMachine).SetStateName(StHitSquash, "HitSquash");
((patch_StateMachine) StateMachine).SetStateName(StLaunch, "Launch");
((patch_StateMachine) StateMachine).SetStateName(StPickup, "Pickup");
((patch_StateMachine) StateMachine).SetStateName(StDreamDash, "DreamDash");
((patch_StateMachine) StateMachine).SetStateName(StSummitLaunch, "SummitLaunch");
((patch_StateMachine) StateMachine).SetStateName(StDummy, "Dummy");
((patch_StateMachine) StateMachine).SetStateName(StIntroWalk, "IntroWalk");
((patch_StateMachine) StateMachine).SetStateName(StIntroJump, "IntroJump");
((patch_StateMachine) StateMachine).SetStateName(StIntroRespawn, "IntroRespawn");
((patch_StateMachine) StateMachine).SetStateName(StIntroWakeUp, "IntroWakeUp");
((patch_StateMachine) StateMachine).SetStateName(StBirdDashTutorial, "BirdDashTutorial");
((patch_StateMachine) StateMachine).SetStateName(StFrozen, "Frozen");
((patch_StateMachine) StateMachine).SetStateName(StReflectionFall, "ReflectionFall");
((patch_StateMachine) StateMachine).SetStateName(StStarFly, "StarFly");
((patch_StateMachine) StateMachine).SetStateName(StTempleFall, "TempleFall");
((patch_StateMachine) StateMachine).SetStateName(StCassetteFly, "CassetteFly");
((patch_StateMachine) StateMachine).SetStateName(StAttract, "Attract");
((patch_StateMachine) StateMachine).SetStateName(StIntroMoonJump, "IntroMoonJump");
((patch_StateMachine) StateMachine).SetStateName(StFlingBird, "FlingBird");
((patch_StateMachine) StateMachine).SetStateName(StIntroThinkForABit, "IntroThinkForABit");
// then allow mods to register new ones
Everest.Events.Player.RegisterStates(this);
}

public extern void orig_Added(Scene scene);
public override void Added(Scene scene) {
if (OverrideIntroType != null) {
Expand Down Expand Up @@ -180,6 +212,21 @@ private Color GetTrailColor(bool wasDashB) {
return wasDashB ? NormalBadelineHairColor : UsedBadelineHairColor;
return wasDashB ? NormalHairColor : UsedHairColor;
}

/// <summary>
/// Adds a new state to this player with the given behaviour, and returns the index of the new state.
///
/// States should always be added at the end of the <c>Player</c> constructor.
/// </summary>
/// <param name="name">The name of this state, for display purposes by mods only.</param>
/// <param name="onUpdate">A function to run every frame during this state, returning the index of the state that should be switched to next frame.</param>
/// <param name="coroutine">A function that creates a coroutine to run when this state is switched to.</param>
/// <param name="begin">An action to run when this state is switched to.</param>
/// <param name="end">An action to run when this state ends.</param>
/// <returns>The index of the new state.</returns>
public int AddState(string name, Func<Player, int> onUpdate, Func<Player, IEnumerator> coroutine = null, Action<Player> begin = null, Action<Player> end = null){
return ((patch_StateMachine)StateMachine).AddState(name, onUpdate, coroutine, begin, end);
}

public Vector2 ExplodeLaunch(Vector2 from, bool snapUp = true) {
return ExplodeLaunch(from, snapUp, false);
Expand Down Expand Up @@ -295,10 +342,11 @@ class PatchPlayerClimbBegin : Attribute { }
class PatchPlayerOrigWallJumpAttribute : Attribute { }

/// <summary>
/// Patches the method to un-hardcode the FMOD event string used to play the footstep and grab sound effect.
/// Patches the method to un-hardcode the FMOD event string used to play the footstep and grab sound effect,
/// and handle player state management.
/// </summary>
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerCtorOnFrameChange))]
class PatchPlayerCtorOnFrameChangeAttribute : Attribute { }
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchPlayerCtor))]
class PatchPlayerCtorAttribute : Attribute { }

/// <summary>
/// Patches the method to fix puffer boosts breaking on respawn.
Expand Down Expand Up @@ -447,7 +495,18 @@ public static void PatchPlayerOrigWallJump(ILContext context, CustomAttribute at
cursor.Remove();
}

public static void PatchPlayerCtorOnFrameChange(MethodDefinition method, CustomAttribute attrib) {
public static void PatchPlayerCtor(MethodDefinition method, CustomAttribute attrib) {
// We need to run player state management code just after the constructor, but can't use regular hooking
// as many mods IL hook the constructor already.
new ILContext(method).Invoke(il => {
MethodDefinition m_Player_PostCtor = il.Module.GetType("Celeste.Player").FindMethod("PostCtor");
ILCursor cursor = new ILCursor(il);
cursor.Index = -1;
cursor.Emit(OpCodes.Ldarg_0);
cursor.Emit(OpCodes.Callvirt, m_Player_PostCtor);
});

// then hook another method given the context available.
method = method.DeclaringType.FindMethod("<.ctor>b__280_1");

new ILContext(method).Invoke(il => {
Expand Down
Loading

0 comments on commit efdca51

Please sign in to comment.