diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 6729f231600..9807a41c4aa 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Content.IntegrationTests.Utility; using YamlDotNet.RepresentationModel; using Content.Server.Administration.Systems; using Content.Server.GameTicking; @@ -80,34 +81,9 @@ namespace Content.IntegrationTests.Tests .Select(glob => new Regex(GlobToRegex(glob), RegexOptions.IgnoreCase | RegexOptions.Compiled)) .ToArray(); - private static readonly string[] GameMaps = - { - "Dev", - "TestTeg", - "CentComm", - "MeteorArena", - "Academy", //DeltaV - "Arena", // DeltaV - "ArenaMedieval", //DeltaV - "Asterisk", // DeltaV - "Byoin", // DeltaV - "Chibi", // DeltaV - "Division", // DeltaV - "Edge", // DeltaV - "Elegance", // DeltaV - "Glacier", // DeltaV - "Hammurabi", // DeltaV - "Lighthouse", // DeltaV - "Micro", // DeltaV - "Ovni", //DeltaV - "Pebble", // DeltaV - "PebbleHiring", // DeltaV - "Shoukou", // DeltaV - "Submarine", //DeltaV - "Terra", //DeltaV - "TheHive", // DeltaV - "Tortuga" // DeltaV - }; + private static readonly string[] GameMaps = GameDataScrounger.PrototypesOfKind().Where(x => x != PoolManager.TestMap).ToArray(); + private static readonly ResPath[] AllMapFiles = GameDataScrounger.FilesInDirectoryInVfs("/Maps/_DV", "*.yml"); + private static readonly ResPath[] ShuttleMapFiles = GameDataScrounger.FilesInDirectoryInVfs("/Maps/_DV/Shuttles", "*.yml"); private static readonly ProtoId DoNotMapCategory = "DoNotMap"; @@ -150,53 +126,45 @@ namespace Content.IntegrationTests.Tests /// Asserts that shuttles are loadable and have been saved as grids and not maps. /// [Test] - public async Task ShuttlesLoadableTest() + [TestCaseSource(nameof(ShuttleMapFiles))] + public async Task ShuttlesLoadableTest(ResPath path) { await using var pair = await PoolManager.GetServerClient(); var server = pair.Server; var entManager = server.ResolveDependency(); - var resMan = server.ResolveDependency(); var mapLoader = entManager.System(); var mapSystem = entManager.System(); var cfg = server.ResolveDependency(); Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); - var shuttleFolder = new ResPath("/Maps/Shuttles"); - var shuttles = resMan - .ContentFindFiles(shuttleFolder) - .Where(filePath => - filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) - .ToArray(); - await server.WaitPost(() => { Assert.Multiple(() => { - foreach (var path in shuttles) + mapSystem.CreateMap(out var mapId); + try { - mapSystem.CreateMap(out var mapId); - try - { - Assert.That(mapLoader.TryLoadGrid(mapId, path, out _), - $"Failed to load shuttle {path}, was it saved as a map instead of a grid?"); - } - catch (Exception ex) - { - throw new Exception($"Failed to load shuttle {path}, was it saved as a map instead of a grid?", - ex); - } - mapSystem.DeleteMap(mapId); + Assert.That(mapLoader.TryLoadGrid(mapId, path, out _), + $"Failed to load shuttle {path}, was it saved as a map instead of a grid?"); } + catch (Exception ex) + { + throw new Exception($"Failed to load shuttle {path}, was it saved as a map instead of a grid?", + ex); + } + mapSystem.DeleteMap(mapId); }); }); + await server.WaitRunTicks(1); await pair.CleanReturnAsync(); } [Test] - public async Task NoSavedPostMapInitTest() + [TestCaseSource(nameof(AllMapFiles))] + public async Task NoSavedPostMapInitTest(ResPath map) { await using var pair = await PoolManager.GetServerClient(); var server = pair.Server; @@ -205,47 +173,41 @@ namespace Content.IntegrationTests.Tests var protoManager = server.ResolveDependency(); var loader = server.System(); - var mapFolder = new ResPath("/Maps"); - var maps = resourceManager - .ContentFindFiles(mapFolder) - .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) - .ToArray(); + var rootedPath = map.ToRootedPath(); - var v7Maps = new List(); - foreach (var map in maps) + var isV7Map = false; + + // ReSharper disable once RedundantLogicalConditionalExpressionOperand + if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) { - var rootedPath = map.ToRootedPath(); + await pair.CleanReturnAsync(); + return; // We just pass immediately. + } - // ReSharper disable once RedundantLogicalConditionalExpressionOperand - if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) - { - continue; - } + if (!resourceManager.TryContentFileRead(rootedPath, out var fileStream)) + { + Assert.Fail($"Map not found: {rootedPath}"); + } - if (!resourceManager.TryContentFileRead(rootedPath, out var fileStream)) - { - Assert.Fail($"Map not found: {rootedPath}"); - } + using var reader = new StreamReader(fileStream); + var yamlStream = new YamlStream(); - using var reader = new StreamReader(fileStream); - var yamlStream = new YamlStream(); + yamlStream.Load(reader); - yamlStream.Load(reader); - - var root = yamlStream.Documents[0].RootNode; - var meta = root["meta"]; - var version = meta["format"].AsInt(); + var root = yamlStream.Documents[0].RootNode; + var meta = root["meta"]; + var version = meta["format"].AsInt(); // TODO MAP TESTS // Move this to some separate test? //CheckDoNotMap(map, root, protoManager); // DeltaV - use MappingCategoriesTest instead - if (version >= 7) - { - v7Maps.Add(map); - continue; - } - + if (version >= 7) + { + isV7Map = true; + } + else + { var postMapInit = meta["postmapinit"].AsBool(); Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit"); } @@ -254,7 +216,7 @@ namespace Content.IntegrationTests.Tests var ev = new BeforeEntityReadEvent(); server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev); - foreach (var map in v7Maps) + if (isV7Map) { Assert.That(IsPreInit(map, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes)); } @@ -527,7 +489,8 @@ namespace Content.IntegrationTests.Tests } [Test] - public async Task NonGameMapsLoadableTest() + [TestCaseSource(nameof(AllMapFiles))] + public async Task NonGameMapsLoadableTest(ResPath mapPath) { await using var pair = await PoolManager.GetServerClient(); var server = pair.Server; @@ -540,24 +503,21 @@ namespace Content.IntegrationTests.Tests var gameMaps = protoManager.EnumeratePrototypes().Select(o => o.MapPath).ToHashSet(); - var mapFolder = new ResPath("/Maps"); - var maps = resourceManager - .ContentFindFiles(mapFolder) - .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) - .ToArray(); - var mapPaths = new List(); - foreach (var map in maps) + if (gameMaps.Contains(mapPath)) { - if (gameMaps.Contains(map)) - continue; + // TODO: You might be able to save like, 1-2 seconds of test time if you eliminate these before + // actually needing a pair. + await pair.CleanReturnAsync(); + return; + } - var rootedPath = map.ToRootedPath(); - if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) - { - continue; - } - mapPaths.Add(rootedPath); + var rootedPath = mapPath.ToRootedPath(); + + if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal)) + { + await pair.CleanReturnAsync(); + return; } await server.WaitPost(() => @@ -576,29 +536,27 @@ namespace Content.IntegrationTests.Tests }; HashSet> maps; - foreach (var path in mapPaths) - { - try - { - Assert.That(mapLoader.TryLoadGeneric(path, out maps, out _, opts)); - } - catch (Exception ex) - { - throw new Exception($"Failed to load map {path}", ex); - } - try + try + { + Assert.That(mapLoader.TryLoadGeneric(mapPath, out maps, out _, opts)); + } + catch (Exception ex) + { + throw new Exception($"Failed to load map {mapPath}", ex); + } + + try + { + foreach (var map in maps) { - foreach (var map in maps) - { - server.EntMan.DeleteEntity(map); - } - } - catch (Exception ex) - { - throw new Exception($"Failed to delete map {path}", ex); + server.EntMan.DeleteEntity(map); } } + catch (Exception ex) + { + throw new Exception($"Failed to delete map {mapPath}", ex); + } }); }); diff --git a/Content.IntegrationTests/Tests/Utility/ScroungerTests.cs b/Content.IntegrationTests/Tests/Utility/ScroungerTests.cs new file mode 100644 index 00000000000..9bfadc284d5 --- /dev/null +++ b/Content.IntegrationTests/Tests/Utility/ScroungerTests.cs @@ -0,0 +1,49 @@ +using Content.IntegrationTests.Utility; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Utility; + +[TestOf(typeof(GameDataScrounger))] +public sealed class ScroungerTests +{ + [Test] + [Description("Assert that the data scrounger finds prototypes by type successfully.")] + public void ScroungeByType() + { + var scrounged = GameDataScrounger.PrototypesOfKind(); + Assert.That(scrounged, Is.Not.Empty); + } + + [Test] + [Description("Assert that the data scrounger finds all files by pattern in a directory successfully.")] + [TestCase("*.yml")] + [TestCase("*.txt")] + public void ScroungeByPattern(string pattern) + { + var files = GameDataScrounger.FilesInDirectory("/", pattern); + + Assert.That(files, Is.Not.Empty); + } + + [Test] + [Description("Assert that the data scrounger finds all files by pattern in a directory successfully, and returns valid VFS paths.")] + public void ScroungeByPatternInVfs() + { + var files = GameDataScrounger.FilesInDirectoryInVfs("/Maps", "*.yml"); + + Assert.That(files, Is.Not.Empty); + + Assert.That(files[0].IsRooted, Is.True); + Assert.That(files[0].ToString(), Does.StartWith("/Maps/")); + } + + [Test] + [Description("Assert that the data scrounger finds entities by component successfully.")] + public void ScroungeByComponent() + { + var items = GameDataScrounger.EntitiesWithComponent("Item"); + + Assert.That(items, Is.Not.Empty); + } +} diff --git a/Content.IntegrationTests/Utility/GameDataScrounger.Files.cs b/Content.IntegrationTests/Utility/GameDataScrounger.Files.cs new file mode 100644 index 00000000000..4236494b4a9 --- /dev/null +++ b/Content.IntegrationTests/Utility/GameDataScrounger.Files.cs @@ -0,0 +1,100 @@ +using System.IO; +using System.Linq; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Utility; + +public static partial class GameDataScrounger +{ + /// + /// Returns all files in a given content location that match a pattern. + /// + /// The directory within the VFS to search + /// A glob pattern to use as a filter + /// Whether to search directories within the given directory. + /// A list of all files within the VFS directory matching the pattern. + public static string[] FilesInDirectory(string location, string pattern, bool recursive = true) + { + var path = GetContentPathOnDisk(location); + + return Directory.EnumerateFiles(path, + pattern ?? "*", + recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .ToArray(); + } + + /// + /// Returns all files in a given content location that match a pattern, as their VFS paths. + /// + /// The directory within the VFS to search + /// A glob pattern to use as a filter + /// Whether to search directories within the given directory. + /// A list of all file paths within the VFS directory matching the pattern. + public static ResPath[] FilesInDirectoryInVfs(string location, string pattern, bool recursive = true) + { + var path = GetContentPathOnDisk(location.TrimEnd('/')); + var resBasePath = ContentResources(); + + return Directory.EnumerateFiles(path, + pattern ?? "*", + recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .Select(x => new ResPath(x.Remove(0, resBasePath.Length))) + .ToArray(); + } + + /// + /// Get the full directory path that the executable is located in. + /// + private static string GetExecutableDirectory() + { + // TODO: remove this shitty hack, either through making it less hardcoded into shared, + // or by making our file structure less spaghetti somehow. + var assembly = typeof(IResourceManager).Assembly; + var location = assembly.Location; + if (location == string.Empty) + { + // See https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location?view=net-5.0#remarks + // This doesn't apply to us really because we don't do that kind of publishing, but whatever. + throw new InvalidOperationException("Cannot find path of executable."); + } + + return Path.GetDirectoryName(location)!; + } + + /// + /// Turns a relative path from the executable directory into a full path. + /// + private static string ExecutableRelativeFile(string file) + { + return Path.GetFullPath(Path.Combine(GetExecutableDirectory(), file)); + } + + /// + /// Mirrors a function in the engine for ease of maintenance. + /// + private static string FindContentRootDir() + { + return "../../"; + } + + /// + /// Mirrors a function in the engine for ease of maintenance. + /// + private static string ContentResources() + { + return ExecutableRelativeFile($"{FindContentRootDir()}Resources"); + } + + /// + /// Gets the real path of a path within the VFS. + /// + /// The path to get the real filesystem path of. + /// The real filesystem path. + public static string GetContentPathOnDisk(string path) + { + Assert.That(path, Does.StartWith("/"), "Path must be rooted."); + + return $"{ContentResources()}{path.ToString()}"; + } +} diff --git a/Content.IntegrationTests/Utility/GameDataScrounger.cs b/Content.IntegrationTests/Utility/GameDataScrounger.cs new file mode 100644 index 00000000000..764fd9dd9ab --- /dev/null +++ b/Content.IntegrationTests/Utility/GameDataScrounger.cs @@ -0,0 +1,369 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Content.IntegrationTests.Utility; + +/// +/// A helper class for when you need prototype or VFS data particularly early, like for test source lists. +/// +/// +/// +/// This does not include engine prototypes, nor anything generated at runtime, as it's made to be simple and fast +/// for usage during test framework startup where we cannot afford to initialize all of . +/// +/// +/// No initialization is required, it initializes itself on usage and all methods can be called immediately. +/// +/// +/// +/// +/// public static readonly string[] Maps = GameDataScrounger.PrototypesOfKind<GameMapPrototype>(); +/// +/// +public static partial class GameDataScrounger +{ + + /// + /// + /// YAML Linter, for Reasons, depends on the entirety of the test suite. + /// As such, scrounging erroring out due to bad YAML can make the linter fail spectacularly. + /// We do not want that, so the linter sets this, and we refuse to do any yaml-ing ourselves so the nicer set of + /// errors get to it. + /// + /// + /// Also, this means obviously bad YAML causes the main test suite to exit early. This is probably a pro, honestly. + /// + /// + /// + /// This should not be set mid-run, as some methods will return data and others will not depending on their implementation. + /// Set it before any operations that would call into GameDataScrounger. + /// + public static bool NoScrounging = false; + + /// + /// Prototype type to ID index. + /// + private static Dictionary>? _prototypeIndex = null; + + /// + /// Component type to prototype ID index. + /// + private static Dictionary>? _entitiesWithComponentIndex = null; + + /// + /// Entity proto to metadata index. + /// + private static Dictionary? _entitiesMetaIndex = null; + + private sealed class EntityMetadata + { + public required string Id; + public required HashSet Components; + public required List Parents; + public required bool Abstract; + } + + /// + /// Lock used to synchronize access to the prototype index. + /// + private static readonly Lock DataLock = new(); + + /// + /// Gets all prototypes of the given type kind. + /// + public static string[] PrototypesOfKind() + where T : IPrototype + { + if (typeof(T).GetCustomAttribute() is { Type: { } ty }) + return PrototypesOfKind(ty); + + return PrototypesOfKind(PrototypeUtility.CalculatePrototypeName(typeof(T).Name)); + } + + /// + /// Gets all prototypes of the given string kind. + /// + public static string[] PrototypesOfKind(string kind) + { + if (NoScrounging) + return Array.Empty(); + + lock (DataLock) + { + Scrounge(); + + return _prototypeIndex[kind].ToArray(); + } + } + + /// + /// Returns all entities with the given component. + /// + /// + /// This does not accept components by type due to a non-public engine API that needs unsealed. + /// When that is done, this will be removed and usages of it upstream will be refactored to go by type. + /// + /// The name (like one would put in YAML) of the component to look for. + /// A list of entity prototype IDs that have the given component. + public static string[] EntitiesWithComponent(string componentId) + { + if (NoScrounging) + return Array.Empty(); + + lock (DataLock) + { + if (_entitiesWithComponentIndex is { } index) + { + return index[componentId].ToArray(); + } + else + { + Scrounge(); + + return _entitiesWithComponentIndex[componentId].ToArray(); + } + } + } + + /// + /// Ensures that the internal indices are initialized and not null. + /// This will not do anything if they're initialized, and will initialize them to be empty if + /// is set. + /// + [MemberNotNull(nameof(_prototypeIndex))] + [MemberNotNull(nameof(_entitiesWithComponentIndex))] + [MemberNotNull(nameof(_entitiesMetaIndex))] + private static void Scrounge() + { + if (_prototypeIndex is not null && _entitiesWithComponentIndex is not null && _entitiesMetaIndex is not null) + return; + + // Initialize regardless of if we actually fill them out. + // As we promise they will not be null, and various methods rely on this even if they're mpty. + _prototypeIndex = new(); + _entitiesWithComponentIndex = new(); + _entitiesMetaIndex = new(); + + if (NoScrounging) + return; + + var resDir = ContentResources(); + Assert.That(Directory.Exists($"{resDir}/Prototypes")); + + var ignoreList = GetIgnoredPrototypes(resDir); + + // Start with our root directory. We use this as a stack of directories to traverse. + var explorationStack = new List() { $"{resDir}/Prototypes" }; + + while (explorationStack.Count > 0) + { + // Take a directory off the stack. + var dir = explorationStack.Pop(); + + if (ignoreList.Contains(dir)) + continue; // It's all abstract anyway. + + explorationStack.AddRange(Directory.EnumerateDirectories(dir)); + + foreach (var file in Directory.EnumerateFiles(dir, "*.yml")) + { + if (ignoreList.Contains(file)) + continue; // It's all abstract anyway. + + foreach (var (kind, id) in IndexPrototypesIn(file)) + { + // alternate universe where .net has rust's Entry api. + if (!_prototypeIndex.TryGetValue(kind, out var list)) + { + _prototypeIndex[kind] = new(); + list = _prototypeIndex[kind]; + } + + list.Add(id); + } + } + } + + PushInheritanceAndIndex(); + } + + private static readonly YamlScalarNode IdNode = new("id"); + private static readonly YamlScalarNode TypeNode = new("type"); + + /// + /// Indexes all prototypes in a folder, adding them to as necessary and + /// yielding all (type, id) pairs. + /// + /// The file to index. + /// An enumerator of all prototypes in the file, regardless of kind. + private static IEnumerable<(string type, string id)> IndexPrototypesIn(string file) + { + var stream = new YamlStream(); + + stream.Load(File.OpenText(file)); + + foreach (var document in stream) + { + Assert.That(document.RootNode, Is.AssignableTo()); + var node = (YamlSequenceNode)document.RootNode; + + foreach (var entry in node.Children) + { + Assert.That(entry, Is.AssignableTo()); + var entryMapping = (YamlMappingNode)entry; + + var id = entryMapping[IdNode]; + var type = entryMapping[TypeNode]; + var @abstract = false; + if (entryMapping.TryGetNode("abstract", out YamlScalarNode? abstractNode)) + { + // TODO: This technically will exclude prototypes that use the abstract field for their own stuff, + // and not for parenting. However no such prototype exists in the game as of writing and solving + // this is mildly nontrivial. + + // We use exact equality to match what serialization does. + if (abstractNode.Value == "true") + @abstract = true; + } + + if (!@abstract) + yield return (((YamlScalarNode)type).Value!, ((YamlScalarNode)id).Value!); + + // If we're an entity prototype.. + if (type is not YamlScalarNode { Value: "entity" }) + continue; + + // then do some metadata indexing that's feasible w/o serializationmanager. + + entryMapping.TryGetNode("components", out YamlSequenceNode? components); + + var parents = new List(); + + if (entryMapping.TryGetNode("parent", out var parentNode)) + { + switch (parentNode) + { + case YamlScalarNode scalar: + { + parents.Add(scalar.Value!); + break; + } + case YamlSequenceNode seq: + { + parents.AddRange(seq.Children.Select(x => x.AsString())); + break; + } + } + } + + + // Assemble metadata for this entity prototype w/o needing serializationmanager. + var entity = new EntityMetadata() + { + Abstract = @abstract, + Components = components?.Children.Select(x => x["type"].ToString()).ToHashSet() ?? new(), + Parents = parents, + Id = id.AsString(), + }; + + _entitiesMetaIndex![id.AsString()] = entity; + } + } + } + + /// + /// Iterates through , filling out + /// as it works and ensuring all inheritance-provided components are found. + /// + private static void PushInheritanceAndIndex() + { + var visitedEntities = new HashSet(); + + foreach (var entity in _entitiesMetaIndex!.Values) + { + VisitEntity(entity, visitedEntities); + + if (entity.Abstract) + continue; // We don't index abstract entities here. + + foreach (var component in entity.Components) + { + if (!_entitiesWithComponentIndex!.TryGetValue(component, out var list)) + { + list = new(); + _entitiesWithComponentIndex[component] = list; + } + + list.Add(entity.Id); + } + } + } + + /// + /// Visits the given entity, potentially recursively in order to discover all of its components. + /// + /// The entity to 'visit' and gather metadata on. + /// The set of all visited entities. + private static void VisitEntity(EntityMetadata entity, in HashSet visitedEntities) + { + // Return if we've visited already. + if (!visitedEntities.Add(entity.Id)) + return; + + foreach (var parent in entity.Parents) + { + var parentMeta = _entitiesMetaIndex![parent]; + VisitEntity(parentMeta, visitedEntities); + + entity.Components.UnionWith(parentMeta.Components); + } + } + + // Did you know there's no way to find the resources folder in the real filesystem + // from content? Makes sense, but ough. So this is unfortunately copy-pasted from engine. + // I don't think it's worth it to add an API for this to engine due to the security implications for sandboxed + // content. + + // This is indeed, unfortunately, a replica of Content.Shared/Entry/EntryPoint.cs:129 + // That code relies on engine tools we can't use here, because we can't even spin up engine. + private static HashSet GetIgnoredPrototypes(string resDir) + { + var ignores = new HashSet(); + var ignoredProtosPath = $"{resDir}/IgnoredPrototypes"; + + if (!Directory.Exists(ignoredProtosPath)) + return ignores; // Nothing to do. + + foreach (var path in Directory.EnumerateFiles($"{resDir}/IgnoredPrototypes")) + { + var stream = new YamlStream(); + + stream.Load(File.OpenText(path)); + + foreach (var document in stream) + { + if (document.RootNode is not YamlSequenceNode seq) + throw new Exception($"The ignored prototypes file at {path} isn't a valid yaml sequence/list."); + + foreach (var entry in seq) + { + if (entry is not YamlScalarNode { Value: {} value }) + throw new Exception($"An entry in {path} is not a valid YAML scalar/string literal. Entry: {entry}"); + + ignores.Add(value); + } + } + } + + return ignores; + } +} diff --git a/Content.YAMLLinter/Program.cs b/Content.YAMLLinter/Program.cs index d37acff8e0d..8351c226674 100644 --- a/Content.YAMLLinter/Program.cs +++ b/Content.YAMLLinter/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Content.IntegrationTests; +using Content.IntegrationTests.Utility; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Markdown.Validation; @@ -17,6 +18,7 @@ namespace Content.YAMLLinter { private static async Task Main(string[] _) { + GameDataScrounger.NoScrounging = true; // Ugly hack for YAML Linter. PoolManager.Startup(); var stopwatch = new Stopwatch(); stopwatch.Start();