Skip to content

Commit

Permalink
fix crashes, add type-safe methods for Player/Seeker/Oshiro states
Browse files Browse the repository at this point in the history
  • Loading branch information
l-Luna committed Apr 29, 2023
1 parent 3a96497 commit 39f3907
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 26 deletions.
32 changes: 32 additions & 0 deletions Celeste.Mod.mm/Patches/AngryOshiro.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Xna.Framework;
using Monocle;
using System;
using System.Collections;

namespace Celeste.Patches {
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.
}

/// <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 at the end of the <c>AngryOshiro(Vector2, bool)</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<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);
}
}
}
102 changes: 76 additions & 26 deletions Celeste.Mod.mm/Patches/Monocle/StateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,94 @@ public void ctor(int maxStates = 10) {

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

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

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;
}

private extern void orig_SetCallbacks(int state, Func<int> onUpdate, Func<IEnumerator> coroutine = null, Action begin = null, Action end = null);
public void SetCallbacks(
int state,
Func<int> onUpdate,
Func<IEnumerator> coroutine = null,
Action begin = null,
Action end = null) {
orig_SetCallbacks(state, onUpdate, coroutine, begin, end);
names[state] ??= state.ToString();

/// <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 state to this StateMachine.
/// 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(string name, Func<Entity, int> onUpdate, Func<Entity, IEnumerator> coroutine = null, Action<Entity> begin = null, Action<Entity> end = null){
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),
coroutine == null ? null : () => coroutine(Entity),
begin == null ? null : () => begin(Entity),
end == null ? null : () => end(Entity));
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;
}

public string GetStateName(int state) => names[state];
public void SetStateName(int state, string name) => names[state] = name;

public string GetCurrentStateName() => names[State];
// 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);
}
}
16 changes: 16 additions & 0 deletions Celeste.Mod.mm/Patches/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using MonoMod.Cil;
using MonoMod.InlineRT;
using MonoMod.Utils;
using System.Collections;

namespace Celeste {
class patch_Player : Player {
Expand Down Expand Up @@ -179,6 +180,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
33 changes: 33 additions & 0 deletions Celeste.Mod.mm/Patches/Seeker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.Xna.Framework;
using Monocle;
using System;
using System.Collections;

namespace Celeste.Patches {
public class patch_Seeker : Seeker {

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

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

/// <summary>
/// Adds a new state to this seeker with the given behaviour, and returns the index of the new state.
///
/// States should always be added at the end of the <c>Seeker(Vector2, Vector2[])</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<Seeker, int> onUpdate, Func<Seeker, IEnumerator> coroutine = null, Action<Seeker> begin = null, Action<Seeker> end = null){
return ((patch_StateMachine)State).AddState(name, onUpdate, coroutine, begin, end);
}
}
}

0 comments on commit 39f3907

Please sign in to comment.