From 8d052233f17e77d9a3011b9204c6f5fb87d91341 Mon Sep 17 00:00:00 2001 From: js6pak Date: Fri, 18 Dec 2020 16:29:02 +0100 Subject: [PATCH] Add text api --- src/Impostor.Api/Innersloth/Palette.cs | 59 +++++ src/Impostor.Api/Innersloth/Text/Text.cs | 204 ++++++++++++++++++ .../Net/Inner/Objects/IInnerPlayerControl.cs | 7 +- .../Net/Inner/Objects/IInnerPlayerInfo.cs | 3 +- .../Inner/Objects/InnerPlayerControl.Api.cs | 7 +- .../Net/Inner/Objects/InnerPlayerInfo.cs | 5 +- src/Impostor.Tests/TextTests.cs | 45 ++++ 7 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 src/Impostor.Api/Innersloth/Palette.cs create mode 100644 src/Impostor.Api/Innersloth/Text/Text.cs create mode 100644 src/Impostor.Tests/TextTests.cs diff --git a/src/Impostor.Api/Innersloth/Palette.cs b/src/Impostor.Api/Innersloth/Palette.cs new file mode 100644 index 000000000..6263b2466 --- /dev/null +++ b/src/Impostor.Api/Innersloth/Palette.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Drawing; +using Impostor.Api.Innersloth.Customization; + +namespace Impostor.Api.Innersloth +{ + public static class Palette + { + public static readonly Color DisabledGrey = Color.FromArgb(76, 76, 76); + public static readonly Color DisabledColor = Color.FromArgb(76, 255, 255, 255); + public static readonly Color EnabledColor = Color.FromArgb(255, 255, 255); + public static readonly Color Black = Color.FromArgb(0, 0, 0); + public static readonly Color ClearWhite = Color.FromArgb(0, 255, 255, 255); + public static readonly Color HalfWhite = Color.FromArgb(128, 255, 255, 255); + public static readonly Color White = Color.FromArgb(255, 255, 255); + public static readonly Color LightBlue = Color.FromArgb(128, 128, 255); + public static readonly Color Blue = Color.FromArgb(51, 51, 255); + public static readonly Color Orange = Color.FromArgb(255, 153, 1); + public static readonly Color Purple = Color.FromArgb(153, 26, 153); + public static readonly Color Brown = Color.FromArgb(184, 110, 28); + + public static readonly Color CrewmateBlue = Color.FromArgb(140, 255, 255); + public static readonly Color ImpostorRed = Color.FromArgb(255, 25, 25); + + public static readonly Dictionary PlayerColors = new Dictionary + { + [ColorType.Red] = Color.FromArgb(198, 17, 17), + [ColorType.Blue] = Color.FromArgb(19, 46, 210), + [ColorType.Green] = Color.FromArgb(17, 128, 45), + [ColorType.Pink] = Color.FromArgb(238, 84, 187), + [ColorType.Orange] = Color.FromArgb(240, 125, 13), + [ColorType.Yellow] = Color.FromArgb(246, 246, 87), + [ColorType.Black] = Color.FromArgb(63, 71, 78), + [ColorType.White] = Color.FromArgb(215, 225, 241), + [ColorType.Purple] = Color.FromArgb(107, 47, 188), + [ColorType.Brown] = Color.FromArgb(113, 73, 30), + [ColorType.Cyan] = Color.FromArgb(56, 255, 221), + [ColorType.Lime] = Color.FromArgb(80, 240, 57), + }; + + public static readonly Dictionary ShadowColors = new Dictionary + { + [ColorType.Red] = Color.FromArgb(122, 8, 56), + [ColorType.Blue] = Color.FromArgb(9, 21, 142), + [ColorType.Green] = Color.FromArgb(10, 77, 46), + [ColorType.Pink] = Color.FromArgb(172, 43, 174), + [ColorType.Orange] = Color.FromArgb(180, 62, 21), + [ColorType.Yellow] = Color.FromArgb(195, 136, 34), + [ColorType.Black] = Color.FromArgb(30, 31, 38), + [ColorType.White] = Color.FromArgb(132, 149, 192), + [ColorType.Purple] = Color.FromArgb(59, 23, 124), + [ColorType.Brown] = Color.FromArgb(94, 38, 21), + [ColorType.Cyan] = Color.FromArgb(36, 169, 191), + [ColorType.Lime] = Color.FromArgb(21, 168, 66), + }; + + public static readonly Color VisorColor = Color.FromArgb(149, 202, 220); + } +} diff --git a/src/Impostor.Api/Innersloth/Text/Text.cs b/src/Impostor.Api/Innersloth/Text/Text.cs new file mode 100644 index 000000000..21e8567f3 --- /dev/null +++ b/src/Impostor.Api/Innersloth/Text/Text.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Impostor.Api.Innersloth.Text +{ + public class Text + { + private readonly List _children = new List(); + + public Text(string content, Color? color = null, string? link = null) + { + Content = content; + Color = color; + Link = link; + } + + public string Content { get; private set; } + + public Color? Color { get; private set; } + + public string? Link { get; private set; } + + public IReadOnlyList Children => _children.AsReadOnly(); + + public static implicit operator Text(string raw) => Parse(raw); + + public static implicit operator string(Text text) => text.ToString(); + + public static Text Parse(string raw) + { + var root = new Text(string.Empty); + var current = root; + + var inBracket = false; + + string? color = null; + string? link = null; + + foreach (var c in raw) + { + if (inBracket) + { + if (c == ']') + { + inBracket = false; + + if (color != null) + { + current.Color = System.Drawing.Color.FromArgb( + int.Parse(color.Substring(6, 2), NumberStyles.HexNumber), + int.Parse(color.Substring(0, 2), NumberStyles.HexNumber), + int.Parse(color.Substring(2, 2), NumberStyles.HexNumber), + int.Parse(color.Substring(4, 2), NumberStyles.HexNumber)); + color = null; + } + else if (link != null) + { + current.Link = link; + link = null; + } + else + { + if (!ReferenceEquals(root, current) && current.Content != string.Empty) + { + root.Append(current); + } + + current = new Text(string.Empty); + } + } + else if (color != null) + { + color += c.ToString(); + } + else if (link != null) + { + link += c.ToString(); + } + else if (int.TryParse(c.ToString(), NumberStyles.HexNumber, null, out var number)) + { + color = c.ToString(); + } + else + { + link = c.ToString(); + } + } + else if (c == '[') + { + if (current.Content != string.Empty) + { + if (!ReferenceEquals(root, current) && current.Content != string.Empty) + { + root.Append(current); + } + + current = new Text(string.Empty); + } + + inBracket = true; + } + else + { + current.Content += c; + } + } + + return root; + } + + public string ToRawString() + { + var builder = new StringBuilder(); + + builder.Append(Content); + + foreach (var child in _children) + { + builder.Append(child.ToRawString()); + } + + return builder.ToString(); + } + + public override string ToString() + { + var builder = new StringBuilder(); + + if (Color != null) + { + builder.Append("["); + builder.Append($"{Color.Value.R:X2}{Color.Value.G:X2}{Color.Value.B:X2}{Color.Value.A:X2}"); + builder.Append("]"); + } + + if (Link != null) + { + builder.Append("["); + builder.Append(Link); + builder.Append("]"); + } + + builder.Append(Content); + + if (Color != null || Link != null) + { + builder.Append("[]"); + } + + foreach (var child in _children) + { + builder.Append(child); + } + + return builder.ToString(); + } + + public Text Append(Text text) + { + _children.Add(text); + + return this; + } + + public Text Append(string content, Color? color = null, string? link = null) + { + return Append(new Text(content, color, link)); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((Text)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(_children, Content, Color, Link); + } + + protected bool Equals(Text other) + { + return Content == other.Content && Color?.ToArgb() == other.Color?.ToArgb() && Link == other.Link && _children.SequenceEqual(other._children); + } + } +} diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs index 04558b96e..ba46ba76e 100644 --- a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs +++ b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Impostor.Api.Innersloth.Customization; +using Impostor.Api.Innersloth.Text; using Impostor.Api.Net.Inner.Objects.Components; namespace Impostor.Api.Net.Inner.Objects @@ -35,7 +36,7 @@ public interface IInnerPlayerControl : IInnerNetObject /// /// A name for the player. /// Task that must be awaited. - ValueTask SetNameAsync(string name); + ValueTask SetNameAsync(Text name); /// /// Sets the color of the current . @@ -91,7 +92,7 @@ public interface IInnerPlayerControl : IInnerNetObject /// /// The message to send. /// Task that must be awaited. - ValueTask SendChatAsync(string text); + ValueTask SendChatAsync(Text text); /// /// Send a chat message as the current . @@ -103,7 +104,7 @@ public interface IInnerPlayerControl : IInnerNetObject /// When left as null, will send message to self. /// /// Task that must be awaited. - ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null); + ValueTask SendChatToPlayerAsync(Text text, IInnerPlayerControl? player = null); /// /// Sets the current to be murdered by an impostor . diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs index 6cb33024a..c70a533a0 100644 --- a/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs +++ b/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Impostor.Api.Innersloth; +using Impostor.Api.Innersloth.Text; namespace Impostor.Api.Net.Inner.Objects { @@ -9,7 +10,7 @@ public interface IInnerPlayerInfo /// /// Gets the name of the player as decided by the host. /// - string PlayerName { get; } + Text PlayerName { get; } /// /// Gets the color of the player. diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs index 0a7997d5c..8b171e3e4 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs @@ -2,6 +2,7 @@ using Impostor.Api; using Impostor.Api.Innersloth; using Impostor.Api.Innersloth.Customization; +using Impostor.Api.Innersloth.Text; using Impostor.Api.Net; using Impostor.Api.Net.Inner.Objects; using Impostor.Api.Net.Inner.Objects.Components; @@ -17,7 +18,7 @@ internal partial class InnerPlayerControl : IInnerPlayerControl IInnerPlayerInfo IInnerPlayerControl.PlayerInfo => PlayerInfo; - public async ValueTask SetNameAsync(string name) + public async ValueTask SetNameAsync(Text name) { PlayerInfo.PlayerName = name; @@ -82,14 +83,14 @@ public ValueTask SetSkinAsync(SkinType skinType) return SetSkinAsync((uint)skinType); } - public async ValueTask SendChatAsync(string text) + public async ValueTask SendChatAsync(Text text) { using var writer = _game.StartRpc(NetId, RpcCalls.SendChat); writer.Write(text); await _game.FinishRpcAsync(writer); } - public async ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null) + public async ValueTask SendChatToPlayerAsync(Text text, IInnerPlayerControl? player = null) { if (player == null) { diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs index f248994ee..e67b85309 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Impostor.Api.Games; using Impostor.Api.Innersloth; +using Impostor.Api.Innersloth.Text; using Impostor.Api.Net.Messages; namespace Impostor.Server.Net.Inner.Objects @@ -17,7 +18,7 @@ public InnerPlayerInfo(byte playerId) public byte PlayerId { get; } - public string PlayerName { get; internal set; } + public Text PlayerName { get; internal set; } public byte ColorId { get; internal set; } @@ -56,7 +57,7 @@ public void Serialize(IMessageWriter writer) public void Deserialize(IMessageReader reader) { - PlayerName = reader.ReadString(); + PlayerName = Text.Parse(reader.ReadString()); ColorId = reader.ReadByte(); HatId = reader.ReadPackedUInt32(); PetId = reader.ReadPackedUInt32(); diff --git a/src/Impostor.Tests/TextTests.cs b/src/Impostor.Tests/TextTests.cs new file mode 100644 index 000000000..44dd873f8 --- /dev/null +++ b/src/Impostor.Tests/TextTests.cs @@ -0,0 +1,45 @@ +using System.Drawing; +using Impostor.Api.Innersloth.Text; +using Xunit; + +namespace Impostor.Tests +{ + public class TextTests + { + private Text CreateTestText() + { + return new Text("white") + .Append(" ") + .Append("red", Color.Red) + .Append(" ") + .Append("link", link: "https://example.com"); + } + + [Fact] + public void Serialize() + { + var text = CreateTestText(); + + Assert.Equal("white [FF0000FF]red[] [https://example.com]link[]", text.ToString()); + Assert.Equal("white red link", text.ToRawString()); + } + + [Fact] + public void Deserialize() + { + var text = CreateTestText(); + var parsed = Text.Parse("white [FF0000FF]red[] [https://example.com]link[]"); + + Assert.Equal(text.ToString(), parsed.ToString()); + } + + [Fact] + public void Roundtrip() + { + var text = CreateTestText(); + var parsed = Text.Parse(text.ToString()); + + Assert.Equal(text.ToString(), parsed.ToString()); + } + } +}