Skip to content

Commit 6f5e4b7

Browse files
committed
Restore "Merge pull request #925 from maddie480/fancy-unicode-characters"
This reverts commit e47f67f.
1 parent e47f67f commit 6f5e4b7

5 files changed

Lines changed: 234 additions & 61 deletions

File tree

Celeste.Mod.mm/Mod/Everest/Emoji.cs

Lines changed: 68 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
using Monocle;
2-
using System.Collections;
1+
using Celeste.Mod.Helpers;
2+
using Monocle;
33
using System.Collections.Generic;
44
using System.Collections.ObjectModel;
5-
using System.Linq;
65
using System.Text;
76
using System.Xml;
87

@@ -16,16 +15,20 @@ public static class Emoji {
1615
/// A list of all registered emoji names, in order of their IDs.
1716
/// </summary>
1817
public static ReadOnlyCollection<string> Registered => new ReadOnlyCollection<string>(_Registered);
18+
1919
public static char Last => (char) ('\uE000' + _Registered.Count - 1);
2020

2121
private static List<string> _Registered = new List<string>();
2222
private static Dictionary<string, int> _IDs = new Dictionary<string, int>();
2323
private static List<bool> _IsMonochrome = new List<bool>();
2424
private static List<PixelFontCharacter> _Chars = new List<PixelFontCharacter>();
2525

26+
private static readonly CachedApply cachedApplies = new CachedApply();
27+
2628
private static bool Initialized = false;
2729
private static Queue<KeyValuePair<string, MTexture>> Queue = new Queue<KeyValuePair<string, MTexture>>();
2830
private static XmlElement _FakeXML;
31+
2932
public static XmlElement FakeXML {
3033
get {
3134
if (_FakeXML != null)
@@ -129,6 +132,7 @@ public static void Register(string name, MTexture emoji, float scale) {
129132
}
130133

131134
_Chars.Add(new PixelFontCharacter(Start + id, emoji, xml));
135+
cachedApplies.Clear();
132136
}
133137

134138
/// <summary>
@@ -174,68 +178,76 @@ public static bool TryGet(string name, out char c) {
174178
public static bool IsMonochrome(char c)
175179
=> _IsMonochrome[c - Start];
176180

177-
/// <summary>
178-
/// Transforms all instances of :emojiname: to \uSTART+ID
179-
/// </summary>
180-
/// <param name="text"></param>
181-
/// <returns></returns>
182-
public static string Apply(string text) {
183-
if (text == null)
184-
return text;
185-
// TODO: This trashes the GC and doesn't allow escaping!
186-
lock (_IDs) {
187-
StringBuilder resultBuilder = new StringBuilder();
188-
int appendStartIndex = 0;
189-
int head = -1, tail = 0;
190-
// suppose text is "aaaa:1111:2222:bbbb", and only ":2222:" is emoji name
191-
// H = head, T = tail, S = appendStartIndex
192-
while (tail < text.Length) {
193-
if (text[tail] == ':') {
194-
if (head >= 0 && text[head] == ':') {
195-
/*
196-
aaaa:1111:2222:bbbb
197-
(2) ^S ^H ^T ^
198-
(4) ^S ^H ^T
199-
now head and tail are pointing to colons so we need to check if the text inside colons is an emoji name
200-
*/
201-
string name = text.Substring(head + 1, (tail - 1) - (head + 1) + 1);
202-
if (_IDs.TryGetValue(name, out int value)) {
203-
// if it is, we need to first append the text before emoji
204-
resultBuilder.Append(text, appendStartIndex, (head - 1) - appendStartIndex + 1);
205-
// then append the emoji itself
206-
resultBuilder.Append((char) (Start + value));
207-
// the emoji name has been replaced, we need to advance tail pointer once
208-
// because the colon is used and can't belong to next emoji
209-
tail++;
210-
appendStartIndex = tail;
181+
private class CachedApply : CacheHelper<string, string> {
182+
public CachedApply() : base(name: "Emoji.Apply", maxSize: 1000) { }
183+
184+
protected override string Compute(string text) {
185+
if (text == null)
186+
return text;
187+
188+
// TODO: This trashes the GC and doesn't allow escaping!
189+
lock (_IDs) {
190+
StringBuilder resultBuilder = new StringBuilder();
191+
int appendStartIndex = 0;
192+
int head = -1, tail = 0;
193+
// suppose text is "aaaa:1111:2222:bbbb", and only ":2222:" is emoji name
194+
// H = head, T = tail, S = appendStartIndex
195+
while (tail < text.Length) {
196+
if (text[tail] == ':') {
197+
if (head >= 0 && text[head] == ':') {
211198
/*
212199
aaaa:1111:2222:bbbb
213-
(5) ^H S^T
200+
(2) ^S ^H ^T ^
201+
(4) ^S ^H ^T
202+
now head and tail are pointing to colons so we need to check if the text inside colons is an emoji name
214203
*/
204+
string name = text.Substring(head + 1, (tail - 1) - (head + 1) + 1);
205+
if (_IDs.TryGetValue(name, out int value)) {
206+
// if it is, we need to first append the text before emoji
207+
resultBuilder.Append(text, appendStartIndex, (head - 1) - appendStartIndex + 1);
208+
// then append the emoji itself
209+
resultBuilder.Append((char) (Start + value));
210+
// the emoji name has been replaced, we need to advance tail pointer once
211+
// because the colon is used and can't belong to next emoji
212+
tail++;
213+
appendStartIndex = tail;
214+
/*
215+
aaaa:1111:2222:bbbb
216+
(5) ^H S^T
217+
*/
218+
}
215219
}
220+
head = tail;
221+
/*
222+
aaaa:1111:2222:bbbb
223+
(1) ^S H^T ^
224+
(3) ^S H^T
225+
when tail is pointing to a colon, we need to let head also points to it
226+
so when tail moves to next colon, text[head..tail] can be an emoji name and we can check then replace it
227+
*/
216228
}
217-
head = tail;
218-
/*
219-
aaaa:1111:2222:bbbb
220-
(1) ^S H^T ^
221-
(3) ^S H^T
222-
when tail is pointing to a colon, we need to let head also points to it
223-
so when tail moves to next colon, text[head..tail] can be an emoji name and we can check then replace it
224-
*/
229+
tail++;
225230
}
226-
tail++;
227-
}
228-
/*
229-
aaaa:1111:2222:bbbb
230-
(6) H^S ^T
231-
there are still text left since last append, so we need to append them
232-
*/
233-
if (appendStartIndex < text.Length) {
234-
resultBuilder.Append(text, appendStartIndex, (text.Length - 1) - appendStartIndex + 1);
231+
/*
232+
aaaa:1111:2222:bbbb
233+
(6) H^S ^T
234+
there are still text left since last append, so we need to append them
235+
*/
236+
if (appendStartIndex < text.Length) {
237+
resultBuilder.Append(text, appendStartIndex, (text.Length - 1) - appendStartIndex + 1);
238+
}
239+
return resultBuilder.ToString();
235240
}
236-
return resultBuilder.ToString();
237241
}
238242
}
239243

244+
/// <summary>
245+
/// Transforms all instances of :emojiname: to \uSTART+ID
246+
/// </summary>
247+
/// <param name="text"></param>
248+
/// <returns></returns>
249+
public static string Apply(string text) {
250+
return cachedApplies.GetCached(text);
251+
}
240252
}
241253
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Collections.Generic;
2+
3+
namespace Celeste.Mod.Helpers {
4+
/**
5+
* A helper class that wraps a <code>compute</code> function, in order to keep the
6+
* latest computed values in memory (holding up to <code>maxSize</code> values),
7+
* to help with performance or with repeated memory allocations.
8+
*/
9+
public abstract class CacheHelper<Key, Value> {
10+
private readonly struct ValueNode {
11+
public LinkedListNode<Key> Node { get; init; }
12+
public Value Value { get; init; }
13+
}
14+
15+
private readonly string name;
16+
private readonly int maxSize;
17+
private readonly Dictionary<Key, ValueNode> cache = new Dictionary<Key, ValueNode>();
18+
private readonly LinkedList<Key> cacheOldestToNewest = new LinkedList<Key>();
19+
20+
public CacheHelper(string name, int maxSize) {
21+
this.name = name;
22+
this.maxSize = maxSize;
23+
}
24+
25+
public Value GetCached(Key key) {
26+
lock (cache) {
27+
if (cache.TryGetValue(key, out ValueNode valueNode)) {
28+
cacheOldestToNewest.Remove(valueNode.Node);
29+
cacheOldestToNewest.AddLast(valueNode.Node);
30+
return valueNode.Value;
31+
}
32+
33+
Value value = Compute(key);
34+
while (cache.Count >= maxSize) {
35+
Key keyToEvict = cacheOldestToNewest.First.Value;
36+
cache.Remove(keyToEvict);
37+
cacheOldestToNewest.RemoveFirst();
38+
}
39+
40+
LinkedListNode<Key> node = cacheOldestToNewest.AddLast(key);
41+
cache.Add(key, new ValueNode { Node = node, Value = value });
42+
Logger.Verbose("CacheHelper", $"[{name}] Cached value for \"{key}\" => \"{value}\", cache size: {cache.Count}");
43+
return value;
44+
}
45+
}
46+
47+
protected abstract Value Compute(Key key);
48+
49+
public void Clear() {
50+
lock (cache) {
51+
cache.Clear();
52+
cacheOldestToNewest.Clear();
53+
}
54+
}
55+
}
56+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using YamlDotNet.Core.Tokens;
4+
5+
namespace Celeste.Mod.Helpers {
6+
public static class UnicodeStringHelper {
7+
// helper class because manipulating generic types in IL is annoying
8+
public class ListInt {
9+
public required int[] Elements;
10+
public int Count => Elements.Length;
11+
public int this[int index] => Elements[index];
12+
}
13+
14+
private class Cacher : CacheHelper<string, ListInt> {
15+
public Cacher() : base(name: "UnicodeStringHelper.ToCodePointList", maxSize: 1000) { }
16+
17+
protected override ListInt Compute(string text) {
18+
return new ListInt {
19+
Elements = text.EnumerateRunes().Select(r => r.Value).ToArray()
20+
};
21+
}
22+
}
23+
24+
private static readonly Cacher cacher = new Cacher();
25+
26+
public static ListInt ToCodePointList(string text) {
27+
return cacher.GetCached(text);
28+
}
29+
}
30+
}

Celeste.Mod.mm/Patches/FancyText.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
using Celeste.Mod;
44
using Microsoft.Xna.Framework;
55
using Monocle;
6+
using MonoMod;
67

78
namespace Celeste {
89
// The FancyText ctor is private.
910
class patch_FancyText {
1011

12+
[PatchTextIteration]
1113
private extern void orig_AddWord(string word);
1214
private void AddWord(string word) {
1315
word = Emoji.Apply(word);

0 commit comments

Comments
 (0)