Repository: TrenchBroom/TrenchBroom (2026-03-29T22:06:40Z)

Issue:
Last entity wins naming convention
Now by default the 1st name takes precedent when reading in entity definitions, all others with same name after the fact in the entity file(s) are considered redundant and ignored.

This being changed to make the last name read in files to take precedent instead. This would make @includes in your entity definitions far more useful.

This change is actually the same behavior hammer uses.

Thank you! 


Discussion:
Note: Hammer also behaves like this: the last definition wins. 
@eGax if you have some time, could you try https://github.com/TrenchBroom/TrenchBroom/pull/5149 and see if that works well?



Patch:
diff --git a/lib/TbMdlLib/src/EntityDefinitionParser.cpp b/lib/TbMdlLib/src/EntityDefinitionParser.cpp
index a7840bb566..583be70e9a 100644
--- a/lib/TbMdlLib/src/EntityDefinitionParser.cpp
+++ b/lib/TbMdlLib/src/EntityDefinitionParser.cpp
@@ -165,7 +165,7 @@ void inheritPropertyDefinitions(
 
 /**
  * Filter out redundant classes. A class is redundant if a class of the same name exists
- * at an earlier position in the given vector, unless the two classes each have one of the
+ * at a later position in the given vector, unless the two classes each have one of the
  * types point and brush each. That is, any duplicate is redundant with the exception of
  * overloaded point and brush classes.
  */
@@ -179,13 +179,24 @@ std::vector<EntityDefinitionClassInfo> filterRedundantClasses(
 
   const auto baseClassMask = getMask(EntityDefinitionClassType::BaseClass);
 
+  // Keep only the last occurrence per (name, type) while preserving overall ordering.
+  auto lastOccurrences =
+    std::unordered_map<std::string, std::unordered_map<int, size_t>>{};
+  for (size_t i = 0; i < classInfos.size(); ++i)
+  {
+    const auto classMask = getMask(classInfos[i].type);
+    lastOccurrences[classInfos[i].name][classMask] = i;
+  }
+
   auto seen = std::unordered_map<std::string, int>{};
-  for (const auto& classInfo : classInfos)
+  for (size_t i = 0; i < classInfos.size(); ++i)
   {
+    const auto& classInfo = classInfos[i];
     auto& seenMask = seen[classInfo.name];
     const auto classMask = getMask(classInfo.type);
+    const auto isLastOccurrence = lastOccurrences[classInfo.name][classMask] == i;
 
-    if (classMask & seenMask)
+    if (!isLastOccurrence)
     {
       status.warn(classInfo.location, "Duplicate class info '" + classInfo.name + "'");
     }
@@ -196,7 +207,7 @@ std::vector<EntityDefinitionClassInfo> filterRedundantClasses(
     else
     {
       result.push_back(classInfo);
-      seenMask |= classMask;
+      seenMask = seenMask | classMask;
     }
   }
 


Test patch:
diff --git a/lib/TbMdlLib/test/src/tst_DefParser.cpp b/lib/TbMdlLib/test/src/tst_DefParser.cpp
index 85b3d23d0a..9209bab8fc 100644
--- a/lib/TbMdlLib/test/src/tst_DefParser.cpp
+++ b/lib/TbMdlLib/test/src/tst_DefParser.cpp
@@ -217,6 +217,40 @@ Set sounds to the cd track to play.
       });
   }
 
+  SECTION("parseDuplicatePointClassKeepsLast")
+  {
+    const auto file = R"(
+    /*QUAKED monster_zombie (1.0 0.0 0.0) (-16 -16 -24) (16 16 32)
+    First definition.
+    */
+
+    /*QUAKED monster_zombie (0.0 1.0 0.0) (-8 -8 -8) (8 8 8)
+    Second definition.
+    */
+    )";
+
+    auto parser = DefParser{file, RgbaF{1.0f, 1.0f, 1.0f, 1.0f}};
+    auto status = TestParserStatus{};
+
+    CHECK(
+      parser.parseDefinitions(status)
+      == std::vector<mdl::EntityDefinition>{
+        {
+          "monster_zombie",
+          RgbF{0.0f, 1.0f, 0.0f},
+          "Second definition.",
+          {},
+          mdl::PointEntityDefinition{
+            {{-8.0, -8.0, -8.0}, {8.0, 8.0, 8.0}},
+            {},
+            {},
+          },
+        },
+      });
+    CHECK(status.countStatus(LogLevel::Warn) == 1u);
+    CHECK(status.countStatus(LogLevel::Error) == 0u);
+  }
+
   SECTION("parseSpawnflagWithSkip")
   {
     const auto file = R"(
diff --git a/lib/TbMdlLib/test/src/tst_EntParser.cpp b/lib/TbMdlLib/test/src/tst_EntParser.cpp
index 14d6681a23..3d84b43e8f 100644
--- a/lib/TbMdlLib/test/src/tst_EntParser.cpp
+++ b/lib/TbMdlLib/test/src/tst_EntParser.cpp
@@ -182,6 +182,38 @@ Updated: 2011-03-02
       });
   }
 
+  SECTION("parseDuplicatePointEntityDefinitionKeepsLast")
+  {
+    const std::string file = R"(
+<?xml version="1.0"?>
+<classes>
+  <point name="_skybox" color="1 0 0" box="-1 -1 -1 1 1 1"></point>
+  <point name="_skybox" color="0 1 0" box="-2 -2 -2 2 2 2"></point>
+</classes>
+)";
+
+    auto parser = EntParser{file, RgbaF{1.0f, 1.0f, 1.0f, 1.0f}};
+    auto status = TestParserStatus{};
+
+    CHECK(
+      parser.parseDefinitions(status)
+      == std::vector<mdl::EntityDefinition>{
+        {
+          "_skybox",
+          RgbF{0.0f, 1.0f, 0.0f},
+          "",
+          {},
+          mdl::PointEntityDefinition{
+            {{-2, -2, -2}, {+2, +2, +2}},
+            {},
+            {},
+          },
+        },
+      });
+    CHECK(status.countStatus(LogLevel::Warn) == 1u);
+    CHECK(status.countStatus(LogLevel::Error) == 0u);
+  }
+
   SECTION("parseSimpleGroupEntityDefinition")
   {
     const std::string file = R"(
diff --git a/lib/TbMdlLib/test/src/tst_EntityDefinitionParser.cpp b/lib/TbMdlLib/test/src/tst_EntityDefinitionParser.cpp
index ebd40fb27a..59fdb4a209 100644
--- a/lib/TbMdlLib/test/src/tst_EntityDefinitionParser.cpp
+++ b/lib/TbMdlLib/test/src/tst_EntityDefinitionParser.cpp
@@ -50,7 +50,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -60,7 +60,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 2},
        "brush",
        std::nullopt,
        std::nullopt,
@@ -72,7 +72,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -82,7 +82,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 2},
        "brush",
        std::nullopt,
        std::nullopt,
@@ -123,7 +123,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 1},
+       {0, 2},
        "b",
        std::nullopt,
        std::nullopt,
@@ -133,7 +133,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 3},
        "b",
        std::nullopt,
        std::nullopt,
@@ -143,7 +143,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 1},
+       {0, 4},
        "c",
        std::nullopt,
        std::nullopt,
@@ -153,7 +153,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 2},
+       {0, 5},
        "c",
        std::nullopt,
        std::nullopt,
@@ -163,7 +163,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 6},
        "c",
        std::nullopt,
        std::nullopt,
@@ -173,7 +173,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 7},
        "d",
        std::nullopt,
        std::nullopt,
@@ -183,7 +183,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 1},
+       {0, 8},
        "d",
        std::nullopt,
        std::nullopt,
@@ -193,7 +193,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 9},
        "e",
        std::nullopt,
        std::nullopt,
@@ -203,7 +203,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 1},
+       {0, 10},
        "e",
        std::nullopt,
        std::nullopt,
@@ -213,7 +213,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 11},
        "f",
        std::nullopt,
        std::nullopt,
@@ -223,7 +223,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 1},
+       {0, 12},
        "f",
        std::nullopt,
        std::nullopt,
@@ -235,7 +235,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::BrushClass,
-       {0, 1},
+       {0, 2},
        "b",
        std::nullopt,
        std::nullopt,
@@ -245,7 +245,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 1},
+       {0, 4},
        "c",
        std::nullopt,
        std::nullopt,
@@ -255,7 +255,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 2},
+       {0, 5},
        "c",
        std::nullopt,
        std::nullopt,
@@ -265,7 +265,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 8},
        "d",
        std::nullopt,
        std::nullopt,
@@ -275,7 +275,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 10},
        "e",
        std::nullopt,
        std::nullopt,
@@ -311,7 +311,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -323,7 +323,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        "description",
        RgbB{1, 2, 3},
@@ -354,7 +354,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        "blah blah",
        RgbB{2, 3, 4},
@@ -366,7 +366,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        "blah blah",
        RgbB{2, 3, 4},
@@ -404,7 +404,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -416,7 +416,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -455,7 +455,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -467,7 +467,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -507,7 +507,7 @@ TEST_CASE("resolveInheritance")
        {a1_1, a2},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -519,7 +519,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -572,7 +572,7 @@ TEST_CASE("resolveInheritance")
        {a1},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 1},
        "point",
        std::nullopt,
        std::nullopt,
@@ -649,7 +649,7 @@ TEST_CASE("resolveInheritance")
        {a1_1, a2},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 1},
        "base2",
        "base2",
        RgbB{1, 2, 3},
@@ -659,7 +659,7 @@ TEST_CASE("resolveInheritance")
        {a1_2, a3},
        {"base1"}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 2},
        "point",
        std::nullopt,
        std::nullopt,
@@ -671,7 +671,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 2},
        "point",
        "base2",
        RgbB{1, 2, 3},
@@ -731,7 +731,7 @@ TEST_CASE("resolveInheritance")
        {a1_1, a2},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 1},
        "base2",
        "base2",
        RgbB{1, 2, 3},
@@ -741,7 +741,7 @@ TEST_CASE("resolveInheritance")
        {a1_2, a3},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 2},
        "point",
        std::nullopt,
        std::nullopt,
@@ -753,7 +753,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 2},
        "point",
        "base1",
        RgbB{1, 2, 3},
@@ -793,7 +793,7 @@ TEST_CASE("resolveInheritance")
        {a1},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 1},
        "base2_1",
        "base2_1",
        RgbB{1, 2, 3},
@@ -803,7 +803,7 @@ TEST_CASE("resolveInheritance")
        {a2_1},
        {"base1"}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 2},
        "base2_2",
        "base2_2",
        std::nullopt,
@@ -813,7 +813,7 @@ TEST_CASE("resolveInheritance")
        {a2_2},
        {"base1"}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 3},
        "point1",
        std::nullopt,
        std::nullopt,
@@ -823,7 +823,7 @@ TEST_CASE("resolveInheritance")
        {a3},
        {"base2_1", "base2_2"}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 4},
        "point2",
        std::nullopt,
        std::nullopt,
@@ -835,7 +835,7 @@ TEST_CASE("resolveInheritance")
     };
     const auto expected = std::vector<EntityDefinitionClassInfo>{
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 3},
        "point1",
        "base2_1",
        RgbB{1, 2, 3},
@@ -845,7 +845,7 @@ TEST_CASE("resolveInheritance")
        {a3, a2_1, a1, a2_2},
        {"base2_1", "base2_2"}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 4},
        "point2",
        "base2_2",
        RgbB{1, 2, 3},
@@ -876,7 +876,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 1},
        "base",
        "brush",
        std::nullopt,
@@ -886,7 +886,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 2},
        "point",
        std::nullopt,
        std::nullopt,
@@ -896,7 +896,7 @@ TEST_CASE("resolveInheritance")
        {},
        {"base"}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 3},
        "brush",
        std::nullopt,
        std::nullopt,
@@ -918,7 +918,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 1},
        "base",
        "brush",
        std::nullopt,
@@ -928,7 +928,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 2},
        "point",
        "point",
        std::nullopt,
@@ -938,7 +938,7 @@ TEST_CASE("resolveInheritance")
        {},
        {"base"}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 3},
        "brush",
        "brush",
        std::nullopt,
@@ -969,7 +969,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 1},
        "base",
        "brush",
        std::nullopt,
@@ -979,7 +979,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BaseClass,
-       {0, 0},
+       {0, 2},
        "mid",
        std::nullopt,
        std::nullopt,
@@ -989,7 +989,7 @@ TEST_CASE("resolveInheritance")
        {},
        {"base"}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 3},
        "point",
        std::nullopt,
        std::nullopt,
@@ -999,7 +999,7 @@ TEST_CASE("resolveInheritance")
        {},
        {"mid"}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 4},
        "brush",
        std::nullopt,
        std::nullopt,
@@ -1021,7 +1021,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 1},
        "base",
        "brush",
        std::nullopt,
@@ -1031,7 +1031,7 @@ TEST_CASE("resolveInheritance")
        {},
        {}},
       {EntityDefinitionClassType::PointClass,
-       {0, 0},
+       {0, 3},
        "point",
        "point",
        std::nullopt,
@@ -1041,7 +1041,7 @@ TEST_CASE("resolveInheritance")
        {},
        {"mid"}},
       {EntityDefinitionClassType::BrushClass,
-       {0, 0},
+       {0, 4},
        "brush",
        "brush",
        std::nullopt,
diff --git a/lib/TbMdlLib/test/src/tst_FgdParser.cpp b/lib/TbMdlLib/test/src/tst_FgdParser.cpp
index 3a44ae2f6c..10df841042 100644
--- a/lib/TbMdlLib/test/src/tst_FgdParser.cpp
+++ b/lib/TbMdlLib/test/src/tst_FgdParser.cpp
@@ -234,6 +234,33 @@ TEST_CASE("FgdParser")
       });
   }
 
+  SECTION("parseDuplicatePointClassKeepsLast")
+  {
+    const auto file = R"(
+    @PointClass = info_notnull : "First definition"
+    [
+    ]
+    @PointClass = info_notnull : "Second definition"
+    [
+    ]
+    )";
+
+    auto parser = FgdParser{file, RgbaF{1.0f, 1.0f, 1.0f, 1.0f}};
+    auto status = TestParserStatus{};
+
+    CHECK(
+      parser.parseDefinitions(status)
+      == std::vector<mdl::EntityDefinition>{
+        {"info_notnull",
+         RgbaF{1.0f, 1.0f, 1.0f, 1.0f},
+         "Second definition",
+         {},
+         mdl::PointEntityDefinition{{{-8, -8, -8}, {8, 8, 8}}, {}, {}}},
+      });
+    CHECK(status.countStatus(LogLevel::Warn) == 1u);
+    CHECK(status.countStatus(LogLevel::Error) == 0u);
+  }
+
   SECTION("parseBaseProperty")
   {
     const auto file = R"(