-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLocaleInjectionPatch.cs
More file actions
190 lines (157 loc) · 6.84 KB
/
LocaleInjectionPatch.cs
File metadata and controls
190 lines (157 loc) · 6.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
using System.Diagnostics.CodeAnalysis;
using BepInEx;
using BepInEx.NET.Common;
using Elements.Assets;
using FrooxEngine;
using HarmonyLib;
namespace BepisLocaleLoader;
/// <summary>
/// Harmony patch that injects mod locales immediately after Resonite loads base locale files.
/// This eliminates race conditions by hooking directly into the locale loading flow.
/// </summary>
[HarmonyPatch(typeof(FrooxEngine.LocaleResource), "LoadTargetVariant")]
internal static class LocaleInjectionPatch
{
private static readonly object _injectionLock = new();
private static string _lastInjectedLocale = string.Empty;
private static DateTime _lastInjectionTime = DateTime.MinValue;
private static readonly TimeSpan _deduplicationWindow = TimeSpan.FromMilliseconds(500);
/// <summary>
/// Postfix that runs after LoadTargetVariant completes.
/// Waits for the async method to finish, then injects all mod locales.
/// </summary>
[HarmonyPostfix]
private static async void Postfix(FrooxEngine.LocaleResource __instance, Task __result, LocaleVariantDescriptor? variant)
{
try
{
await __result.ConfigureAwait(false);
if (__instance.Data == null)
{
Plugin.Log.LogWarning("LoadTargetVariant completed but Data is null - skipping locale injection");
return;
}
string targetLocale = variant?.LocaleCode ?? "en";
// Skip injection for temporary refresh triggers (RML uses "-" to force locale reload)
if (targetLocale == "-")
{
Plugin.Log.LogDebug("Skipping locale injection for refresh trigger (target: -)");
return;
}
lock (_injectionLock)
{
var now = DateTime.UtcNow;
if (_lastInjectedLocale == targetLocale && (now - _lastInjectionTime) < _deduplicationWindow)
{
Plugin.Log.LogDebug($"Skipping duplicate injection for {targetLocale} (called {(now - _lastInjectionTime).TotalMilliseconds:F0}ms after previous)");
return;
}
Plugin.Log.LogDebug($"Injecting mod locales after LoadTargetVariant completed (target: {targetLocale})");
_lastInjectedLocale = targetLocale;
_lastInjectionTime = now;
InjectAllPluginLocales(__instance.Data, targetLocale);
}
}
catch (Exception ex)
{
Plugin.Log.LogError($"Failed to inject mod locales: {ex}");
}
}
/// <summary>
/// Discovers and injects locale files from all BepInEx plugins.
/// </summary>
private static void InjectAllPluginLocales(Elements.Assets.LocaleResource localeData, string targetLocale)
{
if (NetChainloader.Instance?.Plugins == null || NetChainloader.Instance.Plugins.Count == 0)
{
Plugin.Log.LogDebug("No BepInEx plugins loaded - skipping locale injection");
return;
}
int pluginCount = 0;
int messageCount = 0;
foreach (var plugin in NetChainloader.Instance.Plugins.Values)
{
var localeFiles = LocaleLoader.GetPluginLocaleFiles(plugin).ToList();
if (localeFiles.Count == 0)
continue;
Plugin.Log.LogDebug($"Loading locales from {plugin.Metadata?.GUID ?? "unknown"}");
var candidates = LoadLocaleFiles(localeFiles);
var toInject = SelectMatchingLocales(candidates, targetLocale, out bool usingFallback);
messageCount += InjectAndLogLocales(localeData, toInject, usingFallback);
LocaleLoader.TrackPluginWithLocale(plugin);
pluginCount++;
}
if (pluginCount > 0)
{
Plugin.Log.LogInfo($"Injected {messageCount} locale messages from {pluginCount} plugins");
}
}
/// <summary>
/// Loads locale data from the specified list of locale files.
/// </summary>
private static List<(string Path, LocaleData Data)> LoadLocaleFiles(List<string> localeFiles)
{
var candidates = new List<(string Path, LocaleData Data)>();
foreach (string file in localeFiles)
{
var data = LocaleLoader.LoadLocaleDataFromFile(file);
if (data != null)
{
candidates.Add((file, data));
}
}
return candidates;
}
/// <summary>
/// Selects locale data that matches the target locale, with fallback to English if no matches are found.
/// </summary>
private static List<(string Path, LocaleData Data)> SelectMatchingLocales(
List<(string Path, LocaleData Data)> candidates,
string targetLocale,
out bool usingFallback)
{
var matches = candidates.Where(c => IsLocaleMatch(c.Data.LocaleCode, targetLocale)).ToList();
usingFallback = false;
if (matches.Count == 0 && !IsLocaleMatch(targetLocale, "en"))
{
matches = candidates.Where(c => IsLocaleMatch(c.Data.LocaleCode, "en")).ToList();
usingFallback = true;
}
return matches;
}
/// <summary>
/// Injects the selected locale data into the locale resource and logs the results.
/// </summary>
private static int InjectAndLogLocales(
Elements.Assets.LocaleResource localeData,
List<(string Path, LocaleData Data)> toInject,
bool usingFallback)
{
int messageCount = 0;
foreach (var (file, data) in toInject)
{
localeData.LoadDataAdditively(data);
messageCount += data.Messages.Count;
string fileLocale = data.LocaleCode ?? "unknown";
string fallbackSuffix = usingFallback ? " (fallback)" : string.Empty;
Plugin.Log.LogDebug($" - {Path.GetFileName(file)}: {fileLocale}, {data.Messages.Count} messages{fallbackSuffix}");
}
return messageCount;
}
/// <summary>
/// Checks if the file's locale matches the target locale.
/// Handles cases like "en-US" matching "en", or exact matches.
/// </summary>
private static bool IsLocaleMatch(string fileLocale, string targetLocale)
{
if (string.IsNullOrEmpty(fileLocale) || string.IsNullOrEmpty(targetLocale))
return false;
fileLocale = fileLocale.ToLowerInvariant();
targetLocale = targetLocale.ToLowerInvariant();
if (fileLocale == targetLocale)
return true;
string fileBase = Elements.Assets.LocaleResource.GetMainLanguage(fileLocale);
string targetBase = Elements.Assets.LocaleResource.GetMainLanguage(targetLocale);
return fileBase == targetBase;
}
}