From a235725099ddad35737f09022d3e11853131b772 Mon Sep 17 00:00:00 2001 From: Kongtiao Date: Thu, 7 May 2026 20:56:43 +0800 Subject: [PATCH 1/6] Added text color and text outline options for the text overlay --- ConsistencyTracker.csproj | 2 +- ConsistencyTrackerSettings.cs | 78 +++++++++++++++++++++++++++- Dialog/English.txt | 6 +++ Entities/Menu/ColorOption.cs | 95 +++++++++++++++++++++++++++++++++++ Entities/StatTextComponent.cs | 47 ++++++++--------- Entities/TextOverlay.cs | 25 ++++++--- 6 files changed, 221 insertions(+), 32 deletions(-) create mode 100644 Entities/Menu/ColorOption.cs diff --git a/ConsistencyTracker.csproj b/ConsistencyTracker.csproj index 961488f..cf841ec 100644 --- a/ConsistencyTracker.csproj +++ b/ConsistencyTracker.csproj @@ -16,7 +16,7 @@ true $(CelestePrefix)\legacyRef - XNA + XNA FNA $(WINDIR)\Microsoft.NET\assembly\GAC_32\{0}\v4.0_4.0.0.0__842cf8be1de50553\{0}.dll diff --git a/ConsistencyTrackerSettings.cs b/ConsistencyTrackerSettings.cs index 18d318a..8f04116 100644 --- a/ConsistencyTrackerSettings.cs +++ b/ConsistencyTrackerSettings.cs @@ -1189,6 +1189,12 @@ public void CreateExternalOverlayEntry(TextMenu menu, bool inGame) { [SettingIgnore] public bool IngameOverlayText1Enabled { get; set; } = true; + [SettingIgnore] + public bool IngameOverlayText1Outline { get; set; } = true; + + [SettingIgnore] + public Color IngameOverlayText1Color { get; set; } = Color.White; + [SettingIgnore] public StatTextPosition IngameOverlayText1Position { get; set; } = StatTextPosition.TopLeft; @@ -1215,6 +1221,12 @@ public void CreateExternalOverlayEntry(TextMenu menu, bool inGame) { [SettingIgnore] public bool IngameOverlayText2Enabled { get; set; } = true; + [SettingIgnore] + public bool IngameOverlayText2Outline { get; set; } = true; + + [SettingIgnore] + public Color IngameOverlayText2Color { get; set; } = Color.White; + [SettingIgnore] public StatTextPosition IngameOverlayText2Position { get; set; } = StatTextPosition.TopRight; @@ -1241,6 +1253,12 @@ public void CreateExternalOverlayEntry(TextMenu menu, bool inGame) { [SettingIgnore] public bool IngameOverlayText3Enabled { get; set; } = false; + [SettingIgnore] + public bool IngameOverlayText3Outline { get; set; } = true; + + [SettingIgnore] + public Color IngameOverlayText3Color { get; set; } = Color.White; + [SettingIgnore] public StatTextPosition IngameOverlayText3Position { get; set; } = StatTextPosition.BottomLeft; @@ -1267,6 +1285,12 @@ public void CreateExternalOverlayEntry(TextMenu menu, bool inGame) { [SettingIgnore] public bool IngameOverlayText4Enabled { get; set; } = false; + [SettingIgnore] + public bool IngameOverlayText4Outline { get; set; } = true; + + [SettingIgnore] + public Color IngameOverlayText4Color { get; set; } = Color.White; + [SettingIgnore] public StatTextPosition IngameOverlayText4Position { get; set; } = StatTextPosition.BottomRight; @@ -1448,7 +1472,7 @@ public void CreateIngameOverlayEntry(TextMenu menu, bool inGame) { string descAvailableFormats = $"{Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_FORMAT_EDIT_HINT")} 'Celeste/ConsistencyTracker/{StatManager.BaseFolder}/{StatManager.FormatFileName}'"; string descAvailableFormatsGolden = $"{Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_FORMAT_GOLDEN_HINT_1")} '{noneFormat}' {Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_FORMAT_GOLDEN_HINT_2")}"; string descHideWithGolden = Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_FORMAT_GOLDEN_HINT_0"); - + string descColorOption = Dialog.Clean("MODOPTION_CCT_COLOR_OPTION_DESCRIPTION"); bool hasStats = Mod.CurrentChapterStats != null; bool holdingGolden = Mod.CurrentChapterStats.ModState.PlayerIsHoldingGolden; @@ -1490,6 +1514,19 @@ public void CreateIngameOverlayEntry(TextMenu menu, bool inGame) { } }); subMenu.AddDescription(menu, menuItem, descHideWithGolden); + subMenu.Add(new TextMenu.OnOff(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_OUTLINE"), IngameOverlayText1Outline) { + OnValueChange = v => { + IngameOverlayText1Outline = v; + Mod.IngameOverlay.SetTextOutline(1, v); + } + }); + subMenu.Add(menuItem = new ColorOption(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_COLOR"), IngameOverlayText1Color) { + OnValueChange = v => { + IngameOverlayText1Color = v; + Mod.IngameOverlay.SetTextColor(1, v); + } + }); + subMenu.AddDescription(menu, menuItem, descColorOption); subMenu.Add(menuItem = new TextMenuExt.EnumerableSlider(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_SIZE"), PercentageSlider(5, 5, 500), IngameOverlayText1Size) { OnValueChange = (value) => { IngameOverlayText1Size = value; @@ -1547,6 +1584,19 @@ public void CreateIngameOverlayEntry(TextMenu menu, bool inGame) { } }); subMenu.AddDescription(menu, menuItem, descHideWithGolden); + subMenu.Add(new TextMenu.OnOff(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_OUTLINE"), IngameOverlayText2Outline) { + OnValueChange = v => { + IngameOverlayText2Outline = v; + Mod.IngameOverlay.SetTextOutline(2, v); + } + }); + subMenu.Add(menuItem = new ColorOption(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_COLOR"), IngameOverlayText2Color) { + OnValueChange = v => { + IngameOverlayText2Color = v; + Mod.IngameOverlay.SetTextColor(2, v); + } + }); + subMenu.AddDescription(menu, menuItem, descColorOption); subMenu.Add(menuItem = new TextMenuExt.EnumerableSlider(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_SIZE"), PercentageSlider(5, 5, 500), IngameOverlayText2Size) { OnValueChange = (value) => { IngameOverlayText2Size = value; @@ -1603,6 +1653,19 @@ public void CreateIngameOverlayEntry(TextMenu menu, bool inGame) { } }); subMenu.AddDescription(menu, menuItem, descHideWithGolden); + subMenu.Add(new TextMenu.OnOff(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_OUTLINE"), IngameOverlayText3Outline) { + OnValueChange = v => { + IngameOverlayText3Outline = v; + Mod.IngameOverlay.SetTextOutline(3, v); + } + }); + subMenu.Add(menuItem = new ColorOption(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_COLOR"), IngameOverlayText3Color) { + OnValueChange = v => { + IngameOverlayText3Color = v; + Mod.IngameOverlay.SetTextColor(3, v); + } + }); + subMenu.AddDescription(menu, menuItem, descColorOption); subMenu.Add(menuItem = new TextMenuExt.EnumerableSlider(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_SIZE"), PercentageSlider(5, 5, 500), IngameOverlayText3Size) { OnValueChange = (value) => { IngameOverlayText3Size = value; @@ -1660,6 +1723,19 @@ public void CreateIngameOverlayEntry(TextMenu menu, bool inGame) { } }); subMenu.AddDescription(menu, menuItem, descHideWithGolden); + subMenu.Add(new TextMenu.OnOff(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_OUTLINE"), IngameOverlayText4Outline) { + OnValueChange = v => { + IngameOverlayText4Outline = v; + Mod.IngameOverlay.SetTextOutline(4, v); + } + }); + subMenu.Add(menuItem = new ColorOption(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_COLOR"), IngameOverlayText4Color) { + OnValueChange = v => { + IngameOverlayText4Color = v; + Mod.IngameOverlay.SetTextColor(4, v); + } + }); + subMenu.AddDescription(menu, menuItem, descColorOption); subMenu.Add(menuItem = new TextMenuExt.EnumerableSlider(Dialog.Clean("MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_SIZE"), PercentageSlider(5, 5, 500), IngameOverlayText4Size) { OnValueChange = (value) => { IngameOverlayText4Size = value; diff --git a/Dialog/English.txt b/Dialog/English.txt index 35e81d5..2d910d5 100644 --- a/Dialog/English.txt +++ b/Dialog/English.txt @@ -26,6 +26,8 @@ MODOPTION_CCT_NAME= Consistency Tracker +MODOPTION_CCT_COLOR_OPTION_DESCRIPTION= Press this button to import a color from clipboard! + MODOPTION_CCT_PAUSE_DEATH_TRACKING= Pause Death Tracking MODOPTION_CCT_TRACKING_SETTINGS_ON= On @@ -298,6 +300,8 @@ MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_SELECTED_FORMAT= Select MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_SELECTED_FORMAT_GOLDEN= Selected Format With Golden MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_HIDE_IN_GOLDEN= Hide In Golden Run MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_SIZE= Size +MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_OUTLINE= Text Outline +MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_COLOR= Text Color MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_OFFSET_X= Offset X MODOPTION_CCT_IN_GAME_OVERLAY_SETTINGS_TEXT_OVERLAY_TEXT_OFFSET_Y= Offset Y @@ -643,6 +647,8 @@ MODOPTION_CCT_FAQ_OTHER_LOVE_A= MODOPTION_CCT_FAQ_OTHER_HATE_Q= I hate this mod >:( + + # +--------------------+ # | Stats Format | # +--------------------+ diff --git a/Entities/Menu/ColorOption.cs b/Entities/Menu/ColorOption.cs new file mode 100644 index 0000000..fcc195f --- /dev/null +++ b/Entities/Menu/ColorOption.cs @@ -0,0 +1,95 @@ +using System; +using Celeste; +using Celeste.Mod; +using Celeste.Mod.ConsistencyTracker.Utility; +using System.Globalization; +using Microsoft.Xna.Framework; +using Monocle; +using static Celeste.TextMenu; + +public class ColorOption : Item { + + private static string ConfirmSfx = "event:/ui/main/button_select"; + + private string _Label; + + public string Label { get; set; } + + public Color Color { get; set; } + + public Action OnValueChange { get; set; } + + public ColorOption(string label, Color color) { + Selectable = true; + Color = color; + _Label = label; + + Label = GetLabel(_Label, Color); + } + + public override float LeftWidth() { + return ActiveFont.Measure(Label).X; + } + + public override float Height() { + return ActiveFont.LineHeight; + } + + public override void ConfirmPressed() { + Audio.Play(ConfirmSfx); + + if (TryParseColor(TextInput.GetClipboardText(), out Color color)) { + Color = color; + Label = GetLabel(_Label, Color); + OnValueChange?.Invoke(color); + } + } + + public override void Render(Vector2 position, bool highlighted) { + // Stolen from Celeste.TextMenu + float alpha = Container.Alpha; + Color color = Disabled ? Color.DarkSlateGray : ((highlighted ? Container.HighlightColor : Color.White) * alpha); + Color strokeColor = Color.Black * (alpha * alpha * alpha); + ActiveFont.DrawOutline(Label, position, Vector2.UnitY / 2f, Vector2.One, color, 2f, strokeColor); + + // Render the color box + Vector2 measure = ActiveFont.Measure(Label); + Vector2 colorBoxPosition = DrawHelper.MoveCopy(position, measure.X + 10f, -20f); + Draw.Rect(colorBoxPosition, 40f, 40f, Color); + Draw.HollowRect(colorBoxPosition, 40f, 40f, Color.White); + } + + private static string GetLabel(string label, Color color) { + return $"{label}: {ColorToHex(color)}"; + } + + private static bool TryParseColor(string hex, out Color color) { + color = default; + + if (string.IsNullOrEmpty(hex)) { + return false; + } + + hex = hex.Replace("#", "").Trim(); + + if (hex.Length < 6) { + return false; + } + + bool flag = int.TryParse(hex.Substring(0, 2), NumberStyles.HexNumber, null, out int r); + bool flag1 = int.TryParse(hex.Substring(2, 2), NumberStyles.HexNumber, null, out int g); + bool flag2 = int.TryParse(hex.Substring(4, 2), NumberStyles.HexNumber, null, out int b); + + if (flag && flag1 && flag2) { + color = new Color(r, g, b); + return true; + } + + return false; + } + + private static string ColorToHex(Color color) { + return $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + } + +} \ No newline at end of file diff --git a/Entities/StatTextComponent.cs b/Entities/StatTextComponent.cs index 9c8704a..f772196 100644 --- a/Entities/StatTextComponent.cs +++ b/Entities/StatTextComponent.cs @@ -16,14 +16,9 @@ public class StatTextComponent : Component { public bool OptionVisible { get; set; } public bool HideInGolden { get; set; } public float Scale { get; set; } = 1f; - public float Alpha { - get => _Alpha; - set { - _Alpha = value; - UpdateColor(); - } - } - private float _Alpha { get; set; } = 1f; + + public bool TextOutline { get; set; } = true; + public PixelFont Font { get; set; } public float FontFaceSize { get; set; } public Color TextColor { get; set; } = Color.White; @@ -112,24 +107,30 @@ public void SetPosition(StatTextPosition pos) { } } - private void UpdateColor() { - TextColor = new Color(1f, 1f, 1f, Alpha); - StrokeColor = new Color(0f, 0f, 0f, Alpha); - } - public override void Render() { base.Render(); - Font.DrawOutline( - FontFaceSize, - Text, - new Vector2(PosX, PosY), - Justify, - Vector2.One * Scale, - TextColor, - StrokeSize, - StrokeColor - ); + if (TextOutline) { + Font.DrawOutline( + FontFaceSize, + Text, + new Vector2(PosX, PosY), + Justify, + Vector2.One * Scale, + TextColor, + StrokeSize, + StrokeColor + ); + } else { + Font.Draw( + FontFaceSize, + Text, + new Vector2(PosX, PosY), + Justify, + Vector2.One * Scale, + TextColor + ); + } if (DebugShowPosition) { Draw.Circle(new Vector2(PosX, PosY), 10, Color.Red, 10); diff --git a/Entities/TextOverlay.cs b/Entities/TextOverlay.cs index 3b8a47f..165d7e0 100644 --- a/Entities/TextOverlay.cs +++ b/Entities/TextOverlay.cs @@ -52,6 +52,8 @@ private void ApplyModSettings() { SetTextVisible(1, settings.IngameOverlayText1Enabled); SetTextHideInGolden(1, settings.IngameOverlayText1HideWithGolden); + SetTextOutline(1, settings.IngameOverlayText1Outline); + SetTextColor(1, settings.IngameOverlayText1Color); SetTextPosition(1, settings.IngameOverlayText1Position); SetTextOffsetX(1, settings.IngameOverlayText1OffsetX); SetTextOffsetY(1, settings.IngameOverlayText1OffsetY); @@ -59,6 +61,8 @@ private void ApplyModSettings() { SetTextVisible(2, settings.IngameOverlayText2Enabled); SetTextHideInGolden(2, settings.IngameOverlayText2HideWithGolden); + SetTextOutline(2, settings.IngameOverlayText2Outline); + SetTextColor(2, settings.IngameOverlayText2Color); SetTextPosition(2, settings.IngameOverlayText2Position); SetTextOffsetX(2, settings.IngameOverlayText2OffsetX); SetTextOffsetY(2, settings.IngameOverlayText2OffsetY); @@ -66,6 +70,8 @@ private void ApplyModSettings() { SetTextVisible(3, settings.IngameOverlayText3Enabled); SetTextHideInGolden(3, settings.IngameOverlayText3HideWithGolden); + SetTextOutline(3, settings.IngameOverlayText3Outline); + SetTextColor(3, settings.IngameOverlayText3Color); SetTextPosition(3, settings.IngameOverlayText3Position); SetTextOffsetX(3, settings.IngameOverlayText3OffsetX); SetTextOffsetY(3, settings.IngameOverlayText3OffsetY); @@ -73,6 +79,8 @@ private void ApplyModSettings() { SetTextVisible(4, settings.IngameOverlayText4Enabled); SetTextHideInGolden(4, settings.IngameOverlayText4HideWithGolden); + SetTextOutline(4, settings.IngameOverlayText4Outline); + SetTextColor(4, settings.IngameOverlayText4Color); SetTextPosition(4, settings.IngameOverlayText4Position); SetTextOffsetX(4, settings.IngameOverlayText4OffsetX); SetTextOffsetY(4, settings.IngameOverlayText4OffsetY); @@ -157,6 +165,16 @@ private void InitStatTextOptions() { StatText4.FontFaceSize = Dialog.Language.FontFaceSize; } + public void SetTextColor(int textNum, Color color) { + StatTextComponent statText = GetStatText(textNum); + statText.TextColor = color; + } + + public void SetTextOutline(int textNum, bool outline) { + StatTextComponent statText = GetStatText(textNum); + statText.TextOutline = outline; + } + public void SetTextVisible(int textNum, bool visible) { StatTextComponent statText = GetStatText(textNum); statText.OptionVisible = visible; @@ -190,13 +208,6 @@ public void SetTextSize(int textNum, int size) { StatTextComponent statText = GetStatText(textNum); statText.Scale = (float)size / 100; } - - //size in percent as int - public void SetTextAlpha(int textNum, int alpha) { - StatTextComponent statText = GetStatText(textNum); - statText.Alpha = (float)alpha / 100; - } - private StatTextComponent GetStatText(int textNum) { if (textNum == 1) { From 5043fbaa3aebefdecff59be13a4d8dd6bfe5fd8d Mon Sep 17 00:00:00 2001 From: Kongtiao Date: Mon, 11 May 2026 19:22:42 +0800 Subject: [PATCH 2/6] Add color tags that allow users to set colors for each line --- Entities/Menu/ColorOption.cs | 31 +--------- Entities/StatTextComponent.cs | 110 ++++++++++++++++++++++++++-------- Entities/TextOverlay.cs | 6 +- Utility/Util.cs | 32 ++++++++++ 4 files changed, 122 insertions(+), 57 deletions(-) diff --git a/Entities/Menu/ColorOption.cs b/Entities/Menu/ColorOption.cs index fcc195f..7ac4e18 100644 --- a/Entities/Menu/ColorOption.cs +++ b/Entities/Menu/ColorOption.cs @@ -38,7 +38,7 @@ public override float Height() { public override void ConfirmPressed() { Audio.Play(ConfirmSfx); - if (TryParseColor(TextInput.GetClipboardText(), out Color color)) { + if (Util.TryParseColor(TextInput.GetClipboardText(), out Color color)) { Color = color; Label = GetLabel(_Label, Color); OnValueChange?.Invoke(color); @@ -60,36 +60,9 @@ public override void Render(Vector2 position, bool highlighted) { } private static string GetLabel(string label, Color color) { - return $"{label}: {ColorToHex(color)}"; + return $"{label}: {Util.ColorToHex(color)}"; } - private static bool TryParseColor(string hex, out Color color) { - color = default; - if (string.IsNullOrEmpty(hex)) { - return false; - } - - hex = hex.Replace("#", "").Trim(); - - if (hex.Length < 6) { - return false; - } - - bool flag = int.TryParse(hex.Substring(0, 2), NumberStyles.HexNumber, null, out int r); - bool flag1 = int.TryParse(hex.Substring(2, 2), NumberStyles.HexNumber, null, out int g); - bool flag2 = int.TryParse(hex.Substring(4, 2), NumberStyles.HexNumber, null, out int b); - - if (flag && flag1 && flag2) { - color = new Color(r, g, b); - return true; - } - - return false; - } - - private static string ColorToHex(Color color) { - return $"#{color.R:X2}{color.G:X2}{color.B:X2}"; - } } \ No newline at end of file diff --git a/Entities/StatTextComponent.cs b/Entities/StatTextComponent.cs index f772196..8b40de0 100644 --- a/Entities/StatTextComponent.cs +++ b/Entities/StatTextComponent.cs @@ -1,4 +1,5 @@ using Celeste.Mod.ConsistencyTracker.Enums; +using Celeste.Mod.ConsistencyTracker.Utility; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Monocle; @@ -12,16 +13,18 @@ namespace Celeste.Mod.ConsistencyTracker.Entities { public class StatTextComponent : Component { public StatTextPosition Position { get; set; } = StatTextPosition.TopRight; - public string Text { get; set; } = ""; public bool OptionVisible { get; set; } public bool HideInGolden { get; set; } - public float Scale { get; set; } = 1f; + + private float Scale { get; set; } = 1f; public bool TextOutline { get; set; } = true; public PixelFont Font { get; set; } public float FontFaceSize { get; set; } - public Color TextColor { get; set; } = Color.White; + + private Color TextColor { get; set; } = Color.White; + public float StrokeSize { get; set; } = 2f; public Color StrokeColor { get; set; } = Color.Black; @@ -33,6 +36,14 @@ public class StatTextComponent : Component { public float PosX { get; set; } = 0; public float PosY { get; set; } = 0; + private float YOffset { get; set; } = 0; + private float LineHeight { get; set; } = 0; + + // Tuple items: + // 1. Line text + // 2. Line color + private List> TextLines { get; set; } + public bool DebugShowPosition { get; set; } private static readonly int WIDTH = 1920; @@ -42,6 +53,44 @@ public StatTextComponent(bool active, bool visible, StatTextPosition position) : Position = position; } + public void SetText(string text) { + text = text.Replace("\\n", "\n"); + TextLines = text.Split('\n').Select(ParseLineColor).ToList(); + UpdateText(); + } + + private void UpdateText() { + if (TextLines == null) { + return; + } + + LineHeight = Font.Get(FontFaceSize).LineHeight * Scale; + YOffset = TextLines.Count * Justify.Y * LineHeight; + } + + private Tuple ParseLineColor(string line) { + Color? color = null; + + if (!string.IsNullOrEmpty(line) && line.Length > 1 && line[0] == '[') { + int colorLength = line[1] == '#' ? 7 : 6; + if (line.Length >= colorLength + 2 && line[colorLength + 1] == ']' && Util.TryParseColor(line.Substring(1, colorLength), out Color parsedColor)) { + color = parsedColor; + line = line.Substring(colorLength + 2); + } + } + + return new Tuple(line, color); + } + + public void SetTextColor(Color color) { + TextColor = color; + } + + public void SetSize(float size) { + Scale = size; + UpdateText(); + } + public void SetPosition() { SetPosition(Position); } @@ -105,31 +154,42 @@ public void SetPosition(StatTextPosition pos) { Justify = new Vector2(1, 1); break; } + + UpdateText(); } public override void Render() { - base.Render(); - - if (TextOutline) { - Font.DrawOutline( - FontFaceSize, - Text, - new Vector2(PosX, PosY), - Justify, - Vector2.One * Scale, - TextColor, - StrokeSize, - StrokeColor - ); - } else { - Font.Draw( - FontFaceSize, - Text, - new Vector2(PosX, PosY), - Justify, - Vector2.One * Scale, - TextColor - ); + Vector2 pointer = new Vector2(PosX, PosY); + pointer.Y -= YOffset; + + for (int i = 0; i < TextLines.Count; i++) { + Tuple line = TextLines[i]; + string text = line.Item1; + Color color = line.Item2.HasValue ? line.Item2.Value : TextColor; + + if (TextOutline) { + Font.DrawOutline( + FontFaceSize, + text, + pointer, + new Vector2(Justify.X, 0), + Vector2.One * Scale, + color, + StrokeSize, + StrokeColor + ); + } else { + Font.Draw( + FontFaceSize, + text, + pointer, + new Vector2(Justify.X, 0), + Vector2.One * Scale, + color + ); + } + + pointer.Y += LineHeight; } if (DebugShowPosition) { diff --git a/Entities/TextOverlay.cs b/Entities/TextOverlay.cs index 165d7e0..8803aab 100644 --- a/Entities/TextOverlay.cs +++ b/Entities/TextOverlay.cs @@ -167,7 +167,7 @@ private void InitStatTextOptions() { public void SetTextColor(int textNum, Color color) { StatTextComponent statText = GetStatText(textNum); - statText.TextColor = color; + statText.SetTextColor(color); } public void SetTextOutline(int textNum, bool outline) { @@ -186,7 +186,7 @@ public void SetTextHideInGolden(int textNum, bool visible) { } public void SetText(int textNum, string text) { StatTextComponent statText = GetStatText(textNum); - statText.Text = text.Replace("\\n", "\n"); + statText.SetText(text); } public void SetTextPosition(int textNum, StatTextPosition pos) { StatTextComponent statText = GetStatText(textNum); @@ -206,7 +206,7 @@ public void SetTextOffsetY(int textNum, int offset) //size in percent as int public void SetTextSize(int textNum, int size) { StatTextComponent statText = GetStatText(textNum); - statText.Scale = (float)size / 100; + statText.SetSize((float)size / 100); } private StatTextComponent GetStatText(int textNum) { diff --git a/Utility/Util.cs b/Utility/Util.cs index 7cb5bcc..5ae0761 100644 --- a/Utility/Util.cs +++ b/Utility/Util.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using Microsoft.Xna.Framework; namespace Celeste.Mod.ConsistencyTracker.Utility { public static class Util { @@ -157,5 +159,35 @@ public static string DictionaryToString(Dictionary dict, Func Date: Tue, 12 May 2026 18:39:50 +0800 Subject: [PATCH 3/6] Support the color tags in the format editor preview --- Assets/LiveDataEditor/LiveDataEditTool.css | 17 +++++++ Assets/LiveDataEditor/LiveDataEditTool.html | 2 +- Assets/LiveDataEditor/LiveDataEditTool.js | 56 +++++++++++++++++++-- ConsistencyTracker.cs | 2 +- Entities/TextOverlay.cs | 17 +++++-- 5 files changed, 84 insertions(+), 10 deletions(-) diff --git a/Assets/LiveDataEditor/LiveDataEditTool.css b/Assets/LiveDataEditor/LiveDataEditTool.css index 82fd87c..babfb89 100644 --- a/Assets/LiveDataEditor/LiveDataEditTool.css +++ b/Assets/LiveDataEditor/LiveDataEditTool.css @@ -116,12 +116,29 @@ a.button:hover { resize: none; outline: none; } + #main-container > textarea { height: 200px; resize: vertical; } +#format-preview { + width: 80%; + font-family: monospace; + border-radius: 5px; + border: 1px solid rgb(5, 5, 5); + padding: 10px; + background: rgb(77, 77, 77); + overflow-y: auto; + height: 200px; + resize: vertical; +} +#format-preview p { + margin: 0; + padding: 0; + font-size: 0.9rem; +} #sidebar-right { flex: 30; diff --git a/Assets/LiveDataEditor/LiveDataEditTool.html b/Assets/LiveDataEditor/LiveDataEditTool.html index 5bacc25..da0592f 100644 --- a/Assets/LiveDataEditor/LiveDataEditTool.html +++ b/Assets/LiveDataEditor/LiveDataEditTool.html @@ -43,7 +43,7 @@

Editor


Preview

- +

Select a format diff --git a/Assets/LiveDataEditor/LiveDataEditTool.js b/Assets/LiveDataEditor/LiveDataEditTool.js index 19e7fab..021fb53 100644 --- a/Assets/LiveDataEditor/LiveDataEditTool.js +++ b/Assets/LiveDataEditor/LiveDataEditTool.js @@ -62,7 +62,10 @@ function showError(errorCode, errorMessage){ if(CurrentState === ViewStates.MainView){ Elements.LoadingText.innerText = message; }else if(CurrentState === ViewStates.InspectorView){ - Elements.FormatPreview.value = message; + Elements.FormatPreview.innerHTML = ""; + let previewLine = document.createElement("p"); + previewLine.textContent = errorMessage; + Elements.FormatPreview.appendChild(previewLine); } } //#endregion @@ -419,11 +422,43 @@ function setPlaceholderExplanationText(name, description){ Elements.PlaceholderExplanationDescription.innerText = description; } +function isValidColor(str) { + const s = new Option().style; + s.color = str; + return s.color !== ""; +} + +function parseLine(line) { + let color = "#FFFFFF"; + let text = line; + + if (line.startsWith("[")) { + let colorLength = line[1] === "#" ? 7 : 6 + if (line.length >= colorLength + 2 && line[colorLength + 1] === "]") { + let parsedColor = line.substring(1, colorLength + 1); + + if (parsedColor.length === 6 && !parsedColor.startsWith("#")) { + parsedColor = "#" + parsedColor; + } + + if (isValidColor(color)) { + color = parsedColor; + text = text.substring(colorLength + 2); + } + } + } + + return { + Color: color, + Text: text + }; +} + //Launch POST request to /cct/parseFormat with the format text as body function fetchPreview(){ let formatText = Elements.FormatText.value; if(formatText === null || formatText === undefined || formatText === ""){ - Elements.FormatPreview.value = ""; + Elements.FormatPreview.innerHTML = ""; return; } @@ -435,11 +470,22 @@ function fetchPreview(){ .then(responseObj => { if(responseObj.errorCode !== 0){ console.log(responseObj.errorMessage); - Elements.FormatPreview.value = "Could not fetch preview, error code (" + responseObj.errorCode + "): " + responseObj.errorMessage; + + Elements.FormatPreview.innerHTML = ""; + let errorLine = document.createElement("p"); + errorLine.textContent = "Could not fetch preview, error code (" + responseObj.errorCode + "): " + responseObj.errorMessage; + Elements.FormatPreview.appendChild(errorLine); return; } - - Elements.FormatPreview.value = responseObj.formats[0]; + + Elements.FormatPreview.innerHTML = ""; + responseObj.formats[0].split("\n").forEach(line => { + let previewLine = document.createElement("p"); + let parsedLine = parseLine(line); + previewLine.textContent = parsedLine.Text; + previewLine.style.color = parsedLine.Color; + Elements.FormatPreview.appendChild(previewLine); + }); }).catch(error => showError(-1, "Could not fetch preview (is CCT running?)")); } //#endregion diff --git a/ConsistencyTracker.cs b/ConsistencyTracker.cs index dd972f0..d297ab0 100644 --- a/ConsistencyTracker.cs +++ b/ConsistencyTracker.cs @@ -31,7 +31,7 @@ public class ConsistencyTrackerModule : EverestModule { public static class VersionsNewest { public static string Mod => "2.9.8"; public static string Overlay => "2.0.0"; - public static string LiveDataEditor => "1.0.1"; + public static string LiveDataEditor => "1.0.2"; public static string PhysicsInspector => "1.4.2"; } public static class VersionsCurrent { diff --git a/Entities/TextOverlay.cs b/Entities/TextOverlay.cs index 8803aab..ca0152f 100644 --- a/Entities/TextOverlay.cs +++ b/Entities/TextOverlay.cs @@ -24,9 +24,6 @@ public class TextOverlay : Entity { public TextOverlay() { Depth = -101; Tag = Tags.HUD | Tags.Global | Tags.PauseUpdate | Tags.TransitionUpdate; - - Events.Events.OnRunStarted += EventsOnOnRunStarted; - Events.Events.OnRunEnded += EventsOnOnRunEnded; StatText1 = new StatTextComponent(true, true, StatTextPosition.TopLeft); StatText2 = new StatTextComponent(true, true, StatTextPosition.TopRight); @@ -37,6 +34,20 @@ public TextOverlay() { ApplyModSettings(); } + public override void Added(Scene scene) { + base.Added(scene); + + Events.Events.OnRunStarted += EventsOnOnRunStarted; + Events.Events.OnRunEnded += EventsOnOnRunEnded; + } + + public override void Removed(Scene scene) { + base.Removed(scene); + + Events.Events.OnRunStarted -= EventsOnOnRunStarted; + Events.Events.OnRunEnded -= EventsOnOnRunEnded; + } + private void EventsOnOnRunEnded(bool died, bool won) { SetGoldenState(false); } From 42729247ab1269124ca49f85d53a6637676a1c16 Mon Sep 17 00:00:00 2001 From: Kongtiao Date: Wed, 13 May 2026 22:27:01 +0800 Subject: [PATCH 4/6] Small changes --- Entities/TextOverlay.cs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Entities/TextOverlay.cs b/Entities/TextOverlay.cs index ca0152f..b33b4a1 100644 --- a/Entities/TextOverlay.cs +++ b/Entities/TextOverlay.cs @@ -34,18 +34,29 @@ public TextOverlay() { ApplyModSettings(); } + private void SubscribeEvents() { + Events.Events.OnRunStarted += EventsOnOnRunStarted; + Events.Events.OnRunEnded += EventsOnOnRunEnded; + } + + private void UnsubscribeEvents() { + Events.Events.OnRunStarted -= EventsOnOnRunStarted; + Events.Events.OnRunEnded -= EventsOnOnRunEnded; + } + public override void Added(Scene scene) { base.Added(scene); - - Events.Events.OnRunStarted += EventsOnOnRunStarted; - Events.Events.OnRunEnded += EventsOnOnRunEnded; + SubscribeEvents(); } public override void Removed(Scene scene) { base.Removed(scene); + UnsubscribeEvents(); + } - Events.Events.OnRunStarted -= EventsOnOnRunStarted; - Events.Events.OnRunEnded -= EventsOnOnRunEnded; + public override void SceneEnd(Scene scene) { + base.SceneEnd(scene); + UnsubscribeEvents(); } private void EventsOnOnRunEnded(bool died, bool won) { From cea5a06afd0709354b0ca8352f429feb1c5d3d71 Mon Sep 17 00:00:00 2001 From: Kongtiao Date: Wed, 20 May 2026 18:57:08 +0800 Subject: [PATCH 5/6] Migrate the event subscription to the module class --- ConsistencyTracker.cs | 4 +++- Entities/TextOverlay.cs | 35 +---------------------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/ConsistencyTracker.cs b/ConsistencyTracker.cs index d297ab0..25f3bef 100644 --- a/ConsistencyTracker.cs +++ b/ConsistencyTracker.cs @@ -853,10 +853,12 @@ private void Events_OnResetRun() { } private void EventsOnRunStarted() { - + IngameOverlay?.SetGoldenState(true); } + private void EventsOnRunEnded(bool died, bool won) { ChokeRateStat.ChokeRateData = null; //Reset caching + IngameOverlay?.SetGoldenState(false); } private void Events_OnChangedRoom(string roomName, bool isPreviousRoom) { if (DoRecordPath) { diff --git a/Entities/TextOverlay.cs b/Entities/TextOverlay.cs index b33b4a1..922ca3f 100644 --- a/Entities/TextOverlay.cs +++ b/Entities/TextOverlay.cs @@ -34,39 +34,6 @@ public TextOverlay() { ApplyModSettings(); } - private void SubscribeEvents() { - Events.Events.OnRunStarted += EventsOnOnRunStarted; - Events.Events.OnRunEnded += EventsOnOnRunEnded; - } - - private void UnsubscribeEvents() { - Events.Events.OnRunStarted -= EventsOnOnRunStarted; - Events.Events.OnRunEnded -= EventsOnOnRunEnded; - } - - public override void Added(Scene scene) { - base.Added(scene); - SubscribeEvents(); - } - - public override void Removed(Scene scene) { - base.Removed(scene); - UnsubscribeEvents(); - } - - public override void SceneEnd(Scene scene) { - base.SceneEnd(scene); - UnsubscribeEvents(); - } - - private void EventsOnOnRunEnded(bool died, bool won) { - SetGoldenState(false); - } - - private void EventsOnOnRunStarted() { - SetGoldenState(true); - } - private void ApplyModSettings() { ConsistencyTrackerSettings settings = Mod.ModSettings; @@ -249,7 +216,7 @@ private void UpdateTextVisibility() { SetGoldenState(holdingGolden); } - private void SetGoldenState(bool playerHasGolden) { + public void SetGoldenState(bool playerHasGolden) { if (playerHasGolden) { GetStatText(1).Visible = GetStatText(1).OptionVisible && !(GetStatText(1).HideInGolden); GetStatText(2).Visible = GetStatText(2).OptionVisible && !(GetStatText(2).HideInGolden); From 7e8b0d839518179159fd2232eb73adb4a845d6d1 Mon Sep 17 00:00:00 2001 From: Kongtiao Date: Wed, 20 May 2026 19:01:40 +0800 Subject: [PATCH 6/6] Modify LiveDataEditTool.js a bit --- Assets/LiveDataEditor/LiveDataEditTool.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Assets/LiveDataEditor/LiveDataEditTool.js b/Assets/LiveDataEditor/LiveDataEditTool.js index 021fb53..c81782f 100644 --- a/Assets/LiveDataEditor/LiveDataEditTool.js +++ b/Assets/LiveDataEditor/LiveDataEditTool.js @@ -422,12 +422,6 @@ function setPlaceholderExplanationText(name, description){ Elements.PlaceholderExplanationDescription.innerText = description; } -function isValidColor(str) { - const s = new Option().style; - s.color = str; - return s.color !== ""; -} - function parseLine(line) { let color = "#FFFFFF"; let text = line; @@ -437,14 +431,12 @@ function parseLine(line) { if (line.length >= colorLength + 2 && line[colorLength + 1] === "]") { let parsedColor = line.substring(1, colorLength + 1); - if (parsedColor.length === 6 && !parsedColor.startsWith("#")) { + if (!parsedColor.startsWith("#")) { parsedColor = "#" + parsedColor; } - if (isValidColor(color)) { - color = parsedColor; - text = text.substring(colorLength + 2); - } + color = parsedColor; + text = text.substring(colorLength + 2); } }