diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs
index 55d5dfacd..eac6933d1 100644
--- a/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs
+++ b/Celeste.Mod.mm/Mod/Everest/Everest.Events.cs
@@ -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;
@@ -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 {
@@ -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);
+ ///
+ /// Called before skipping a cutscene.
+ ///
+ public static SkipCutsceneHandler OnSkipCutscene;
+ internal static void SkipCutscene(_Level level) => OnSkipCutscene?.Invoke(level);
+
public delegate void CreatePauseMenuButtonsHandler(_Level level, patch_TextMenu menu, bool minimal);
///
/// Called when the Level's pause menu is created.
@@ -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 args, Stack colorStack, _FancyText.Portrait[] lastPortrait);
+ public static event ParseCustomCommandHandler OnParseCustomCommand;
+ internal static bool ParseCustomCommand(_FancyText fancyText, string command, List args, Stack 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> AddCustomEventsHandler(_Textbox textbox, string dialog, Language language);
+ public static event AddCustomEventsHandler OnAddCustomEvents;
+ internal static List> AddCustomEvents(_Textbox textbox, string dialog, Language language) {
+ List> 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> events)
+ extraEvents.AddRange(events);
+ }
+
+ return extraEvents;
+ }
+ }
}
}
}
diff --git a/Celeste.Mod.mm/Patches/FancyText.cs b/Celeste.Mod.mm/Patches/FancyText.cs
index d317fcbe3..7d1eefcdb 100644
--- a/Celeste.Mod.mm/Patches/FancyText.cs
+++ b/Celeste.Mod.mm/Patches/FancyText.cs
@@ -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 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);
@@ -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 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 args, Stack 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 {
+ ///
+ /// A patch for FancyText parsing, allowing mods to register custom commands.
+ ///
+ [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchFancyTextParse))]
+ class PatchFancyTextParse : Attribute { }
+
+ ///
+ /// A patch for FancyText word-adding, allowing mods to manipulate each word.
+ ///
+ [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");
+ VariableDefinition v_colorStack = context.Body.Variables.First(v => v.VariableType.FullName.StartsWith("System.Collections.Generic.Stack`1"));
+ 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::Add
+
+ cursor.GotoNext(MoveType.Before,
+ instr => instr.MatchCallOrCallvirt(out var method)
+ && method.DeclaringType.FullName == "System.Collections.Generic.List`1"
+ && 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);
+ }
}
}
diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs
index b32cfaf6e..bde3446bd 100644
--- a/Celeste.Mod.mm/Patches/Level.cs
+++ b/Celeste.Mod.mm/Patches/Level.cs
@@ -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);
diff --git a/Celeste.Mod.mm/Patches/Textbox.cs b/Celeste.Mod.mm/Patches/Textbox.cs
new file mode 100644
index 000000000..7ffcbdf7f
--- /dev/null
+++ b/Celeste.Mod.mm/Patches/Textbox.cs
@@ -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[] events;
+
+ public patch_Textbox(string dialog, Language language, params Func[] 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[] events);
+ [MonoModConstructor]
+ public void ctor(string dialog, Language language, params Func[] 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];
+ }
+ }
+
+ }
+}