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}$"; + } } }