diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 683a75f8b4..67350f50a4 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -978,7 +978,7 @@ namespace OpenRA Order.Command($"state {Session.ClientState.Ready}") }; - var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.PackageName) == launchMap); + var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Path) == launchMap); if (map == null) throw new ArgumentException($"Could not find map '{launchMap}'."); diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index 451b5c7630..a8bd71819b 100644 --- a/OpenRA.Game/Map/MapCache.cs +++ b/OpenRA.Game/Map/MapCache.cs @@ -87,8 +87,6 @@ namespace OpenRA if (!modData.Manifest.Contains()) return; - var mapGrid = modData.Manifest.Get(); - // Enumerate map directories foreach (var kv in modData.Manifest.MapFolders) { @@ -120,28 +118,29 @@ namespace OpenRA } mapLocations.Add(package, classification); - mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification)); + mapDirectoryTrackers.Add(new MapDirectoryTracker(package, classification)); } // PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML. var modDataRules = modData.GetRulesYaml(); + var gridType = modData.Manifest.Get().Type; foreach (var kv in MapLocations) { foreach (var map in kv.Key.Contents) - LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules); + LoadMapInternal(map, kv.Key, kv.Value, null, gridType, modDataRules); } // We only want to track maps in runtime, not at loadtime LastModifiedMap = null; } - public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap) + public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, string oldMap) { - LoadMapInternal(map, package, classification, mapGrid, oldMap, null); + LoadMapInternal(map, package, classification, oldMap); } - void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, - IEnumerable> modDataRules) + void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, string oldMap, + MapGridType? gridType = null, IEnumerable> modDataRules = null) { IReadOnlyPackage mapPackage = null; try @@ -152,10 +151,8 @@ namespace OpenRA if (mapPackage != null) { var uid = Map.ComputeUID(mapPackage); - previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules); - - // Freeing the package to save memory if there is a lot of Maps - previews[uid].DisposePackage(); + previews[uid].UpdateFromMapWithoutOwningPackage(mapPackage, package, classification, gridType, modDataRules); + mapPackage.Dispose(); if (oldMap != uid) { @@ -257,7 +254,7 @@ namespace OpenRA var yaml = MiniYaml.FromStream(result, url, stringPool: stringPool); foreach (var kv in yaml) - previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived); + previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived); foreach (var uid in batchUids) { diff --git a/OpenRA.Game/Map/MapDirectoryTracker.cs b/OpenRA.Game/Map/MapDirectoryTracker.cs index 69001cfa5a..04c75c23cf 100644 --- a/OpenRA.Game/Map/MapDirectoryTracker.cs +++ b/OpenRA.Game/Map/MapDirectoryTracker.cs @@ -20,7 +20,6 @@ namespace OpenRA public sealed class MapDirectoryTracker : IDisposable { readonly FileSystemWatcher watcher; - readonly MapGrid mapGrid; readonly IReadOnlyPackage package; readonly MapClassification classification; @@ -29,17 +28,16 @@ namespace OpenRA bool dirty = false; - public MapDirectoryTracker(MapGrid mapGrid, IReadOnlyPackage package, MapClassification classification) + public MapDirectoryTracker(IReadOnlyPackage package, MapClassification classification) { - this.mapGrid = mapGrid; this.package = package; this.classification = classification; watcher = new FileSystemWatcher(package.Name); - watcher.Changed += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Update, e.FullPath); - watcher.Created += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Add, e.FullPath); - watcher.Deleted += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Delete, e.FullPath); - watcher.Renamed += (object sender, RenamedEventArgs e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath); + watcher.Changed += (_, e) => AddMapAction(MapAction.Update, e.FullPath); + watcher.Created += (_, e) => AddMapAction(MapAction.Add, e.FullPath); + watcher.Deleted += (_, e) => AddMapAction(MapAction.Delete, e.FullPath); + watcher.Renamed += (_, e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath); watcher.IncludeSubdirectories = true; watcher.EnableRaisingEvents = true; @@ -86,7 +84,7 @@ namespace OpenRA dirty = false; foreach (var mapAction in mapActionQueue) { - var map = mapcache.FirstOrDefault(x => x.PackageName == mapAction.Key && x.Status == MapStatus.Available); + var map = mapcache.FirstOrDefault(x => x.Path == mapAction.Key && x.Status == MapStatus.Available); if (map != null) { if (mapAction.Value == MapAction.Delete) @@ -98,7 +96,7 @@ namespace OpenRA { Console.WriteLine(mapAction.Key + " was updated"); map.Invalidate(); - mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, map.Uid); + mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, map.Uid); } } else @@ -106,7 +104,7 @@ namespace OpenRA if (mapAction.Value != MapAction.Delete) { Console.WriteLine(mapAction.Key + " was added"); - mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, null); + mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, null); } } } diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index e4c511aaf1..18a76c88c3 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -198,19 +198,23 @@ namespace OpenRA static readonly CPos[] NoSpawns = Array.Empty(); readonly MapCache cache; readonly ModData modData; + IReadOnlyPackage package; public readonly string Uid; - public string PackageName { get; private set; } - IReadOnlyPackage package; - public IReadOnlyPackage Package - { - get - { - package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles); - return package; - } - private set => package = value; + public string Path { get; private set; } + + void LoadPackage() + { + if (package == null && parentPackage != null) + package = parentPackage.OpenPackage(Path, modData.ModFiles); + } + + public Map ToMap() + { + LoadPackage(); + using (new PerfTimer("Map")) + return new Map(modData, package); } IReadOnlyPackage parentPackage; @@ -317,9 +321,28 @@ namespace OpenRA }; } - public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, - string[] mapCompatibility, MapGridType gridType, IEnumerable> modDataRules) + /// + /// Updates internal state from a map without taking ownership of its package. + /// A new copy of the map package will be opened lazily when needed. + /// + public void UpdateFromMapWithoutOwningPackage(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, + MapGridType? gridType = null, IEnumerable> modDataRules = null) { + UpdateFromMap(p, classification, gridType, modDataRules); + parentPackage = parent; + package = null; + } + + /// + /// Updates internal state from a map and takes ownership of its package. + /// The package remains in memory and must not be disposed. + /// + public void UpdateFromMap(IReadOnlyPackage p, MapClassification classification, + MapGridType? gridType = null, IEnumerable> modDataRules = null) + { + Path = p.Name; + package = p; + Dictionary yaml; using (var yamlStream = p.GetStream("map.yaml")) { @@ -329,12 +352,9 @@ namespace OpenRA yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary(); } - PackageName = p.Name; - parentPackage = parent; - var newData = innerData.Clone(); - newData.GridType = gridType; newData.Class = classification; + newData.GridType = gridType ?? modData.Manifest.Get().Type; if (yaml.TryGetValue("MapFormat", out var temp)) { @@ -368,7 +388,7 @@ namespace OpenRA if (yaml.TryGetValue("MapFormat", out temp)) newData.MapFormat = FieldLoader.GetValue("MapFormat", temp.Value); - newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ? + newData.Status = modData.Manifest.MapCompatibility.Contains(requiresMod) ? MapStatus.Available : MapStatus.Unavailable; try @@ -420,7 +440,7 @@ namespace OpenRA innerData = newData; } - public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action parseMetadata = null) + public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action parseMetadata = null) { var newData = innerData.Clone(); newData.Status = status; @@ -478,7 +498,7 @@ namespace OpenRA // Map is for a different mod: update its information so it can be displayed // in the cross-mod server browser UI, but mark it as unavailable so it can't // be selected in a server for the current mod. - if (!mapCompatibility.Contains(r.game_mod)) + if (!modData.Manifest.MapCompatibility.Contains(r.game_mod)) newData.Status = MapStatus.Unavailable; } catch (Exception e) @@ -553,12 +573,12 @@ namespace OpenRA mapInstallPackage.Update(mapFilename, fileStream.ToArray()); Log.Write("debug", $"Downloaded map to '{mapFilename}'"); - var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles); - if (package == null) + var p = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles); + if (p == null) innerData.Status = MapStatus.DownloadError; else { - UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null); + UpdateFromMapWithoutOwningPackage(p, mapInstallPackage, MapClassification.User, GridType); Game.RunAfterTick(onSuccess); } } @@ -577,11 +597,6 @@ namespace OpenRA } public void Dispose() - { - DisposePackage(); - } - - public void DisposePackage() { if (package != null) { @@ -593,14 +608,15 @@ namespace OpenRA public void Delete() { Invalidate(); - (parentPackage as IReadWritePackage)?.Delete(Package.Name); + (parentPackage as IReadWritePackage)?.Delete(Path); } Stream IReadOnlyFileSystem.Open(string filename) { // Explicit package paths never refer to a map - if (!filename.Contains('|') && Package.Contains(filename)) - return Package.GetStream(filename); + LoadPackage(); + if (!filename.Contains('|') && package.Contains(filename)) + return package.GetStream(filename); return modData.DefaultFileSystem.Open(filename); } @@ -616,7 +632,8 @@ namespace OpenRA // Explicit package paths never refer to a map if (!filename.Contains('|')) { - s = Package.GetStream(filename); + LoadPackage(); + s = package.GetStream(filename); if (s != null) return true; } @@ -627,7 +644,8 @@ namespace OpenRA bool IReadOnlyFileSystem.Exists(string filename) { // Explicit package paths never refer to a map - if (!filename.Contains('|') && Package.Contains(filename)) + LoadPackage(); + if (!filename.Contains('|') && package.Contains(filename)) return true; return modData.DefaultFileSystem.Exists(filename); diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index 200bc578bd..13b2f23aba 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -148,11 +148,8 @@ namespace OpenRA if (MapCache[uid].Status != MapStatus.Available) throw new InvalidDataException($"Invalid map uid: {uid}"); - Map map; - using (new Support.PerfTimer("Map")) - map = new Map(this, MapCache[uid].Package); - // Reinitialize all our assets + var map = MapCache[uid].ToMap(); InitializeLoaders(map); map.Sequences.LoadSprites(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs index 8a83e979b2..4fb607d7db 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs @@ -247,7 +247,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (map.Package?.Name != combinedPath) { // When creating a new map or when file paths don't match - if (modData.MapCache.Any(m => m.Status == MapStatus.Available && m.PackageName == combinedPath)) + if (modData.MapCache.Any(m => m.Status == MapStatus.Available && m.Path == combinedPath)) { ConfirmationDialogs.ButtonPrompt(modData, title: OverwriteMapFailedTitle, diff --git a/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs index e57eb4dcfe..0a3c2a7edb 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs @@ -166,7 +166,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic .Select(p => new { Preview = p, - Index = missionMapPaths.IndexOf(Path.GetFileName(p.PackageName)) + Index = missionMapPaths.IndexOf(Path.GetFileName(p.Path)) }) .Where(x => x.Index != -1) .OrderBy(x => x.Index)