Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#352: Add custom map directory file watcher #358

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DXMainClient/DXGUI/Generic/LoadingScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ private void LoadMaps()
{
mapLoader = new MapLoader();
mapLoader.LoadMaps();
mapLoader.StartCustomMapFileWatcher();
}

private void Finish()
Expand Down
39 changes: 15 additions & 24 deletions DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public CnCNetGameLobby(WindowManager windowManager, string iniName,
"Change the used CnCNet tunnel server (game host only)".L10N("UI:Main:ChangeTunnel"),
true, (s) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer"))));
AddChatBoxCommand(new ChatBoxCommand("DOWNLOADMAP",
"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"),
"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"),
false, DownloadMapByIdCommand));
}

Expand Down Expand Up @@ -1595,32 +1595,23 @@ private void MapSharer_HandleMapDownloadComplete(SHA1EventArgs e)
{
string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName);
Logger.Log("Map " + mapFileName + " downloaded, parsing.");
string mapPath = "Maps/Custom/" + mapFileName;
Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now handled by the file watcher to prevent a race condition

if (map != null)
{
AddNotice(returnMessage);
if (lastMapSHA1 == e.SHA1)
{
GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapSHA1);
ChangeMap(GameModeMap);
}
}
else if (chatCommandDownloadedMaps.Contains(e.SHA1))
{
// Somehow the user has managed to download an already existing sha1 hash.
// This special case prevents user confusion from the file successfully downloading but showing an error anyway.
AddNotice(returnMessage, Color.Yellow);
AddNotice("Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("UI:Main:DownloadMapCommandDuplicateMapFileLoaded"),
Color.Yellow);
}
else
string mapPath = MapLoader.CustomMapsDirectory + mapFileName;
GameModeMap gameModeMap = MapLoader.GetLoadedMapBySha1(e.SHA1);

if (gameModeMap == null)
{
AddNotice(returnMessage, Color.Red);
AddNotice($"Failed to download map {e.SHA1}", Color.Red);
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"));
mapSharingConfirmationPanel.SetFailedStatus();
channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9);
}

AddNotice($"Map {gameModeMap.Map.Name} loaded succesfully.");
if (lastMapSHA1 == e.SHA1)
{
GameModeMap = MapLoader.GetLoadedMapBySha1(lastMapSHA1);
ChangeMap(GameModeMap);
}
}

private void MapSharer_MapUploadFailed(object sender, MapEventArgs e) =>
Expand Down Expand Up @@ -1789,7 +1780,7 @@ private void DownloadMapByIdCommand(string parameters)
{
// The user did not supply a map name.
sha1 = parameters;
mapName = "user_chat_command_download";
mapName = MapLoader.MAP_CHAT_COMMAND_FILENAME_PREFIX;
}
else
{
Expand All @@ -1804,7 +1795,7 @@ private void DownloadMapByIdCommand(string parameters)
sha1 = sha1.Replace("?", "");

// See if the user already has this map, with any filename, before attempting to download it.
GameModeMap loadedMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == sha1);
GameModeMap loadedMap = MapLoader.GetLoadedMapBySha1(sha1);

if (loadedMap != null)
{
Expand Down
58 changes: 58 additions & 0 deletions DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ DiscordHandler discordHandler
) : base(windowManager)
{
_iniSectionName = iniName;

MapLoader = mapLoader;
MapLoader.GameModeMapsUpdated += MapLoader_GameModeMapsUpdated;

this.isMultiplayer = isMultiplayer;
this.discordHandler = discordHandler;
}
Expand Down Expand Up @@ -2318,5 +2321,60 @@ public bool LoadGameOptionPreset(string name)
}

protected abstract bool AllowPlayerOptionsChange();

/// <summary>
/// Handle the GameModeMapsUpdated event from the MapLoader.
///
/// Updates the gamemode dropdown for new maps being added while the client is running.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MapLoader_GameModeMapsUpdated(object sender, MapLoaderEventArgs e) => RefreshGameModeDropdown();

/// <summary>
/// Update the gamemode dropdown.
///
/// Allows us to show gamemodes for maps that were loaded after the client was started.
/// This function will do in-place modifications to `ddGameModeMapFilter.Items`.
/// </summary>
public void RefreshGameModeDropdown()
{
// Use a hashset to store the existing gamemodes in the dropdown for instant lookups.
// This is the set of existing dropdown items. Add anything from GameModeMaps, that isn't in this set, to the dropdown.
HashSet<string> existingDdGameModes = new HashSet<string>(ddGameModeMapFilter.Items.Select(ddItem => ddItem.Text));
// This is the updated list of game modes. Anything not in this set, that is in existingDdGameModes, should be removed from the dropdown.
HashSet<string> gameModeUpdated = new HashSet<string>(GameModeMaps.GameModes.Select(gm => gm.UIName));
// Don't accidentally remove favorite maps item.
gameModeUpdated.Add(FavoriteMapsLabel);

XNADropDownItem currentItem = ddGameModeMapFilter.SelectedItem;

Logger.Log($"Updating game modes dropdown display: lobbyType={this.GetType().Name}");

// Add any new game modes.
foreach (GameMode gm in GameModeMaps.GameModes)
{
// Skip the game mode if it is already in the dropdown.
if (existingDdGameModes.Contains(gm.UIName))
continue;

// If the gamemode was not present, then add it.
ddGameModeMapFilter.AddItem(CreateGameFilterItem(gm.UIName, new GameModeMapFilter(GetGameModeMaps(gm))));
}

// Now remove game modes that should no longer be displayed.
ddGameModeMapFilter.Items.RemoveAll(ddItem => !gameModeUpdated.Contains(ddItem.Text));

// Make sure we keep the same game mode selected after adding or removing game modes.
// If the game mode is no longer available then switch to 0, aka, favorite maps.
int newIndex = 0;
for (int i = 0; i < ddGameModeMapFilter.Items.Count; i++)
{
if (ddGameModeMapFilter.Items[i].Text == currentItem.Text)
newIndex = i;
}

ddGameModeMapFilter.SelectedIndex = newIndex;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public MultiplayerGameLobby(WindowManager windowManager, string iniName,
s => SetMaxAhead(s)),
new ChatBoxCommand("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("UI:Main:ChatboxCommandProtocolVersionHelp"), true,
s => SetProtocolVersion(s)),
new ChatBoxCommand("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("UI:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap),
new ChatBoxCommand("LOADMAP", $"Load a custom map with given filename from {MapLoader.CustomMapsDirectory} folder.".L10N("UI:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap),
new ChatBoxCommand("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("UI:Main:ChatboxCommandRandomStartsHelp"), true,
s => SetStartingLocationClearance(s)),
new ChatBoxCommand("ROLL", "Roll dice, for example /roll 3d6".L10N("UI:Main:ChatboxCommandRollHelp"), false, RollDiceCommand),
Expand Down Expand Up @@ -487,7 +487,7 @@ private void RollDiceCommand(string dieType)
/// <param name="mapName">Name of the map given as a parameter, without file extension.</param>
private void LoadCustomMap(string mapName)
{
Map map = MapLoader.LoadCustomMap($"Maps/Custom/{mapName}", out string resultMessage);
Map map = MapLoader.LoadCustomMap(SafePath.CombineFilePath(MapLoader.CustomMapsDirectory, mapName), out string resultMessage);
if (map != null)
{
AddNotice(resultMessage);
Expand Down
47 changes: 32 additions & 15 deletions DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,18 +365,27 @@ public static string GetMapFileName(string sha1, string mapName)

private static string DownloadMain(string sha1, string myGame, string mapName, out bool success)
{
string customMapsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom");
string customMapsPath = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom");
string tempDownloadPath = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "temp");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a temporary download directory so that the filewatcher doesn't prematurely load the .map file before it is fully extracted and renamed.


string mapFileName = GetMapFileName(sha1, mapName);

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

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

destinationFile.Delete();
newFile.Delete();
if (tempMapFile.Exists)
{
Logger.Log($"DownloadMain: tempMapFile already exists, deleting: tempMapFile={zipDestinationFile.FullName}");
tempMapFile.Delete();
}

using (TWebClient webClient = new TWebClient())
{
Expand All @@ -385,7 +394,7 @@ private static string DownloadMain(string sha1, string myGame, string mapName, o
try
{
Logger.Log("MapSharer: Downloading URL: " + "http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip");
webClient.DownloadFile("http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip", destinationFile.FullName);
webClient.DownloadFile("http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip", zipDestinationFile.FullName);
}
catch (Exception ex)
{
Expand All @@ -404,27 +413,35 @@ private static string DownloadMain(string sha1, string myGame, string mapName, o
}
}

destinationFile.Refresh();
zipDestinationFile.Refresh();

if (!destinationFile.Exists)
if (!zipDestinationFile.Exists)
{
success = false;
return null;
}

string extractedFile = ExtractZipFile(destinationFile.FullName, customMapsDirectory);
string extractedFile = ExtractZipFile(zipDestinationFile.FullName, tempDownloadDirectory.FullName);

if (String.IsNullOrEmpty(extractedFile))
{
success = false;
return null;
}

// We can safely assume that there will not be a duplicate file due to deleting it
// earlier if one already existed.
File.Move(SafePath.CombineFilePath(customMapsDirectory, extractedFile), newFile.FullName);
FileInfo newMapFile = SafePath.GetFile(customMapsPath, FormattableString.Invariant($"{mapFileName}{MapLoader.MAP_FILE_EXTENSION}"));

// We need to delete potentially conflicting map files because .Move won't overwrite.
if (newMapFile.Exists)
{
Logger.Log($"DownloadMain: newMapFile already exists, deleting: newMapFile={newMapFile.FullName}");
newMapFile.Delete();
}

File.Move(SafePath.CombineFilePath(tempDownloadDirectory.FullName, extractedFile), newMapFile.FullName);

destinationFile.Delete();
zipDestinationFile.Delete();
Directory.Delete(tempDownloadDirectory.FullName, true);

success = true;
return extractedFile;
Expand Down
Loading