Files

620 lines
24 KiB
C#
Raw Permalink Normal View History

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;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Content.Shared.Station.Components;
2024-12-22 15:13:10 +13:00
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
2024-12-24 12:27:33 +13:00
using Robust.Shared.IoC;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
using Robust.Shared.Map.Events;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
public sealed class PostMapInitTest
{
private const bool SkipTestMaps = true;
private const string TestMapsPath = "/Maps/Test/";
private static readonly string[] NoSpawnMaps =
{
2025-02-17 11:43:41 +03:00
//CrystallEdge Map replacement
//CrystallEdge Map replacement end
};
private static readonly string[] Grids =
{
2025-02-17 11:43:41 +03:00
//CrystallEdge Map replacement
2025-02-16 23:54:55 +03:00
2025-02-17 11:43:41 +03:00
//CrystallEdge Map replacement end
AdminTestArenaSystem.ArenaMapPath
};
/// <summary>
/// A dictionary linking maps to collections of entity prototype ids that should be exempt from "DoNotMap" restrictions.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private static readonly Dictionary<string, HashSet<EntProtoId>> 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"]},
};
/// <summary>
/// Maps listed here are given blanket freedom to contain "DoNotMap" entities. Use sparingly.
/// </summary>
/// <remarks>
/// It is also possible to whitelist entire directories here. For example, adding
/// "/Maps/Shuttles/**" will whitelist all shuttle maps.
/// </remarks>
private static readonly string[] DoNotMapWhitelist =
{
//CrystallEdge Maps
2025-02-17 11:43:41 +03:00
"/Maps/_CP14/dev_map.yml",
"/Maps/_CP14/Dungeon/artifact_room.yml",
//CrystallEdge Maps end
"/Maps/centcomm.yml",
"/Maps/Shuttles/AdminSpawn/**" // admin gaming
};
/// <summary>
/// Converts the above globs into regex so your eyes dont bleed trying to add filepaths.
/// </summary>
private static readonly Regex[] DoNotMapWhiteListRegexes = DoNotMapWhitelist
.Select(glob => new Regex(GlobToRegex(glob), RegexOptions.IgnoreCase | RegexOptions.Compiled))
.ToArray();
private static readonly string[] GameMaps =
2025-02-16 23:54:55 +03:00
{
//CrystallEdge Map replacement
2024-07-23 21:17:41 +03:00
"Dev",
2025-02-17 12:24:14 +03:00
"MeteorArena",
"Comoss",
"Venicialis",
Gathering resourses redesign (#1594) * Add Crash to Windlands survival gamemode and map Introduces the CP14CrashToWindlandsRule and its component for a new survival gamemode where a ship crashes into wildlands. Adds the 'nautilus_ship' map, updates English and Russian locale files with new gamemode titles and descriptions, and modifies relevant prototype and map pool files to support the new mode. * fix FTL map * firebombing is real * fix biome dungen all grid overriding * Update PostMapInitTest.cs * Update DungeonJob.CP14Biome.cs * Refactor demiplane generation and crash rules Replaces the old demiplane job system with a new procedural location generation system (CP14LocationGenerationSystem and CP14SpawnProceduralLocationJob). Splits the crash-to-windlands rule into CP14CrashingShipRule (handles explosions) and CP14ExpeditionToWindlandsRule (handles map generation and FTL), with corresponding new components. Updates roundstart game rule prototype and moves/renames several files for clarity and modularity. * Refactor location generation to support optional seed and position Updated the GenerateLocation method to accept an optional seed and position, defaulting to a random seed if none is provided. Adjusted all call sites and the procedural job to support these changes, improving flexibility and consistency in procedural location generation. * procedural integration into game map * Demiplanes deletion * clear demiplane content * remapping procedural + frigid coast deletion * clear demiplane guidebook * dungeons generations * Refactor procedural location configs and add ComossIsland Consolidated and renamed procedural location and dungeonConfig prototypes for demiplane locations, replacing T1-prefixed and legacy IDs with new, consistent names. Updated map YAMLs to reference new location IDs and configs. Added a new ComossIsland location and dungeonConfig. Refactored code to support passing custom dungeon layers and removed unused ExamineProb field from CP14ProceduralLocationPrototype. * Enhance procedural world gen and location configs Improved procedural world generation by adding location generation probability, adjusting level ranges, and refining modifier uniqueness. Updated CP14ProceduralLocationPrototype and CP14ProceduralModifierPrototype, refactored node data generation logic, and made related test and map changes. Added new venicialis_fort station map and updated several procedural location and modifier YAMLs for consistency. * fix * connections room spawners * track finishing global world generation * real connection * Update PostMapInitTest.cs * Update venicialis.yml * Update venicialis.yml * fix raids, decrease city island sizes * Update migration.yml * Update migration.yml * fix shutdowning * Update CP14SpawnProceduralLocationJob.cs
2025-08-04 13:35:55 +03:00
//"NautilusShip",
//CrystallEdge Map replacement end
};
private static readonly ProtoId<EntityCategoryPrototype> DoNotMapCategory = "DoNotMap";
/// <summary>
/// Asserts that specific files have been saved as grids and not maps.
/// </summary>
[Test, TestCaseSource(nameof(Grids))]
public async Task GridsLoadableTest(string mapFile)
{
2023-08-25 02:56:51 +02:00
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var mapLoader = entManager.System<MapLoaderSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var cfg = server.ResolveDependency<IConfigurationManager>();
2023-05-31 11:13:02 +10:00
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
2024-12-22 15:13:10 +13:00
var path = new ResPath(mapFile);
await server.WaitPost(() =>
{
mapSystem.CreateMap(out var mapId);
try
{
2024-12-22 15:13:10 +13:00
Assert.That(mapLoader.TryLoadGrid(mapId, path, out var grid));
}
catch (Exception ex)
{
throw new Exception($"Failed to load map {mapFile}, was it saved as a map instead of a grid?", ex);
}
2024-12-22 15:13:10 +13:00
mapSystem.DeleteMap(mapId);
});
await server.WaitRunTicks(1);
2023-08-25 02:56:51 +02:00
await pair.CleanReturnAsync();
}
2025-01-18 16:29:21 +13:00
/// <summary>
/// Asserts that shuttles are loadable and have been saved as grids and not maps.
/// </summary>
[Test]
public async Task ShuttlesLoadableTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var resMan = server.ResolveDependency<IResourceManager>();
var mapLoader = entManager.System<MapLoaderSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var cfg = server.ResolveDependency<IConfigurationManager>();
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(() =>
{
2025-01-18 16:29:21 +13:00
foreach (var path in shuttles)
{
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);
}
});
});
await server.WaitRunTicks(1);
2023-08-25 02:56:51 +02:00
await pair.CleanReturnAsync();
}
[Test]
public async Task NoSavedPostMapInitTest()
{
2023-08-25 02:56:51 +02:00
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var resourceManager = server.ResolveDependency<IResourceManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
2024-12-23 14:22:27 +13:00
var loader = server.System<MapLoaderSystem>();
var mapFolder = new ResPath("/Maps");
var maps = resourceManager
.ContentFindFiles(mapFolder)
.Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal))
.ToArray();
2024-12-23 14:22:27 +13:00
var v7Maps = new List<ResPath>();
foreach (var map in maps)
{
var rootedPath = map.ToRootedPath();
// 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}");
}
using var reader = new StreamReader(fileStream);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
var root = yamlStream.Documents[0].RootNode;
var meta = root["meta"];
2024-12-23 14:22:27 +13:00
var version = meta["format"].AsInt();
2025-02-17 01:33:05 +13:00
// TODO MAP TESTS
// Move this to some separate test?
CheckDoNotMap(map, root, protoManager);
2024-12-23 14:22:27 +13:00
if (version >= 7)
{
v7Maps.Add(map);
continue;
}
2024-12-23 14:22:27 +13:00
var postMapInit = meta["postmapinit"].AsBool();
Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit");
}
2024-12-23 14:22:27 +13:00
var deps = server.ResolveDependency<IEntitySystemManager>().DependencyCollection;
var ev = new BeforeEntityReadEvent();
server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev);
2024-12-23 14:22:27 +13:00
foreach (var map in v7Maps)
{
Assert.That(IsPreInit(map, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes));
2024-12-24 12:27:33 +13:00
}
2024-12-23 14:22:27 +13:00
2024-12-24 12:27:33 +13:00
// Check that the test actually does manage to catch post-init maps and isn't just blindly passing everything.
// To that end, create a new post-init map and try verify it.
var mapSys = server.System<SharedMapSystem>();
MapId id = default;
await server.WaitPost(() => mapSys.CreateMap(out id, runMapInit: false));
await server.WaitPost(() => server.EntMan.Spawn(null, new MapCoordinates(0, 0, id)));
2024-12-23 14:22:27 +13:00
2024-12-24 12:27:33 +13:00
// First check that a pre-init version passes
var path = new ResPath($"{nameof(NoSavedPostMapInitTest)}.yml");
2024-12-24 18:57:52 +13:00
Assert.That(loader.TrySaveMap(id, path));
Assert.That(IsPreInit(path, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes));
2024-12-24 12:27:33 +13:00
// and the post-init version fails.
await server.WaitPost(() => mapSys.InitializeMap(id));
2024-12-24 18:57:52 +13:00
Assert.That(loader.TrySaveMap(id, path));
Assert.That(IsPreInit(path, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes), Is.False);
2024-12-23 14:22:27 +13:00
2023-08-25 02:56:51 +02:00
await pair.CleanReturnAsync();
}
private bool IsWhitelistedForMap(EntProtoId protoId, ResPath map)
{
if (!DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var allowedProtos))
return false;
return allowedProtos.Contains(protoId);
}
2025-02-17 01:33:05 +13:00
/// <summary>
/// Check that maps do not have any entities that belong to the DoNotMap entity category
/// </summary>
private void CheckDoNotMap(ResPath map, YamlNode node, IPrototypeManager protoManager)
{
foreach (var regex in DoNotMapWhiteListRegexes)
{
if (regex.IsMatch(map.ToString()))
return;
}
2025-02-17 01:33:05 +13:00
var yamlEntities = node["entities"];
var dnmCategory = protoManager.Index(DoNotMapCategory);
2025-02-17 01:33:05 +13:00
// Make a set containing all the specific whitelisted proto ids for this map
HashSet<EntProtoId> unusedExemptions = DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var exemptions) ? new(exemptions) : [];
2025-02-17 01:33:05 +13:00
Assert.Multiple(() =>
{
foreach (var yamlEntity in (YamlSequenceNode)yamlEntities)
{
var protoId = yamlEntity["proto"].AsString();
// This doesn't properly handle prototype migrations, but thats not a significant issue.
if (!protoManager.TryIndex(protoId, out var proto))
2025-02-17 01:33:05 +13:00
continue;
Assert.That(!proto.Categories.Contains(dnmCategory) || IsWhitelistedForMap(protoId, map),
2025-02-17 01:33:05 +13:00
$"\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);
2025-02-17 01:33:05 +13:00
}
});
// 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)}");
2025-02-17 01:33:05 +13:00
}
private bool IsPreInit(ResPath map,
MapLoaderSystem loader,
IDependencyCollection deps,
Dictionary<string, string> renamedPrototypes,
HashSet<string> deletedPrototypes)
2024-12-24 12:27:33 +13:00
{
if (!loader.TryReadFile(map, out var data))
{
Assert.Fail($"Failed to read {map}");
return false;
}
var reader = new EntityDeserializer(deps,
data,
DeserializationOptions.Default,
renamedPrototypes,
deletedPrototypes);
2024-12-24 12:27:33 +13:00
if (!reader.TryProcessData())
{
Assert.Fail($"Failed to process {map}");
return false;
}
foreach (var mapId in reader.MapYamlIds)
{
var mapData = reader.YamlEntities[mapId];
if (mapData.PostInit)
return false;
}
return true;
}
[Test, TestCaseSource(nameof(GameMaps))]
public async Task GameMapsLoadableTest(string mapProto)
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true // Stations spawn a bunch of nullspace entities and maps like centcomm.
});
2023-08-25 02:56:51 +02:00
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entManager = server.ResolveDependency<IEntityManager>();
2022-11-13 17:47:48 +11:00
var mapLoader = entManager.System<MapLoaderSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var ticker = entManager.EntitySysManager.GetEntitySystem<GameTicker>();
var shuttleSystem = entManager.EntitySysManager.GetEntitySystem<ShuttleSystem>();
var cfg = server.ResolveDependency<IConfigurationManager>();
2023-05-31 11:13:02 +10:00
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
await server.WaitPost(() =>
{
2024-12-22 15:13:10 +13:00
MapId mapId;
try
{
var opts = DeserializationOptions.Default with { InitializeMaps = true };
2024-12-22 15:13:10 +13:00
ticker.LoadGameMap(protoManager.Index<GameMapPrototype>(mapProto), out mapId, opts);
}
catch (Exception ex)
{
throw new Exception($"Failed to load map {mapProto}", ex);
}
mapSystem.CreateMap(out var shuttleMap);
var largest = 0f;
EntityUid? targetGrid = null;
var memberQuery = entManager.GetEntityQuery<StationMemberComponent>();
var grids = mapManager.GetAllGrids(mapId).ToList();
var gridUids = grids.Select(o => o.Owner).ToList();
2023-09-13 22:06:15 +10:00
targetGrid = gridUids.First();
foreach (var grid in grids)
{
var gridEnt = grid.Owner;
if (!memberQuery.HasComponent(gridEnt))
continue;
var area = grid.Comp.LocalAABB.Width * grid.Comp.LocalAABB.Height;
if (area > largest)
{
largest = area;
targetGrid = gridEnt;
}
}
// Test shuttle can dock.
// This is done inside gamemap test because loading the map takes ages and we already have it.
var station = entManager.GetComponent<StationMemberComponent>(targetGrid!.Value).Station;
/*
if (entManager.TryGetComponent<StationEmergencyShuttleComponent>(station, out var stationEvac))
{
var shuttlePath = stationEvac.EmergencyShuttlePath;
2024-12-22 15:13:10 +13:00
Assert.That(mapLoader.TryLoadGrid(shuttleMap, shuttlePath, out var shuttle),
$"Failed to load {shuttlePath}");
Assert.That(
2024-12-22 15:13:10 +13:00
shuttleSystem.TryFTLDock(shuttle!.Value.Owner,
entManager.GetComponent<ShuttleComponent>(shuttle!.Value.Owner),
targetGrid.Value),
$"Unable to dock {shuttlePath} to {mapProto}");
}
2024-12-22 15:13:10 +13:00
mapSystem.DeleteMap(shuttleMap);
mapManager.DeleteMap(shuttleMap);
*/ //CP14 Disable FTL test
if (entManager.HasComponent<StationJobsComponent>(station))
{
// Test that the map has valid latejoin spawn points or container spawn points
if (!NoSpawnMaps.Contains(mapProto))
{
var lateSpawns = 0;
lateSpawns += GetCountLateSpawn<SpawnPointComponent>(gridUids, entManager);
lateSpawns += GetCountLateSpawn<ContainerSpawnPointComponent>(gridUids, entManager);
Assert.That(lateSpawns, Is.GreaterThan(0), $"Found no latejoin spawn points on {mapProto}");
}
// Test all availableJobs have spawnPoints
// This is done inside gamemap test because loading the map takes ages and we already have it.
var comp = entManager.GetComponent<StationJobsComponent>(station);
var jobs = new HashSet<ProtoId<JobPrototype>>(comp.SetupAvailableJobs.Keys);
var spawnPoints = entManager.EntityQuery<SpawnPointComponent>()
.Where(x => x.SpawnType is SpawnPointType.Job or SpawnPointType.Unset && x.Job != null) //CP14 Job or Unset (only Job in upstream)
.Select(x => x.Job.Value);
jobs.ExceptWith(spawnPoints);
spawnPoints = entManager.EntityQuery<ContainerSpawnPointComponent>()
.Where(x => x.SpawnType is SpawnPointType.Job or SpawnPointType.Unset && x.Job != null)
.Select(x => x.Job.Value);
jobs.ExceptWith(spawnPoints);
Assert.That(jobs, Is.Empty, $"There is no spawnpoints for {string.Join(", ", jobs)} on {mapProto}.");
}
try
{
2024-12-22 15:13:10 +13:00
mapSystem.DeleteMap(mapId);
}
catch (Exception ex)
{
throw new Exception($"Failed to delete map {mapProto}", ex);
}
});
await server.WaitRunTicks(1);
2023-08-25 02:56:51 +02:00
await pair.CleanReturnAsync();
}
private static int GetCountLateSpawn<T>(List<EntityUid> gridUids, IEntityManager entManager)
where T : ISpawnPoint, IComponent
{
var resultCount = 0;
var queryPoint = entManager.AllEntityQueryEnumerator<T, TransformComponent>();
#nullable enable
while (queryPoint.MoveNext(out T? comp, out var xform))
{
var spawner = (ISpawnPoint)comp;
if (spawner.SpawnType is not SpawnPointType.LateJoin
|| xform.GridUid == null
|| !gridUids.Contains(xform.GridUid.Value))
{
continue;
}
#nullable disable
resultCount++;
break;
}
return resultCount;
}
[Test]
public async Task AllMapsTested()
{
2023-08-25 02:56:51 +02:00
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoMan = server.ResolveDependency<IPrototypeManager>();
var gameMaps = protoMan.EnumeratePrototypes<GameMapPrototype>()
2023-08-25 02:56:51 +02:00
.Where(x => !pair.IsTestPrototype(x))
.Select(x => x.ID)
.ToHashSet();
Assert.That(gameMaps.Remove(PoolManager.TestMap));
Assert.That(gameMaps, Is.EquivalentTo(GameMaps.ToHashSet()), "Game map prototype missing from test cases.");
2023-08-25 02:56:51 +02:00
await pair.CleanReturnAsync();
}
[Test]
public async Task NonGameMapsLoadableTest()
{
2023-08-25 02:56:51 +02:00
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
2022-11-13 17:47:48 +11:00
var mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
var resourceManager = server.ResolveDependency<IResourceManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var cfg = server.ResolveDependency<IConfigurationManager>();
2023-05-31 11:13:02 +10:00
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
var gameMaps = protoManager.EnumeratePrototypes<GameMapPrototype>().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();
2024-12-22 15:13:10 +13:00
var mapPaths = new List<ResPath>();
foreach (var map in maps)
{
if (gameMaps.Contains(map))
continue;
var rootedPath = map.ToRootedPath();
if (SkipTestMaps && rootedPath.ToString().StartsWith(TestMapsPath, StringComparison.Ordinal))
{
continue;
}
2024-12-22 15:13:10 +13:00
mapPaths.Add(rootedPath);
}
await server.WaitPost(() =>
{
Assert.Multiple(() =>
{
2024-12-22 15:13:10 +13:00
// This bunch of files contains a random mixture of both map and grid files.
// TODO MAPPING organize files
var opts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with
{
InitializeMaps = true,
LogOrphanedGrids = false
}
};
HashSet<Entity<MapComponent>> maps;
foreach (var path in mapPaths)
{
try
{
2024-12-24 21:00:53 +13:00
Assert.That(mapLoader.TryLoadGeneric(path, out maps, out _, opts));
}
catch (Exception ex)
{
2024-12-22 15:13:10 +13:00
throw new Exception($"Failed to load map {path}", ex);
}
try
{
2024-12-22 15:13:10 +13:00
foreach (var map in maps)
{
server.EntMan.DeleteEntity(map);
}
}
catch (Exception ex)
{
2024-12-22 15:13:10 +13:00
throw new Exception($"Failed to delete map {path}", ex);
}
}
});
});
await server.WaitRunTicks(1);
2023-08-25 02:56:51 +02:00
await pair.CleanReturnAsync();
}
/// <summary>
/// Lets us the convert the filepaths to regex without eyeglaze trying to add new paths.
/// </summary>
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}$";
}
}
}