diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..1442d44
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+ obj\$(MSBuildProjectName)\
+ bin\$(Configuration)\$(MSBuildProjectName)\
+ $(DefaultItemExcludes);obj\**;obj\**\*;bin\**;bin\**\*
+
+
diff --git a/FontMod/FontMod-KM.csproj b/FontMod/FontMod-KM.csproj
index f5f29b6..0fce140 100644
--- a/FontMod/FontMod-KM.csproj
+++ b/FontMod/FontMod-KM.csproj
@@ -5,7 +5,7 @@
net481
FontMod
FontMod
- 1.1.2
+ 1.1.3
true
latest
FontMod
@@ -28,7 +28,7 @@
-
+
diff --git a/FontMod/FontMod-RT.csproj b/FontMod/FontMod-RT.csproj
index 972f65c..2c9f575 100644
--- a/FontMod/FontMod-RT.csproj
+++ b/FontMod/FontMod-RT.csproj
@@ -5,7 +5,7 @@
net481
FontMod
FontMod
- 1.1.2
+ 1.1.3
true
latest
FontMod
@@ -41,7 +41,7 @@
-
+
diff --git a/FontMod/FontMod-WOTR.csproj b/FontMod/FontMod-WOTR.csproj
index 92c375e..a47e47d 100644
--- a/FontMod/FontMod-WOTR.csproj
+++ b/FontMod/FontMod-WOTR.csproj
@@ -5,7 +5,7 @@
net481
FontMod
FontMod
- 1.1.2
+ 1.1.3
true
latest
FontMod
diff --git a/FontMod/FontSwap/FontDataModel-KM.cs b/FontMod/FontSwap/FontDataModel-KM.cs
index 5720ca8..7b0e9e2 100644
--- a/FontMod/FontSwap/FontDataModel-KM.cs
+++ b/FontMod/FontSwap/FontDataModel-KM.cs
@@ -1,18 +1,23 @@
#if KM
using Newtonsoft.Json;
using System;
-using System.Drawing;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Kingmaker.Localization;
using TMPro;
using TMPro.EditorUtilities;
-using UnityEngine;
-using static RootMotion.FinalIK.GrounderQuadruped;
namespace FontMod.FontSwap;
[Serializable]
public class FontDataModel
{
+ private const int FontAtlasCacheKeyVersion = 2;
+ private static string requiredCharacters;
+
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
@@ -20,7 +25,22 @@ public class FontDataModel
[JsonIgnore]
public string FontPath { get; set; }
[JsonIgnore]
- public TMP_FontAsset TMP_FontAsset { get; private set; }
+ public TMP_FontAsset TMP_FontAsset
+ {
+ get
+ {
+ // We're lazy because large fonts (e.g. chinese fonts) can take a lot of time
+ if (!fontAssetCreationAttempted && !IsIgnored && !string.IsNullOrEmpty(FontPath)) {
+ fontAssetCreationAttempted = true;
+ tmpFontAsset = CreateFontAsset(FontPath);
+ }
+
+ return tmpFontAsset;
+ }
+ }
+
+ private TMP_FontAsset tmpFontAsset;
+ private bool fontAssetCreationAttempted;
private FontDataModel() { }
@@ -33,7 +53,6 @@ private FontDataModel(string fontPath)
FontPath = fontPath;
Name = Path.GetFileNameWithoutExtension(fontPath);
- TMP_FontAsset = CreateFontAsset(fontPath);
}
catch (Exception e)
{
@@ -44,7 +63,7 @@ private FontDataModel(string fontPath)
public override bool Equals(object obj)
{
if (obj is FontDataModel other)
- return Equals(FontPath, other.FontPath) && Equals(TMP_FontAsset, other.TMP_FontAsset);
+ return Equals(FontPath, other.FontPath);
return false;
}
@@ -55,7 +74,6 @@ public override int GetHashCode()
{
int hash = 17;
hash = hash * 31 + (FontPath != null ? FontPath.GetHashCode() : 0);
- hash = hash * 31 + (TMP_FontAsset != null ? TMP_FontAsset.GetHashCode() : 0);
return hash;
}
}
@@ -74,20 +92,47 @@ public static TMP_FontAsset CreateFontAsset(string fontPath)
var create = new TMPro_FontAssetCreatorWindow();
create.font_TTF_path = fontPath;
- create.GenerateFontAtlas();
+ var characters = GetRequiredCharacters();
+ create.SetCharacterSet(characters);
+ var renderType = create.UseBitmapFontAsset ? "bitmap" : "SDF";
+ var cachePath = GetFontAtlasCachePath(fontPath, characters, create);
+ bool generated = false;
+
+ if (create.TryLoadFontAtlasCache(cachePath))
+ {
+ Main.Logger.Log($"Loaded cached {renderType} atlas for {name}.");
+ }
+ else
+ {
+ generated = true;
+ Main.Logger.Log($"Generating {create.CharacterCount} glyphs for {name} in a {create.AtlasSize}x{create.AtlasSize} {renderType} atlas.");
+ create.GenerateFontAtlas();
+ }
+
create.CreateFontTexture();
- asset = create.Save_SDF_FontAsset();
+ asset = create.UseBitmapFontAsset ? create.Save_Normal_FontAsset() : create.Save_SDF_FontAsset();
if (asset == null)
throw new NullReferenceException($"Creation of TMP_FontAsset failed for font {name}");
- else
- Main.Logger.Log($"Created font asset {asset.name}");
-
asset.name = name;
-
- if (asset != null)
- MaterialReferenceManager.AddFontAsset(asset);
+ asset.ReadFontDefinition();
+
+ if (generated)
+ {
+ try
+ {
+ create.SaveFontAtlasCache(cachePath);
+ }
+ catch (Exception e)
+ {
+ Main.Logger.Warning($"Could not cache the generated atlas for {name}: {e.Message}");
+ }
+ }
+
+ Main.Logger.Log($"Created font asset {asset.name}");
+
+ MaterialReferenceManager.AddFontAsset(asset);
}
catch (Exception e)
{
@@ -96,7 +141,77 @@ public static TMP_FontAsset CreateFontAsset(string fontPath)
return asset;
}
+
+ private static string GetFontAtlasCachePath(string fontPath, string characters, TMPro_FontAssetCreatorWindow creator)
+ {
+ byte[] fontHash;
+ using (var stream = File.OpenRead(fontPath)) {
+ using var hash = SHA256.Create();
+ fontHash = hash.ComputeHash(stream);
+ }
+
+ string settings = $"{FontAtlasCacheKeyVersion}\n{creator.AtlasSize}\n{creator.UseBitmapFontAsset}\n{characters}";
+ byte[] settingsBytes = Encoding.UTF8.GetBytes(settings);
+ byte[] keyData = new byte[fontHash.Length + settingsBytes.Length];
+ Buffer.BlockCopy(fontHash, 0, keyData, 0, fontHash.Length);
+ Buffer.BlockCopy(settingsBytes, 0, keyData, fontHash.Length, settingsBytes.Length);
+
+ byte[] cacheHash;
+ using (var hash = SHA256.Create()) {
+ cacheHash = hash.ComputeHash(keyData);
+ }
+
+ string cacheKey = BitConverter.ToString(cacheHash).Replace("-", "").ToLowerInvariant();
+ return Path.Combine(Main.ModEntry.Path, "Cache", cacheKey + ".fontatlas");
+ }
+
+ private static string GetRequiredCharacters()
+ {
+ if (requiredCharacters != null) {
+ return requiredCharacters;
+ }
+
+ var characters = new HashSet();
+ // Printable ASCII is required for TMP rich-text tags and ordinary UI text.
+ for (char character = ' '; character <= '~'; character++) {
+ characters.Add(character);
+ }
+
+ // TMP support characters: non-breaking space, zero-width space, ellipsis, and missing-glyph box.
+ characters.Add('\u00a0');
+ characters.Add('\u200b');
+ characters.Add('\u2026');
+ characters.Add('\u25a1');
+
+ AddLocalizationCharacters(characters, LocalizationManager.CurrentPack);
+ AddLocalizationCharacters(characters, LocalizationManager.CurrentPackFast);
+
+ requiredCharacters = new string(characters.OrderBy(character => character).ToArray());
+ Main.Logger.Log($"Preparing {requiredCharacters.Length} distinct characters for the current Kingmaker localization.");
+ return requiredCharacters;
+ }
+
+ private static void AddLocalizationCharacters(HashSet characters, LocalizationPack pack)
+ {
+ if (pack?.Strings == null) {
+ return;
+ }
+
+ foreach (string value in pack.Strings.Values)
+ {
+ if (string.IsNullOrEmpty(value)) {
+ continue;
+ }
+
+ foreach (char character in value)
+ {
+ if (!char.IsControl(character)) {
+ characters.Add(character);
+ }
+ }
+ }
+ }
}
-#endif
\ No newline at end of file
+#endif
diff --git a/FontMod/FontSwap/FontSwapperPatches.cs b/FontMod/FontSwap/FontSwapperPatches.cs
index 490f4e4..89f35f6 100644
--- a/FontMod/FontSwap/FontSwapperPatches.cs
+++ b/FontMod/FontSwap/FontSwapperPatches.cs
@@ -1,10 +1,12 @@
using FontMod.Utility;
using HarmonyLib;
using Kingmaker.UI.Common;
+#if KM
+using Kingmaker.Localization;
+#endif
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
-using System.Text.RegularExpressions;
using TMPro;
using UnityEngine;
namespace FontMod.FontSwap;
@@ -12,6 +14,22 @@ namespace FontMod.FontSwap;
[HarmonyPatch]
public static class TMPTestPach
{
+ private static bool CanSwapFonts
+ {
+ get
+ {
+#if KM
+ return LocalizationManager.CurrentPack != null;
+#else
+ return true;
+#endif
+ }
+ }
+
+ private static TMP_FontAsset GetMappedFontAsset(TMP_FontAsset fontAsset)
+ {
+ return fontAsset == null || !CanSwapFonts ? fontAsset : FontMapper.Instance.GetFontMapped(fontAsset);
+ }
#if RT
static bool _afterDelay = false;
@@ -31,8 +49,7 @@ static void TextPatch(TextMeshProUGUI __instance)
if (!_afterDelay)
return;
#endif
- if (__instance.m_fontAsset != null)
- __instance.m_fontAsset = FontMapper.Instance.GetFontMapped(__instance.m_fontAsset);
+ __instance.m_fontAsset = GetMappedFontAsset(__instance.m_fontAsset);
}
[HarmonyPatch(typeof(MaterialReferenceManager), nameof(MaterialReferenceManager.TryGetFontAsset))]
@@ -45,8 +62,7 @@ static void TryGetFontAsset(ref TMP_FontAsset fontAsset)
return;
#endif
- if (fontAsset != null)
- fontAsset = FontMapper.Instance.GetFontMapped(fontAsset);
+ fontAsset = GetMappedFontAsset(fontAsset);
}
[HarmonyPatch(typeof(TMP_Text), nameof(TMP_Text.ValidateHtmlTag))]
@@ -68,9 +84,8 @@ static IEnumerable TagPatch(IEnumerable instru
var patchCodes = new CodeInstruction[]
{
- new(OpCodes.Call, AccessTools.PropertyGetter(typeof(FontMapper), nameof(FontMapper.Instance))),
new(OpCodes.Ldloc_S, ldlocs.operand),
- new(OpCodes.Callvirt, AccessTools.Method(typeof(FontMapper), nameof(FontMapper.GetFontMapped))),
+ new(OpCodes.Call, AccessTools.Method(typeof(TMPTestPach), nameof(GetMappedFontAsset))),
new(OpCodes.Stloc_S, ldlocs.operand)
};
@@ -91,12 +106,29 @@ static IEnumerable TagPatch(IEnumerable instru
[HarmonyPrefix]
static void GetSaberBookFormatPatch(string name, Color color, int size, ref Material material)
{
- if (material == null)
+ if (material == null || !CanSwapFonts)
return;
- material = FontMapper.Instance.FontMappings.ContainsKey("Saber_Dist32") ?
- FontMapper.Instance.FontMappings["Saber_Dist32"].TMP_FontAsset.material :
- FontMapper.Instance.DefaultFontMapping.TMP_FontAsset.material;
+ FontMapper mapper = FontMapper.Instance;
+ FontDataModel mapping;
+
+ if (mapper.FontMappings.TryGetValue("Saber_Dist32", out mapping))
+ {
+ // An ignored mapping means that both the original font and its original material must pass through untouched.
+ // => Otherwise e.g. nameplate disappears?
+ if (mapping.IsIgnored) {
+ return;
+ }
+ }
+ else
+ {
+ mapping = mapper.DefaultFontMapping;
+ }
+
+ Material mappedMaterial = mapping?.TMP_FontAsset?.material;
+ if (mappedMaterial != null) {
+ material = mappedMaterial;
+ }
}
#endif
-}
\ No newline at end of file
+}
diff --git a/FontMod/Info.json b/FontMod/Info.json
index 09e87ed..dad02f9 100644
--- a/FontMod/Info.json
+++ b/FontMod/Info.json
@@ -2,7 +2,7 @@
"Id": "FontMod",
"DisplayName": "FontMod",
"Author": "Hambeard",
- "Version": "1.1.2",
+ "Version": "1.1.3",
"ManagerVersion": "0.23.0",
"Requirements": [],
"AssemblyName": "FontMod.dll",
diff --git a/FontMod/TMP_Utils/TMPro_FontAssetCreatorWindow.cs b/FontMod/TMP_Utils/TMPro_FontAssetCreatorWindow.cs
index ec281a6..560d9fd 100644
--- a/FontMod/TMP_Utils/TMPro_FontAssetCreatorWindow.cs
+++ b/FontMod/TMP_Utils/TMPro_FontAssetCreatorWindow.cs
@@ -1,5 +1,6 @@
using UnityEngine;
using UnityEditor;
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
@@ -79,6 +80,10 @@ private enum PreviewSelectionTypes { PreviewFont, PreviewTexture, PreviewDistanc
private int font_atlas_width = 512;
private int font_atlas_height = 512;
+ public int CharacterCount => characterSequence.Distinct().Count();
+ public int AtlasSize => font_atlas_width;
+ public bool UseBitmapFontAsset { get; private set; }
+
//private int m_shaderSelectionIndex;
//private Shader m_shaderSelection;
//private string[] m_availableShaderNames;
@@ -98,6 +103,214 @@ private enum PreviewSelectionTypes { PreviewFont, PreviewTexture, PreviewDistanc
private Texture2D m_destination_Atlas;
private bool includeKerningPairs = true;
private int[] m_kerningSet;
+ private KerningTable m_kerningTable;
+
+ private const string FontAtlasCacheMagic = "FontModAtlas";
+ private const int FontAtlasCacheVersion = 2;
+ private const int MaxKerningPairCount = 7500;
+
+ // Allocate a large enough atlas.
+ // The sizes are guesses; a smaller atlas might be good enough?
+ public void SetCharacterSet(string characters)
+ {
+ if (string.IsNullOrEmpty(characters)) {
+ return;
+ }
+
+ characterSequence = new string([.. characters.Distinct().OrderBy(c => c)]);
+ font_CharacterSet_Selection = 7;
+
+ int characterCount = characterSequence.Length;
+ // Large localized sets are prohibitively slow ._.
+ // Game startup took like 5 minutes with the normal renderer
+ UseBitmapFontAsset = characterCount > 512;
+ font_renderMode = UseBitmapFontAsset ? RenderModes.Smooth : RenderModes.DistanceField16;
+
+ int atlasSize = characterCount switch
+ {
+ <= 128 => 512,
+ <= 512 => 1024,
+ <= 2048 => 2048,
+ <= 8192 => 4096,
+ _ => 8192
+ };
+
+ font_atlas_width = atlasSize;
+ font_atlas_height = atlasSize;
+ }
+
+ public bool TryLoadFontAtlasCache(string path)
+ {
+ if (!File.Exists(path)) {
+ return false;
+ }
+
+ try
+ {
+ using var stream = File.OpenRead(path);
+ using var reader = new BinaryReader(stream);
+
+ if (reader.ReadString() != FontAtlasCacheMagic || reader.ReadInt32() != FontAtlasCacheVersion) {
+ return false;
+ }
+
+ int atlasWidth = reader.ReadInt32();
+ int atlasHeight = reader.ReadInt32();
+ var renderMode = (RenderModes)reader.ReadInt32();
+ int glyphCount = reader.ReadInt32();
+ int expectedTextureLength = checked(font_atlas_width * font_atlas_height);
+
+ if (atlasWidth != font_atlas_width || atlasHeight != font_atlas_height ||
+ renderMode != font_renderMode || glyphCount != CharacterCount) {
+ return false;
+ }
+
+ var faceInfo = new FT_FaceInfo
+ {
+ name = reader.ReadString(),
+ pointSize = reader.ReadInt32(),
+ padding = reader.ReadInt32(),
+ lineHeight = reader.ReadSingle(),
+ baseline = reader.ReadSingle(),
+ ascender = reader.ReadSingle(),
+ descender = reader.ReadSingle(),
+ centerLine = reader.ReadSingle(),
+ underline = reader.ReadSingle(),
+ underlineThickness = reader.ReadSingle(),
+ characterCount = reader.ReadInt32(),
+ atlasWidth = reader.ReadInt32(),
+ atlasHeight = reader.ReadInt32()
+ };
+
+ var glyphInfo = new FT_GlyphInfo[glyphCount];
+ for (int i = 0; i < glyphInfo.Length; i++)
+ {
+ glyphInfo[i].id = reader.ReadInt32();
+ glyphInfo[i].x = reader.ReadSingle();
+ glyphInfo[i].y = reader.ReadSingle();
+ glyphInfo[i].width = reader.ReadSingle();
+ glyphInfo[i].height = reader.ReadSingle();
+ glyphInfo[i].xOffset = reader.ReadSingle();
+ glyphInfo[i].yOffset = reader.ReadSingle();
+ glyphInfo[i].xAdvance = reader.ReadSingle();
+ }
+
+ int kerningCount = reader.ReadInt32();
+ if (kerningCount < 0 || kerningCount > MaxKerningPairCount) {
+ return false;
+ }
+
+ var kerningTable = new KerningTable { kerningPairs = new List(kerningCount) };
+ for (int i = 0; i < kerningCount; i++)
+ {
+ kerningTable.kerningPairs.Add(new KerningPair(
+ reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadSingle()));
+ }
+
+ if (reader.ReadInt32() != expectedTextureLength) {
+ return false;
+ }
+
+ byte[] textureBuffer = reader.ReadBytes(expectedTextureLength);
+ if (textureBuffer.Length != expectedTextureLength || stream.Position != stream.Length) {
+ return false;
+ }
+
+ m_font_faceInfo = faceInfo;
+ m_font_glyphInfo = glyphInfo;
+ m_kerningTable = kerningTable;
+ m_character_Count = glyphCount;
+ m_texture_buffer = textureBuffer;
+ isRenderingDone = true;
+ return true;
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return false;
+ }
+ catch (OverflowException)
+ {
+ return false;
+ }
+ }
+
+ public void SaveFontAtlasCache(string path)
+ {
+ if (m_texture_buffer == null || m_font_glyphInfo == null) {
+ throw new InvalidOperationException("A font atlas must be generated before it can be cached.");
+ }
+
+ string directory = Path.GetDirectoryName(path);
+ Directory.CreateDirectory(directory);
+ string temporaryPath = path + ".tmp";
+
+ try
+ {
+ using (var stream = File.Create(temporaryPath))
+ using (var writer = new BinaryWriter(stream))
+ {
+ writer.Write(FontAtlasCacheMagic);
+ writer.Write(FontAtlasCacheVersion);
+ writer.Write(font_atlas_width);
+ writer.Write(font_atlas_height);
+ writer.Write((int)font_renderMode);
+ writer.Write(m_font_glyphInfo.Length);
+
+ writer.Write(m_font_faceInfo.name ?? string.Empty);
+ writer.Write(m_font_faceInfo.pointSize);
+ writer.Write(m_font_faceInfo.padding);
+ writer.Write(m_font_faceInfo.lineHeight);
+ writer.Write(m_font_faceInfo.baseline);
+ writer.Write(m_font_faceInfo.ascender);
+ writer.Write(m_font_faceInfo.descender);
+ writer.Write(m_font_faceInfo.centerLine);
+ writer.Write(m_font_faceInfo.underline);
+ writer.Write(m_font_faceInfo.underlineThickness);
+ writer.Write(m_font_faceInfo.characterCount);
+ writer.Write(m_font_faceInfo.atlasWidth);
+ writer.Write(m_font_faceInfo.atlasHeight);
+
+ foreach (var glyph in m_font_glyphInfo)
+ {
+ writer.Write(glyph.id);
+ writer.Write(glyph.x);
+ writer.Write(glyph.y);
+ writer.Write(glyph.width);
+ writer.Write(glyph.height);
+ writer.Write(glyph.xOffset);
+ writer.Write(glyph.yOffset);
+ writer.Write(glyph.xAdvance);
+ }
+
+ var kerningPairs = m_kerningTable?.kerningPairs ?? new List();
+ writer.Write(kerningPairs.Count);
+ foreach (var pair in kerningPairs)
+ {
+ writer.Write(pair.firstGlyph);
+ writer.Write(pair.secondGlyph);
+ writer.Write(pair.xOffset);
+ }
+
+ writer.Write(m_texture_buffer.Length);
+ writer.Write(m_texture_buffer);
+ }
+
+ if (File.Exists(path)) {
+ File.Delete(path);
+ }
+ File.Move(temporaryPath, path);
+ }
+ finally
+ {
+ if (File.Exists(temporaryPath)) {
+ File.Delete(temporaryPath);
+ }
+ }
+ }
// Image Down Sampling Fields
//private Texture2D sdf_Atlas;
@@ -368,6 +581,9 @@ public TMP_FontAsset Save_Normal_FontAsset()
string tex_FileName = Path.GetFileNameWithoutExtension(font_TTF_path);
+ // Name is included in Hash
+ font_asset.name = tex_FileName;
+
//Set Font Asset Type
font_asset.fontAssetType = TMP_FontAsset.FontAssetTypes.Bitmap;
@@ -417,6 +633,9 @@ public TMP_FontAsset Save_SDF_FontAsset()
string tex_FileName = Path.GetFileNameWithoutExtension(font_TTF_path);
+ // Name is included in Hash
+ font_asset.name = tex_FileName;
+
// Reference to the source font file
//font_asset.sourceFontFile = font_TTF as Font;
@@ -569,11 +788,15 @@ TMP_Glyph[] GetGlyphInfo(FT_GlyphInfo[] ft_glyphs, int scaleFactor)
// Get Kerning Pairs
public KerningTable GetKerningTable(string fontFilePath, int pointSize)
{
+ if (m_kerningTable != null) {
+ return m_kerningTable;
+ }
+
KerningTable kerningInfo = new KerningTable();
kerningInfo.kerningPairs = new List();
// Temporary Array to hold the kerning pairs from the Native Plug-in.
- FT_KerningPair[] kerningPairs = new FT_KerningPair[7500];
+ FT_KerningPair[] kerningPairs = new FT_KerningPair[MaxKerningPairCount];
int kpCount = TMPro_FontPlugin.FT_GetKerningPairs(fontFilePath, m_kerningSet, m_kerningSet.Length, kerningPairs);
@@ -592,7 +815,8 @@ public KerningTable GetKerningTable(string fontFilePath, int pointSize)
}
- return kerningInfo;
+ m_kerningTable = kerningInfo;
+ return m_kerningTable;
}
}
-}
\ No newline at end of file
+}
diff --git a/Repository.json b/Repository.json
index c77c286..a8117e6 100644
--- a/Repository.json
+++ b/Repository.json
@@ -2,7 +2,7 @@
"Releases": [
{
"Id": "FontMod",
- "Version": "1.0.0"
+ "Version": "1.0.3"
}
]
}
\ No newline at end of file