diff --git a/README.md b/README.md index 7fea0c1..03eba0f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ See [`CLAUDE.md`](CLAUDE.md) for the full conventions and repo layout. The **original mod code and the Slovak translation** in this repository are released under the [MIT License](LICENSE). +The bundled UI font in `assets/slovakfont` is **[Cabin](https://github.com/impallari/Cabin)** +by the Cabin Project Authors, used under the **SIL Open Font License 1.1** +(see [`assets/Cabin-OFL.txt`](assets/Cabin-OFL.txt)). It is only used for the few +menu/HUD elements whose stock font was never baked with Slovak caron glyphs — see +[`SlovakFont.cs`](SlovakFont.cs). + This is an **unofficial** fan project. It is **not affiliated with, approved by, or endorsed by Mobius Digital or Annapurna Interactive.** *Outer Wilds*, its story, dialogue, characters, and other assets are **© Mobius Digital / Annapurna diff --git a/SlovakFont.cs b/SlovakFont.cs new file mode 100644 index 0000000..526b068 --- /dev/null +++ b/SlovakFont.cs @@ -0,0 +1,186 @@ +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); + } + } +} diff --git a/SlovakTranslation.cs b/SlovakTranslation.cs index 49308af..580d54f 100644 --- a/SlovakTranslation.cs +++ b/SlovakTranslation.cs @@ -1,3 +1,4 @@ +using HarmonyLib; using OWML.Common; using OWML.ModHelper; @@ -6,21 +7,29 @@ namespace SlovakTranslation /// /// Registers Slovenčina as a selectable language via xen-42's Localization /// Utility (xen.LocalizationUtility). All the heavy lifting lives in the - /// utility; this mod just points it at our translated strings. + /// utility; this mod just points it at our translated strings and patches the + /// few menu/HUD fonts that were never baked with Slovak glyphs (see + /// ). /// public class SlovakTranslation : ModBehaviour { public static SlovakTranslation Instance; + /// Name the language is registered (and displayed) under. + public const string LanguageName = "Slovenčina"; + /// Path (relative to the mod folder) to the translated strings. public static string translationFile = "assets/Translation.xml"; + /// The Localization Utility's API, kept for reflection in . + internal static ILocalizationAPI LocalizationApi; + private void Start() { Instance = this; - var api = ModHelper.Interaction.TryGetModApi("xen.LocalizationUtility"); - if (api == null) + LocalizationApi = ModHelper.Interaction.TryGetModApi("xen.LocalizationUtility"); + if (LocalizationApi == null) { ModHelper.Console.WriteLine( "Could not find xen.LocalizationUtility — is Interplanetary Polyglot installed?", @@ -28,8 +37,18 @@ namespace SlovakTranslation return; } - api.RegisterLanguage(this, "Slovenčina", translationFile); - ModHelper.Console.WriteLine("Registered language: Slovenčina", MessageType.Success); + LocalizationApi.RegisterLanguage(this, LanguageName, translationFile); + ModHelper.Console.WriteLine("Registered language: " + LanguageName, MessageType.Success); + + // Deliberately *not* AddLanguageFont: that would make IsLanguageLatin false + // and pull dialogue, the translator and the ship log off their correct + // vanilla fonts. We only patch the fonts that can't draw Slovak. + SlovakFont.Load(this); + if (SlovakFont.Loaded) + { + new Harmony("Homer.SlovakTranslation").PatchAll(); + ModHelper.Console.WriteLine("Slovak menu/HUD font (Cabin) loaded and patched in.", MessageType.Success); + } } } } diff --git a/SlovakTranslation.csproj b/SlovakTranslation.csproj index 0cbfdc8..7ac0f69 100644 --- a/SlovakTranslation.csproj +++ b/SlovakTranslation.csproj @@ -35,5 +35,8 @@ PreserveNewest + + PreserveNewest + diff --git a/assets/Cabin-OFL.txt b/assets/Cabin-OFL.txt new file mode 100644 index 0000000..32dcc07 --- /dev/null +++ b/assets/Cabin-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Cabin Project Authors (https://github.com/impallari/Cabin.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/slovakfont b/assets/slovakfont new file mode 100644 index 0000000..dbbe243 Binary files /dev/null and b/assets/slovakfont differ