Unstable 0.17.0.0

This commit is contained in:
Markus Isberg
2022-02-26 02:43:01 +09:00
parent a83f375681
commit 3974067915
913 changed files with 32472 additions and 32364 deletions
@@ -0,0 +1,35 @@
#nullable enable
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
public class AddedPunctuationLString : LocalizedString
{
private readonly ImmutableArray<LocalizedString> nestedStrs;
private readonly char punctuationSymbol;
public AddedPunctuationLString(char symbol, params LocalizedString[] nStrs) { nestedStrs = nStrs.ToImmutableArray(); punctuationSymbol = symbol; }
public override bool Loaded => nestedStrs.All(s => s.Loaded);
public override void RetrieveValue()
{
string separator = "";
if (GameSettings.CurrentConfig.Language == "French".ToLanguageIdentifier())
{
bool addNonBreakingSpace =
punctuationSymbol == ':' || punctuationSymbol == ';' ||
punctuationSymbol == '!' || punctuationSymbol == '?';
separator = addNonBreakingSpace ?
new string(new char[] { (char)(0xA0), punctuationSymbol, ' ' }) :
new string(new char[] { punctuationSymbol, ' ' });
}
else
{
separator = new string(new char[] { punctuationSymbol, ' ' });
}
cachedValue = string.Join(separator, nestedStrs.Select(str => str.Value));
UpdateLanguage();
}
}
}
@@ -0,0 +1,25 @@
#nullable enable
namespace Barotrauma
{
public class CapitalizeLString : LocalizedString
{
private readonly LocalizedString nestedStr;
public CapitalizeLString(LocalizedString nStr) { nestedStr = nStr; }
public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue()
{
string str = nestedStr.Value;
if (!string.IsNullOrEmpty(str))
{
cachedValue = char.ToUpper(str[0]) + str[1..];
}
else
{
cachedValue = "";
}
UpdateLanguage();
}
}
}
@@ -0,0 +1,21 @@
#nullable enable
namespace Barotrauma
{
public class ConcatLString : LocalizedString
{
private readonly LocalizedString left;
private readonly LocalizedString right;
public ConcatLString(LocalizedString l, LocalizedString r)
{
left = l; right = r;
}
public override bool Loaded => left.Loaded || right.Loaded;
public override void RetrieveValue()
{
cachedValue = left.Value + right.Value;
UpdateLanguage();
}
}
}
@@ -0,0 +1,44 @@
#nullable enable
namespace Barotrauma
{
public class FallbackLString : LocalizedString
{
private readonly LocalizedString primary;
private readonly LocalizedString fallback;
private bool primaryIsLoaded = false;
public FallbackLString(LocalizedString primary, LocalizedString fallback)
{
if (primary is FallbackLString {primary: { } innerPrimary, fallback: { } innerFallback})
{
this.primary = innerPrimary;
this.fallback = innerFallback.Fallback(fallback);
}
else
{
this.primary = primary;
this.fallback = fallback;
}
}
protected override bool MustRetrieveValue()
{
return base.MustRetrieveValue()
|| MustRetrieveValue(primary)
|| MustRetrieveValue(fallback)
|| primaryIsLoaded != primary.Loaded;
}
public override bool Loaded => primary.Loaded || fallback.Loaded;
public override void RetrieveValue()
{
cachedValue = primary.Value;
primaryIsLoaded = primary.Loaded;
if (!primary.Loaded)
{
cachedValue = fallback.Value;
}
}
}
}
@@ -0,0 +1,25 @@
#nullable enable
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
public class FormattedLString : LocalizedString
{
private readonly LocalizedString str;
private readonly ImmutableArray<LocalizedString> subStrs;
public FormattedLString(LocalizedString str, params LocalizedString[] subStrs)
{
this.str = str;
this.subStrs = subStrs.ToImmutableArray();
}
public override bool Loaded => str.Loaded && subStrs.All(s => s.Loaded);
public override void RetrieveValue()
{
//TODO: possibly broken!
cachedValue = string.Format(str.Value, subStrs.Select(s => s.Value as object).ToArray());
UpdateLanguage();
}
}
}
@@ -0,0 +1,33 @@
#nullable enable
using System;
namespace Barotrauma
{
public class InputTypeLString : LocalizedString
{
private readonly LocalizedString nestedStr;
public InputTypeLString(LocalizedString nStr) { nestedStr = nStr; }
protected override bool MustRetrieveValue()
{
//TODO: check for config changes!
return base.MustRetrieveValue();
}
public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue()
{
cachedValue = nestedStr.Value;
#if CLIENT
//TODO: server shouldn't have this type at all
foreach (InputType? inputType in Enum.GetValues(typeof(InputType)))
{
if (!inputType.HasValue) { continue; }
cachedValue = cachedValue.Replace($"[{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase);
cachedValue = cachedValue.Replace($"[InputType.{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase);
}
#endif
UpdateLanguage();
}
}
}
@@ -0,0 +1,24 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
public class JoinLString : LocalizedString
{
private readonly IEnumerable<LocalizedString> subStrs;
private readonly string separator;
public JoinLString(string separator, IEnumerable<LocalizedString> subStrs)
{
this.separator = separator; this.subStrs = subStrs;
}
public override bool Loaded => subStrs.All(s => s.Loaded);
public override void RetrieveValue()
{
cachedValue = string.Join(separator, subStrs);
UpdateLanguage();
}
}
}
@@ -0,0 +1,177 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace Barotrauma
{
public abstract class LocalizedString : IComparable
{
protected enum LoadedSuccessfully
{
Unknown,
No,
Yes
}
protected LanguageIdentifier language { get; private set; } = LanguageIdentifier.None;
private int languageVersion = 0;
protected string cachedValue = "";
public string Value
{
get
{
if (MustRetrieveValue()) { RetrieveValue(); }
return cachedValue;
}
}
public int Length => Value.Length;
public abstract bool Loaded { get; }
protected void UpdateLanguage()
{
language = GameSettings.CurrentConfig.Language;
languageVersion = TextManager.LanguageVersion;
}
protected virtual bool MustRetrieveValue() //this can't be called on other LocalizedStrings by derived classes
{
return language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion;
}
protected static bool MustRetrieveValue(LocalizedString str) //this can be called by derived classes
{
return str.MustRetrieveValue();
}
public abstract void RetrieveValue();
public static implicit operator LocalizedString(string value) => new RawLString(value);
public static implicit operator LocalizedString(char value) => new RawLString(value.ToString());
public static LocalizedString operator+(LocalizedString left, LocalizedString right) => new ConcatLString(left, right);
public static LocalizedString operator+(LocalizedString left, object right) => left + (right.ToString() ?? "");
public static LocalizedString operator+(object left, LocalizedString right) => (left.ToString() ?? "") + right;
public static bool operator==(LocalizedString? left, LocalizedString? right)
{
return left?.Value == right?.Value;
}
public static bool operator!=(LocalizedString? left, LocalizedString? right)
{
return !(left == right);
}
public override string ToString()
{
return Value;
}
public bool Contains(string subStr, StringComparison comparison = StringComparison.Ordinal)
{
return !Value.IsNullOrEmpty() && Value.Contains(subStr, comparison);
}
public bool Contains(char chr, StringComparison comparison = StringComparison.Ordinal)
{
return Value.Contains(chr, comparison);
}
public virtual LocalizedString ToUpper()
{
return new UpperLString(this);
}
public static LocalizedString Join(string separator, params LocalizedString[] subStrs)
{
return Join(separator, (IEnumerable<LocalizedString>)subStrs);
}
public static LocalizedString Join(string separator, IEnumerable<LocalizedString> subStrs)
{
return new JoinLString(separator, subStrs);
}
public LocalizedString Fallback(LocalizedString fallback)
{
return new FallbackLString(this, fallback);
}
public IReadOnlyList<LocalizedString> Split(params char[] separators)
{
var splitter = new LStringSplitter(this, separators);
return splitter.Substrings;
}
public LocalizedString Replace(Identifier find, LocalizedString replace, StringComparison stringComparison = StringComparison.Ordinal)
{
return new ReplaceLString(this, stringComparison, (find, replace));
}
public LocalizedString Replace(string find, LocalizedString replace, StringComparison stringComparison = StringComparison.Ordinal)
{
return new ReplaceLString(this, stringComparison, (find.ToIdentifier(), replace));
}
public LocalizedString Replace(LocalizedString find, LocalizedString replace,
StringComparison stringComparison = StringComparison.Ordinal)
{
return new ReplaceLString(this, stringComparison, (find, replace));
}
public LocalizedString TrimStart()
{
return new TrimLString(this, TrimLString.Mode.Start);
}
public LocalizedString TrimEnd()
{
return new TrimLString(this, TrimLString.Mode.End);
}
public LocalizedString ToLower()
{
return new LowerLString(this);
}
public override bool Equals(object? obj)
{
if (obj is LocalizedString lStr) { return Equals(lStr, StringComparison.Ordinal); }
if (obj is string str) { return Equals(str, StringComparison.Ordinal); }
return base.Equals(obj);
}
public bool Equals(LocalizedString other, StringComparison comparison = StringComparison.Ordinal)
{
return Equals(other.Value, comparison);
}
public bool Equals(string other, StringComparison comparison = StringComparison.Ordinal)
{
return string.Equals(Value, other, comparison);
}
public bool StartsWith(LocalizedString other, StringComparison comparison = StringComparison.Ordinal)
{
return StartsWith(other.Value, comparison);
}
public bool StartsWith(string other, StringComparison comparison = StringComparison.Ordinal)
{
return Value.StartsWith(other, comparison);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public int CompareTo(object? obj)
{
return Value.CompareTo(obj?.ToString() ?? "");
}
}
}
@@ -0,0 +1,20 @@
#nullable enable
namespace Barotrauma
{
public class LowerLString : LocalizedString
{
private readonly LocalizedString nestedStr;
public LowerLString(LocalizedString nestedStr)
{
this.nestedStr = nestedStr;
}
public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue()
{
cachedValue = nestedStr.Value.ToLowerInvariant();
UpdateLanguage();
}
}
}
@@ -0,0 +1,13 @@
#nullable enable
namespace Barotrauma
{
public class RawLString : LocalizedString
{
public RawLString(string value) { cachedValue = value; }
protected override bool MustRetrieveValue() => false;
public override bool Loaded => true;
public override void RetrieveValue() { }
}
}
@@ -0,0 +1,75 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
public class ReplaceLString : LocalizedString
{
private readonly LocalizedString nestedStr;
private readonly ImmutableDictionary<LocalizedString, (LocalizedString Value, FormatCapitals FormatCapitals)> replacements;
private readonly StringComparison stringComparison;
public ReplaceLString(LocalizedString nStr, StringComparison sc, IEnumerable<(LocalizedString Key, LocalizedString Value, FormatCapitals FormatCapitals)> r)
{
nestedStr = nStr;
replacements = r.Select(kvf => (kvf.Key, (kvf.Value, kvf.FormatCapitals))).ToImmutableDictionary();
stringComparison = sc;
}
public ReplaceLString(LocalizedString nStr, StringComparison sc, params (LocalizedString Key, LocalizedString Value)[] r)
: this(nStr, sc, r.Select(kv => (kv.Key, kv.Value, FormatCapitals.No))) { }
public ReplaceLString(LocalizedString nStr, StringComparison sc, IEnumerable<(Identifier Key, LocalizedString Value, FormatCapitals FormatCapitals)> r)
: this(nStr, sc, r.Select(p => ((LocalizedString)p.Key.Value, p.Value, p.FormatCapitals))) { }
public ReplaceLString(LocalizedString nStr, StringComparison sc, params (Identifier Key, LocalizedString Value)[] r)
: this(nStr, sc, r.Select(kv => ((LocalizedString)kv.Key.Value, kv.Value, FormatCapitals.No))) { }
private static string HandleVariableCapitalization(string text, string variableTag, string variableValue)
{
int index = text.IndexOf(variableTag, StringComparison.InvariantCulture) - 1;
if (index == -1)
{
return variableValue;
}
for (int i = index; i >= 0; i--)
{
if (char.IsWhiteSpace(text[i])) { continue; }
if (text[i] != '.')
{
variableValue = variableValue.ToLowerInvariant();
}
else
{
variableValue = TextManager.Capitalize(variableValue).Value;
break;
}
}
return variableValue;
}
public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue()
{
cachedValue = nestedStr.Value;
foreach (var varName in replacements.Keys)
{
string key = varName.Value;
string value = replacements[varName].Value.Value;
if (replacements[varName].FormatCapitals == FormatCapitals.Yes)
{
value = HandleVariableCapitalization(cachedValue, key, value);
}
cachedValue = cachedValue.Replace(key, value, stringComparison);
}
UpdateLanguage();
}
}
}
@@ -0,0 +1,185 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
namespace Barotrauma
{
public class ServerMsgLString : LocalizedString
{
private static readonly Regex reFormattedMessage =
new Regex(@"^(?<variable>[\[\].a-z0-9_]+?)=(?<formatter>[a-z0-9_]+?)\((?<value>.+?)\)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex reReplacedMessage = new Regex(@"^(?<variable>[\[\].a-z0-9_]+?)=(?<message>.*)$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly ImmutableDictionary<Identifier, Func<string, string?>> messageFormatters =
new Dictionary<Identifier, Func<string, string?>>
{
{
"duration".ToIdentifier(),
secondsValue => double.TryParse(secondsValue, out var seconds)
? $"{TimeSpan.FromSeconds(seconds):g}"
: null
}
}.ToImmutableDictionary();
private static readonly ImmutableHashSet<char> serverMessageCharacters =
new [] {'~', '[', ']', '='}.ToImmutableHashSet();
private readonly string serverMessage;
private readonly ImmutableArray<string> messageSplit;
public ServerMsgLString(string serverMsg)
{
serverMessage = serverMsg;
messageSplit = serverMessage.Split("/").ToImmutableArray();
}
private static bool IsServerMessageWithVariables(string message) =>
serverMessageCharacters.All(message.Contains);
private LoadedSuccessfully loadedSuccessfully = LoadedSuccessfully.Unknown;
public override bool Loaded
{
get
{
if (loadedSuccessfully == LoadedSuccessfully.Unknown) { RetrieveValue(); }
return loadedSuccessfully == LoadedSuccessfully.Yes;
}
}
public override void RetrieveValue()
{
Dictionary<string, string> replacedMessages = new Dictionary<string, string>();
bool translationsFound = false;
string? TranslateMessage(string input)
{
string? message = input;
if (message.EndsWith("~", StringComparison.Ordinal))
{
message = message.Substring(0, message.Length - 1);
}
if (!IsServerMessageWithVariables(message) && !message.Contains('=')) // No variables, try to translate
{
foreach (var replacedMessage in replacedMessages)
{
message = message.Replace(replacedMessage.Key, replacedMessage.Value);
}
if (message.Contains(" "))
{
return message;
} // Spaces found, do not translate
var msg = TextManager.Get(message);
if (msg.Loaded) // If a translation was found, otherwise use the original
{
message = msg.Value;
translationsFound = true;
}
}
else
{
string? messageVariable = null;
var matchFormatted = reFormattedMessage.Match(message);
if (matchFormatted.Success)
{
var formatter = matchFormatted.Groups["formatter"].ToString().ToIdentifier();
if (messageFormatters.TryGetValue(formatter, out var formatterFn))
{
var formattedValue = formatterFn(matchFormatted.Groups["value"].ToString());
if (formattedValue != null)
{
messageVariable = matchFormatted.Groups["variable"].ToString();
message = formattedValue;
}
}
}
if (messageVariable == null)
{
var matchReplaced = reReplacedMessage.Match(message);
if (matchReplaced.Success)
{
messageVariable = matchReplaced.Groups["variable"].ToString();
message = matchReplaced.Groups["message"].ToString();
}
}
foreach (var replacedMessage in replacedMessages)
{
message = message.Replace(replacedMessage.Key, replacedMessage.Value);
}
string[] messageWithVariables = message.Split('~');
var msg = TextManager.Get(messageWithVariables[0]);
if (msg.Loaded) // If a translation was found, otherwise use the original
{
message = msg.Value;
translationsFound = true;
}
else if (messageVariable == null)
{
return message; // No translation found, probably caused by player input -> skip variable handling
}
// First index is always the message identifier -> start at 1
for (int j = 1; j < messageWithVariables.Length; j++)
{
string[] variableAndValue = messageWithVariables[j].Split('=');
message = message.Replace(variableAndValue[0],
variableAndValue[1].Length > 1 && variableAndValue[1][0] == '§'
? TextManager.Get(variableAndValue[1].Substring(1)).Value
: variableAndValue[1]);
}
if (messageVariable != null)
{
replacedMessages[messageVariable] = message;
message = null;
}
}
return message;
}
try
{
string translatedServerMessage = "";
for (int i = 0; i < messageSplit.Length; i++)
{
string? message = TranslateMessage(messageSplit[i]);
if (message != null)
{
translatedServerMessage += message;
}
}
cachedValue = translationsFound ? translatedServerMessage : serverMessage;
loadedSuccessfully = LoadedSuccessfully.Yes;
}
catch (IndexOutOfRangeException exception)
{
string errorMsg = $"Failed to translate server message \"{serverMessage}\".";
#if DEBUG
DebugConsole.ThrowError(errorMsg, exception);
#endif
GameAnalyticsManager.AddErrorEventOnce($"TextManager.GetServerMessage:{serverMessage}",
GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
cachedValue = errorMsg;
loadedSuccessfully = LoadedSuccessfully.No;
}
UpdateLanguage();
}
}
}
@@ -0,0 +1,93 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Barotrauma
{
public class LStringSplitter
{
public IReadOnlyList<LocalizedString> Substrings => substrings;
private class SubstringList : IReadOnlyList<LocalizedString>
{
public SubstringList(LStringSplitter splitter) { this.splitter = splitter; }
private LStringSplitter splitter;
private readonly List<LocalizedString> underlyingList = new List<LocalizedString>();
public List<LocalizedString> UnderlyingList
{
get
{
splitter.UpdateSubstrings();
return underlyingList;
}
}
public IEnumerator<LocalizedString> GetEnumerator() => UnderlyingList.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int Count => UnderlyingList.Count;
public LocalizedString this[int index] => UnderlyingList[index];
}
private readonly SubstringList substrings;
private readonly char[] separators;
private readonly LocalizedString originalString;
private string[] substrValues;
private string cachedOriginal;
public bool Loaded => originalString.Loaded;
public LStringSplitter(LocalizedString input, params char[] separators)
{
originalString = input;
substrings = new SubstringList(this);
substrValues = Array.Empty<string>();
this.separators = separators;
cachedOriginal = "";
}
private void UpdateSubstrings()
{
if (originalString.Value != cachedOriginal)
{
cachedOriginal = originalString.Value;
substrValues = cachedOriginal.Split(separators);
substrings.UnderlyingList.Clear();
substrings.UnderlyingList.AddRange(Enumerable.Range(0, substrValues.Length).Select(i => new SplitLString(this, i) as LocalizedString));
}
}
public string GetValue(int index)
{
UpdateSubstrings();
return substrValues[index];
}
}
public class SplitLString : LocalizedString
{
private bool loaded = false;
private readonly LStringSplitter splitter;
private readonly int index;
public SplitLString(LStringSplitter splitter, int index)
{
this.splitter = splitter; this.index = index;
}
public override bool Loaded => loaded && splitter.Loaded;
public override void RetrieveValue()
{
loaded = true;
cachedValue = splitter.GetValue(index);
UpdateLanguage();
}
}
}
@@ -0,0 +1,71 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Extensions;
namespace Barotrauma
{
public class TagLString : LocalizedString
{
private readonly ImmutableArray<Identifier> tags;
public TagLString(params Identifier[] tags)
{
this.tags = tags.ToImmutableArray();
}
private LoadedSuccessfully loadedSuccessfully = LoadedSuccessfully.Unknown;
public override bool Loaded
{
get
{
if (loadedSuccessfully == LoadedSuccessfully.Unknown) { RetrieveValue(); }
return loadedSuccessfully == LoadedSuccessfully.Yes;
}
}
public override void RetrieveValue()
{
UpdateLanguage();
(string value, bool loaded) tryLoad(LanguageIdentifier lang)
{
IReadOnlyList<string> candidates = Array.Empty<string>();
int tagIndex = 0;
if (TextManager.TextPacks.TryGetValue(lang, out var packs))
{
while (candidates.Count == 0 && tagIndex < tags.Length)
{
foreach (var pack in packs)
{
if (pack.Texts.TryGetValue(tags[tagIndex], out var texts))
{
candidates = candidates.ListConcat(texts);
}
}
tagIndex++;
}
}
bool loaded = candidates.Count > 0;
return (loaded ? candidates.GetRandomUnsynced() : "", loaded);
}
var (value, loaded) = tryLoad(language);
loadedSuccessfully = loaded ? LoadedSuccessfully.Yes : LoadedSuccessfully.No;
cachedValue = value;
if (!loaded && language != TextManager.DefaultLanguage)
{
(value, _) = tryLoad(TextManager.DefaultLanguage);
cachedValue = value;
//Notice how we don't set loadedSuccessfully again here.
//This is by design; falling back to English means that
//this text did NOT load successfully, so Loaded must
//return false.
}
}
}
}
@@ -0,0 +1,28 @@
#nullable enable
using System;
namespace Barotrauma
{
public class TrimLString : LocalizedString
{
[Flags]
public enum Mode { Start = 0x1, End = 0x2, Both=0x3 }
private readonly LocalizedString nestedStr;
private readonly Mode mode;
public TrimLString(LocalizedString nestedStr, Mode mode)
{
this.nestedStr = nestedStr;
this.mode = mode;
}
public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue()
{
cachedValue = nestedStr.Value;
if (mode.HasFlag(Mode.Start)) { cachedValue = cachedValue.TrimStart(); }
if (mode.HasFlag(Mode.End)) { cachedValue = cachedValue.TrimEnd(); }
UpdateLanguage();
}
}
}
@@ -0,0 +1,25 @@
#nullable enable
namespace Barotrauma
{
public class UpperLString : LocalizedString
{
private readonly LocalizedString nestedStr;
public UpperLString(LocalizedString nestedStr)
{
this.nestedStr = nestedStr;
}
public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue()
{
cachedValue = nestedStr.Value.ToUpper();
UpdateLanguage();
}
public override LocalizedString ToUpper()
{
return this;
}
}
}
@@ -0,0 +1,199 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Diagnostics;
namespace Barotrauma
{
public class RichString
{
protected bool loaded = false;
protected LanguageIdentifier language = LanguageIdentifier.None;
protected string cachedSanitizedValue = "";
public string SanitizedValue
{
get
{
if (MustRetrieveValue()) { RetrieveValue(); }
return cachedSanitizedValue;
}
}
public int Length => SanitizedValue.Length;
private readonly Func<string, string>? postProcess;
private readonly bool shouldParseRichTextData;
private readonly LocalizedString originalStr;
public LocalizedString NestedStr { get; private set; }
public readonly LocalizedString SanitizedString;
#if CLIENT
private readonly GUIFont? font;
private readonly GUIComponentStyle? componentStyle;
private bool forceUpperCase = false;
private bool fontOrStyleForceUpperCase
=> font is { ForceUpperCase: true } || componentStyle is { ForceUpperCase: true };
#endif
public ImmutableArray<RichTextData>? RichTextData { get; private set; }
#if CLIENT
private RichString(
LocalizedString nestedStr, bool shouldParseRichTextData, Func<string, string>? postProcess = null,
GUIFont? font = null, GUIComponentStyle? componentStyle = null) : this(nestedStr, shouldParseRichTextData, postProcess)
{
this.font = font;
this.componentStyle = componentStyle;
}
#endif
private RichString(LocalizedString nestedStr, bool shouldParseRichTextData, Func<string,string>? postProcess = null)
{
originalStr = nestedStr;
NestedStr = originalStr;
this.shouldParseRichTextData = shouldParseRichTextData;
this.postProcess = postProcess;
SanitizedString = new StripRichTagsLString(this);
#if CLIENT
this.font = null;
this.componentStyle = null;
#endif
}
public static RichString Rich(LocalizedString str, Func<string, string>? postProcess = null)
{
return new RichString(str, true, postProcess);
}
public static RichString Plain(LocalizedString str)
{
return new RichString(str, false, postProcess: null);
}
public static implicit operator LocalizedString(RichString richStr) => richStr.NestedStr;
public static implicit operator RichString(LocalizedString lStr)
{
#if DEBUG
if (!lStr.IsNullOrEmpty() && lStr.Contains("‖"))
{
if (Debugger.IsAttached) { Debugger.Break(); }
}
#endif
return Plain(lStr);
}
public static implicit operator RichString(string str) => (LocalizedString)str;
protected virtual bool MustRetrieveValue()
{
return NestedStr.Loaded != loaded
|| language != GameSettings.CurrentConfig.Language
#if CLIENT
|| (fontOrStyleForceUpperCase != forceUpperCase)
#endif
;
}
public void RetrieveValue()
{
#if CLIENT
NestedStr = fontOrStyleForceUpperCase ? originalStr.ToUpper() : originalStr;
#endif
if (shouldParseRichTextData)
{
RichTextData = Barotrauma.RichTextData.GetRichTextData(NestedStr.Value, out cachedSanitizedValue);
}
else
{
cachedSanitizedValue = NestedStr.Value;
}
if (postProcess != null) { cachedSanitizedValue = postProcess(cachedSanitizedValue); }
language = GameSettings.CurrentConfig.Language;
loaded = NestedStr.Loaded;
}
#if CLIENT
public RichString CaseTiedToFontAndStyle(GUIFont? font, GUIComponentStyle? componentStyle)
{
return new RichString(originalStr, shouldParseRichTextData, postProcess, font, componentStyle);
}
#endif
public RichString ToUpper()
{
return new RichString(NestedStr.ToUpper(), shouldParseRichTextData, postProcess);
}
public RichString ToLower()
{
return new RichString(NestedStr.ToLower(), shouldParseRichTextData, postProcess);
}
public RichString Replace(string from, string to, StringComparison stringComparison = StringComparison.Ordinal)
{
return new RichString(NestedStr.Replace(from, to, stringComparison), shouldParseRichTextData, postProcess);
}
public override string ToString()
{
return SanitizedValue;
}
public bool Contains(string str, StringComparison stringComparison = StringComparison.Ordinal) =>
SanitizedValue.Contains(str, stringComparison);
public bool Contains(char chr, StringComparison stringComparison = StringComparison.Ordinal) =>
SanitizedValue.Contains(chr, stringComparison);
public static bool operator ==(RichString? a, RichString? b)
=> a?.SanitizedValue == b?.SanitizedValue
#if CLIENT
&& a?.font == b?.font
&& a?.componentStyle == b?.componentStyle
#endif
;
public static bool operator !=(RichString? a, RichString? b) => !(a == b);
public static bool operator ==(RichString? a, LocalizedString? b)
=> a?.SanitizedValue == b?.Value;
public static bool operator !=(RichString? a, LocalizedString? b) => !(a == b);
public static bool operator ==(LocalizedString? a, RichString? b)
=> a?.Value == b?.SanitizedValue;
public static bool operator !=(LocalizedString? a, RichString? b) => !(a == b);
public static bool operator ==(RichString? a, string? b)
=> a?.SanitizedValue == b;
public static bool operator !=(RichString? a, string? b) => !(a == b);
public static bool operator ==(string? a, RichString? b)
=> a == b?.SanitizedValue;
public static bool operator !=(string? a, RichString? b) => !(a == b);
}
public class StripRichTagsLString : LocalizedString
{
public readonly RichString RichStr;
public StripRichTagsLString(RichString richStr)
{
RichStr = richStr;
}
public override bool Loaded => RichStr.NestedStr.Loaded;
public override void RetrieveValue()
{
cachedValue = RichStr.SanitizedValue;
}
}
}
@@ -0,0 +1,404 @@
#nullable enable
using Barotrauma.IO;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace Barotrauma
{
public enum FormatCapitals
{
Yes, No
}
public static class TextManager
{
public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier();
public readonly static ConcurrentDictionary<LanguageIdentifier, ImmutableHashSet<TextPack>> TextPacks = new ConcurrentDictionary<LanguageIdentifier, ImmutableHashSet<TextPack>>();
public static IEnumerable<LanguageIdentifier> AvailableLanguages => TextPacks.Keys;
private readonly static Dictionary<Identifier, WeakReference<TagLString>> cachedStrings =
new Dictionary<Identifier, WeakReference<TagLString>>();
private static ImmutableHashSet<Identifier> nonCacheableTags =
ImmutableHashSet<Identifier>.Empty;
public static int LanguageVersion { get; private set; } = 0;
private readonly static Regex isCJK = new Regex(
@"\p{IsHangulJamo}|" +
@"\p{IsHiragana}|" +
@"\p{IsKatakana}|" +
@"\p{IsCJKRadicalsSupplement}|" +
@"\p{IsCJKSymbolsandPunctuation}|" +
@"\p{IsEnclosedCJKLettersandMonths}|" +
@"\p{IsCJKCompatibility}|" +
@"\p{IsCJKUnifiedIdeographsExtensionA}|" +
@"\p{IsCJKUnifiedIdeographs}|" +
@"\p{IsHangulSyllables}|" +
@"\p{IsCJKCompatibilityForms}");
/// <summary>
/// Does the string contain symbols from Chinese, Japanese or Korean languages
/// </summary>
public static bool IsCJK(LocalizedString text)
{
return IsCJK(text.Value);
}
public static bool IsCJK(string text)
{
if (string.IsNullOrEmpty(text)) { return false; }
return isCJK.IsMatch(text);
}
public static bool ContainsTag(string tag)
{
return ContainsTag(tag.ToIdentifier());
}
public static bool ContainsTag(Identifier tag)
{
return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag));
}
public static IEnumerable<string> GetAll(string tag)
=> GetAll(tag.ToIdentifier());
public static IEnumerable<string> GetAll(Identifier tag)
{
return TextPacks[GameSettings.CurrentConfig.Language]
.SelectMany(p => p.Texts.TryGetValue(tag, out var value)
? (IEnumerable<string>)value
: Array.Empty<string>());
}
public static IEnumerable<KeyValuePair<Identifier, string>> GetAllTagTextPairs()
{
return TextPacks[GameSettings.CurrentConfig.Language]
.SelectMany(p => p.Texts)
.SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair<Identifier, string>(kvp.Key, v)));
}
public static IEnumerable<string> GetTextFiles()
{
return GetTextFilesRecursive(Path.Combine("Content", "Texts"));
}
private static IEnumerable<string> GetTextFilesRecursive(string directory)
{
foreach (string file in Directory.GetFiles(directory))
{
yield return file.CleanUpPath();
}
foreach (string subDir in Directory.GetDirectories(directory))
{
foreach (string file in GetTextFilesRecursive(subDir))
{
yield return file;
}
}
}
public static string GetTranslatedLanguageName(LanguageIdentifier languageIdentifier)
{
return TextPacks[languageIdentifier].First().TranslatedName;
}
public static void ClearCache()
{
lock (cachedStrings)
{
cachedStrings.Clear();
nonCacheableTags.Clear();
}
}
public static LocalizedString Get(params Identifier[] tags)
{
TagLString? str = null;
lock (cachedStrings)
{
if (tags.Length == 1 && !nonCacheableTags.Contains(tags[0]))
{
var tag = tags[0];
if (cachedStrings.TryGetValue(tag, out var strRef))
{
if (!strRef.TryGetTarget(out str))
{
cachedStrings.Remove(tag);
}
}
if (str is null && TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
{
int count = 0;
foreach (var pack in TextPacks[GameSettings.CurrentConfig.Language])
{
if (pack.Texts.TryGetValue(tag, out var texts))
{
count += texts.Length;
if (count > 1) { break; }
}
}
if (count > 1)
{
nonCacheableTags = nonCacheableTags.Add(tag);
}
else
{
str = new TagLString(tags);
cachedStrings.Add(tag, new WeakReference<TagLString>(str));
}
}
}
}
return str ?? new TagLString(tags);
}
public static LocalizedString Get(params string[] tags)
=> Get(tags.ToIdentifiers());
public static LocalizedString AddPunctuation(char punctuationSymbol, params LocalizedString[] texts)
{
return new AddedPunctuationLString(punctuationSymbol, texts);
}
public static LocalizedString GetFormatted(Identifier tag, params object[] args)
{
return GetFormatted(new TagLString(tag), args);
}
public static LocalizedString GetFormatted(LocalizedString str, params object[] args)
{
LocalizedString[] argStrs = new LocalizedString[args.Length];
for (int i = 0; i < args.Length; i++)
{
if (args[i] is LocalizedString ls) { argStrs[i] = ls; }
else { argStrs[i] = new RawLString(args[i].ToString() ?? ""); }
}
return new FormattedLString(str, argStrs);
}
public static string FormatServerMessage(string str) => $"{str}~";
public static string FormatServerMessage(string message, params (string Key, string Value)[] keysWithValues)
{
if (keysWithValues.Length == 0)
{
return FormatServerMessage(message);
}
var startIndex = message.LastIndexOf('/') + 1;
var endIndex = message.IndexOf('~', startIndex);
if (endIndex == -1)
{
endIndex = message.Length - 1;
}
var textId = message.Substring(startIndex, endIndex - startIndex + 1);
var prefixEntries = keysWithValues.Select((kv, index) =>
{
if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1)
{
var kvStartIndex = kv.Value.LastIndexOf('/') + 1;
return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}";
}
else
{
return null;
}
}).Where(e => e != null).ToArray();
return string.Join("",
(prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""),
message,
string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}"))
);
}
internal static string FormatServerMessageWithPronouns(CharacterInfo charInfo, string message, params (string Key, string Value)[] keysWithValues)
{
var pronounCategory = charInfo.Prefab.Pronouns;
(string Key, string Value)[] pronounKwv = new[]
{
("[PronounLowercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]Lowercase")),
("[PronounUppercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]")),
("[PronounPossessiveLowercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]Lowercase")),
("[PronounPossessiveUppercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]")),
("[PronounReflexiveLowercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]Lowercase")),
("[PronounReflexiveUppercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]"))
};
return FormatServerMessage(message, keysWithValues.Concat(pronounKwv).ToArray());
}
// Same as string.Join(separator, parts) but performs the operation taking into account server message string replacements.
public static string JoinServerMessages(string separator, string[] parts, string namePrefix = "part.")
{
return string.Join("/",
string.Join("/", parts.Select((part, index) =>
{
var partStart = part.LastIndexOf('/') + 1;
return partStart > 0 ? $"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $"[{namePrefix}{index}]={part.Substring(partStart)}";
})),
string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]")));
}
public static LocalizedString ParseInputTypes(LocalizedString str)
{
return new InputTypeLString(str);
}
public static LocalizedString GetWithVariable(string tag, string varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No)
{
return GetWithVariable(tag.ToIdentifier(), varName.ToIdentifier(), value, formatCapitals);
}
public static LocalizedString GetWithVariable(Identifier tag, Identifier varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No)
{
return GetWithVariables(tag, (varName, value));
}
public static LocalizedString GetWithVariables(string tag, params (string Key, string Value)[] replacements)
{
return GetWithVariables(
tag.ToIdentifier(),
replacements.Select(kv =>
(kv.Key.ToIdentifier(),
(LocalizedString)new RawLString(kv.Value),
FormatCapitals.No)));
}
public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value)[] replacements)
{
return GetWithVariables(
tag.ToIdentifier(),
replacements.Select(kv =>
(kv.Key.ToIdentifier(),
kv.Value,
FormatCapitals.No)));
}
public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value, FormatCapitals FormatCapitals)[] replacements)
{
return GetWithVariables(
tag.ToIdentifier(),
replacements.Select(kv =>
(kv.Key.ToIdentifier(),
kv.Value,
kv.FormatCapitals)));
}
public static LocalizedString GetWithVariables(string tag, params (string Key, string Value, FormatCapitals FormatCapitals)[] replacements)
{
return GetWithVariables(
tag.ToIdentifier(),
replacements.Select(kv =>
(kv.Key.ToIdentifier(),
(LocalizedString)new RawLString(kv.Value),
kv.FormatCapitals)));
}
public static LocalizedString GetWithVariables(Identifier tag, params (Identifier Key, LocalizedString Value)[] replacements)
{
return GetWithVariables(tag, replacements.Select(kv => (kv.Key, kv.Value, FormatCapitals.No)));
}
public static LocalizedString GetWithVariables(Identifier tag, IEnumerable<(Identifier, LocalizedString, FormatCapitals)> replacements)
{
return new ReplaceLString(new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements);
}
public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement)
{
/*
<Description tag="talentdescription.simultaneousskillgain">
<Replace tag="[skillname1]" value="skillname.helm"/>
<Replace tag="[skillname2]" value="skillname.weapons"/>
<Replace tag="[somevalue]" value="45.3"/>
</Description>
*/
LocalizedString extraDescriptionLine = Get(descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty));
foreach (XElement replaceElement in descriptionElement.Elements())
{
if (replaceElement.NameAsIdentifier() != "replace") { continue; }
Identifier tag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty);
string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty<string>());
LocalizedString replacementValue = string.Empty;
for (int i = 0; i < replacementValues.Length; i++)
{
replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]);
if (i < replacementValues.Length - 1)
{
replacementValue += ", ";
}
}
if (replaceElement.Attribute("color") != null)
{
string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255");
replacementValue = $"‖color:{colorStr}‖" + replacementValue + "‖color:end‖";
}
extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue);
}
if (!(description is RawLString { Value: "" })) { description += "\n"; } //TODO: this is cursed
description += extraDescriptionLine;
}
public static LocalizedString GetServerMessage(string serverMessage)
{
return new ServerMsgLString(serverMessage);
}
public static LocalizedString Capitalize(this LocalizedString str)
{
return new CapitalizeLString(str);
}
public static void IncrementLanguageVersion()
{
LanguageVersion++;
ClearCache();
}
#if DEBUG
public static void CheckForDuplicates(LanguageIdentifier lang)
{
if (!TextPacks.ContainsKey(lang))
{
DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
return;
}
int packIndex = 0;
foreach (TextPack textPack in TextPacks[lang])
{
textPack.CheckForDuplicates(packIndex);
packIndex++;
}
}
public static void WriteToCSV()
{
LanguageIdentifier lang = DefaultLanguage;
if (!TextPacks.ContainsKey(lang))
{
DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
return;
}
int packIndex = 0;
foreach (TextPack textPack in TextPacks[lang])
{
textPack.WriteToCSV(packIndex);
packIndex++;
}
}
#endif
}
}
@@ -0,0 +1,174 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma
{
public readonly struct LanguageIdentifier
{
public static readonly LanguageIdentifier None = "None".ToLanguageIdentifier();
public readonly Identifier Value;
public int ValueHash => Value.GetHashCode();
public LanguageIdentifier(Identifier value) { Value = value; }
public override bool Equals(object obj)
{
if (obj is LanguageIdentifier other) { return this == other; }
return base.Equals(obj);
}
public override int GetHashCode() => ValueHash;
public static bool operator ==(LanguageIdentifier a, LanguageIdentifier b) => a.Value == b.Value;
public static bool operator !=(LanguageIdentifier a, LanguageIdentifier b) => !(a==b);
public override string ToString() => Value.ToString();
}
public static class LanguageIdentifierExtensions
{
public static LanguageIdentifier ToLanguageIdentifier(this Identifier identifier)
{
return new LanguageIdentifier(identifier);
}
public static LanguageIdentifier ToLanguageIdentifier(this string str)
{
return str.ToIdentifier().ToLanguageIdentifier();
}
}
public class TextPack
{
public readonly TextFile ContentFile;
public readonly LanguageIdentifier Language;
public readonly ImmutableDictionary<Identifier, ImmutableArray<string>> Texts;
public readonly string TranslatedName;
public readonly bool NoWhitespace;
public TextPack(TextFile file, ContentXElement mainElement, LanguageIdentifier language)
{
ContentFile = file;
var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value);
Language = language;
TranslatedName = mainElement.GetAttributeString("translatedname", languageName.Value);
NoWhitespace = mainElement.GetAttributeBool("nowhitespace", false);
Dictionary<Identifier, List<string>> texts = new Dictionary<Identifier, List<string>>();
foreach (var element in mainElement.Elements())
{
Identifier elemName = element.NameAsIdentifier();
if (!texts.ContainsKey(elemName)) { texts.Add(elemName, new List<string>()); }
texts[elemName].Add(element.ElementInnerText()
.Replace(@"\n", "\n")
.Replace("&amp;", "&")
.Replace("&lt;", "<")
.Replace("&gt;", ">")
.Replace("&quot;", "\"")
.Replace("&apos;", "'"));
}
Texts = texts.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
}
#if DEBUG
public void CheckForDuplicates(int index)
{
Dictionary<Identifier, int> tagCounts = new Dictionary<Identifier, int>();
Dictionary<string, int> contentCounts = new Dictionary<string, int>();
XDocument doc = XMLExtensions.TryLoadXml(ContentFile.Path);
if (doc == null) { return; }
foreach (var subElement in doc.Root.Elements())
{
Identifier infoName = subElement.NameAsIdentifier();
if (!tagCounts.ContainsKey(infoName))
{
tagCounts.Add(infoName, 1);
}
else
{
tagCounts[infoName] += 1;
}
string infoContent = subElement.Value;
if (string.IsNullOrEmpty(infoContent)) continue;
if (!contentCounts.ContainsKey(infoContent))
{
contentCounts.Add(infoContent, 1);
}
else
{
contentCounts[infoContent] += 1;
}
}
StringBuilder sb = new StringBuilder();
sb.Append("Language: " + Language);
sb.AppendLine();
sb.Append("Duplicate tags:");
sb.AppendLine();
sb.AppendLine();
for (int i = 0; i < tagCounts.Keys.Count; i++)
{
if (tagCounts[Texts.Keys.ElementAt(i)] > 1)
{
sb.Append(Texts.Keys.ElementAt(i) + " | Count: " + tagCounts[Texts.Keys.ElementAt(i)]);
sb.AppendLine();
}
}
sb.AppendLine();
sb.AppendLine();
sb.Append("Duplicate content:");
sb.AppendLine();
sb.AppendLine();
for (int i = 0; i < contentCounts.Keys.Count; i++)
{
if (contentCounts[contentCounts.Keys.ElementAt(i)] > 1)
{
sb.Append(contentCounts.Keys.ElementAt(i) + " | Count: " + contentCounts[contentCounts.Keys.ElementAt(i)]);
sb.AppendLine();
}
}
Barotrauma.IO.File.WriteAllText($"duplicate_{Language.ToString().ToLower()}_{index}.txt", sb.ToString());
}
public void WriteToCSV(int index)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < Texts.Count; i++)
{
Identifier key = Texts.Keys.ElementAt(i);
Texts.TryGetValue(key, out ImmutableArray<string> infoList);
for (int j = 0; j < infoList.Length; j++)
{
sb.Append(key); // ID
sb.Append('*');
sb.Append(infoList[j]); // Original
sb.Append('*');
// Translated
sb.Append('*');
// Comments
sb.AppendLine();
}
}
Barotrauma.IO.File.WriteAllText($"csv_{Language.ToString().ToLower()}_{index}.csv", sb.ToString());
}
#endif
}
}