-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathTeletextHandler.cs
More file actions
413 lines (352 loc) · 16.9 KB
/
TeletextHandler.cs
File metadata and controls
413 lines (352 loc) · 16.9 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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MWC_Localization_Core
{
/// <summary>
/// Handles direct translation of Teletext/TV content by modifying underlying data sources
/// This is MUCH more efficient than constantly updating TextMesh components
/// Based on My Summer Car's ExtraMod.cs approach
///
/// Supports category-based translations from translate_teletext.txt:
/// [day]
/// Monday = Monday (localized)
/// [kotimaa]
/// News headline = News headline (localized)
///
/// NOTE: FSM pattern matching moved to unified PatternMatcher system
/// </summary>
public class TeletextHandler
{
// Category-based translations: [referenceName][originalText] = translatedText (for key-based lookup)
private Dictionary<string, Dictionary<string, string>> categoryTranslations =
new Dictionary<string, Dictionary<string, string>>();
// Index-based translations: [categoryName][index] = translatedText (for runtime replacement)
private Dictionary<string, List<string>> indexBasedTranslations =
new Dictionary<string, List<string>>();
// Track which arrays have been translated already
private HashSet<string> translatedArrays = new HashSet<string>();
// GameObject path to category mapping
private Dictionary<string, string> pathPrefixes = new Dictionary<string, string>
{
{ "Systems/TV/Teletext/VKTekstiTV/Database", "" }, // Use referenceName directly
{ "Systems/TV/ChatMessages", "ChatMessages" }, // Prefix with "ChatMessages."
{ "Systems/TV/TVGraphics/CHAT/Day", "Chat.Day" } // Prefix with "Chat.Day."
};
// Path Prefix Proxy cache
private Dictionary<string, PlayMakerArrayListProxy[]> proxyCache =
new Dictionary<string, PlayMakerArrayListProxy[]>();
public TeletextHandler()
{
}
/// <summary>
/// Load teletext translations from INI-style file with category sections
/// Supports both key-value pairs and index-based translations (in order)
/// </summary>
public void LoadTeletextTranslations(string filePath)
{
if (!System.IO.File.Exists(filePath))
{
CoreConsole.Warning($"Teletext translation file not found: {filePath}");
return;
}
try
{
categoryTranslations.Clear();
indexBasedTranslations.Clear();
string currentCategory = null;
Dictionary<string, string> currentDict = null;
List<string> currentIndexList = null;
int loadedCount = 0;
string[] lines = System.IO.File.ReadAllLines(filePath, System.Text.Encoding.UTF8);
List<string> keyLines = new List<string>();
List<string> valueLines = new List<string>();
bool readingValue = false;
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
string trimmed = line.Trim();
// Skip comments
if (trimmed.StartsWith("#"))
continue;
// Check for category header [categoryName]
if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
{
// Save previous entry if exists
if (keyLines.Count > 0 && currentDict != null)
{
SaveEntry(currentDict, currentIndexList, keyLines, valueLines, ref loadedCount);
}
keyLines.Clear();
valueLines.Clear();
readingValue = false;
currentCategory = trimmed.Substring(1, trimmed.Length - 2);
if (!categoryTranslations.ContainsKey(currentCategory))
categoryTranslations[currentCategory] = new Dictionary<string, string>();
if (!indexBasedTranslations.ContainsKey(currentCategory))
indexBasedTranslations[currentCategory] = new List<string>();
currentDict = categoryTranslations[currentCategory];
currentIndexList = indexBasedTranslations[currentCategory];
continue;
}
// Skip empty lines outside of key/value context
if (string.IsNullOrEmpty(trimmed))
{
// Empty line between entries - save current entry
if (keyLines.Count > 0 && currentDict != null)
{
SaveEntry(currentDict, currentIndexList, keyLines, valueLines, ref loadedCount);
keyLines.Clear();
valueLines.Clear();
readingValue = false;
}
continue;
}
// Check if this line is just "=" (separator)
if (trimmed == "=")
{
readingValue = true;
continue;
}
// Check if line contains unescaped "=" (single-line format)
int equalsIndex = FindUnescapedEquals(line);
if (equalsIndex > 0 && !readingValue)
{
// Single-line format: KEY = VALUE
string key = line.Substring(0, equalsIndex).Trim();
string value = line.Substring(equalsIndex + 1).Trim();
// Unescape special characters
key = UnescapeString(key);
value = UnescapeString(value);
if (!string.IsNullOrEmpty(key) && currentDict != null)
{
currentDict[key] = value;
currentIndexList.Add(value); // Add to index list in order
loadedCount++;
}
continue;
}
// Accumulate lines for multi-line key or value
if (currentDict != null)
{
if (readingValue)
{
valueLines.Add(line);
}
else
{
keyLines.Add(line);
}
}
}
// Save last entry if exists
if (keyLines.Count > 0 && currentDict != null)
{
SaveEntry(currentDict, currentIndexList, keyLines, valueLines, ref loadedCount);
}
// Create alias: ChatMessages.Messages uses ChatMessages.All translations
if (categoryTranslations.ContainsKey("ChatMessages.All"))
{
categoryTranslations["ChatMessages.Messages"] = categoryTranslations["ChatMessages.All"];
}
CoreConsole.Print($"[TeletextHandler] Loaded {loadedCount} teletext translations across {categoryTranslations.Count} categories");
}
catch (System.Exception ex)
{
CoreConsole.Error($"[TeletextHandler] Error loading teletext translations: {ex.Message}");
}
}
/// <summary>
/// Find the index of the first unescaped '=' character
/// Returns -1 if no unescaped '=' is found
/// </summary>
private int FindUnescapedEquals(string line)
{
for (int i = 0; i < line.Length; i++)
{
if (line[i] == '=')
{
// Check if it's escaped (preceded by backslash)
if (i > 0 && line[i - 1] == '\\')
{
// This equals is escaped, skip it
continue;
}
return i;
}
}
return -1;
}
/// <summary>
/// Unescape special characters: \= -> =, \n -> newline
/// </summary>
private string UnescapeString(string input)
{
if (string.IsNullOrEmpty(input))
return input;
// Replace escape sequences
return input.Replace("\\=", "=").Replace("\\n", "\n");
}
/// <summary>
/// Helper method to save a multi-line entry
/// </summary>
private void SaveEntry(Dictionary<string, string> dict, List<string> indexList, List<string> keyLines, List<string> valueLines, ref int count)
{
if (keyLines.Count == 0) return;
// Join lines with newlines, preserving original formatting
string key = string.Join("\n", keyLines.ToArray());
string value = valueLines.Count > 0 ? string.Join("\n", valueLines.ToArray()) : "";
// Trim trailing/leading empty lines but preserve internal structure
key = key.Trim();
value = value.Trim();
// Unescape special characters in both key and value
key = UnescapeString(key);
value = UnescapeString(value);
if (!string.IsNullOrEmpty(key))
{
dict[key] = value;
indexList.Add(value); // Add to index list in order
count++;
}
}
/// <summary>
/// Monitor and translate teletext arrays
/// Returns number of new items translated
/// </summary>
public int MonitorAndTranslateArrays()
{
try
{
int totalTranslated = 0;
foreach (var pathPrefix in pathPrefixes.Keys)
{
PlayMakerArrayListProxy[] proxies;
if (!proxyCache.ContainsKey(pathPrefix))
{
// First time accessing this path - cache proxies
GameObject dataObject = MLCUtils.FindGameObjectCached(pathPrefix);
if (dataObject == null) continue;
proxies = dataObject.GetComponents<PlayMakerArrayListProxy>();
proxyCache[pathPrefix] = proxies;
}
else
{
// Use cached proxies
proxies = proxyCache[pathPrefix];
}
for (int i = 0; i < proxies.Length; i++)
{
string refName = proxies[i].referenceName;
if (string.IsNullOrEmpty(refName)) continue;
string prefix = pathPrefixes[pathPrefix];
string categoryName = string.IsNullOrEmpty(prefix) ? refName : $"{prefix}.{refName}";
// Create unique key for this array
string arrayKey = $"{pathPrefix}[{i}]:{refName}";
// Try translating only if not already done
if (!translatedArrays.Contains(arrayKey))
{
int translated = TranslateArrayListProxy(proxies[i], categoryName);
// Mark as processed
// ... if it doesn't require constant monitoring...
bool isDynamic = categoryName == "ChatMessages.Messages";
// ... or if the array is already populated ...
bool isPopulated = proxies[i] != null && proxies[i]._arrayList != null && proxies[i]._arrayList.Count > 0;
// ... or if there are no translations available (to avoid repeated checks)
bool isTranslationAvailable = categoryTranslations.ContainsKey(categoryName) &&
categoryTranslations[categoryName].Count > 0;
if (translated > 0 || isPopulated || !isTranslationAvailable)
{
if (translated > 0)
{
CoreConsole.Print($"[TeletextHandler] Translated '{categoryName}' with {translated} items");
totalTranslated += translated;
}
if (!isDynamic) translatedArrays.Add(arrayKey); // Mark as translated
}
}
}
}
return totalTranslated;
}
catch (System.Exception ex)
{
CoreConsole.Error($"[Teletext] Error monitoring teletext arrays: {ex.Message}");
return 0;
}
}
/// <summary>
/// Translate a single PlayMakerArrayListProxy component using key-value lookup
/// Loops through original array and looks up each element's translation
/// Falls back to index-based translation if exact key match fails
/// Preserves empty/null elements naturally
/// </summary>
private int TranslateArrayListProxy(PlayMakerArrayListProxy proxy, string categoryName)
{
if (proxy == null || proxy._arrayList == null)
return 0;
// Get translation dictionary for this category
if (!categoryTranslations.ContainsKey(categoryName))
return 0;
Dictionary<string, string> translations = categoryTranslations[categoryName];
if (translations.Count == 0)
return 0;
int translatedCount = 0;
int fallbackCount = 0;
try
{
// Translate array in-place by looking up each element
ArrayList arrayList = proxy._arrayList;
// Check if we have index-based translations as fallback
bool hasIndexFallback = indexBasedTranslations.ContainsKey(categoryName) &&
indexBasedTranslations[categoryName].Count > 0;
int nonEmptySourceIndex = -1;
for (int i = 0; i < arrayList.Count; i++)
{
// Skip null or empty elements
if (arrayList[i] == null)
continue;
string original = arrayList[i].ToString();
string normalizedOriginal = original.Trim();
if (string.IsNullOrEmpty(normalizedOriginal))
continue;
// Keep fallback alignment based on non-empty source entries only.
nonEmptySourceIndex++;
// Try exact key match first
if (translations.TryGetValue(normalizedOriginal, out string translation))
{
arrayList[i] = translation;
translatedCount++;
}
// Fallback: Use index-based translation if available
else if (hasIndexFallback && nonEmptySourceIndex < indexBasedTranslations[categoryName].Count)
{
string indexTranslation = indexBasedTranslations[categoryName][nonEmptySourceIndex];
if (!string.IsNullOrEmpty(indexTranslation))
{
arrayList[i] = indexTranslation;
translatedCount++;
fallbackCount++;
}
}
}
if (fallbackCount > 0)
{
CoreConsole.Print($"[TeletextHandler] '{categoryName}': Used index fallback for {fallbackCount} items");
}
}
catch (System.Exception ex)
{
CoreConsole.Error($"[TeletextHandler] Error translating category '{categoryName}': {ex.Message}");
}
return translatedCount;
}
/// <summary>
/// Reset translation state (useful for testing or scene changes)
/// </summary>
public void Reset()
{
translatedArrays.Clear();
proxyCache.Clear();
}
}
}