using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Linguini.Bundle; using Linguini.Bundle.Errors; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.IoC; using Robust.Shared.Prototypes; namespace Robust.Shared.Localization { internal abstract partial class LocalizationManager { // Concurrent dict so that multiple threads "reading" .Name won't cause a concurrent write issue // when the cache gets populated. private readonly ConcurrentDictionary _entityCache = new(); private void FlushEntityCache() { _logSawmill.Debug("Flushing entity localization cache."); _entityCache.Clear(); } private bool TryGetEntityLocAttrib(EntityUid entity, string attribute, [NotNullWhen(true)] out string? value) { if (_entMan.TryGetComponent(entity, out GrammarComponent? grammar) && grammar.Attributes.TryGetValue(attribute, out value)) { return true; } if (_entMan.GetComponent(entity).EntityPrototype is not {} prototype) { value = null; return false; } var data = GetEntityData(prototype.ID); return data.Attributes.TryGetValue(attribute, out value); } // Flush caches conservatively on prototype/localization changes. private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) { if (args.WasModified()) FlushEntityCache(); } private EntityLocData CalcEntityLoc(string prototypeId) { string? name = null; string? desc = null; string? suffix = null; Dictionary? attributes = null; foreach (var prototype in _prototype.EnumerateParents(prototypeId, true)) { var locId = prototype?.CustomLocalizationID ?? $"ent-{prototypeId}"; if (TryGetMessage(locId, out var bundle, out var msg)) { // Localization override exists. var msgAttrs = msg.Attributes; if (name == null && msg.Value != null) { // Only set name if the value isn't empty. // So that you can override *only* a desc/suffix. name = bundle.FormatPattern(msg.Value, null, out var fmtErr); WriteWarningForErrs(fmtErr, locId); } if (msgAttrs.Count != 0) { foreach (var (attrId, pattern) in msg.Attributes) { var attrib = attrId.ToString(); if (attrib.Equals("desc") || attrib.Equals("suffix")) continue; attributes ??= new Dictionary(); if (!attributes.ContainsKey(attrib)) { var value = bundle.FormatPattern(pattern, null, out var errors); WriteWarningForErrs(errors, locId); attributes[attrib] = value; } } var allErrors = new List(); if (desc == null && !bundle.TryGetMessage(locId, "desc", null, out var err1, out desc)) { desc = null; allErrors.AddRange(err1); } if (suffix == null && !bundle.TryGetMessage(locId, "suffix", null, out var err, out suffix)) { suffix = null; } WriteWarningForErrs(allErrors, locId); } } name ??= prototype?.SetName; desc ??= prototype?.SetDesc; suffix ??= prototype?.SetSuffix; if (prototype?.LocProperties != null && prototype.LocProperties.Count != 0) { foreach (var (attrib, value) in prototype.LocProperties) { attributes ??= new Dictionary(); if (!attributes.ContainsKey(attrib)) { attributes[attrib] = value; } } } } // Attempt to infer suffix from entity categories if (suffix == null) { var suffixes = _prototype.Index(prototypeId).Categories .Where(x => x.Suffix != null) .Select(x => GetString(x.Suffix!)); suffix = string.Join(", ", suffixes); } return new EntityLocData( name ?? "", desc ?? "", suffix, attributes?.ToImmutableDictionary() ?? ImmutableDictionary.Empty); } public EntityLocData GetEntityData(string prototypeId) { return _entityCache.GetOrAdd(prototypeId, (id, t) => t.CalcEntityLoc(id), this); } } }