Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Celeste.Mod.mm/Mod/Everest/Everest.Events.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Celeste.Mod.UI;
using Microsoft.Xna.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using _Decal = Celeste.Decal;
using _EventTrigger = Celeste.EventTrigger;
Expand All @@ -13,6 +14,8 @@
using _Seeker = Celeste.Seeker;
using _AngryOshiro = Celeste.AngryOshiro;
using _SubHudRenderer = Celeste.Mod.UI.SubHudRenderer;
using _FancyText = Celeste.FancyText;
using _Textbox = Celeste.Textbox;
using Monocle;

namespace Celeste.Mod {
Expand Down Expand Up @@ -133,6 +136,13 @@ internal static void Pause(_Level level, int startIndex, bool minimal, bool quic
public static event UnpauseHandler OnUnpause;
internal static void Unpause(_Level level) => OnUnpause?.Invoke(level);

public delegate void SkipCutsceneHandler(_Level level);
/// <summary>
/// Called before skipping a cutscene.
/// </summary>
public static SkipCutsceneHandler OnSkipCutscene;
internal static void SkipCutscene(_Level level) => OnSkipCutscene?.Invoke(level);

public delegate void CreatePauseMenuButtonsHandler(_Level level, patch_TextMenu menu, bool minimal);
/// <summary>
/// Called when the Level's pause menu is created.
Expand Down Expand Up @@ -382,6 +392,57 @@ public static class SubHudRenderer {
internal static void BeforeRender(_SubHudRenderer renderer, Scene scene)
=> OnBeforeRender?.Invoke(renderer, scene);
}

public static class FancyText {
public delegate bool ParseCustomCommandHandler(_FancyText fancyText, string command, List<string> args, Stack<Color> colorStack, _FancyText.Portrait[] lastPortrait);
public static event ParseCustomCommandHandler OnParseCustomCommand;
internal static bool ParseCustomCommand(_FancyText fancyText, string command, List<string> args, Stack<Color> colorStack, _FancyText.Portrait[] lastPortrait)
=> OnParseCustomCommand?.InvokeWhileFalse(fancyText, command, args, colorStack, lastPortrait) ?? false;

public delegate void BeforeParseHandler(_FancyText fancyText);
public static event BeforeParseHandler OnBeforeParse;
internal static void BeforeParse(_FancyText fancyText)
=> OnBeforeParse?.Invoke(fancyText);

public delegate void AfterParseHandler(_FancyText fancyText);
public static event AfterParseHandler OnAfterParse;
internal static void AfterParse(_FancyText fancyText)
=> OnAfterParse?.Invoke(fancyText);

public delegate void WordAddedHandler(_FancyText fancyText, string word, int[] codepoints, List<_FancyText.Char> chars);
public static event WordAddedHandler OnWordAdded;
internal static void WordAdded(_FancyText fancyText, string word, int[] codepoints, List<_FancyText.Char> chars)
=> OnWordAdded?.Invoke(fancyText, word, codepoints, chars);

public delegate void BeforeDrawHandler(_FancyText fancyText, Vector2 position, Vector2 justify, Vector2 scale, float alpha, int start, int end);
public static event BeforeDrawHandler OnBeforeDraw;
internal static void BeforeDraw(_FancyText fancyText, Vector2 position, Vector2 justify, Vector2 scale, float alpha, int start, int end)
=> OnBeforeDraw?.Invoke(fancyText, position, justify, scale, alpha, start, end);

public delegate void AfterDrawHandler(_FancyText fancyText, Vector2 position, Vector2 justify, Vector2 scale, float alpha, int start, int end);
public static event AfterDrawHandler OnAfterDraw;
internal static void AfterDraw(_FancyText fancyText, Vector2 position, Vector2 justify, Vector2 scale, float alpha, int start, int end)
=> OnAfterDraw?.Invoke(fancyText, position, justify, scale, alpha, start, end);
}

public static class Textbox {
public delegate IEnumerable<Func<IEnumerator>> AddCustomEventsHandler(_Textbox textbox, string dialog, Language language);
public static event AddCustomEventsHandler OnAddCustomEvents;
internal static List<Func<IEnumerator>> AddCustomEvents(_Textbox textbox, string dialog, Language language) {
List<Func<IEnumerator>> extraEvents = new();

if (OnAddCustomEvents is null)
return extraEvents;

foreach (Delegate del in OnAddCustomEvents.GetInvocationList()) {
var res = del.DynamicInvoke(new object[] {textbox, dialog, language});
if (res is IEnumerable<Func<IEnumerator>> events)
extraEvents.AddRange(events);
}

return extraEvents;
}
}
}
}
}
212 changes: 211 additions & 1 deletion Celeste.Mod.mm/Patches/FancyText.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
#pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Celeste.Mod;
using Celeste.Mod.Helpers;
using Microsoft.Xna.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Monocle;
using MonoMod;
using MonoMod.Cil;
using MonoMod.InlineRT;
using MonoMod.Utils;

namespace Celeste {
// The FancyText ctor is private.
// The FancyText ctor is private, so this cannot inherit from it.
class patch_FancyText {

// helper class because manipulating generic types in IL is annoying
internal class ListChar {
public List<FancyText.Char> elements = new();
public static void Add(FancyText.Char c, ListChar self) => self.elements.Add(c);
}

[PatchTextIteration]
[PatchFancyTextAddWord]
private extern void orig_AddWord(string word);
private void AddWord(string word) {
word = Emoji.Apply(word);
Expand All @@ -33,5 +50,198 @@ public class patch_Char : FancyText.Char {
}
}

[MonoModIgnore] // We don't want to change anything about the method...
[PatchFancyTextParse] // ... except for manually manipulating the method via MonoModRules
private extern FancyText.Text Parse();

private static Color ApplyOpacityArgument(Color col, List<string> args) {
if (args.Count > 0 && float.TryParse(args[0], NumberStyles.Float, CultureInfo.InvariantCulture, out float opacity))
return col * opacity;
return col;
}

private void ParseCustomCommand(string command, List<string> args, Stack<Color> colorStack, FancyText.Portrait[] lastPortrait) {
if (Everest.Events.FancyText.ParseCustomCommand((FancyText)(object)this, command, args, colorStack, lastPortrait))
return;

Logger.Warn("EventTrigger", $"FancyText command '{command}' does not exist!");
}

private void BeforeParse() => Everest.Events.FancyText.BeforeParse((FancyText)(object)this);
private void AfterParse() => Everest.Events.FancyText.AfterParse((FancyText)(object)this);

private void WordAdded(string word, UnicodeStringHelper.ListInt codepoints, ListChar chars)
=> Everest.Events.FancyText.WordAdded((FancyText)(object)this, word, codepoints.Elements, chars.elements);

public extern void orig_Draw(Vector2 position, Vector2 justify, Vector2 scale, float alpha, int start, int end);
public void Draw(Vector2 position, Vector2 justify, Vector2 scale, float alpha, int start = 0, int end = int.MaxValue) {
Everest.Events.FancyText.BeforeDraw((FancyText)(object)this, position, justify, scale, alpha, start, end);
orig_Draw(position, justify, scale, alpha, start, end);
Everest.Events.FancyText.AfterDraw((FancyText)(object)this, position, justify, scale, alpha, start, end);
}
}
}

namespace MonoMod {
/// <summary>
/// A patch for FancyText parsing, allowing mods to register custom commands.
/// </summary>
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchFancyTextParse))]
class PatchFancyTextParse : Attribute { }

/// <summary>
/// A patch for FancyText word-adding, allowing mods to manipulate each word.
/// </summary>
[MonoModCustomMethodAttribute(nameof(MonoModRules.PatchFancyTextAddWord))]
class PatchFancyTextAddWord : Attribute { }

static partial class MonoModRules {

public static void PatchFancyTextParse(ILContext context, CustomAttribute attrib) {
TypeDefinition t_FancyText = MonoModRule.Modder.FindType("Celeste.FancyText").Resolve();
TypeDefinition t_Calc = MonoModRule.Modder.FindType("Monocle.Calc").Resolve();

MethodDefinition m_BeforeParse = t_FancyText.FindMethod("BeforeParse");
MethodDefinition m_FancyText_ParseCustomCommand = t_FancyText.FindMethod("ParseCustomCommand");
MethodDefinition m_FancyText_ApplyOpacityArgument= t_FancyText.FindMethod("ApplyOpacityArgument");
MethodDefinition m_AfterParse = t_FancyText.FindMethod("AfterParse");
MethodDefinition m_Calc_HexToColorWithAlpha = t_Calc.FindMethod("HexToColorWithAlpha");

VariableDefinition v_command = context.Body.Variables.First(v => v.VariableType.FullName == "System.String");
VariableDefinition v_args = context.Body.Variables.First(v => v.VariableType.FullName == "System.Collections.Generic.List`1<System.String>");
VariableDefinition v_colorStack = context.Body.Variables.First(v => v.VariableType.FullName.StartsWith("System.Collections.Generic.Stack`1<Microsoft.Xna.Framework.Color>"));
VariableDefinition v_lastPortrait = context.Body.Variables.First(v => v.VariableType.FullName == "Celeste.FancyText/Portrait[]");

ILCursor cursor = new ILCursor(context);

// + ldarg.0
// + call Events.FancyText.BeforeParse

cursor.EmitLdarg0();
cursor.EmitCall(m_BeforeParse);

// ... (getting color from {#hex})
// - call Calc::HexToColor
// + call Calc::HexToColorWithAlpha
// + ldloc.8 (args)
// + call FancyText::ApplyOpacityArgument
// stfld valuetype Color FancyText::currentColor

cursor.GotoNext(MoveType.After,
instr => instr.MatchCallOrCallvirt("Monocle.Calc", "HexToColor"));

cursor.Previous.Operand = m_Calc_HexToColorWithAlpha;
cursor.EmitLdloc(v_args);
cursor.EmitCall(m_FancyText_ApplyOpacityArgument);

// ldstr "savedata"
// callvirt System.Boolean System.String::Equals(System.String)
// - brfalse continue
//
// + brtrue savedata
// + ldarg.0
// + ldloc.7 (command)
// + ldloc.8 (args)
// + ldloc.3 (stack)
// + ldloc.4 (lastPortrait)
// + call FancyText.ParseCustomCommand(this, command, args, colorStack, lastPortrait)
// + br continue
//
// + savedata:
// (code for handling {savedata})
// continue:

ILLabel label_continue = null;
ILLabel label_savedata = cursor.DefineLabel();

cursor.GotoNext(MoveType.After,
instr => instr.MatchLdstr("savedata"),
instr => instr.MatchCallOrCallvirt(out var _),
instr => instr.MatchBrfalse(out label_continue));

cursor.Index--;
cursor.Remove();

cursor.EmitBrtrue(label_savedata);
cursor.EmitLdarg0();
cursor.EmitLdloc(v_command);
cursor.EmitLdloc(v_args);
cursor.EmitLdloc(v_colorStack);
cursor.EmitLdloc(v_lastPortrait);
cursor.EmitCall(m_FancyText_ParseCustomCommand);
cursor.EmitBr(label_continue);

cursor.MarkLabel(label_savedata);

// + ldarg.0
// + call Events.FancyText.AfterParse
// ldarg.0
// ldfld FancyText::group
// ret

cursor.GotoNext(MoveType.Before,
instr => instr.MatchLdarg0(),
instr => instr.MatchLdfld(out var _),
instr => instr.MatchRet());

cursor.EmitLdarg0();
cursor.EmitCall(m_AfterParse);
}

public static void PatchFancyTextAddWord(ILContext context, CustomAttribute attrib) {
TypeDefinition t_FancyText = MonoModRule.Modder.FindType("Celeste.FancyText").Resolve();
TypeDefinition t_ListChar = t_FancyText.NestedTypes.First(t => t.Name == "ListChar");

MethodDefinition m_FancyText_WordAdded = t_FancyText.FindMethod("WordAdded");
MethodDefinition m_ListChar_ctor = t_ListChar.FindMethod(".ctor", true);
MethodDefinition m_ListChar_add = t_ListChar.FindMethod("Add");

VariableDefinition v_listOfCodePoints = context.Body.Variables.First(v => v.VariableType.FullName == "Celeste.Mod.Helpers.UnicodeStringHelper/ListInt");
VariableDefinition v_chars = new VariableDefinition(t_ListChar);
context.Body.Variables.Add(v_chars);

ILCursor cursor = new ILCursor(context);

// ... (creating the codepoint list)
// + newobj FancyText/ListChar
// + stloc.7 (chars)

cursor.GotoNext(MoveType.After,
instr => instr.MatchStloc(out int loc) && loc == v_listOfCodePoints.Index);

cursor.EmitNewobj(m_ListChar_ctor);
cursor.EmitStloc(v_chars);

// ... (creating this Char)
// + dup
// + ldloc.7 (chars)
// + call FancyText/ListChar::Add
// callvirt List<FancyText/Node>::Add

cursor.GotoNext(MoveType.Before,
instr => instr.MatchCallOrCallvirt(out var method)
&& method.DeclaringType.FullName == "System.Collections.Generic.List`1<Celeste.FancyText/Node>"
&& method.Name == "Add");

cursor.EmitDup();
cursor.EmitLdloc(v_chars);
cursor.EmitCall(m_ListChar_add);

// + ldarg.0
// + ldarg.1 (word)
// + ldloc.6 (listOfCodePoints)
// + ldloc.7 (chars)
// + call FancyText/WordAdded
// ret

cursor.GotoNext(MoveType.Before,
instr => instr.MatchRet());

cursor.EmitLdarg0();
cursor.EmitLdarg1();
cursor.EmitLdloc(v_listOfCodePoints);
cursor.EmitLdloc(v_chars);
cursor.EmitCall(m_FancyText_WordAdded);
}
}
}
6 changes: 6 additions & 0 deletions Celeste.Mod.mm/Patches/Level.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ public void Unpause() {
}
}

public extern void orig_SkipCutscene();
public new void SkipCutscene() {
Everest.Events.Level.SkipCutscene(this);
orig_SkipCutscene();
}

public extern void orig_TransitionTo(LevelData next, Vector2 direction);
public new void TransitionTo(LevelData next, Vector2 direction) {
orig_TransitionTo(next, direction);
Expand Down
36 changes: 36 additions & 0 deletions Celeste.Mod.mm/Patches/Textbox.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#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

using System;
using System.Collections;
using Celeste.Mod;
using MonoMod;

namespace Celeste {
class patch_Textbox : Textbox {

// We're effectively in Textbox, but still need to "expose" private fields to our mod.
private Func<IEnumerator>[] events;

public patch_Textbox(string dialog, Language language, params Func<IEnumerator>[] events)
: base(dialog, language, events) {
// no-op. MonoMod ignores this - we only need this to make the compiler shut up.
}

public extern void orig_ctor(string dialog, Language language, params Func<IEnumerator>[] events);
[MonoModConstructor]
public void ctor(string dialog, Language language, params Func<IEnumerator>[] events) {
orig_ctor(dialog, language, events);

var extraEvents = Everest.Events.Textbox.AddCustomEvents(this, dialog, language);
if (extraEvents.Count > 0) {
int initialCount = events.Length;
Array.Resize(ref events, initialCount + extraEvents.Count);

for (int i = 0; i < extraEvents.Count; i++)
events[initialCount + i] = extraEvents[i];
}
}

}
}