From b254eb0f3db83b5f592a3bb55713d3bd8077fa56 Mon Sep 17 00:00:00 2001 From: Gustas <37534529+Punsho@users.noreply.github.com> Date: Thu, 10 Feb 2022 14:57:43 +0200 Subject: [PATCH] Add dynamic map refresh --- OpenRA.Game/Map/MapCache.cs | 77 ++++++++--- OpenRA.Game/Map/MapDirectoryTracker.cs | 129 ++++++++++++++++++ .../Widgets/Logic/Editor/SaveMapLogic.cs | 9 +- .../Widgets/Logic/Lobby/LobbyLogic.cs | 61 ++++++++- .../Widgets/Logic/MainMenuLogic.cs | 10 +- .../Widgets/Logic/MissionBrowserLogic.cs | 23 +++- 6 files changed, 263 insertions(+), 46 deletions(-) create mode 100644 OpenRA.Game/Map/MapDirectoryTracker.cs diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index 9149e2f9ac..4543eebeef 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,14 @@ namespace OpenRA public Dictionary StringPool { get; } = new Dictionary(); + readonly List mapDirectoryTrackers = new List(); + + /// + /// If a map was added oldUID will be null, if updated oldUId will point to the outdated map + /// Event is not called when map is deleted + /// + public event Action MapUpdated = (oldUID, newUID) => { }; + public MapCache(ModData modData) { this.modData = modData; @@ -48,12 +56,20 @@ namespace OpenRA sheetBuilder = new SheetBuilder(SheetType.BGRA); } + public void UpdateMaps() + { + foreach (var tracker in mapDirectoryTrackers) + tracker.UpdateMaps(this); + } + 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 +101,42 @@ namespace OpenRA } 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, null); + } + } - var uid = Map.ComputeUID(mapPackage); - previews[uid].UpdateFromMap(mapPackage, kv.Key, kv.Value, modData.Manifest.MapCompatibility, mapGrid.Type); - } - } - catch (Exception e) + public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap) + { + 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); + + if (oldMap != uid) + MapUpdated(oldMap, 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); + } } public IEnumerable EnumerateMapDirPackages(MapClassification classification = MapClassification.System) @@ -345,10 +367,18 @@ namespace OpenRA 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 +398,9 @@ namespace OpenRA 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 0000000000..a99f92a570 --- /dev/null +++ b/OpenRA.Game/Map/MapDirectoryTracker.cs @@ -0,0 +1,129 @@ +#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; + 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(mapAction.Key + " was deleted"); + map.Invalidate(); + } + else + { + Console.WriteLine(mapAction.Key + " was updated"); + map.Invalidate(); + mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, map.Uid); + } + } + else + { + if (mapAction.Value != MapAction.Delete) + { + Console.WriteLine(mapAction.Key + " was added"); + mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, null); + } + } + } + + mapActionQueue.Clear(); + } + } + + 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 c9435dcb1c..6868a14850 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 @@ namespace OpenRA.Mods.Common.Widgets.Logic 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 @@ namespace OpenRA.Mods.Common.Widgets.Logic 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 cbaddcc2ce..82729ba394 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,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic MapPreview map; Session.MapStatus mapStatus; + string oldMapUid; + string newMapUid; + string lastUpdatedUid; bool chatEnabled; bool addBotOnMapLoad; @@ -129,6 +132,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Game.LobbyInfoChanged += UpdateSpawnOccupants; Game.BeforeGameStart += OnGameStart; Game.ConnectionStateChanged += ConnectionStateChanged; + modData.MapCache.MapUpdated += TrackRelevantMapUpdates; var name = lobby.GetOrNull("SERVER_NAME"); if (name != null) @@ -182,20 +186,26 @@ namespace OpenRA.Mods.Common.Widgets.Logic { 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; + lastUpdatedUid = null; }); + // Check for updated maps, if the user has edited a map we'll preselect it for them + modData.MapCache.UpdateMaps(); + Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { - { "initialMap", map.Uid }, + { "initialMap", lastUpdatedUid ?? map.Uid }, { "initialTab", MapClassification.System }, - { "onExit", DoNothing }, + { "onExit", Game.IsHost ? new Action(() => UpdateSelectedMap()) : null }, { "onSelect", Game.IsHost ? onSelect : null }, { "filter", MapVisibility.Lobby }, }); @@ -364,8 +374,14 @@ namespace OpenRA.Mods.Common.Widgets.Logic // Force start panel Action startGame = () => { - gameStarting = true; - orderManager.IssueOrder(Order.Command("startgame")); + // Refresh MapCache and check if the selected map is available before attempting to start the game + if (modData.MapCache[map.Uid].Status == MapStatus.Available) + { + gameStarting = true; + orderManager.IssueOrder(Order.Command("startgame")); + } + else + UpdateSelectedMap(); }; var startGameButton = lobby.GetOrNull("START_GAME_BUTTON"); @@ -484,6 +500,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Game.LobbyInfoChanged -= UpdateSpawnOccupants; Game.BeforeGameStart -= OnGameStart; Game.ConnectionStateChanged -= ConnectionStateChanged; + modData.MapCache.MapUpdated -= TrackRelevantMapUpdates; } base.Dispose(disposing); @@ -822,6 +839,36 @@ namespace OpenRA.Mods.Common.Widgets.Logic 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; + } + + if (newUid != null) + lastUpdatedUid = newUid; + } + + void UpdateSelectedMap() + { + if (modData.MapCache[map.Uid].Status == MapStatus.Available) + return; + + if (oldMapUid == map.Uid && modData.MapCache[newMapUid].Status == MapStatus.Available) + { + orderManager.IssueOrder(Order.Command("map " + newMapUid)); + Game.Settings.Server.Map = newMapUid; + Game.Settings.Save(); + newMapUid = null; + oldMapUid = null; + lastUpdatedUid = null; + } + } } public class LobbyFaction diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index 8f42fd31d6..7dc3692a16 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 @@ namespace OpenRA.Mods.Common.Widgets.Logic // 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/MissionBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs index eaf22d2dfc..2084ee76fa 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.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 @@ -119,7 +119,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (previews.Any()) { - CreateMissionGroup(kv.Key, previews); + CreateMissionGroup(kv.Key, previews, onExit); allPreviews.AddRange(previews); } } @@ -131,7 +131,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (loosePreviews.Any()) { - CreateMissionGroup("Missions", loosePreviews); + CreateMissionGroup("Missions", loosePreviews, onExit); allPreviews.AddRange(loosePreviews); } @@ -146,7 +146,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic }).Start(); var startButton = widget.Get("STARTGAME_BUTTON"); - startButton.OnClick = StartMissionClicked; + startButton.OnClick = () => StartMissionClicked(onExit); startButton.IsDisabled = () => selectedMap == null; widget.Get("BACK_BUTTON").OnClick = () => @@ -179,7 +179,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic base.Dispose(disposing); } - void CreateMissionGroup(string title, IEnumerable previews) + void CreateMissionGroup(string title, IEnumerable previews, Action onExit) { var header = ScrollItemWidget.Setup(headerTemplate, () => true, () => { }); header.Get("LABEL").GetText = () => title; @@ -190,7 +190,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var item = ScrollItemWidget.Setup(template, () => selectedMap != null && selectedMap.Uid == preview.Uid, () => SelectMap(preview), - StartMissionClicked); + () => StartMissionClicked(onExit)); var label = item.Get("TITLE"); WidgetUtils.TruncateLabelToTooltip(label, preview.Title); @@ -365,10 +365,19 @@ namespace OpenRA.Mods.Common.Widgets.Logic playingVideo = PlayingVideo.None; } - void StartMissionClicked() + void StartMissionClicked(Action onExit) { StopVideo(videoPlayer); + // If selected mission becomes unavailable, exit MissionBrowser to refresh + if (modData.MapCache[selectedMap.Uid].Status != MapStatus.Available) + { + Game.Disconnect(); + Ui.CloseWindow(); + onExit(); + return; + } + var orders = new List(); if (difficulty != null) orders.Add(Order.Command($"option difficulty {difficulty}"));