Add dynamic map refresh

This commit is contained in:
Gustas
2022-02-10 14:57:43 +02:00
committed by Matthias Mailänder
parent 61df7974b0
commit b254eb0f3d
6 changed files with 263 additions and 46 deletions

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information #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 * 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 * available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of * as published by the Free Software Foundation, either version 3 of
@@ -39,6 +39,14 @@ namespace OpenRA
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>(); public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new List<MapDirectoryTracker>();
/// <summary>
/// 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
/// </summary>
public event Action<string, string> MapUpdated = (oldUID, newUID) => { };
public MapCache(ModData modData) public MapCache(ModData modData)
{ {
this.modData = modData; this.modData = modData;
@@ -48,12 +56,20 @@ namespace OpenRA
sheetBuilder = new SheetBuilder(SheetType.BGRA); sheetBuilder = new SheetBuilder(SheetType.BGRA);
} }
public void UpdateMaps()
{
foreach (var tracker in mapDirectoryTrackers)
tracker.UpdateMaps(this);
}
public void LoadMaps() public void LoadMaps()
{ {
// Utility mod that does not support maps // Utility mod that does not support maps
if (!modData.Manifest.Contains<MapGrid>()) if (!modData.Manifest.Contains<MapGrid>())
return; return;
var mapGrid = modData.Manifest.Get<MapGrid>();
// Enumerate map directories // Enumerate map directories
foreach (var kv in modData.Manifest.MapFolders) foreach (var kv in modData.Manifest.MapFolders)
{ {
@@ -85,24 +101,32 @@ namespace OpenRA
} }
mapLocations.Add(package, classification); mapLocations.Add(package, classification);
mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification));
} }
var mapGrid = modData.Manifest.Get<MapGrid>();
foreach (var kv in MapLocations) foreach (var kv in MapLocations)
{ {
foreach (var map in kv.Key.Contents) foreach (var map in kv.Key.Contents)
LoadMap(map, kv.Key, kv.Value, mapGrid, null);
}
}
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap)
{ {
IReadOnlyPackage mapPackage = null; IReadOnlyPackage mapPackage = null;
try try
{ {
using (new Support.PerfTimer(map)) using (new Support.PerfTimer(map))
{ {
mapPackage = kv.Key.OpenPackage(map, modData.ModFiles); mapPackage = package.OpenPackage(map, modData.ModFiles);
if (mapPackage == null) if (mapPackage != null)
continue; {
var uid = Map.ComputeUID(mapPackage); var uid = Map.ComputeUID(mapPackage);
previews[uid].UpdateFromMap(mapPackage, kv.Key, kv.Value, modData.Manifest.MapCompatibility, mapGrid.Type); previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type);
if (oldMap != uid)
MapUpdated(oldMap, uid);
}
} }
} }
catch (Exception e) catch (Exception e)
@@ -114,8 +138,6 @@ namespace OpenRA
Log.Write("debug", "Details: {0}", e); Log.Write("debug", "Details: {0}", e);
} }
} }
}
}
public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System) public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System)
{ {
@@ -345,10 +367,18 @@ namespace OpenRA
return initialUid; return initialUid;
} }
public MapPreview this[string key] => previews[key]; public MapPreview this[string key]
{
get
{
UpdateMaps();
return previews[key];
}
}
public IEnumerator<MapPreview> GetEnumerator() public IEnumerator<MapPreview> GetEnumerator()
{ {
UpdateMaps();
return previews.Values.GetEnumerator(); return previews.Values.GetEnumerator();
} }
@@ -368,6 +398,9 @@ namespace OpenRA
foreach (var p in previews.Values) foreach (var p in previews.Values)
p.Dispose(); p.Dispose();
foreach (var t in mapDirectoryTrackers)
t.Dispose();
// We need to let the loader thread exit before we can dispose our sheet builder. // 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. // 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. // Instead, we'll queue disposal to be run once it has exited.

View File

@@ -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<string, MapAction> mapActionQueue = new Dictionary<string, MapAction>();
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];
}
}
}

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information #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 * 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 * available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of * 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)); 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 try
{ {
if (!(map.Package is IReadWritePackage package) || package.Name != combinedPath) if (!(map.Package is IReadWritePackage package) || package.Name != combinedPath)
@@ -195,9 +191,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
map.Save(package); 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); Console.WriteLine("Saved current map at {0}", combinedPath);
Ui.CloseWindow(); Ui.CloseWindow();

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information #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 * 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 * available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of * as published by the Free Software Foundation, either version 3 of
@@ -59,6 +59,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
MapPreview map; MapPreview map;
Session.MapStatus mapStatus; Session.MapStatus mapStatus;
string oldMapUid;
string newMapUid;
string lastUpdatedUid;
bool chatEnabled; bool chatEnabled;
bool addBotOnMapLoad; bool addBotOnMapLoad;
@@ -129,6 +132,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
Game.LobbyInfoChanged += UpdateSpawnOccupants; Game.LobbyInfoChanged += UpdateSpawnOccupants;
Game.BeforeGameStart += OnGameStart; Game.BeforeGameStart += OnGameStart;
Game.ConnectionStateChanged += ConnectionStateChanged; Game.ConnectionStateChanged += ConnectionStateChanged;
modData.MapCache.MapUpdated += TrackRelevantMapUpdates;
var name = lobby.GetOrNull<LabelWidget>("SERVER_NAME"); var name = lobby.GetOrNull<LabelWidget>("SERVER_NAME");
if (name != null) if (name != null)
@@ -182,20 +186,26 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{ {
var onSelect = new Action<string>(uid => var onSelect = new Action<string>(uid =>
{ {
// Don't select the same map again // Don't select the same map again, and handle map becoming unavailable
if (uid == map.Uid) if (uid == map.Uid && modData.MapCache[uid].Status != MapStatus.Available)
return; return;
orderManager.IssueOrder(Order.Command("map " + uid)); orderManager.IssueOrder(Order.Command("map " + uid));
Game.Settings.Server.Map = uid; Game.Settings.Server.Map = uid;
Game.Settings.Save(); 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() Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs()
{ {
{ "initialMap", map.Uid }, { "initialMap", lastUpdatedUid ?? map.Uid },
{ "initialTab", MapClassification.System }, { "initialTab", MapClassification.System },
{ "onExit", DoNothing }, { "onExit", Game.IsHost ? new Action(() => UpdateSelectedMap()) : null },
{ "onSelect", Game.IsHost ? onSelect : null }, { "onSelect", Game.IsHost ? onSelect : null },
{ "filter", MapVisibility.Lobby }, { "filter", MapVisibility.Lobby },
}); });
@@ -363,9 +373,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic
// Force start panel // Force start panel
Action startGame = () => Action 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; gameStarting = true;
orderManager.IssueOrder(Order.Command("startgame")); orderManager.IssueOrder(Order.Command("startgame"));
}
else
UpdateSelectedMap();
}; };
var startGameButton = lobby.GetOrNull<ButtonWidget>("START_GAME_BUTTON"); var startGameButton = lobby.GetOrNull<ButtonWidget>("START_GAME_BUTTON");
@@ -484,6 +500,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
Game.LobbyInfoChanged -= UpdateSpawnOccupants; Game.LobbyInfoChanged -= UpdateSpawnOccupants;
Game.BeforeGameStart -= OnGameStart; Game.BeforeGameStart -= OnGameStart;
Game.ConnectionStateChanged -= ConnectionStateChanged; Game.ConnectionStateChanged -= ConnectionStateChanged;
modData.MapCache.MapUpdated -= TrackRelevantMapUpdates;
} }
base.Dispose(disposing); base.Dispose(disposing);
@@ -822,6 +839,36 @@ namespace OpenRA.Mods.Common.Widgets.Logic
onStart(); 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 public class LobbyFaction

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information #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 * 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 * available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of * 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 // Loading into the map editor
Game.BeforeGameStart += RemoveShellmapUI; Game.BeforeGameStart += RemoveShellmapUI;
var onSelect = new Action<string>(uid => LoadMapIntoEditor(modData.MapCache[uid].Uid)); var onSelect = new Action<string>(uid =>
{
if (modData.MapCache[uid].Status != MapStatus.Available)
SwitchMenu(MenuType.Extras);
else
LoadMapIntoEditor(modData.MapCache[uid].Uid);
});
var newMapButton = widget.Get<ButtonWidget>("NEW_MAP_BUTTON"); var newMapButton = widget.Get<ButtonWidget>("NEW_MAP_BUTTON");
newMapButton.OnClick = () => newMapButton.OnClick = () =>

View File

@@ -1,6 +1,6 @@
#region Copyright & License Information #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 * 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 * available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of * as published by the Free Software Foundation, either version 3 of
@@ -119,7 +119,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
if (previews.Any()) if (previews.Any())
{ {
CreateMissionGroup(kv.Key, previews); CreateMissionGroup(kv.Key, previews, onExit);
allPreviews.AddRange(previews); allPreviews.AddRange(previews);
} }
} }
@@ -131,7 +131,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
if (loosePreviews.Any()) if (loosePreviews.Any())
{ {
CreateMissionGroup("Missions", loosePreviews); CreateMissionGroup("Missions", loosePreviews, onExit);
allPreviews.AddRange(loosePreviews); allPreviews.AddRange(loosePreviews);
} }
@@ -146,7 +146,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
}).Start(); }).Start();
var startButton = widget.Get<ButtonWidget>("STARTGAME_BUTTON"); var startButton = widget.Get<ButtonWidget>("STARTGAME_BUTTON");
startButton.OnClick = StartMissionClicked; startButton.OnClick = () => StartMissionClicked(onExit);
startButton.IsDisabled = () => selectedMap == null; startButton.IsDisabled = () => selectedMap == null;
widget.Get<ButtonWidget>("BACK_BUTTON").OnClick = () => widget.Get<ButtonWidget>("BACK_BUTTON").OnClick = () =>
@@ -179,7 +179,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
base.Dispose(disposing); base.Dispose(disposing);
} }
void CreateMissionGroup(string title, IEnumerable<MapPreview> previews) void CreateMissionGroup(string title, IEnumerable<MapPreview> previews, Action onExit)
{ {
var header = ScrollItemWidget.Setup(headerTemplate, () => true, () => { }); var header = ScrollItemWidget.Setup(headerTemplate, () => true, () => { });
header.Get<LabelWidget>("LABEL").GetText = () => title; header.Get<LabelWidget>("LABEL").GetText = () => title;
@@ -190,7 +190,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var item = ScrollItemWidget.Setup(template, var item = ScrollItemWidget.Setup(template,
() => selectedMap != null && selectedMap.Uid == preview.Uid, () => selectedMap != null && selectedMap.Uid == preview.Uid,
() => SelectMap(preview), () => SelectMap(preview),
StartMissionClicked); () => StartMissionClicked(onExit));
var label = item.Get<LabelWithTooltipWidget>("TITLE"); var label = item.Get<LabelWithTooltipWidget>("TITLE");
WidgetUtils.TruncateLabelToTooltip(label, preview.Title); WidgetUtils.TruncateLabelToTooltip(label, preview.Title);
@@ -365,10 +365,19 @@ namespace OpenRA.Mods.Common.Widgets.Logic
playingVideo = PlayingVideo.None; playingVideo = PlayingVideo.None;
} }
void StartMissionClicked() void StartMissionClicked(Action onExit)
{ {
StopVideo(videoPlayer); 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<Order>(); var orders = new List<Order>();
if (difficulty != null) if (difficulty != null)
orders.Add(Order.Command($"option difficulty {difficulty}")); orders.Add(Order.Command($"option difficulty {difficulty}"));