Client LuaCsForBarotrauma
TextManager.cs
1 #nullable enable
2 
3 using Barotrauma.IO;
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Concurrent;
8 using System.Collections.Immutable;
9 using System.Linq;
10 using System.Xml.Linq;
11 using System.Globalization;
12 using System.Text.Unicode;
13 
14 namespace Barotrauma
15 {
16  public enum FormatCapitals
17  {
18  Yes, No
19  }
20 
21  public static class TextManager
22  {
23  public static bool DebugDraw;
24 
25  public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier();
26  public readonly static ConcurrentDictionary<LanguageIdentifier, ImmutableList<TextPack>> TextPacks = new ConcurrentDictionary<LanguageIdentifier, ImmutableList<TextPack>>();
27  public static IEnumerable<LanguageIdentifier> AvailableLanguages => TextPacks.Keys;
28 
29  private readonly static Dictionary<Identifier, WeakReference<TagLString>> cachedStrings =
30  new Dictionary<Identifier, WeakReference<TagLString>>();
31  private static ImmutableHashSet<Identifier> nonCacheableTags =
32  ImmutableHashSet<Identifier>.Empty;
33 
34  public static int LanguageVersion { get; private set; } = 0;
35 
36  private static ImmutableArray<Range<int>> UnicodeToIntRanges(params UnicodeRange[] ranges)
37  => ranges
38  .Select(r => new Range<int>(r.FirstCodePoint, r.FirstCodePoint + r.Length - 1))
39  .OrderBy(r => r.Start)
40  .ToImmutableArray();
41 
42  [Flags]
43  public enum SpeciallyHandledCharCategory
44  {
45  None = 0x0,
46 
47  CJK = 0x1,
48  Cyrillic = 0x2,
49  Japanese = 0x4,
50 
51  All = 0x7
52  }
53 
54  public static readonly ImmutableArray<SpeciallyHandledCharCategory> SpeciallyHandledCharCategories
55  = Enum.GetValues<SpeciallyHandledCharCategory>()
56  .Where(c => c is not (SpeciallyHandledCharCategory.None or SpeciallyHandledCharCategory.All))
57  .ToImmutableArray();
58 
59  private static readonly ImmutableDictionary<SpeciallyHandledCharCategory, ImmutableArray<Range<int>>> SpeciallyHandledCharacterRanges
60  = new[]
61  {
62  (SpeciallyHandledCharCategory.CJK, UnicodeToIntRanges(
63  UnicodeRanges.HalfwidthandFullwidthForms,
64  UnicodeRanges.HangulJamo,
65  UnicodeRanges.HangulCompatibilityJamo,
66  UnicodeRanges.CjkRadicalsSupplement,
67  UnicodeRanges.CjkSymbolsandPunctuation,
68  UnicodeRanges.EnclosedCjkLettersandMonths,
69  UnicodeRanges.CjkCompatibility,
70  UnicodeRanges.CjkUnifiedIdeographsExtensionA,
71  UnicodeRanges.CjkUnifiedIdeographs,
72  UnicodeRanges.HangulSyllables,
73  UnicodeRanges.CjkCompatibilityForms,
74  //not really CJK symbols, but these seem to be present in the CJK fonts but not in the default ones, so we can use them as a fallback
75  UnicodeRanges.BlockElements
76  )),
77  (SpeciallyHandledCharCategory.Japanese, UnicodeToIntRanges(
78  UnicodeRanges.Hiragana,
79  UnicodeRanges.Katakana
80  )),
81  (SpeciallyHandledCharCategory.Cyrillic, UnicodeToIntRanges(
82  UnicodeRanges.Cyrillic,
83  UnicodeRanges.CyrillicSupplement,
84  UnicodeRanges.CyrillicExtendedA,
85  UnicodeRanges.CyrillicExtendedB,
86  UnicodeRanges.CyrillicExtendedC
87  ))
88  }.ToImmutableDictionary();
89 
90  public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(LocalizedString text)
91  => GetSpeciallyHandledCategories(text.Value);
92 
93  public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(string text)
94  {
95  if (string.IsNullOrEmpty(text)) { return SpeciallyHandledCharCategory.None; }
96 
97  var retVal = SpeciallyHandledCharCategory.None;
98  for (int i = 0; i < text.Length; i++)
99  {
100  char chr = text[i];
101 
102  foreach (var category in SpeciallyHandledCharCategories)
103  {
104  if (retVal.HasFlag(category)) { continue; }
105 
106  for (int j = 0; j < SpeciallyHandledCharacterRanges[category].Length; j++)
107  {
108  var range = SpeciallyHandledCharacterRanges[category][j];
109 
110  // If chr < range.Start, we know that it can't
111  // be in any of the following ranges, so let's
112  // not even bother checking them
113  if (chr < range.Start) { break; }
114 
115  // This character is in a range, set the flag
116  if (range.Contains(chr))
117  {
118  retVal |= category;
119  break;
120  }
121  }
122  }
123 
124  if (retVal == SpeciallyHandledCharCategory.All)
125  {
126  // Input contains characters from all
127  // specially handled categories, there's
128  // no need to inspect the string further
129  return SpeciallyHandledCharCategory.All;
130  }
131  }
132  return retVal;
133  }
134 
135  public static bool IsCJK(LocalizedString text)
136  => IsCJK(text.Value);
137 
138  public static bool IsCJK(string text)
139  => GetSpeciallyHandledCategories(text).HasFlag(SpeciallyHandledCharCategory.CJK);
140 
144  public static void VerifyLanguageAvailable()
145  {
146  if (!TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
147  {
148  DebugConsole.ThrowError($"Could not find the language \"{GameSettings.CurrentConfig.Language}\". Trying to switch to English...");
149  var config = GameSettings.CurrentConfig;
150  config.Language = "English".ToLanguageIdentifier();;
151  GameSettings.SetCurrentConfig(config);
152  }
153  }
154 
155  public static bool ContainsTag(string tag)
156  {
157  return ContainsTag(tag.ToIdentifier());
158  }
159 
160  public static bool ContainsTag(Identifier tag)
161  {
162  return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag));
163  }
164 
165  public static bool ContainsTag(Identifier tag, LanguageIdentifier language)
166  {
167  return TextPacks[language].Any(p => p.Texts.ContainsKey(tag));
168  }
169  public static IEnumerable<string> GetAll(string tag)
170  => GetAll(tag.ToIdentifier());
171 
172  public static IEnumerable<string> GetAll(Identifier tag)
173  {
174  var allTexts = TextPacks[GameSettings.CurrentConfig.Language]
175  .SelectMany(p => p.Texts.TryGetValue(tag, out var value)
176  ? (IEnumerable<TextPack.Text>)value
177  : Array.Empty<TextPack.Text>()).ToList();
178 
179  var firstOverride = allTexts.FirstOrDefault(t => t.IsOverride);
180  if (firstOverride != default)
181  {
182  return allTexts.Where(t => t.IsOverride && t.TextPack == firstOverride.TextPack).Select(t => t.String);
183  }
184  else
185  {
186  return allTexts.Select(t => t.String);
187  }
188  }
189 
190  public static IEnumerable<KeyValuePair<Identifier, string>> GetAllTagTextPairs()
191  {
192  var allTexts = TextPacks[GameSettings.CurrentConfig.Language]
193  .SelectMany(p => p.Texts);
194 
195  foreach (var textList in allTexts)
196  {
197  var firstOverride = textList.Value.FirstOrDefault(t => t.IsOverride);
198  if (firstOverride != default)
199  {
200  //if there's any overrides for this tag, only return the overrides
201  foreach (var text in textList.Value)
202  {
203  if (!text.IsOverride) { continue; }
204  yield return new KeyValuePair<Identifier, string>(textList.Key, text.String);
205  }
206  }
207  else
208  {
209  foreach (var text in textList.Value)
210  {
211  yield return new KeyValuePair<Identifier, string>(textList.Key, text.String);
212  }
213  }
214  }
215  }
216 
217  public static IEnumerable<string> GetTextFiles()
218  {
219  return GetTextFilesRecursive(Path.Combine("Content", "Texts"));
220  }
221 
222  private static IEnumerable<string> GetTextFilesRecursive(string directory)
223  {
224  foreach (string file in Directory.GetFiles(directory))
225  {
226  yield return file.CleanUpPath();
227  }
228  foreach (string subDir in Directory.GetDirectories(directory))
229  {
230  foreach (string file in GetTextFilesRecursive(subDir))
231  {
232  yield return file;
233  }
234  }
235  }
236 
237  public static string GetTranslatedLanguageName(LanguageIdentifier languageIdentifier)
238  {
239  return TextPacks[languageIdentifier].First().TranslatedName;
240  }
241 
242  public static void ClearCache()
243  {
244  lock (cachedStrings)
245  {
246  cachedStrings.Clear();
247  nonCacheableTags.Clear();
248  }
249  }
250 
251  public static LocalizedString Get(params Identifier[] tags)
252  {
253  if (tags.Length == 1)
254  {
255  return Get(tags[0]);
256  }
257  return new TagLString(tags);
258  }
259 
260  public static LocalizedString Get(Identifier tag)
261  {
262  TagLString? str = null;
263  lock (cachedStrings)
264  {
265  if (!nonCacheableTags.Contains(tag))
266  {
267  if (cachedStrings.TryGetValue(tag, out var strRef))
268  {
269  if (!strRef.TryGetTarget(out str))
270  {
271  cachedStrings.Remove(tag);
272  }
273  }
274 
275  if (str is null && TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
276  {
277  int count = 0;
278  foreach (var pack in TextPacks[GameSettings.CurrentConfig.Language])
279  {
280  if (pack.Texts.TryGetValue(tag, out var texts))
281  {
282  count += texts.Length;
283  if (count > 1) { break; }
284  }
285  }
286 
287  if (count > 1)
288  {
289  nonCacheableTags = nonCacheableTags.Add(tag);
290  }
291  else
292  {
293  str = new TagLString(tag);
294  cachedStrings.Add(tag, new WeakReference<TagLString>(str));
295  }
296  }
297  }
298  }
299  return str ?? new TagLString(tag);
300  }
301 
302  public static LocalizedString Get(string tag)
303  => Get(tag.ToIdentifier());
304 
305  public static LocalizedString Get(params string[] tags)
306  => Get(tags.ToIdentifiers());
307 
308  public static LocalizedString AddPunctuation(char punctuationSymbol, params LocalizedString[] texts)
309  {
310  return new AddedPunctuationLString(punctuationSymbol, texts);
311  }
312 
313  public static LocalizedString GetFormatted(Identifier tag, params object[] args)
314  {
315  return GetFormatted(new TagLString(tag), args);
316  }
317 
318  public static LocalizedString GetFormatted(LocalizedString str, params object[] args)
319  {
320  LocalizedString[] argStrs = new LocalizedString[args.Length];
321  for (int i = 0; i < args.Length; i++)
322  {
323  if (args[i] is LocalizedString ls) { argStrs[i] = ls; }
324  else { argStrs[i] = new RawLString(args[i].ToString() ?? ""); }
325  }
326  return new FormattedLString(str, argStrs);
327  }
328 
329  public static string FormatServerMessage(string str) => $"{str}~";
330 
331  public static string FormatServerMessage(string message, params (string Key, string Value)[] keysWithValues)
332  {
333  if (keysWithValues.Length == 0)
334  {
335  return FormatServerMessage(message);
336  }
337  var startIndex = message.LastIndexOf('/') + 1;
338  var endIndex = message.IndexOf('~', startIndex);
339  if (endIndex == -1)
340  {
341  endIndex = message.Length - 1;
342  }
343  var textId = message.Substring(startIndex, endIndex - startIndex + 1);
344  var prefixEntries = keysWithValues.Select((kv, index) =>
345  {
346  if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1)
347  {
348  var kvStartIndex = kv.Value.LastIndexOf('/') + 1;
349  return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}";
350  }
351  else
352  {
353  return null;
354  }
355  }).Where(e => e != null).ToArray();
356  return string.Join("",
357  (prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""),
358  message,
359  string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}"))
360  );
361  }
362 
363  internal static string FormatServerMessageWithPronouns(CharacterInfo charInfo, string message, params (string Key, string Value)[] keysWithValues)
364  {
365  var pronounCategory = charInfo.Prefab.Pronouns;
366  (string Key, string Value)[] pronounKwv = new[]
367  {
368  ("[PronounLowercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]Lowercase")),
369  ("[PronounUppercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]")),
370  ("[PronounPossessiveLowercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]Lowercase")),
371  ("[PronounPossessiveUppercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]")),
372  ("[PronounReflexiveLowercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]Lowercase")),
373  ("[PronounReflexiveUppercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]"))
374  };
375  return FormatServerMessage(message, keysWithValues.Concat(pronounKwv).ToArray());
376  }
377 
378  // Same as string.Join(separator, parts) but performs the operation taking into account server message string replacements.
379  public static string JoinServerMessages(string separator, string[] parts, string namePrefix = "part.")
380  {
381 
382  return string.Join("/",
383  string.Join("/", parts.Select((part, index) =>
384  {
385  var partStart = part.LastIndexOf('/') + 1;
386  return partStart > 0 ? $"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $"[{namePrefix}{index}]={part.Substring(partStart)}";
387  })),
388  string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]")));
389  }
390 
391  public static LocalizedString ParseInputTypes(LocalizedString str, bool useColorHighlight = false)
392  {
393  return new InputTypeLString(str, useColorHighlight);
394  }
395 
396  public static LocalizedString GetWithVariable(string tag, string varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No)
397  {
398  return GetWithVariable(tag.ToIdentifier(), varName.ToIdentifier(), value, formatCapitals);
399  }
400 
401  public static LocalizedString GetWithVariable(Identifier tag, Identifier varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No)
402  {
403  return GetWithVariables(tag, (varName, value));
404  }
405 
406  public static LocalizedString GetWithVariables(string tag, params (string Key, string Value)[] replacements)
407  {
408  return GetWithVariables(
409  tag.ToIdentifier(),
410  replacements.Select(kv =>
411  (kv.Key.ToIdentifier(),
412  (LocalizedString)new RawLString(kv.Value),
413  FormatCapitals.No)));
414  }
415 
416  public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value)[] replacements)
417  {
418  return GetWithVariables(
419  tag.ToIdentifier(),
420  replacements.Select(kv =>
421  (kv.Key.ToIdentifier(),
422  kv.Value,
423  FormatCapitals.No)));
424  }
425 
426  public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value, FormatCapitals FormatCapitals)[] replacements)
427  {
428  return GetWithVariables(
429  tag.ToIdentifier(),
430  replacements.Select(kv =>
431  (kv.Key.ToIdentifier(),
432  kv.Value,
433  kv.FormatCapitals)));
434  }
435 
436  public static LocalizedString GetWithVariables(string tag, params (string Key, string Value, FormatCapitals FormatCapitals)[] replacements)
437  {
438  return GetWithVariables(
439  tag.ToIdentifier(),
440  replacements.Select(kv =>
441  (kv.Key.ToIdentifier(),
442  (LocalizedString)new RawLString(kv.Value),
443  kv.FormatCapitals)));
444  }
445 
446  public static LocalizedString GetWithVariables(Identifier tag, params (Identifier Key, LocalizedString Value)[] replacements)
447  {
448  return GetWithVariables(tag, replacements.Select(kv => (kv.Key, kv.Value, FormatCapitals.No)));
449  }
450 
451  public static LocalizedString GetWithVariables(Identifier tag, IEnumerable<(Identifier, LocalizedString, FormatCapitals)> replacements)
452  {
453  return new ReplaceLString(new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements);
454  }
455 
456  public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement, Func<string, string>? customTagReplacer = null)
457  {
458  /*
459  <Description tag="talentdescription.simultaneousskillgain">
460  <Replace tag="[skillname1]" value="skillname.helm"/>
461  <Replace tag="[skillname2]" value="skillname.weapons"/>
462  <Replace tag="[somevalue]" value="45.3"/>
463  </Description>
464  */
465  Identifier tag = descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty);
466  LocalizedString extraDescriptionLine = Get(tag).Fallback(tag.Value);
467  foreach (XElement replaceElement in descriptionElement.Elements())
468  {
469  if (replaceElement.NameAsIdentifier() != "replace") { continue; }
470 
471  Identifier variableTag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty);
472  LocalizedString replacementValue = string.Empty;
473  if (customTagReplacer != null)
474  {
475  replacementValue = customTagReplacer(replaceElement.GetAttributeString("value", string.Empty));
476  }
477  if (replacementValue.IsNullOrWhiteSpace())
478  {
479  string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty<string>());
480  for (int i = 0; i < replacementValues.Length; i++)
481  {
482  replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]);
483  if (i < replacementValues.Length - 1)
484  {
485  replacementValue += ", ";
486  }
487  }
488  }
489  if (replaceElement.Attribute("color") != null)
490  {
491  string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255");
492  replacementValue = $"‖color:{colorStr}‖" + replacementValue + "‖color:end‖";
493  }
494  extraDescriptionLine = extraDescriptionLine.Replace(variableTag, replacementValue);
495  }
496  if (!(description is RawLString { Value: "" })) { description += "\n"; } //TODO: this is cursed
497  description += extraDescriptionLine;
498  }
499 
500  public static LocalizedString FormatCurrency(int amount, bool includeCurrencySymbol = true)
501  {
502  string valueString = string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount);
503  return includeCurrencySymbol ?
504  GetWithVariable("currencyformat", "[credits]", valueString) :
505  valueString;
506  }
507 
508  public static LocalizedString GetServerMessage(string serverMessage)
509  {
510  return new ServerMsgLString(serverMessage);
511  }
512 
513  public static LocalizedString Capitalize(this LocalizedString str)
514  {
515  return new CapitalizeLString(str);
516  }
517 
518  public static void IncrementLanguageVersion()
519  {
520  LanguageVersion++;
521  ClearCache();
522  }
523 
524 #if DEBUG
525  public static void CheckForDuplicates(LanguageIdentifier lang)
526  {
527  if (!TextPacks.ContainsKey(lang))
528  {
529  DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
530  return;
531  }
532 
533  int packIndex = 0;
534  foreach (TextPack textPack in TextPacks[lang])
535  {
536  textPack.CheckForDuplicates(packIndex);
537  packIndex++;
538  }
539  }
540 
541  public static void WriteToCSV()
542  {
543  LanguageIdentifier lang = DefaultLanguage;
544 
545  if (!TextPacks.ContainsKey(lang))
546  {
547  DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
548  return;
549  }
550 
551  int packIndex = 0;
552  foreach (TextPack textPack in TextPacks[lang])
553  {
554  textPack.WriteToCSV(packIndex);
555  packIndex++;
556  }
557  }
558 #endif
559  }
560 }