From 5b9c3ccb7d969e26dafc66c41903b7aa62d42e93 Mon Sep 17 00:00:00 2001 From: codebykoi Date: Sat, 4 Jul 2026 11:13:40 +0300 Subject: [PATCH 1/6] Fix: entity localization parent fallback --- .../LocalizationManager.Entity.cs | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/Robust.Shared/Localization/LocalizationManager.Entity.cs b/Robust.Shared/Localization/LocalizationManager.Entity.cs index a50f54be467..e403fe6efef 100644 --- a/Robust.Shared/Localization/LocalizationManager.Entity.cs +++ b/Robust.Shared/Localization/LocalizationManager.Entity.cs @@ -54,11 +54,21 @@ private EntityLocData CalcEntityLoc(string prototypeId) string? name = null; string? desc = null; string? suffix = null; + + string? fallbackName = null; + string? fallbackDesc = null; + string? fallbackSuffix = null; + Dictionary? attributes = null; - foreach (var prototype in _prototype.EnumerateParents(prototypeId, true)) + // A child can have a locale entry just to override its suffix, while + // still inheriting the description from a parent. Hold these errors + // until we know no parent supplied a description either. + List<(string locId, List errs)>? deferredDescErrors = null; + + foreach (var (id, prototype) in _prototype.EnumerateAllParents(prototypeId, true)) { - var locId = prototype?.CustomLocalizationID ?? $"ent-{prototypeId}"; + var locId = prototype?.CustomLocalizationID ?? $"ent-{id}"; if (TryGetMessage(locId, out var bundle, out var msg)) { @@ -91,12 +101,13 @@ private EntityLocData CalcEntityLoc(string prototypeId) } } - var allErrors = new List(); if (desc == null && !bundle.TryGetMessage(locId, "desc", null, out var err1, out desc)) { desc = null; - allErrors.AddRange(err1); + // Defer: a parent prototype may still provide .desc. + deferredDescErrors ??= new List<(string, List)>(); + deferredDescErrors.Add((locId, err1.ToList())); } if (suffix == null @@ -104,16 +115,17 @@ private EntityLocData CalcEntityLoc(string prototypeId) { suffix = null; } - - WriteWarningForErrs(allErrors, locId); } } - name ??= prototype?.SetName; - desc ??= prototype?.SetDesc; - suffix ??= prototype?.SetSuffix; + if (prototype == null) + continue; - if (prototype?.LocProperties != null && prototype.LocProperties.Count != 0) + fallbackName ??= prototype.SetName; + fallbackDesc ??= prototype.SetDesc; + fallbackSuffix ??= prototype.SetSuffix; + + if (prototype.LocProperties.Count != 0) { foreach (var (attrib, value) in prototype.LocProperties) { @@ -126,6 +138,24 @@ private EntityLocData CalcEntityLoc(string prototypeId) } } + name ??= fallbackName; + desc ??= fallbackDesc; + suffix ??= fallbackSuffix; + + // Plenty of prototypes have no description at all, such as spawners, + // markers, and debug/admin entities. Keep the missing .desc warning + // visible for debugging, but don't treat it as a real localization issue. + if (desc == null && deferredDescErrors != null) + { + foreach (var (locId, errs) in deferredDescErrors) + { + foreach (var err in errs) + { + _logSawmill.Debug("Missing .desc for `{locId}`\n{e1}", locId, err); + } + } + } + // Attempt to infer suffix from entity categories if (suffix == null) { From e8f12d692342ba5b118654b1ae5f57b050672ce4 Mon Sep 17 00:00:00 2001 From: codebykoi Date: Sat, 4 Jul 2026 12:14:42 +0300 Subject: [PATCH 2/6] fallback fix --- .../Localization/LocalizationManager.Entity.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Robust.Shared/Localization/LocalizationManager.Entity.cs b/Robust.Shared/Localization/LocalizationManager.Entity.cs index e403fe6efef..d666ab9e391 100644 --- a/Robust.Shared/Localization/LocalizationManager.Entity.cs +++ b/Robust.Shared/Localization/LocalizationManager.Entity.cs @@ -55,10 +55,6 @@ private EntityLocData CalcEntityLoc(string prototypeId) string? desc = null; string? suffix = null; - string? fallbackName = null; - string? fallbackDesc = null; - string? fallbackSuffix = null; - Dictionary? attributes = null; // A child can have a locale entry just to override its suffix, while @@ -121,9 +117,9 @@ private EntityLocData CalcEntityLoc(string prototypeId) if (prototype == null) continue; - fallbackName ??= prototype.SetName; - fallbackDesc ??= prototype.SetDesc; - fallbackSuffix ??= prototype.SetSuffix; + name ??= prototype.SetName; + desc ??= prototype.SetDesc; + suffix ??= prototype.SetSuffix; if (prototype.LocProperties.Count != 0) { @@ -138,10 +134,6 @@ private EntityLocData CalcEntityLoc(string prototypeId) } } - name ??= fallbackName; - desc ??= fallbackDesc; - suffix ??= fallbackSuffix; - // Plenty of prototypes have no description at all, such as spawners, // markers, and debug/admin entities. Keep the missing .desc warning // visible for debugging, but don't treat it as a real localization issue. From 013722e8069af8cef2f6d16ae0b7df3aa56c48b8 Mon Sep 17 00:00:00 2001 From: codebykoi Date: Sat, 4 Jul 2026 15:58:21 +0300 Subject: [PATCH 3/6] fallback fix + new tests --- .../Localization/LocalizationTests.cs | 17 +++++++++++++++++ .../Localization/LocalizationManager.Entity.cs | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Robust.Shared.IntegrationTests/Localization/LocalizationTests.cs b/Robust.Shared.IntegrationTests/Localization/LocalizationTests.cs index e630f0981e9..25d3582ecef 100644 --- a/Robust.Shared.IntegrationTests/Localization/LocalizationTests.cs +++ b/Robust.Shared.IntegrationTests/Localization/LocalizationTests.cs @@ -77,6 +77,14 @@ public void Setup() gender: male proper: true +# Values specified in fluent at PARENT and supplemented by empty child fluent. +- type: entity + id: TestInheritEmptyValueParent + +- type: entity + id: TestInheritEmptyValueChild + parent: TestInheritEmptyValueParent + # Attribures stored in grammar component - type: entity id: PropsInGrammar @@ -141,6 +149,14 @@ [1] B .gender = female .proper = false +ent-TestInheritEmptyValueParent = A + .desc = B + .gender = male + .proper = true + +ent-TestInheritEmptyValueChild = + .suffix = C + test-message-gender = { GENDER($entity) -> [male] male @@ -234,6 +250,7 @@ public void TestNumbers(object o1, object o2, object o3) [TestCase("PropsInLocOverriding")] [TestCase("PropsInGrammar")] [TestCase("TestInheritOverridingChild")] + [TestCase("TestInheritEmptyValueChild")] public void TestLocData(string prototype) { var loc = IoCManager.Resolve(); diff --git a/Robust.Shared/Localization/LocalizationManager.Entity.cs b/Robust.Shared/Localization/LocalizationManager.Entity.cs index d666ab9e391..e147791d2d7 100644 --- a/Robust.Shared/Localization/LocalizationManager.Entity.cs +++ b/Robust.Shared/Localization/LocalizationManager.Entity.cs @@ -75,8 +75,11 @@ private EntityLocData CalcEntityLoc(string prototypeId) { // 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); + var locName = bundle.FormatPattern(msg.Value, null, out var fmtErr); WriteWarningForErrs(fmtErr, locId); + + if (!string.IsNullOrEmpty(locName)) + name = locName; } if (msgAttrs.Count != 0) From 3003043789c9223cf2250641cf95cf3c41106b41 Mon Sep 17 00:00:00 2001 From: codebykoi Date: Sat, 4 Jul 2026 16:06:09 +0300 Subject: [PATCH 4/6] added NeverPushInheritance to entity prototype fields. --- Robust.Shared/Prototypes/EntityPrototype.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 8cf4d7098f9..26fe0d412d0 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -36,6 +36,7 @@ public sealed partial class EntityPrototype : IPrototype, IInheritingPrototype, private const int DEFAULT_RANGE = 200; [DataField("loc")] + [NeverPushInheritance] private Dictionary? _locPropertiesSet; /// @@ -51,12 +52,15 @@ public sealed partial class EntityPrototype : IPrototype, IInheritingPrototype, /// /// [DataField("name")] + [NeverPushInheritance] public string? SetName { get; private set; } [DataField("description")] + [NeverPushInheritance] public string? SetDesc { get; private set; } [DataField("suffix")] + [NeverPushInheritance] public string? SetSuffix { get; private set; } [DataField("categories"), Access(typeof(PrototypeManager))] From e363b7775f89d18944d386d6a8fb68880664d5a9 Mon Sep 17 00:00:00 2001 From: codebykoi Date: Sat, 4 Jul 2026 16:40:42 +0300 Subject: [PATCH 5/6] fix: if no FTL/YAML/Inheritance name, fallback to protoId --- Robust.Shared/Localization/LocalizationManager.Entity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Localization/LocalizationManager.Entity.cs b/Robust.Shared/Localization/LocalizationManager.Entity.cs index e147791d2d7..f2caa004945 100644 --- a/Robust.Shared/Localization/LocalizationManager.Entity.cs +++ b/Robust.Shared/Localization/LocalizationManager.Entity.cs @@ -161,7 +161,7 @@ private EntityLocData CalcEntityLoc(string prototypeId) } return new EntityLocData( - name ?? "", + name ?? prototypeId, desc ?? "", suffix, attributes?.ToImmutableDictionary() ?? ImmutableDictionary.Empty); From 5c3d265efb0b8d287415c1a75346e31ae04653df Mon Sep 17 00:00:00 2001 From: codebykoi Date: Sat, 4 Jul 2026 17:25:58 +0300 Subject: [PATCH 6/6] rerun tests --- Robust.Shared/Localization/LocalizationManager.Entity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Localization/LocalizationManager.Entity.cs b/Robust.Shared/Localization/LocalizationManager.Entity.cs index f2caa004945..0fe08d04ff7 100644 --- a/Robust.Shared/Localization/LocalizationManager.Entity.cs +++ b/Robust.Shared/Localization/LocalizationManager.Entity.cs @@ -165,7 +165,7 @@ private EntityLocData CalcEntityLoc(string prototypeId) desc ?? "", suffix, attributes?.ToImmutableDictionary() ?? ImmutableDictionary.Empty); - } + } public EntityLocData GetEntityData(string prototypeId)