diff --git a/Assets/Scripts/Editor/EmojiCheck.cs b/Assets/Scripts/Editor/EmojiCheck.cs index 48afc0018..5080315b3 100644 --- a/Assets/Scripts/Editor/EmojiCheck.cs +++ b/Assets/Scripts/Editor/EmojiCheck.cs @@ -6,22 +6,20 @@ using TMPro; using UnityEditor; using UnityEngine; -// Every single-code-point emoji the pinned font set draws, and the Pirates product names, render through an -// ordinary TMP text on the game's body font with no missing code point and with ink in the emoji atlas. +// Every single-code-point emoji of the pinned Unicode Emoji version that Twemoji draws, and the Pirates product +// names read from the shipped catalog, render through an ordinary TMP text on the game's body font with no +// missing code point, no blank glyph, ink in the emoji atlas, and each glyph served by the emoji asset or by the +// body font itself, never by a third font. TMP Settings must route emoji by exactly one entry: the global +// fallback (the emoji fallback list stays empty), so dropping that entry or re-adding the second route fails. // Run as -executeMethod EmojiCheck.Run (exit 1 on failure in batchmode, as EngineAssetCheck). Corpus: -// tools/emoji/emoji-singles.txt; -emojiLimit N spreads N entries evenly across it for the spike. +// tools/emoji/emoji-singles.txt (from emoji-test.txt); the emoji Twemoji lacks are listed in +// tools/emoji/emoji-unsupported.txt and reported, not rendered; -emojiLimit N spreads N entries evenly for a spike. public static class EmojiCheck { const string BodyFontPath = "Assets/Fonts/Ubuntu/Ubuntu-L Small SDF.asset"; + const string PiratesFaction = "Pirates"; const int Chunk = 64; - // The eight Pirates products (addenda "The Pirates record r2"), written as code points. - static readonly string[] PiratesNames = - { - "\U0001FAF3\U0001F52B", "\U0001FAA6\U0001F920", "\U0001F9FE\U0001F6AB", "\U0001F3A8\U0001F648", - "\U0001F525", "\U0001F440\U0001F3AF", "\U0001F511\U0001F3C3\U0001F4A8", "\U0001F4E6\U0001F92B", - }; - [MenuItem("Aetheria/Text/Check Emoji Rendering")] public static void Run() { @@ -32,7 +30,8 @@ public static class EmojiCheck if (Application.isBatchMode) EditorApplication.Exit(failures.Count == 0 ? 0 : 1); } - static string RunCheck(List failures) + // Public so a scratch driver can mutate TMP Settings in memory and watch this fail; adds to failures. + public static string RunCheck(List failures) { var emoji = AssetDatabase.LoadAssetAtPath(EmojiFont.AssetPath); var body = AssetDatabase.LoadAssetAtPath(BodyFontPath); @@ -42,7 +41,14 @@ public static class EmojiCheck return "nothing checked"; } - var corpus = LoadSingles(); + // One route: the global fallback names the emoji asset, and the emoji fallback list is empty. + if (TMP_Settings.emojiFallbackTextAssets != null && TMP_Settings.emojiFallbackTextAssets.Count > 0) + failures.Add("TMP Settings' emoji fallback list is not empty: a second emoji route exists (EmojiFont.Build leaves it empty)."); + if (TMP_Settings.fallbackFontAssets == null || !TMP_Settings.fallbackFontAssets.Contains(emoji)) + failures.Add("TMP Settings' global fallback list does not name Emoji.asset: the only emoji route is gone."); + + var unsupported = new HashSet(LoadCodePoints("emoji-unsupported.txt")); + var corpus = LoadSingles().Where(cp => !unsupported.Contains(cp)).ToList(); var missing = new HashSet(); TMP_Text.MissingCharacterEventCallback onMissing = (unicode, index, source, asset, component) => missing.Add(unicode); TMP_Text.OnMissingCharacter += onMissing; @@ -54,7 +60,8 @@ public static class EmojiCheck text.textWrappingMode = TextWrappingModes.NoWrap; text.rectTransform.sizeDelta = new Vector2(100000, 100000); - int viaEmoji = 0, viaOther = 0, blank = 0; + int viaEmoji = 0, viaBody = 0, blank = 0, pirateNames = 0; + var strays = new HashSet(); try { void Render(string content, string label) @@ -65,7 +72,10 @@ public static class EmojiCheck { var info = text.textInfo.characterInfo[i]; if (info.character == ' ' || info.elementType != TMP_TextElementType.Character) continue; - if (info.fontAsset == emoji) viaEmoji++; else viaOther++; + if (info.fontAsset == emoji) viaEmoji++; + else if (info.fontAsset == body) viaBody++; + else if (strays.Add($"{info.fontAsset.name}:{(int)info.character:X}")) + failures.Add($"U+{(int)info.character:X4} in {label} was served by {info.fontAsset.name}, not Emoji.asset or the body font"); if (info.fontAsset == emoji && info.textElement.glyph.glyphRect.width <= 0) { blank++; @@ -79,7 +89,24 @@ public static class EmojiCheck var slice = corpus.Skip(i).Take(Chunk).ToList(); Render(string.Join(" ", slice.Select(char.ConvertFromUtf32)), $"chunk {i}"); } - foreach (var name in PiratesNames) Render(name, "Pirates name"); + + // The Pirates names as the shipped catalog stores them, through the game's own catalog reader. + var catalogPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "GameData", "Aetheria.cc"); + using (var catalog = AetheriaStores.Open(catalogPath)) + { + var pirates = catalog.GetAll().SingleOrDefault(faction => faction.Name == PiratesFaction); + if (pirates == null) failures.Add($"the catalog has no {PiratesFaction} faction"); + else + { + var key = catalog.RefOf(pirates); + foreach (var product in catalog.GetAll().Where(product => product.Manufacturer.Equals(key))) + { + pirateNames++; + Render(product.Name, $"Pirates product name \"{product.Name}\""); + } + if (pirateNames == 0) failures.Add($"the catalog has no {PiratesFaction} products"); + } + } } finally { @@ -89,12 +116,13 @@ public static class EmojiCheck foreach (var cp in missing.OrderBy(c => c)) failures.Add($"missing code point U+{cp:X4}"); + // TMP over-allocates atlasTextures by doubling its capacity when a page is added, so entries past + // atlasTextureCount are null by design (3 pages in a 4-slot array). Only the used pages count. long ink = 0, colour = 0; - int pages = 0, nullPages = 0; - foreach (var page in emoji.atlasTextures) + for (var i = 0; i < emoji.atlasTextureCount; i++) { - if (page == null) { nullPages++; continue; } - pages++; + var page = emoji.atlasTextures[i]; + if (page == null) { failures.Add($"emoji atlas page {i} of {emoji.atlasTextureCount} is null"); continue; } var pixels = page.GetPixels32(); ink += pixels.LongCount(p => p.a != 0); colour += pixels.LongCount(p => p.a != 0 && (p.r != p.g || p.g != p.b)); @@ -102,20 +130,30 @@ public static class EmojiCheck if (ink == 0) failures.Add("the emoji atlas holds no ink after rendering"); else if (colour == 0) failures.Add("the emoji atlas holds no coloured pixel: the glyphs rendered monochrome"); + var pages = emoji.atlasTextureCount; // Dynamic atlas content is cache: drop it so this check leaves the asset as Build wrote it. emoji.ClearFontAssetData(true); - return $"checked {corpus.Count} single code points + {PiratesNames.Length} Pirates names, {missing.Count} missing, " + - $"{viaEmoji} glyphs via the emoji asset, {viaOther} via other fonts, {blank} blank, {ink} atlas ink pixels of which {colour} coloured, {pages} atlas pages ({nullPages} null)"; + if (unsupported.Count > 0) + Debug.LogWarning($"EmojiCheck: Twemoji lacks {unsupported.Count} single-code-point emoji of the Emoji version: " + + string.Join(" ", unsupported.OrderBy(c => c).Select(c => $"U+{c:X4}"))); + return $"checked {corpus.Count} single code points ({unsupported.Count} not drawn by Twemoji, listed in the warning) + {pirateNames} Pirates product names, " + + $"{missing.Count} missing, {viaEmoji} glyphs via the emoji asset, {viaBody} via the body font, {strays.Count} via other fonts, {blank} blank, " + + $"{ink} atlas ink pixels of which {colour} coloured, {pages} atlas pages"; } static List LoadSingles() { - var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "tools", "emoji", "emoji-singles.txt"); - var all = File.ReadAllLines(path).Where(l => l.Length > 0).Select(l => Convert.ToInt32(l, 16)).ToList(); + var all = LoadCodePoints("emoji-singles.txt"); var args = Environment.GetCommandLineArgs(); var at = Array.IndexOf(args, "-emojiLimit"); if (at < 0 || at + 1 >= args.Length) return all; var limit = int.Parse(args[at + 1]); return Enumerable.Range(0, limit).Select(i => all[i * all.Count / limit]).ToList(); } + + static List LoadCodePoints(string file) + { + var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "tools", "emoji", file); + return File.ReadAllLines(path).Where(l => l.Length > 0).Select(l => Convert.ToInt32(l, 16)).ToList(); + } } diff --git a/Assets/Scripts/Editor/EmojiFont.cs b/Assets/Scripts/Editor/EmojiFont.cs index c7ce97aaf..0c1d4eac2 100644 --- a/Assets/Scripts/Editor/EmojiFont.cs +++ b/Assets/Scripts/Editor/EmojiFont.cs @@ -6,8 +6,10 @@ using UnityEngine.TextCore.LowLevel; // The one writer of the emoji font asset and of TMP Settings' emoji routing. The font file is the // output of tools/emoji/fetch_emoji_font.py; the asset is a dynamic COLOR font asset whose atlas pages -// are cache, filled on first use at runtime and cleared here. Every TMP text reaches emoji through -// TMP Settings' emoji fallback and global fallback list; no component names an emoji font. +// are cache, filled on first use at runtime and cleared here. Every TMP text reaches emoji through ONE +// route, TMP Settings' global fallback list; the emoji fallback list stays empty, and no component names +// an emoji font. (Measured: the global list alone resolves every single code point; the emoji list alone +// leaves 200 text-presentation emoji unresolved. A second route could only disagree with the first.) public static class EmojiFont { public const string FontPath = "Assets/Fonts/Emoji/Twemoji.ttf"; @@ -61,16 +63,15 @@ public static class EmojiFont // The atlas pages are cache: Build leaves them empty, the same state EmojiCheck restores. asset.ClearFontAssetData(true); - // Text-presentation emoji (U+263A without FE0F) are not routed through the emoji list, so the - // asset is also the global fallback. - TMP_Settings.emojiFallbackTextAssets = new List { asset }; + // One route: the global fallback. It also serves text-presentation emoji (U+263A without FE0F). + TMP_Settings.emojiFallbackTextAssets = new List(); TMP_Settings.fallbackFontAssets = new List { asset }; TMP_Settings.defaultSpriteAsset = null; EditorUtility.SetDirty(asset); EditorUtility.SetDirty(TMP_Settings.instance); AssetDatabase.SaveAssets(); - message = "EmojiFont: built " + AssetPath + " and routed TMP Settings' emoji fallback to it."; + message = "EmojiFont: built " + AssetPath + " and made it TMP Settings' only emoji route (the global fallback)."; return true; } } diff --git a/Assets/TextMesh Pro/Resources/TMP Settings.asset b/Assets/TextMesh Pro/Resources/TMP Settings.asset index 0371c3b56..d7c619e31 100644 --- a/Assets/TextMesh Pro/Resources/TMP Settings.asset +++ b/Assets/TextMesh Pro/Resources/TMP Settings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f00aad0de57d69c610a4a421bf4652e93f45c2a39bacc76dec8a7921bf699ecb -size 1930 +oid sha256:1e678e96aa2e2acd6a205a7c33df946e3a59e8dfae43e0cbbc98ddecc6983a58 +size 1861