832a92783b
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>
187 lines
7.9 KiB
C#
187 lines
7.9 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|