using HarmonyLib; using OWML.Common; using OWML.ModHelper; using System; using System.Collections.Generic; using System.Reflection; using UnityEngine; using UnityEngine.UI; namespace SlovakTranslation { /// /// Fixes the missing Slovak caron glyphs (č ď ĺ ľ ň ŕ ť ž) in the menus and HUD. /// /// Outer Wilds renders Latin languages with a handful of *static* font atlases, /// and only some of them were baked with Latin Extended-A: /// /// Gill Sans MT / Gill Sans MT Menu (menus, prompts) 243 glyphs — no carons /// ParaType - Futura PT Medium (language font) 663 glyphs — complete /// SpaceMono-Regular (translator) 612 glyphs — complete /// /// That is why dialogue and the Nomai translator always looked right while /// "Pokračovať" came out as "Pokra ova". Registering a whole custom language /// font via the Localization Utility fixes the menus but also flips /// TextTranslation.IsLanguageLatin() to false, which drags dialogue, /// the translator and the ship log off their proper fonts too. /// /// So instead we leave the language font alone and swap fonts *only* where the /// vanilla one physically cannot draw Slovak — checked at runtime with /// Font.HasCharacter, so we never touch a font that renders fine. /// internal static class SlovakFont { /// Cabin (SIL OFL 1.1), a humanist sans close to the vanilla Gill Sans MT. /// Shipped as a dynamic font, so it can draw any glyph Slovak needs. private static Font _font; /// Characters the vanilla menu atlas is missing. If a font can draw all /// of these it is left completely alone. private static readonly char[] Probe = { 'č', 'ď', 'ĺ', 'ľ', 'ň', 'ŕ', 'ť', 'ž', 'ô', 'ä' }; private static readonly Dictionary _coverage = new Dictionary(); internal static bool Loaded => _font != null; internal static void Load(ModBehaviour mod) { var path = mod.ModHelper.Manifest.ModFolderPath + "assets/slovakfont"; var bundle = AssetBundle.LoadFromFile(path); if (bundle == null) { mod.ModHelper.Console.WriteLine("Could not load font bundle at " + path, MessageType.Error); return; } foreach (var font in bundle.LoadAllAssets()) { if (font.name != "SlovakUI") continue; _font = font; UnityEngine.Object.DontDestroyOnLoad(font); break; } if (_font == null) mod.ModHelper.Console.WriteLine("Font bundle has no 'SlovakUI' font.", MessageType.Error); } /// Returns unchanged unless Slovenčina is the /// active language and that font cannot draw Slovak. internal static Font Fix(Font font) { if (_font == null || font == null || font == _font) return font; // Dynamic fonts rasterise straight from an embedded font file; every one the // game ships for Latin covers Slovak, and HasCharacter is unreliable for them. if (font.dynamic) return font; if (!IsActive()) return font; var id = font.GetInstanceID(); if (!_coverage.TryGetValue(id, out var complete)) { complete = true; foreach (var c in Probe) { if (font.HasCharacter(c)) continue; complete = false; break; } _coverage[id] = complete; } return complete ? font : _font; } #region "is Slovenčina selected?" // The Localization Utility invents a TextTranslation.Language value for each // custom language, so it is not in the enum at compile time. Ask the utility // once, then cache the value and compare enums from then on. private static bool _resolved; private static TextTranslation.Language _slovak; private static MethodInfo _tryGetLanguage; private static object _utility; private static bool IsActive() { if (TextTranslation.s_theTable == null) return false; var current = TextTranslation.Get().GetLanguage(); if (_resolved) return current == _slovak; if (_utility == null) { var type = SlovakTranslation.LocalizationApi?.GetType().Assembly.GetType("LocalizationUtility.LocalizationUtility"); if (type == null) return false; _utility = type.GetField("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null); foreach (var m in type.GetMethods()) { var p = m.GetParameters(); if (m.Name == "TryGetLanguage" && p.Length == 2 && p[0].ParameterType == typeof(TextTranslation.Language)) { _tryGetLanguage = m; break; } } if (_utility == null || _tryGetLanguage == null) return false; } var args = new object[] { current, null }; if (!(bool)_tryGetLanguage.Invoke(_utility, args) || args[1] == null) return false; var name = args[1].GetType().GetProperty("Name")?.GetValue(args[1], null) as string; if (name != SlovakTranslation.LanguageName) return false; _slovak = current; _resolved = true; return true; } #endregion } /// /// Every place the game hands out a font that is not the language font. Each one /// runs through , which is a no-op unless the font /// really is missing Slovak glyphs. /// [HarmonyPatch] internal static class SlovakFontPatches { [HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetMenuFont))] public static void MenuFont(ref Font __result) => __result = SlovakFont.Fix(__result); [HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetTranslatorFont))] public static void TranslatorFont(ref Font __result) => __result = SlovakFont.Fix(__result); [HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetShipLogFont))] public static void ShipLogFont(ref Font __result) => __result = SlovakFont.Fix(__result); [HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetShipLogCardFont))] public static void ShipLogCardFont(ref Font __result) => __result = SlovakFont.Fix(__result); // Prompts ("Podrž", "Pokračovať"…) cache their font in a private field and then // rebuild, so patch after the fact and rebuild once more. [HarmonyPostfix, HarmonyPatch(typeof(PromptManager), "OnLanguageChanged")] public static void PromptFont(PromptManager __instance) { var field = AccessTools.Field(typeof(PromptManager), "_currentFont"); var current = field.GetValue(__instance) as Font; var fixedFont = SlovakFont.Fix(current); if (fixedFont == current) return; field.SetValue(__instance, fixedFont); AccessTools.Method(typeof(PromptManager), "RebuildUI")?.Invoke(__instance, null); } [HarmonyPostfix, HarmonyPatch(typeof(GameOverController), nameof(GameOverController.SetupGameOverScreen))] public static void DeathFont(GameOverController __instance) { var text = AccessTools.Field(typeof(GameOverController), "_deathText").GetValue(__instance) as Text; if (text != null) text.font = SlovakFont.Fix(text.font); } } }