Skip to content

Commit 4af348c

Browse files
committed
#352: Add custom map file watcher
- Address review feedback to fix comments, make handlers private, and switch one-liners to expressions - Switch MapSharer to download zip to a temp directory to avoid race condition with map file watcher. - Add https://mapdb.cncnet.org/search/ to `downloadmap` help message Issue: #352 PR: #358
1 parent d102a6d commit 4af348c

File tree

5 files changed

+70
-68
lines changed

5 files changed

+70
-68
lines changed

DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public CnCNetGameLobby(WindowManager windowManager, string iniName,
9595
"Change the used CnCNet tunnel server (game host only)".L10N("UI:Main:ChangeTunnel"),
9696
true, (s) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer"))));
9797
AddChatBoxCommand(new ChatBoxCommand("DOWNLOADMAP",
98-
"Download a map from CNCNet's map server using a map ID and an optional filename.\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("UI:Main:DownloadMapCommandDescription"),
98+
"Download a map from CNCNet's map server using a map ID and an optional filename.\nYou can find maps at https://mapdb.cncnet.org/search/\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("UI:Main:DownloadMapCommandDescription"),
9999
false, DownloadMapByIdCommand));
100100
}
101101

@@ -1596,31 +1596,22 @@ private void MapSharer_HandleMapDownloadComplete(SHA1EventArgs e)
15961596
string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName);
15971597
Logger.Log("Map " + mapFileName + " downloaded, parsing.");
15981598
string mapPath = MapLoader.CustomMapsDirectory + mapFileName;
1599-
Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage);
1600-
if (map != null)
1601-
{
1602-
AddNotice(returnMessage);
1603-
if (lastMapSHA1 == e.SHA1)
1604-
{
1605-
GameModeMap = MapLoader.GetLoadedMapBySha1(lastMapSHA1);
1606-
ChangeMap(GameModeMap);
1607-
}
1608-
}
1609-
else if (chatCommandDownloadedMaps.Contains(e.SHA1))
1610-
{
1611-
// Somehow the user has managed to download an already existing sha1 hash.
1612-
// This special case prevents user confusion from the file successfully downloading but showing an error anyway.
1613-
AddNotice(returnMessage, Color.Yellow);
1614-
AddNotice("Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("UI:Main:DownloadMapCommandDuplicateMapFileLoaded"),
1615-
Color.Yellow);
1616-
}
1617-
else
1599+
GameModeMap gameModeMap = MapLoader.GetLoadedMapBySha1(e.SHA1);
1600+
1601+
if (gameModeMap == null)
16181602
{
1619-
AddNotice(returnMessage, Color.Red);
1603+
AddNotice($"Failed to download map {e.SHA1}", Color.Red);
16201604
AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("UI:Main:MapTransferFailed"));
16211605
mapSharingConfirmationPanel.SetFailedStatus();
16221606
channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9);
16231607
}
1608+
1609+
AddNotice($"Map {gameModeMap.Map.Name} loaded succesfully.");
1610+
if (lastMapSHA1 == e.SHA1)
1611+
{
1612+
GameModeMap = MapLoader.GetLoadedMapBySha1(lastMapSHA1);
1613+
ChangeMap(GameModeMap);
1614+
}
16241615
}
16251616

16261617
private void MapSharer_MapUploadFailed(object sender, MapEventArgs e) =>
@@ -1789,7 +1780,7 @@ private void DownloadMapByIdCommand(string parameters)
17891780
{
17901781
// The user did not supply a map name.
17911782
sha1 = parameters;
1792-
mapName = "user_chat_command_download";
1783+
mapName = MapLoader.MAP_CHAT_COMMAND_FILENAME_PREFIX;
17931784
}
17941785
else
17951786
{

DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2325,14 +2325,11 @@ public bool LoadGameOptionPreset(string name)
23252325
/// <summary>
23262326
/// Handle the GameModeMapsUpdated event from the MapLoader.
23272327
///
2328-
/// Updates the gamemode dropdown for new maps being added while the client is running
2328+
/// Updates the gamemode dropdown for new maps being added while the client is running.
23292329
/// </summary>
23302330
/// <param name="sender"></param>
23312331
/// <param name="e"></param>
2332-
private void MapLoader_GameModeMapsUpdated(object sender, MapLoaderEventArgs e)
2333-
{
2334-
RefreshGameModeDropdown();
2335-
}
2332+
private void MapLoader_GameModeMapsUpdated(object sender, MapLoaderEventArgs e) => RefreshGameModeDropdown();
23362333

23372334
/// <summary>
23382335
/// Update the gamemode dropdown.
@@ -2357,7 +2354,7 @@ public void RefreshGameModeDropdown()
23572354
// Add any new game modes.
23582355
foreach (GameMode gm in GameModeMaps.GameModes)
23592356
{
2360-
//skip the game mode if it is already in the dropdown.
2357+
// Skip the game mode if it is already in the dropdown.
23612358
if (existingDdGameModes.Contains(gm.UIName))
23622359
continue;
23632360

DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -365,18 +365,27 @@ public static string GetMapFileName(string sha1, string mapName)
365365

366366
private static string DownloadMain(string sha1, string myGame, string mapName, out bool success)
367367
{
368-
string customMapsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom");
368+
string customMapsPath = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom");
369+
string tempDownloadPath = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "temp");
369370

370371
string mapFileName = GetMapFileName(sha1, mapName);
371372

372-
FileInfo destinationFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($"{mapFileName}.zip"));
373+
// Store the unzipped mapfile in a temp directory while we unzip and rename it to avoid a race condition with the map file watcher
374+
DirectoryInfo tempDownloadDirectory = Directory.CreateDirectory(tempDownloadPath);
375+
FileInfo zipDestinationFile = SafePath.GetFile(tempDownloadDirectory.FullName, FormattableString.Invariant($"{mapFileName}.zip"));
376+
FileInfo tempMapFile = SafePath.GetFile(tempDownloadDirectory.FullName, FormattableString.Invariant($"{mapFileName}{MapLoader.MAP_FILE_EXTENSION}"));
373377

374-
// This string is up here so we can check that there isn't already a .map file for this download.
375-
// This prevents the client from crashing when trying to rename the unzipped file to a duplicate filename.
376-
FileInfo newFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($"{mapFileName}{MapLoader.MAP_FILE_EXTENSION}"));
378+
if (zipDestinationFile.Exists)
379+
{
380+
Logger.Log($"DownloadMain: zipDestinationFile already exists, deleting: zipDestinationFile={zipDestinationFile.FullName}");
381+
zipDestinationFile.Delete();
382+
}
377383

378-
destinationFile.Delete();
379-
newFile.Delete();
384+
if (tempMapFile.Exists)
385+
{
386+
Logger.Log($"DownloadMain: tempMapFile already exists, deleting: tempMapFile={zipDestinationFile.FullName}");
387+
tempMapFile.Delete();
388+
}
380389

381390
using (TWebClient webClient = new TWebClient())
382391
{
@@ -385,7 +394,7 @@ private static string DownloadMain(string sha1, string myGame, string mapName, o
385394
try
386395
{
387396
Logger.Log("MapSharer: Downloading URL: " + "http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip");
388-
webClient.DownloadFile("http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip", destinationFile.FullName);
397+
webClient.DownloadFile("http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip", zipDestinationFile.FullName);
389398
}
390399
catch (Exception ex)
391400
{
@@ -404,27 +413,35 @@ private static string DownloadMain(string sha1, string myGame, string mapName, o
404413
}
405414
}
406415

407-
destinationFile.Refresh();
416+
zipDestinationFile.Refresh();
408417

409-
if (!destinationFile.Exists)
418+
if (!zipDestinationFile.Exists)
410419
{
411420
success = false;
412421
return null;
413422
}
414423

415-
string extractedFile = ExtractZipFile(destinationFile.FullName, customMapsDirectory);
424+
string extractedFile = ExtractZipFile(zipDestinationFile.FullName, tempDownloadDirectory.FullName);
416425

417426
if (String.IsNullOrEmpty(extractedFile))
418427
{
419428
success = false;
420429
return null;
421430
}
422431

423-
// We can safely assume that there will not be a duplicate file due to deleting it
424-
// earlier if one already existed.
425-
File.Move(SafePath.CombineFilePath(customMapsDirectory, extractedFile), newFile.FullName);
432+
FileInfo newMapFile = SafePath.GetFile(customMapsPath, FormattableString.Invariant($"{mapFileName}{MapLoader.MAP_FILE_EXTENSION}"));
433+
434+
// We need to delete potentially conflicting map files because .Move won't overwrite.
435+
if (newMapFile.Exists)
436+
{
437+
Logger.Log($"DownloadMain: newMapFile already exists, deleting: newMapFile={newMapFile.FullName}");
438+
newMapFile.Delete();
439+
}
440+
441+
File.Move(SafePath.CombineFilePath(tempDownloadDirectory.FullName, extractedFile), newMapFile.FullName);
426442

427-
destinationFile.Delete();
443+
zipDestinationFile.Delete();
444+
Directory.Delete(tempDownloadDirectory.FullName, true);
428445

429446
success = true;
430447
return extractedFile;

DXMainClient/Domain/Multiplayer/MapLoader.cs

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace DTAClient.Domain.Multiplayer
1414
public class MapLoader
1515
{
1616
public const string MAP_FILE_EXTENSION = ".map";
17+
public const string MAP_CHAT_COMMAND_FILENAME_PREFIX = "user_chat_command_download";
1718
private const string CUSTOM_MAPS_DIRECTORY = "Maps/Custom";
1819
private static readonly string CUSTOM_MAPS_CACHE = SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "custom_map_cache");
1920
private const string MultiMapsSection = "MultiMaps";
@@ -64,20 +65,14 @@ public class MapLoader
6465
/// </summary>
6566
/// <param name="sha1">The map ID to search the loaded maps for.</param>
6667
/// <returns></returns>
67-
public bool IsMapAlreadyLoaded(string sha1)
68-
{
69-
return GetLoadedMapBySha1(sha1) != null;
70-
}
68+
public bool IsMapAlreadyLoaded(string sha1) => GetLoadedMapBySha1(sha1) != null;
7169

7270
/// <summary>
7371
/// Search the loaded maps for the SHA-1, return the map if a match is found.
7472
/// </summary>
7573
/// <param name="sha1">The map ID to search the loaded maps for.</param>
7674
/// <returns>The map matching the SHA-1 if one was found.</returns>
77-
public GameModeMap GetLoadedMapBySha1(string sha1)
78-
{
79-
return GameModeMaps.Find(gmm => gmm.Map.SHA1 == sha1);
80-
}
75+
public GameModeMap GetLoadedMapBySha1(string sha1) => GameModeMaps.Find(gmm => gmm.Map.SHA1 == sha1);
8176

8277
/// <summary>
8378
/// Loads multiplayer map info asynchonously.
@@ -115,16 +110,16 @@ public void StartCustomMapFileWatcher()
115110
/// </summary>
116111
/// <param name="sender">Sent by the file system watcher</param>
117112
/// <param name="e">Sent by the file system watcher</param>
118-
public void HandleCustomMapFolder_Created(object sender, FileSystemEventArgs e)
113+
private void HandleCustomMapFolder_Created(object sender, FileSystemEventArgs e)
119114
{
120115
// Get the map filename without the extension.
121116
// The extension gets added in LoadCustomMap so we need to excise it to avoid "file.map.map".
122117
string name = Path.GetFileNameWithoutExtension(e.Name);
123118

124-
if (name.StartsWith())
125-
126119
string relativeMapPath = SafePath.CombineFilePath(CustomMapsDirectory, name);
120+
Logger.Log($"HandleCustomMapFolder_Created: Calling LoadCustomMap: mapPath={relativeMapPath}");
127121
Map map = LoadCustomMap(relativeMapPath, out string result);
122+
Logger.Log($"HandleCustomMapFolder_Created: Ended LoadCustomMap: mapPath={relativeMapPath}");
128123

129124
if (map == null)
130125
{
@@ -135,11 +130,11 @@ public void HandleCustomMapFolder_Created(object sender, FileSystemEventArgs e)
135130
/// <summary>
136131
/// Handle a .map file being removed from the custom map directory.
137132
///
138-
/// This function will attempt to remove the map from the client if it was deleted from the folder
133+
/// This function will attempt to remove the map from the client if it was deleted from the folder.
139134
/// </summary>
140-
/// <param name="sender">Sent by the file system watcher</param>
135+
/// <param name="sender">Sent by the file system watcher.</param>
141136
/// <param name="e">Sent by the file system watcher.</param>
142-
public void HandleCustomMapFolder_Deleted(object sender, FileSystemEventArgs e)
137+
private void HandleCustomMapFolder_Deleted(object sender, FileSystemEventArgs e)
143138
{
144139
Logger.Log($"Map was deleted: map={e.Name}");
145140
// Use the filename without the extension so we can remove maps that had their extension changed.
@@ -165,7 +160,7 @@ public void HandleCustomMapFolder_Deleted(object sender, FileSystemEventArgs e)
165160
/// </summary>
166161
/// <param name="sender"></param>
167162
/// <param name="e"></param>
168-
public void HandleCustomMapFolder_Renamed(object sender, RenamedEventArgs e)
163+
private void HandleCustomMapFolder_Renamed(object sender, RenamedEventArgs e)
169164
{
170165
string name = Path.GetFileNameWithoutExtension(e.Name);
171166
string relativeMapPath = SafePath.CombineFilePath(CustomMapsDirectory, name);
@@ -176,27 +171,30 @@ public void HandleCustomMapFolder_Renamed(object sender, RenamedEventArgs e)
176171
// This is just for logging to help debug.
177172
if (!oldPathIsMap && newPathIsMap)
178173
{
179-
Logger.Log($"Renaming file changed the file extension. User is likely renaming a '.yrm' from Final Alert 2: old={e.OldName}, new={e.Name}");
174+
Logger.Log($"HandleCustomMapFolder_Renamed: Changed the file extension. User is likely renaming a '.yrm' from Final Alert 2: old={e.OldName}, new={e.Name}");
180175
}
181176
else if (oldPathIsMap && !newPathIsMap)
182177
{
183178
// A bit hacky, but this is a rare case.
184-
Logger.Log($"Renaming file changed the file extension to no longer be '.map' for some reason, removing from map list: old={e.OldName}, new={e.Name}");
179+
Logger.Log($"HandleCustomMapFolder_Renamed: Changed the file extension to no longer be '.map' for some reason, removing from map list: old={e.OldName}, new={e.Name}");
185180
HandleCustomMapFolder_Deleted(sender, e);
181+
return;
186182
}
187-
188-
if (!newPathIsMap)
183+
else if (!newPathIsMap)
189184
{
190-
Logger.Log($"Renaming file. New extension is not '{MAP_FILE_EXTENSION}', moving on: file={e.Name}");
185+
Logger.Log($"HandleCustomMapFolder_Renamed: New extension is not '{MAP_FILE_EXTENSION}', moving on: file={e.Name}");
191186
return;
192187
}
193188

194189
Map map = LoadCustomMap(relativeMapPath, out string result);
195190

196-
if (map == null)
191+
if (map != null)
197192
{
198-
Logger.Log($"Failed to load renamed map file. Map is likely already loaded: original={e.OldName}, new={e.Name}, reason={result}");
193+
Logger.Log($"HandleCustomMapFolder_Renamed: Loaded renamed file as map: file={e.Name}, mapName={map.Name}");
194+
return;
199195
}
196+
197+
Logger.Log($"Failed to load renamed map file. Map is likely already loaded, filepath: original={e.OldName}, new={e.Name}, reason={result}");
200198
}
201199

202200
/// <summary>
@@ -206,7 +204,7 @@ public void HandleCustomMapFolder_Renamed(object sender, RenamedEventArgs e)
206204
/// </summary>
207205
/// <param name="sender"></param>
208206
/// <param name="e"></param>
209-
public void HandleCustomMapFolder_Error(object sender, ErrorEventArgs e)
207+
private void HandleCustomMapFolder_Error(object sender, ErrorEventArgs e)
210208
{
211209
Exception exc = e.GetException();
212210
Logger.Log($"The custom map folder file watcher crashed: error={exc.Message}");
@@ -445,7 +443,7 @@ public Map LoadCustomMap(string mapPath, out string resultMessage)
445443
// This checks the SHA-1, so duplicate maps in two .map files with different filenames can still be detected.
446444
if (IsMapAlreadyLoaded(map.SHA1))
447445
{
448-
Logger.Log("LoadCustomMap: Custom map " + customMapFile.FullName + " is already loaded!");
446+
Logger.Log($"LoadCustomMap: Custom map {customMapFile.FullName} is already loaded! SHA1={map.SHA1}");
449447
resultMessage = $"Map {customMapFile.FullName} is already loaded.";
450448

451449
return null;

DXMainClient/Domain/Multiplayer/MapLoaderEventArgs.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ public MapLoaderEventArgs(Map map)
1313
}
1414

1515
public Map Map { get; private set; }
16-
1716
}
1817
}

0 commit comments

Comments
 (0)