diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index 9149e2f9acb0..135684aa841c 100644 --- a/OpenRA.Game/Map/MapCache.cs +++ b/OpenRA.Game/Map/MapCache.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of @@ -39,6 +39,15 @@ public sealed class MapCache : IEnumerable, IDisposable public Dictionary StringPool { get; } = new Dictionary(); + readonly List mapDirectoryTrackers = new List(); + public string LastUpdatedUid; + + /// + /// Called when a map has been updated. Provides old and new uid's of the map + /// Not called when map is added or deleted + /// + public event Action MapUpdated = (oldUid, newuid) => { }; + public MapCache(ModData modData) { this.modData = modData; @@ -48,12 +57,25 @@ public MapCache(ModData modData) sheetBuilder = new SheetBuilder(SheetType.BGRA); } + void UpdateMaps() + { + foreach (var tracker in mapDirectoryTrackers) + tracker.UpdateMaps(this); + } + + public void CallMapUpdated(string oldUid, string newUid) + { + MapUpdated(oldUid, newUid); + } + public void LoadMaps() { // Utility mod that does not support maps if (!modData.Manifest.Contains()) return; + var mapGrid = modData.Manifest.Get(); + // Enumerate map directories foreach (var kv in modData.Manifest.MapFolders) { @@ -85,36 +107,42 @@ public void LoadMaps() } mapLocations.Add(package, classification); + mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification)); } - var mapGrid = modData.Manifest.Get(); foreach (var kv in MapLocations) { foreach (var map in kv.Key.Contents) - { - IReadOnlyPackage mapPackage = null; - try - { - using (new Support.PerfTimer(map)) - { - mapPackage = kv.Key.OpenPackage(map, modData.ModFiles); - if (mapPackage == null) - continue; + LoadMap(map, kv.Key, kv.Value, mapGrid); + } + } - var uid = Map.ComputeUID(mapPackage); - previews[uid].UpdateFromMap(mapPackage, kv.Key, kv.Value, modData.Manifest.MapCompatibility, mapGrid.Type); - } - } - catch (Exception e) + public string LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid) + { + IReadOnlyPackage mapPackage = null; + try + { + using (new Support.PerfTimer(map)) + { + mapPackage = package.OpenPackage(map, modData.ModFiles); + if (mapPackage != null) { - mapPackage?.Dispose(); - Console.WriteLine("Failed to load map: {0}", map); - Console.WriteLine("Details: {0}", e); - Log.Write("debug", "Failed to load map: {0}", map); - Log.Write("debug", "Details: {0}", e); + var uid = Map.ComputeUID(mapPackage); + previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type); + return uid; } } } + catch (Exception e) + { + mapPackage?.Dispose(); + Console.WriteLine("Failed to load map: {0}", map); + Console.WriteLine("Details: {0}", e); + Log.Write("debug", "Failed to load map: {0}", map); + Log.Write("debug", "Details: {0}", e); + } + + return null; } public IEnumerable EnumerateMapDirPackages(MapClassification classification = MapClassification.System) @@ -345,10 +373,18 @@ public string ChooseInitialMap(string initialUid, MersenneTwister random) return initialUid; } - public MapPreview this[string key] => previews[key]; + public MapPreview this[string key] + { + get + { + UpdateMaps(); + return previews[key]; + } + } public IEnumerator GetEnumerator() { + UpdateMaps(); return previews.Values.GetEnumerator(); } @@ -368,6 +404,9 @@ public void Dispose() foreach (var p in previews.Values) p.Dispose(); + foreach (var t in mapDirectoryTrackers) + t.Dispose(); + // We need to let the loader thread exit before we can dispose our sheet builder. // Ideally we should dispose our resources before returning, but we don't to block waiting on the loader thread to exit. // Instead, we'll queue disposal to be run once it has exited. diff --git a/OpenRA.Game/Map/MapDirectoryTracker.cs b/OpenRA.Game/Map/MapDirectoryTracker.cs new file mode 100644 index 000000000000..1a8c19c172c2 --- /dev/null +++ b/OpenRA.Game/Map/MapDirectoryTracker.cs @@ -0,0 +1,135 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenRA.FileSystem; + +namespace OpenRA +{ + public sealed class MapDirectoryTracker : IDisposable + { + readonly FileSystemWatcher watcher; + readonly MapGrid mapGrid; + readonly IReadOnlyPackage package; + readonly MapClassification classification; + + enum MapAction { Add, Delete, Update } + readonly Dictionary mapActionQueue = new Dictionary(); + + bool dirty = false; + + public MapDirectoryTracker(MapGrid mapGrid, 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.IncludeSubdirectories = true; + watcher.EnableRaisingEvents = true; + } + + public void Dispose() + { + watcher.Dispose(); + } + + void AddMapAction(MapAction mapAction, string fullpath, string oldFullPath = null) + { + lock (mapActionQueue) + { + dirty = true; + + // if path is not root, update map instead + var path = RemoveSubDirs(fullpath); + if (fullpath == path) + mapActionQueue[path] = mapAction; + else + mapActionQueue[path] = MapAction.Update; + + // called when file has been renamed / changed location + if (oldFullPath != null) + { + var oldpath = RemoveSubDirs(oldFullPath); + if (oldpath != null) + if (oldFullPath == oldpath) + mapActionQueue[oldpath] = MapAction.Delete; + else + mapActionQueue[oldpath] = MapAction.Update; + } + } + } + + public void UpdateMaps(MapCache mapcache) + { + lock (mapActionQueue) + { + if (!dirty) + return; + + dirty = false; + string lastUid = null; + foreach (var mapAction in mapActionQueue) + { + var map = mapcache.FirstOrDefault(x => x.Package?.Name == mapAction.Key && x.Status == MapStatus.Available); + if (map != null) + { + if (mapAction.Value == MapAction.Delete) + { + Console.WriteLine("Delete " + mapAction.Key); + map.Invalidate(); + } + else + { + Console.WriteLine("Update " + mapAction.Key); + map.Invalidate(); + lastUid = mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid); + + // Provide game lobby with information about our latest update + if (lastUid != null && map.Uid != lastUid) + mapcache.CallMapUpdated(map.Uid, lastUid); + } + } + else + { + if (mapAction.Value != MapAction.Delete) + { + Console.WriteLine("Add " + mapAction.Key); + lastUid = mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid); + } + } + } + + mapActionQueue.Clear(); + mapcache.LastUpdatedUid = lastUid ?? mapcache.LastUpdatedUid; + } + } + + string RemoveSubDirs(string path) + { + var endPath = path.Replace(package.Name + Path.DirectorySeparatorChar, ""); + + // if file moved from out outside directory, ignore it + if (path == endPath) + return null; + + return package.Name + Path.DirectorySeparatorChar + endPath.Split(Path.DirectorySeparatorChar)[0]; + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs index c9435dcb1c45..6868a148501c 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of @@ -178,10 +178,6 @@ public SaveDirectory(Folder folder, string displayName, MapClassification classi var combinedPath = Platform.ResolvePath(Path.Combine(selectedDirectory.Folder.Name, filename.Text + fileTypes[fileType].Extension)); - // Invalidate the old map metadata - if (map.Uid != null && map.Package != null && map.Package.Name == combinedPath) - modData.MapCache[map.Uid].Invalidate(); - try { if (!(map.Package is IReadWritePackage package) || package.Name != combinedPath) @@ -195,9 +191,6 @@ public SaveDirectory(Folder folder, string displayName, MapClassification classi map.Save(package); - // Update the map cache so it can be loaded without restarting the game - modData.MapCache[map.Uid].UpdateFromMap(map.Package, selectedDirectory.Folder, selectedDirectory.Classification, null, map.Grid.Type); - Console.WriteLine("Saved current map at {0}", combinedPath); Ui.CloseWindow(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index f39306c6938b..3b4f0dc19f48 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of @@ -59,6 +59,8 @@ enum PanelType { Players, Options, Music, Servers, Kick, ForceStart } MapPreview map; Session.MapStatus mapStatus; + string oldMapUid; + string newMapUid; bool chatEnabled; bool addBotOnMapLoad; @@ -129,6 +131,7 @@ void ConnectionStateChanged(OrderManager om, string password, NetworkConnection Game.LobbyInfoChanged += UpdateSpawnOccupants; Game.BeforeGameStart += OnGameStart; Game.ConnectionStateChanged += ConnectionStateChanged; + modData.MapCache.MapUpdated += TrackRelevantMapUpdates; var name = lobby.GetOrNull("SERVER_NAME"); if (name != null) @@ -182,20 +185,30 @@ void ConnectionStateChanged(OrderManager om, string password, NetworkConnection { var onSelect = new Action(uid => { - // Don't select the same map again - if (uid == map.Uid) + // Don't select the same map again, and handle map becoming unavailable + if (uid == map.Uid && modData.MapCache[uid].Status != MapStatus.Available) return; orderManager.IssueOrder(Order.Command("map " + uid)); Game.Settings.Server.Map = uid; Game.Settings.Save(); + newMapUid = null; + oldMapUid = null; + }); + + var onCancel = new Action(() => + { + if (oldMapUid == map.Uid) + onSelect(newMapUid); + newMapUid = null; + oldMapUid = null; }); Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { { "initialMap", map.Uid }, { "initialTab", MapClassification.System }, - { "onExit", DoNothing }, + { "onExit", Game.IsHost ? onCancel : null }, { "onSelect", Game.IsHost ? onSelect : null }, { "filter", MapVisibility.Lobby }, }); @@ -484,6 +497,7 @@ protected override void Dispose(bool disposing) Game.LobbyInfoChanged -= UpdateSpawnOccupants; Game.BeforeGameStart -= OnGameStart; Game.ConnectionStateChanged -= ConnectionStateChanged; + modData.MapCache.MapUpdated -= TrackRelevantMapUpdates; } base.Dispose(disposing); @@ -822,6 +836,17 @@ void OnGameStart() onStart(); } + + void TrackRelevantMapUpdates(string oldUid, string newUid) + { + // We need to handle map being updated multiple times without a refresh + if (map.Uid == oldUid || oldUid == newMapUid) + { + if (oldMapUid == null) + oldMapUid = oldUid; + newMapUid = newUid; + } + } } public class LobbyFaction diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index 8f42fd31d6f1..7dc3692a16da 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of @@ -158,7 +158,13 @@ public MainMenuLogic(Widget widget, World world, ModData modData) // Loading into the map editor Game.BeforeGameStart += RemoveShellmapUI; - var onSelect = new Action(uid => LoadMapIntoEditor(modData.MapCache[uid].Uid)); + var onSelect = new Action(uid => + { + if (modData.MapCache[uid].Status != MapStatus.Available) + SwitchMenu(MenuType.Extras); + else + LoadMapIntoEditor(modData.MapCache[uid].Uid); + }); var newMapButton = widget.Get("NEW_MAP_BUTTON"); newMapButton.OnClick = () => diff --git a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs index 02f3bc5d3490..2b6da1132b7a 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of @@ -126,6 +126,18 @@ public class MapChooserLogic : ChromeLogic SetupMapTab(MapClassification.User, filter, "USER_MAPS_TAB_BUTTON", "USER_MAPS_TAB", itemTemplate); SetupMapTab(MapClassification.System, filter, "SYSTEM_MAPS_TAB_BUTTON", "SYSTEM_MAPS_TAB", itemTemplate); + if (modData.MapCache.LastUpdatedUid != null) + { + selectedUid = modData.MapCache.LastUpdatedUid; + currentTab = tabMaps.Keys.FirstOrDefault(k => tabMaps[k].Select(mp => mp.Uid).Contains(selectedUid)); + + if (currentTab != MapClassification.Unknown) + { + SwitchTab(currentTab, itemTemplate); + return; + } + } + if (initialMap == null && tabMaps.Keys.Contains(initialTab) && tabMaps[initialTab].Any()) { selectedUid = Game.ModData.MapCache.ChooseInitialMap(tabMaps[initialTab].Select(mp => mp.Uid).First(),