using HarmonyLib;
using OWML.Common;
using OWML.ModHelper;
using System.Collections.Generic;
using System.Text;
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:
///
/// Adobe - SerifGothicStd (+ExtraBold) menus 253 glyphs — no carons
/// Gill Sans MT (+Menu, Bold) HUD prompts 243 glyphs — no carons
/// ParaType - Futura PT Medium dialogue 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 it also flips TextTranslation.IsLanguageLatin() to
/// false, which drags dialogue, the translator and the ship log off their
/// proper fonts. And UIStyleManager.GetMenuFont() — the obvious hook —
/// is dead code with no callers; the fonts are assigned straight onto the
/// components in the scenes.
///
/// So we substitute at the Text level, but the decision is made **per font, not
/// per label**: a font that cannot draw Slovak is replaced everywhere it is
/// used, so a menu never mixes two typefaces. Fonts that can draw Slovak —
/// the dialogue and translator fonts — are left completely alone.
///
/// The replacement is preferably the game's *own* dynamic cut of the same
/// typeface (it ships several, e.g. Adobe - SerifGothicStd_Dynamic), which keeps
/// the menu pixel-identical to vanilla. Bundled Cabin is the fallback.
///
internal static class SlovakFont
{
/// Cabin (SIL OFL 1.1), a humanist sans in the Gill Sans lineage, used
/// where the game has no dynamic cut of its own. Dynamic, so it can draw any
/// glyph Slovak needs.
private static Font _font;
/// The letters Slovak adds on top of plain ASCII. Only these are worth
/// probing — anything else in the text was already fine in English.
private const string SlovakLetters = "áäčďéíĺľňóôŕšťúýžÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽ";
/// Stylised display faces (the ship's readouts, seven-segment digits).
/// Replacing these wholesale would cost more in looks than it gains, so they
/// are only substituted on the individual labels that actually need a glyph.
private static readonly string[] Stylised = { "VCR_OSD_MONO", "digital-7", "PCBius" };
/// Font instance ID → the Slovak letters that font cannot draw.
private static readonly Dictionary _gaps = new Dictionary();
/// Labels we substituted, and the font the game wanted on them.
private static readonly Dictionary _swapped = new Dictionary();
/// Original font instance ID → what we put in its place.
private static readonly Dictionary _substitutes = new Dictionary();
/// The game's own dynamic fonts, which can draw glyphs their static
/// siblings were never baked with.
private static Font[] _gameDynamicFonts = new Font[0];
/// When set, a static font is replaced by the game's own dynamic cut of the
/// same typeface where one exists, keeping the vanilla look. Cabin otherwise.
internal static bool PreferGameTypeface = true;
private static ModBehaviour _mod;
internal static bool Loaded => _font != null;
internal static void Load(ModBehaviour mod)
{
_mod = 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;
Object.DontDestroyOnLoad(font);
break;
}
if (_font == null)
mod.ModHelper.Console.WriteLine("Font bundle has no 'SlovakUI' font.", MessageType.Error);
}
/// Give a font that can draw Slovak, or hand back
/// the game's own font once it is no longer needed.
internal static void Apply(Text text)
{
if (_font == null || text == null) return;
var current = text.font;
Font stored;
var swapped = _swapped.TryGetValue(text, out stored) && stored != null &&
current == SubstituteFor(stored);
// If anything else assigned a font since our swap, that is the new baseline.
var wanted = swapped ? stored : current;
if (!swapped) _swapped.Remove(text);
if (wanted == null) return;
var needsHelp = NeedsSubstitute(wanted, text.text);
if (needsHelp == swapped) return;
if (needsHelp)
{
_swapped[text] = wanted;
text.font = SubstituteFor(wanted);
}
else
{
_swapped.Remove(text);
text.font = wanted;
}
}
/// What to draw 's labels with instead. The
/// game ships dynamic cuts of several of its typefaces; borrowing the matching one
/// keeps the menu looking exactly like vanilla, which no substitute font can.
private static Font SubstituteFor(Font original)
{
Font cached;
if (_substitutes.TryGetValue(original.GetInstanceID(), out cached) && cached != null) return cached;
if (!PreferGameTypeface) return _font;
if (_gameDynamicFonts.Length == 0) RefreshGameFonts();
Font best = null;
var bestLength = 0;
foreach (var candidate in _gameDynamicFonts)
{
var baseName = candidate.name.EndsWith("_Dynamic")
? candidate.name.Substring(0, candidate.name.Length - "_Dynamic".Length)
: candidate.name;
if (baseName.Length <= bestLength) continue;
if (!original.name.StartsWith(baseName, System.StringComparison.Ordinal)) continue;
if (GapsOf(candidate).Length != 0) continue;
best = candidate;
bestLength = baseName.Length;
}
if (best == null) return _font;
_substitutes[original.GetInstanceID()] = best;
_mod?.ModHelper.Console.WriteLine(
$"drawing \"{original.name}\" with the game's own \"{best.name}\"", MessageType.Info);
return best;
}
private static void RefreshGameFonts()
{
var found = new List();
foreach (var font in Resources.FindObjectsOfTypeAll())
{
if (font != null && font.dynamic && font != _font) found.Add(font);
}
_gameDynamicFonts = found.ToArray();
}
/// Drop every cached decision, so a settings change takes effect at once.
internal static void ForgetChoices()
{
_substitutes.Clear();
_gameDynamicFonts = new Font[0];
foreach (var pair in _swapped)
{
if (pair.Key != null && pair.Value != null) pair.Key.font = pair.Value;
}
_swapped.Clear();
}
/// Re-check every live label. Scene loads and language changes reassign
/// fonts wholesale, and menus are built lazily, so sweep periodically too.
internal static void ApplyToAll()
{
if (_font == null) return;
// Labels that have since been destroyed would otherwise pile up.
var dead = new List();
foreach (var pair in _swapped)
{
if (pair.Key == null) dead.Add(pair.Key);
}
foreach (var text in dead) _swapped.Remove(text);
RefreshGameFonts();
foreach (var text in Object.FindObjectsOfType()) Apply(text);
}
private static bool NeedsSubstitute(Font font, string content)
{
if (!CustomLanguageActive()) return false;
var gaps = GapsOf(font);
if (gaps.Length == 0) return false;
// Ordinary UI faces are replaced everywhere they appear, so a menu never
// mixes typefaces. Stylised readouts only give way when a glyph is at stake.
if (!IsStylised(font.name)) return true;
if (string.IsNullOrEmpty(content)) return false;
foreach (var c in content)
{
if (gaps.IndexOf(c) >= 0) return true;
}
return false;
}
private static string GapsOf(Font font)
{
var id = font.GetInstanceID();
string gaps;
if (_gaps.TryGetValue(id, out gaps)) return gaps;
// Dynamic fonts only report a character as present once it has been
// rasterised, so ask for the whole Slovak set before probing.
if (font.dynamic) font.RequestCharactersInTexture(SlovakLetters);
var builder = new StringBuilder();
foreach (var c in SlovakLetters)
{
if (!font.HasCharacter(c)) builder.Append(c);
}
gaps = builder.ToString();
_gaps[id] = gaps;
if (gaps.Length > 0)
{
_mod?.ModHelper.Console.WriteLine(
$"font \"{font.name}\" cannot draw \"{gaps}\" — " +
(IsStylised(font.name) ? "substituting only where needed" : "substituting everywhere"),
MessageType.Info);
}
return gaps;
}
private static bool IsStylised(string fontName)
{
foreach (var stylised in Stylised)
{
if (fontName.IndexOf(stylised, System.StringComparison.OrdinalIgnoreCase) >= 0) return true;
}
return false;
}
/// True while a mod-registered language is selected. The Localization
/// Utility appends custom languages past Language.TOTAL, and treats
/// everything up to it as vanilla — same test it uses itself.
private static bool CustomLanguageActive()
{
if (TextTranslation.s_theTable == null) return false;
return TextTranslation.Get().GetLanguage() > TextTranslation.Language.TOTAL;
}
}
///
/// Hooks: every label as it appears or changes, plus a sweep whenever the game
/// reassigns fonts wholesale.
///
[HarmonyPatch]
internal static class SlovakFontPatches
{
[HarmonyPostfix, HarmonyPatch(typeof(Text), "OnEnable")]
public static void TextEnabled(Text __instance) => SlovakFont.Apply(__instance);
[HarmonyPostfix, HarmonyPatch(typeof(Text), "text", MethodType.Setter)]
public static void TextChanged(Text __instance) => SlovakFont.Apply(__instance);
[HarmonyPostfix, HarmonyPatch(typeof(TextTranslation), nameof(TextTranslation.SetLanguage))]
public static void LanguageChanged() => SlovakFont.ApplyToAll();
}
}