Render Slovak carons in menus and HUD without disturbing the other text
The stock menu font (Gill Sans MT Menu) is a static 243-glyph atlas with no Latin Extended-A, so "Pokračovať" rendered as "Pokra ova" and "Podrž" as "Podr". Dialogue, the Nomai translator and the ship log were always fine — they use Futura PT (663) and Space Mono (612), both of which cover Slovak. Registering a language font via AddLanguageFont fixes the menus but flips TextTranslation.IsLanguageLatin() to false, and the game uses that flag to decide whether dialogue, the translator and the ship log keep their own fonts or fall back to the language font. So the blanket fix dragged all three onto the wrong font, oversized and clipping. Instead: register no language font at all, and postfix only the font getters (UIStyleManager, PromptManager, GameOverController). Each result goes through SlovakFont.Fix, which probes the font with Font.HasCharacter and returns it untouched unless it genuinely cannot draw Slovak — so nothing that already renders correctly is ever replaced, and the patches are inert in every other language. The substitute is Cabin (SIL OFL 1.1), a humanist sans in the Gill Sans lineage, shipped as a dynamic font with its vertical metrics rewritten to Gill Sans MT's exact ratios so line height matches and text does not clip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+186
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>TextTranslation.IsLanguageLatin()</c> 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
|
||||
/// <c>Font.HasCharacter</c>, so we never touch a font that renders fine.
|
||||
/// </summary>
|
||||
internal static class SlovakFont
|
||||
{
|
||||
/// <summary>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.</summary>
|
||||
private static Font _font;
|
||||
|
||||
/// <summary>Characters the vanilla menu atlas is missing. If a font can draw all
|
||||
/// of these it is left completely alone.</summary>
|
||||
private static readonly char[] Probe = { 'č', 'ď', 'ĺ', 'ľ', 'ň', 'ŕ', 'ť', 'ž', 'ô', 'ä' };
|
||||
|
||||
private static readonly Dictionary<int, bool> _coverage = new Dictionary<int, bool>();
|
||||
|
||||
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<Font>())
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Returns <paramref name="font"/> unchanged unless Slovenčina is the
|
||||
/// active language and that font cannot draw Slovak.</summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every place the game hands out a font that is not the language font. Each one
|
||||
/// runs through <see cref="SlovakFont.Fix"/>, which is a no-op unless the font
|
||||
/// really is missing Slovak glyphs.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-5
@@ -1,3 +1,4 @@
|
||||
using HarmonyLib;
|
||||
using OWML.Common;
|
||||
using OWML.ModHelper;
|
||||
|
||||
@@ -6,21 +7,29 @@ namespace SlovakTranslation
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="SlovakFont"/>).
|
||||
/// </summary>
|
||||
public class SlovakTranslation : ModBehaviour
|
||||
{
|
||||
public static SlovakTranslation Instance;
|
||||
|
||||
/// <summary>Name the language is registered (and displayed) under.</summary>
|
||||
public const string LanguageName = "Slovenčina";
|
||||
|
||||
/// <summary>Path (relative to the mod folder) to the translated strings.</summary>
|
||||
public static string translationFile = "assets/Translation.xml";
|
||||
|
||||
/// <summary>The Localization Utility's API, kept for reflection in <see cref="SlovakFont"/>.</summary>
|
||||
internal static ILocalizationAPI LocalizationApi;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
var api = ModHelper.Interaction.TryGetModApi<ILocalizationAPI>("xen.LocalizationUtility");
|
||||
if (api == null)
|
||||
LocalizationApi = ModHelper.Interaction.TryGetModApi<ILocalizationAPI>("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +35,8 @@
|
||||
<None Update="assets/Translation.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="assets/slovakfont">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
Reference in New Issue
Block a user