Skip to content

Commit 83d7b19

Browse files
committed
feat: add support for standalone language packs
Load translated text and fonts from folder or ZIP language packs. Persist custom selections and fall back to English when a pack is unavailable.
1 parent 48409c5 commit 83d7b19

8 files changed

Lines changed: 741 additions & 27 deletions

File tree

‎Docs/createmods.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ Code mods should use `FezGame.Tools.ModText` to read their custom keys. `GetStri
5656
var dialogue = ModText.GetString("MY_MOD_INTRO");
5757
```
5858

59+
### Creating a standalone language pack
60+
61+
Language packs are independent of mods. Put a folder or ZIP archive in `FEZ/Languages`; a pack must contain `Language.xml` and an `Assets` directory. ZIP archives contain the pack root directly, rather than an additional enclosing directory.
62+
63+
```xml
64+
<Language>
65+
<Code>nl</Code>
66+
<DisplayName>Dutch</DisplayName>
67+
<Locale>LanguageDutch</Locale>
68+
<SmallFont>Fonts\Latin Small</SmallFont>
69+
<BigFont>Fonts\Latin Big</BigFont>
70+
</Language>
71+
```
72+
73+
`Code` is a required lowercase two-letter code. `DisplayName` is the stable English name stored in settings. `Locale` is an optional StaticText key, such as `LanguageDutch`, used to localize the slider label; when it is absent or untranslated, `DisplayName` is used. `SmallFont` and `BigFont` are required asset paths, relative to `Assets`, without the `.xnb` extension. The declared font assets must exist in the pack.
74+
75+
Place translated FEZ resources at these normal asset paths:
76+
77+
- `Assets/Resources/StaticText.xnb`
78+
- `Assets/Resources/GameText.xnb`
79+
- `Assets/Resources/CreditsText.xnb`
80+
81+
Each resource uses FEZ's existing `Dictionary<string, Dictionary<string, string>>` format. Use the pack code (`nl` in the example) as its localized dictionary key. Missing resource keys fall back to FEZ's original language text.
82+
5983
## Creating custom logic mod
6084

6185
Mod loader loads library file given in metadata as an assembly, then attempts to create instances of every non-abstract public class extending the `GameComponent` class before initialization (before any services are created). After the game has been initialized (that is, as soon as all necessary services are initiated), it adds created instances into the list of game's components and initializes them, allowing their `Update` and `Draw` (use `DrawableGameComponent`) to be properly executed within the game's loop.

‎Plugin/Patches/MenuItemPatch.cs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
using FezEngine.Tools;
4+
using HatModLoader.Source;
5+
using HatModLoader.Source.Languages;
6+
7+
namespace FezGame.Structure
8+
{
9+
internal class patch_MenuItem<T>
10+
{
11+
[MethodImpl(MethodImplOptions.ForwardRef)]
12+
public extern void orig_Slide(int direction);
13+
14+
public void Slide(int direction)
15+
{
16+
if (typeof(T) != typeof(Language) || !IsLanguageSlider(this))
17+
{
18+
orig_Slide(direction);
19+
return;
20+
}
21+
22+
var language = (Language)GetSliderValue(this);
23+
var nextLanguage = Hat.Instance.LanguagePacks.ChangeLanguage(language, direction);
24+
SetLanguageToSet(this, nextLanguage);
25+
}
26+
27+
[MethodImpl(MethodImplOptions.ForwardRef)]
28+
public extern void orig_OnSelected();
29+
30+
public void OnSelected()
31+
{
32+
orig_OnSelected();
33+
if (typeof(T) == typeof(Language) && IsLanguageSlider(this))
34+
{
35+
// FEZ serializes custom enum values as null, so correct the file after saving.
36+
SettingsManager.Save();
37+
LanguagePackManager.PersistSelectedLanguage();
38+
}
39+
}
40+
41+
private static bool IsLanguageSlider(object item)
42+
{
43+
var type = item.GetType();
44+
var localized = (bool)type.GetProperty("LocalizeSliderValue")!.GetValue(item);
45+
var format = (string)type.GetProperty("LocalizationTagFormat")!.GetValue(item);
46+
return localized && string.Equals(format, "Language{0}", StringComparison.Ordinal);
47+
}
48+
49+
private static object GetSliderValue(object item)
50+
{
51+
var getter = (Delegate)item.GetType().GetField("SliderValueGetter")!.GetValue(item);
52+
return getter.DynamicInvoke();
53+
}
54+
55+
private static void SetLanguageToSet(object item, Language language)
56+
{
57+
var getter = (Delegate)item.GetType().GetField("SliderValueGetter")!.GetValue(item);
58+
var field = getter.Target.GetType().GetField("languageToSet",
59+
BindingFlags.Instance | BindingFlags.NonPublic);
60+
field!.SetValue(getter.Target, language);
61+
}
62+
}
63+
}

‎Plugin/Patches/Program.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using HatModLoader.Source;
33
using System.Globalization;
44
using System.Runtime.CompilerServices;
5+
using HatModLoader.Source.Languages;
56

67
namespace FezGame
78
{
@@ -15,6 +16,9 @@ private static void Main(string[] args)
1516
// Ensuring that required dependencies can be resolved before anything else.
1617
Hat.RegisterRequiredDependencyResolvers();
1718

19+
// Resolve a pack display name before FEZ deserializes its settings enum.
20+
LanguagePackManager.RestorePersistedLanguage();
21+
1822
// Ensure uniform culture
1923
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
2024
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
@@ -24,6 +28,9 @@ private static void Main(string[] args)
2428
// game is shutting down. We want to keep track of it.
2529

2630
Logger.Try(orig_Main, args);
31+
32+
// FEZ's final save writes custom enum values as null.
33+
LanguagePackManager.PersistSelectedLanguage();
2734
}
2835
}
2936
}

‎Plugin/Patches/TextPatch.cs‎

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ public static class patch_StaticText
2626
public static extern bool orig_TryGetString(string tag, out string text);
2727
public static bool TryGetString(string tag, out string text)
2828
{
29-
return Hat.Instance.TextResources.TryGetString(tag, TextLookupScope.Localized, out text) || orig_TryGetString(tag, out text);
29+
return Hat.Instance.TextResources.TryGetString(tag, TextLookupScope.Localized, out text) ||
30+
Hat.Instance.LanguagePacks.TryGetDisplayName(tag, out text) ||
31+
Hat.Instance.LanguagePacks.TryGetText("Resources\\StaticText", tag, out text) ||
32+
orig_TryGetString(tag, out text);
3033
}
3134

3235
[MethodImpl(MethodImplOptions.ForwardRef)]
@@ -50,27 +53,21 @@ public static class patch_GameText
5053
public static extern string orig_GetString(string tag);
5154
public static string GetString(string tag)
5255
{
53-
if (TextPatch.TryGetRawString(tag, out var text))
54-
{
55-
return text;
56-
}
57-
58-
return ModText.TryGetString(tag, out text)
59-
? text :
60-
orig_GetString(tag);
56+
return TextPatch.TryGetRawString(tag, out var text) ||
57+
ModText.TryGetString(tag, out text) ||
58+
Hat.Instance.LanguagePacks.TryGetText("Resources\\GameText", tag, out text)
59+
? text
60+
: orig_GetString(tag);
6161
}
6262

6363
[MethodImpl(MethodImplOptions.ForwardRef)]
6464
public static extern string orig_GetStringRaw(string tag);
6565
public static string GetStringRaw(string tag)
6666
{
67-
if (TextPatch.TryGetRawString(tag, out var text))
68-
{
69-
return text;
70-
}
71-
72-
return ModText.TryGetStringRaw(tag, out text)
73-
? text
67+
return TextPatch.TryGetRawString(tag, out var text) ||
68+
ModText.TryGetStringRaw(tag, out text) ||
69+
Hat.Instance.LanguagePacks.TryGetText("Resources\\GameText", tag, out text)
70+
? text
7471
: orig_GetStringRaw(tag);
7572
}
7673
}
@@ -81,13 +78,10 @@ public static class patch_CreditsText
8178
public static extern string orig_GetString(string tag);
8279
public static string GetString(string tag)
8380
{
84-
if (TextPatch.TryGetRawString(tag, out var text))
85-
{
86-
return text;
87-
}
88-
89-
return ModText.TryGetStringRaw(tag, out text)
90-
? text
81+
return TextPatch.TryGetRawString(tag, out var text) ||
82+
ModText.TryGetStringRaw(tag, out text) ||
83+
Hat.Instance.LanguagePacks.TryGetText("Resources\\CreditsText", tag, out text)
84+
? text
9185
: orig_GetString(tag);
9286
}
9387
}

‎Plugin/Source/Hat.cs‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using HatModLoader.Source.AssemblyResolving;
77
using HatModLoader.Source.Assets;
88
using HatModLoader.Source.FileProxies;
9+
using HatModLoader.Source.Languages;
910
using HatModLoader.Source.ModDefinition;
1011
using HatModLoader.Source.Worlds;
1112

@@ -25,6 +26,8 @@ public class Hat
2526

2627
internal TextResourceManager TextResources { get; }
2728

29+
internal LanguagePackManager LanguagePacks { get; }
30+
2831
public WorldsManifest Worlds { get; private set; }
2932

3033
public int InvalidModsCount { get; private set; }
@@ -50,6 +53,7 @@ public Hat(Fez fez)
5053
Instance = this;
5154
_fezGame = fez;
5255
TextResources = new TextResourceManager(this);
56+
LanguagePacks = new LanguagePackManager();
5357
AssetManager = new AssetManager(this);
5458
AssetManager.InitializeHooks();
5559
}
@@ -64,13 +68,12 @@ public void Initialize()
6468
{
6569
ResolveDependencies(mods);
6670
LoadMods();
67-
Worlds = new WorldsManifest(Mods);
68-
InitializeAssemblies();
69-
return; // HAT initialized
7071
}
7172
}
7273

73-
Logger.Log("HAT", LogSeverity.Warning, "Skip the initialization process...");
74+
LanguagePacks.Load();
75+
Worlds = new WorldsManifest(Mods);
76+
InitializeAssemblies();
7477
}
7578

7679
private static bool GetModProxies(out IEnumerable<IFileProxy> proxies)
@@ -175,6 +178,7 @@ public void InitializeComponents()
175178

176179
public void OnGameActivated()
177180
{
181+
LanguagePacks.ReloadActivePack();
178182
var changedAssets = new List<Asset>();
179183
foreach (var mod in Mods)
180184
{
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Xml.Serialization;
2+
3+
namespace HatModLoader.Source.Languages
4+
{
5+
[Serializable]
6+
[XmlRoot("Language")]
7+
public class LanguageMetadata
8+
{
9+
public string Code { get; set; }
10+
11+
public string DisplayName { get; set; }
12+
13+
public string Locale { get; set; }
14+
15+
public string SmallFont { get; set; }
16+
17+
public string BigFont { get; set; }
18+
}
19+
}

0 commit comments

Comments
 (0)