diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index 22db3ca31f..87c996452e 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.RegularExpressions;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
using Content.Server.Maps;
@@ -44,17 +45,43 @@ namespace Content.IntegrationTests.Tests
AdminTestArenaSystem.ArenaMapPath
};
+ ///
+ /// A dictionary linking maps to collections of entity prototype ids that should be exempt from "DoNotMap" restrictions.
+ ///
+ ///
+ /// This declares that the listed entity prototypes are allowed to be present on the map
+ /// despite being categorized as "DoNotMap", while any unlisted prototypes will still
+ /// cause the test to fail.
+ ///
+ private static readonly Dictionary> DoNotMapWhitelistSpecific = new()
+ {
+ {"/Maps/bagel.yml", ["RubberStampMime"]},
+ {"/Maps/reach.yml", ["HandheldCrewMonitor"]},
+ {"/Maps/Shuttles/ShuttleEvent/honki.yml", ["GoldenBikeHorn", "RubberStampClown"]},
+ {"/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", ["RubberStampSyndicate"]},
+ {"/Maps/Shuttles/ShuttleEvent/cruiser.yml", ["ShuttleGunPerforator"]},
+ {"/Maps/Shuttles/ShuttleEvent/instigator.yml", ["ShuttleGunFriendship"]},
+ };
+
+ ///
+ /// Maps listed here are given blanket freedom to contain "DoNotMap" entities. Use sparingly.
+ ///
+ ///
+ /// It is also possible to whitelist entire directories here. For example, adding
+ /// "/Maps/Shuttles/**" will whitelist all shuttle maps.
+ ///
private static readonly string[] DoNotMapWhitelist =
{
"/Maps/centcomm.yml",
- "/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
- "/Maps/reach.yml", // Contains handheld crew monitor
- "/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
- "/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp
- "/Maps/Shuttles/ShuttleEvent/instigator.yml", // Contains EXP-320g "Friendship"
- "/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", // Contains syndicate rubber stamp
};
+ ///
+ /// Converts the above globs into regex so your eyes dont bleed trying to add filepaths.
+ ///
+ private static readonly Regex[] DoNotMapWhiteListRegexes = DoNotMapWhitelist
+ .Select(glob => new Regex(GlobToRegex(glob), RegexOptions.IgnoreCase | RegexOptions.Compiled))
+ .ToArray();
+
private static readonly string[] GameMaps =
{
"Dev",
@@ -247,17 +274,30 @@ namespace Content.IntegrationTests.Tests
await pair.CleanReturnAsync();
}
+ private bool IsWhitelistedForMap(EntProtoId protoId, ResPath map)
+ {
+ if (!DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var allowedProtos))
+ return false;
+
+ return allowedProtos.Contains(protoId);
+ }
+
///
/// Check that maps do not have any entities that belong to the DoNotMap entity category
///
private void CheckDoNotMap(ResPath map, YamlNode node, IPrototypeManager protoManager)
{
- if (DoNotMapWhitelist.Contains(map.ToString()))
- return;
+ foreach (var regex in DoNotMapWhiteListRegexes)
+ {
+ if (regex.IsMatch(map.ToString()))
+ return;
+ }
var yamlEntities = node["entities"];
var dnmCategory = protoManager.Index(DoNotMapCategory);
+ // Make a set containing all the specific whitelisted proto ids for this map
+ HashSet unusedExemptions = DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var exemptions) ? new(exemptions) : [];
Assert.Multiple(() =>
{
foreach (var yamlEntity in (YamlSequenceNode)yamlEntities)
@@ -268,10 +308,17 @@ namespace Content.IntegrationTests.Tests
if (!protoManager.TryIndex(protoId, out var proto))
continue;
- Assert.That(!proto.Categories.Contains(dnmCategory),
+ Assert.That(!proto.Categories.Contains(dnmCategory) || IsWhitelistedForMap(protoId, map),
$"\nMap {map} contains entities in the DO NOT MAP category ({proto.Name})");
+
+ // The proto id is used on this map, so remove it from the set
+ unusedExemptions.Remove(protoId);
}
});
+
+ // If there are any proto ids left, they must not have been used in the map!
+ Assert.That(unusedExemptions, Is.Empty,
+ $"Map {map} has DO NOT MAP entities whitelisted that are not present in the map: {string.Join(", ", unusedExemptions)}");
}
private bool IsPreInit(ResPath map,
@@ -332,7 +379,7 @@ namespace Content.IntegrationTests.Tests
MapId mapId;
try
{
- var opts = DeserializationOptions.Default with {InitializeMaps = true};
+ var opts = DeserializationOptions.Default with { InitializeMaps = true };
ticker.LoadGameMap(protoManager.Index(mapProto), out mapId, opts);
}
catch (Exception ex)
@@ -439,7 +486,7 @@ namespace Content.IntegrationTests.Tests
#nullable enable
while (queryPoint.MoveNext(out T? comp, out var xform))
{
- var spawner = (ISpawnPoint) comp;
+ var spawner = (ISpawnPoint)comp;
if (spawner.SpawnType is not SpawnPointType.LateJoin
|| xform.GridUid == null
@@ -553,5 +600,20 @@ namespace Content.IntegrationTests.Tests
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
+
+ ///
+ /// Lets us the convert the filepaths to regex without eyeglaze trying to add new paths.
+ ///
+ private static string GlobToRegex(string glob)
+ {
+ var regex = Regex.Escape(glob)
+ .Replace(@"\*\*", "**") // replace **
+ .Replace(@"\*", "*") // replace *
+ .Replace("**", ".*") // ** → match across folders
+ .Replace("*", @"[^/]*") // * → match within a single folder
+ .Replace(@"\?", "."); // ? → any single character
+
+ return $"^{regex}$";
+ }
}
}