diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 787856b1d..72a80621f 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -264,7 +264,7 @@ public void RefreshSettings() public string[] TrustedDomains => clientDefinitionsIni.GetStringListValue(SETTINGS, "TrustedDomains", string.Empty); - public string[] AlwaysTrustedDomains = {"cncnet.org", "gamesurge.net", "dronebl.org", "discord.com", "discord.gg", "youtube.com", "youtu.be"}; + public string[] AlwaysTrustedDomains = { "cncnet.org", "gamesurge.net", "dronebl.org", "discord.com", "discord.gg", "youtube.com", "youtu.be" }; public (string Name, string Path) GetThemeInfoFromIndex(int themeIndex) => clientDefinitionsIni.GetStringValue("Themes", themeIndex.ToString(), ",").Split(',').AsTuple2(); @@ -287,6 +287,30 @@ public string GetThemePath(string themeName) return null; } + private string[] GetCoopDifficultyINIPaths() + { + List paths = new List(); + var pathString = clientDefinitionsIni.GetStringValue(SETTINGS, nameof(CoopDifficultyINIPaths), string.Empty); + + if (string.IsNullOrEmpty(pathString)) + { + paths.Add(SafePath.CombineFilePath("INI", "Map Code", "Coop Difficulty Easy.ini")); + paths.Add(SafePath.CombineFilePath("INI", "Map Code", "Coop Difficulty Medium.ini")); + paths.Add(SafePath.CombineFilePath("INI", "Map Code", "Coop Difficulty Hard.ini")); + } + else + { + var pathStrings = pathString.Split(',', StringSplitOptions.RemoveEmptyEntries); + + foreach (var path in pathStrings) + { + paths.Add(SafePath.CombineFilePath(path)); + } + } + + return paths.ToArray(); + } + public string SettingsIniName => clientDefinitionsIni.GetStringValue(SETTINGS, "SettingsFile", "Settings.ini"); public string TranslationIniName => clientDefinitionsIni.GetStringValue(TRANSLATIONS, nameof(TranslationIniName), "Translation.ini"); @@ -348,6 +372,8 @@ private List ParseTranslationGameFiles() public string MPMapsIniPath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, "MPMapsPath", SafePath.CombineFilePath("INI", "MPMaps.ini"))); + public string[] CoopDifficultyINIPaths => GetCoopDifficultyINIPaths(); + public string KeyboardINI => clientDefinitionsIni.GetStringValue(SETTINGS, "KeyboardINI", "Keyboard.ini"); public int MinimumIngameWidth => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumIngameWidth", 640); diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index d5b9ae2bf..c38d1f851 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -26,8 +26,8 @@ public static class ProgramConstants public const string QRES_EXECUTABLE = "qres.dat"; - public const string CNCNET_PROTOCOL_REVISION = "R13"; - public const string LAN_PROTOCOL_REVISION = "RL8"; + public const string CNCNET_PROTOCOL_REVISION = "R14"; + public const string LAN_PROTOCOL_REVISION = "RL9"; public const int LAN_PORT = 1234; public const int LAN_INGAME_PORT = 1234; public const int LAN_LOBBY_PORT = 1232; @@ -112,6 +112,7 @@ public static string GetAILevelName(int aiLevel) // Static fields might be initialized before the translation file is loaded. Change to readonly properties here. public static List AI_PLAYER_NAMES => new List { "Easy AI".L10N("Client:Main:EasyAIName"), "Medium AI".L10N("Client:Main:MediumAIName"), "Hard AI".L10N("Client:Main:HardAIName") }; + public static List DIFFICULTY_NAMES => new List { "Easy".L10N("Client:Main:DifficultyEasy"), "Medium".L10N("Client:Main:DifficultyMedium"), "Hard".L10N("Client:Main:DifficultyHard") }; public static string LogFileName { get; set; } diff --git a/DXMainClient/DXGUI/Campaign/CampaignSelector.cs b/DXMainClient/DXGUI/Campaign/CampaignSelector.cs index 4f1dd1722..674e1fa80 100644 --- a/DXMainClient/DXGUI/Campaign/CampaignSelector.cs +++ b/DXMainClient/DXGUI/Campaign/CampaignSelector.cs @@ -28,8 +28,6 @@ public class CampaignSelector : XNAWindow private const string SETTINGS_PATH = "Client/CampaignSettings.ini"; - private static string[] DifficultyNames = new string[] { "Easy", "Medium", "Hard" }; - private static string[] DifficultyIniPaths = new string[] { "INI/Map Code/Difficulty Easy.ini", @@ -364,7 +362,7 @@ private void LaunchMission(Mission mission) spawnIni.WriteIniFile(); var difficultyIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, DifficultyIniPaths[trbDifficultySelector.Value])); - string difficultyName = DifficultyNames[trbDifficultySelector.Value]; + string difficultyName = ProgramConstants.DIFFICULTY_NAMES[trbDifficultySelector.Value]; if (copyMapsToSpawnmapINI) { diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index e64806ae8..8e32a8de7 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -719,6 +719,8 @@ private void BroadcastGame() sb.Append(ClientConfiguration.Instance.DefaultSkillLevelIndex); // we don't know the original skill level sb.Append(";"); sb.Append(string.Empty); // MapHash + sb.Append(";"); + sb.Append(CoopDifficulty); broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 71ff28856..598578da4 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -1504,7 +1504,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr string msg = e.Message.Substring(5); // Cut out GAME part string[] splitMessage = msg.Split(new char[] { ';' }); - if (splitMessage.Length != 13) + if (splitMessage.Length != 14) { Logger.Log("Ignoring CTCP game message because of an invalid amount of parameters."); @@ -1550,6 +1550,8 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr string loadedGameId = splitMessage[10]; int skillLevel = int.Parse(splitMessage[11]); string mapHash = splitMessage[12]; + int difficulty = int.Parse(splitMessage[13]); + CnCNetGame cncnetGame = gameCollection.GameList.Find(g => g.GameBroadcastChannel == channel.ChannelName); @@ -1611,6 +1613,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr game.Incompatible = cncnetGame == localGame && game.GameVersion != ProgramConstants.GAME_VERSION; game.TunnelServer = tunnel; game.SkillLevel = skillLevel; + game.Difficulty = difficulty; if (isClosed) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs b/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs index 3f790a1b9..95b484587 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs @@ -164,6 +164,9 @@ public void SetInfo(GenericHostedGame game) string translatedGameModeName = string.IsNullOrEmpty(game.GameMode) ? "Unknown".L10N("Client:Main:Unknown") : game.GameMode.L10N($"INI:GameModes:{game.GameMode}:UIName", notify: false); + if (game.Difficulty >= 0 && game.Difficulty < ProgramConstants.DIFFICULTY_NAMES.Count) + translatedGameModeName += $" ({ProgramConstants.DIFFICULTY_NAMES[game.Difficulty]})"; + lblGameMode.Text = Renderer.GetStringWithLimitedWidth("Game mode:".L10N("Client:Main:GameInfoGameMode") + " " + Renderer.GetSafeString(translatedGameModeName, lblGameMode.FontIndex), lblGameMode.FontIndex, Width - lblGameMode.X); lblGameMode.Visible = true; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 5884f87a7..638b247ec 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -63,6 +63,8 @@ public GameLoadingLobbyBase(WindowManager windowManager, DiscordHandler discordH protected XNAClientButton btnLoadGame; protected XNAClientButton btnLeaveGame; + protected int CoopDifficulty; + private List MPColors = new List(); private string loadedGameID; @@ -75,6 +77,8 @@ public GameLoadingLobbyBase(WindowManager windowManager, DiscordHandler discordH public override void Initialize() { + CoopDifficulty = -1; + Name = "GameLoadingLobby"; ClientRectangle = new Rectangle(0, 0, 590, 510); BackgroundTexture = AssetLoader.LoadTexture("loadmpsavebg.png"); @@ -409,6 +413,10 @@ public virtual void Refresh(bool isHost) uniqueGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); int playerCount = spawnSGIni.GetIntValue("Settings", "PlayerCount", 0); + CoopDifficulty = spawnSGIni.GetIntValue("Settings", "CoopDifficulty", -1); + + if (CoopDifficulty >= 0 && CoopDifficulty < ProgramConstants.DIFFICULTY_NAMES.Count) + lblMapNameValue.Text += $" ({ProgramConstants.DIFFICULTY_NAMES[CoopDifficulty]})"; SavedGamePlayer localPlayer = new SavedGamePlayer(); localPlayer.Name = ProgramConstants.PLAYERNAME; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 93bf73ad9..39f012fbb 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -1,23 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; + using ClientCore; +using ClientCore.Extensions; + using ClientGUI; -using DTAClient.Domain.Multiplayer; + using DTAClient.Domain; +using DTAClient.Domain.Multiplayer; +using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Generic; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers; using DTAClient.Online; using DTAClient.Online.EventArguments; + using Microsoft.Xna.Framework; + using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using DTAClient.Domain.Multiplayer.CnCNet; -using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer.GameLobby { @@ -1049,6 +1055,7 @@ protected override void OnGameOptionChanged() sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map?.UntranslatedName ?? string.Empty); + sb.Append(ddDifficulty?.SelectedIndex ?? -1); channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); } @@ -1223,6 +1230,15 @@ private void ApplyGameOptions(string sender, string message) Convert.ToInt32(RemoveStartingLocations))); SetRandomStartingLocations(removeStartingLocations); + if (GameMode != null && GameMode.UseDifficultyDropDown && ddDifficulty != null) + { + int newIndex = Conversions.IntFromString(parts[partIndex + 9], ddDifficulty.SelectedIndex); + ddDifficulty.SelectedIndex = newIndex; + + if (ddDifficulty.SelectedIndex >= 0) + AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), "Difficulty".L10N("Client:Main:DifficultyOptionName"), ddDifficulty.SelectedItem.Text)); + } + RandomSeed = randomSeed; } @@ -2086,10 +2102,13 @@ private void BroadcastGame() sb.Append(skillLevel); // SkillLevel sb.Append(";"); sb.Append(Map?.SHA1); + sb.Append(";"); + sb.Append(ddDifficulty?.SelectedIndex ?? -1); broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } + #endregion public override string GetSwitchName() => "Game Lobby".L10N("Client:Main:GameLobby"); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 579a7faa2..93daa65eb 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -146,6 +146,8 @@ protected GameModeMap GameModeMap protected ToolTip mapListTooltip; protected XNAClientDropDown ddGameModeMapFilter; protected XNALabel lblGameModeSelect; + protected XNAClientDropDown ddDifficulty; + protected XNALabel lblDifficulty; protected XNAContextMenu mapContextMenu; private XNAContextMenuItem toggleFavoriteItem; @@ -173,6 +175,7 @@ protected GameModeMap GameModeMap protected int UniqueGameID { get; set; } protected int SideCount { get; private set; } protected int RandomSelectorCount { get; private set; } = 1; + protected int PreviousSelectedDifficulty { get; set; } protected List RandomSelectors = new List(); @@ -299,6 +302,21 @@ public override void Initialize() lblGameModeSelect = FindChild(nameof(lblGameModeSelect)); + ddDifficulty = FindChild(nameof(ddDifficulty), true); + lblDifficulty = FindChild(nameof(lblDifficulty), true); + + if (ddDifficulty != null) + { + // Hardcode difficulty items just in case someone tries something stupid. + ddDifficulty.Items.Clear(); + + foreach (string difficultyName in ProgramConstants.DIFFICULTY_NAMES) + ddDifficulty.AddItem(difficultyName); + + ddDifficulty.SelectedIndex = -1; + ddDifficulty.SelectedIndexChanged += DdDifficulty_SelectedIndexChanged; + } + InitBtnMapSort(); tbMapSearch = FindChild(nameof(tbMapSearch)); @@ -568,9 +586,23 @@ protected void DdGameModeMapFilter_SelectedIndexChanged(object sender, EventArgs ListMaps(); if (lbGameModeMapList.SelectedIndex == -1) + { lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap + UpdateDifficultyDropdown(); + } else + { ChangeMap(GameModeMap); + } + } + + protected void DdDifficulty_SelectedIndexChanged(object sender, EventArgs e) + { + if (ddDifficulty.SelectedIndex >= 0) + PreviousSelectedDifficulty = ddDifficulty.SelectedIndex; + + OnGameOptionChanged(); + SetMapLabels(); } protected void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e) @@ -694,7 +726,7 @@ protected void ListMaps() { // Note: StatisticsManager.Statistics must be initialized to call `HasBeatCoOpMap()`. This means StatisticsWindow must be initialized before any lobbies extending GameLobbyBase. if (StatisticsManager.Instance.HasBeatCoOpMap(gameModeMap.Map.UntranslatedName, gameModeMap.GameMode.UntranslatedUIName)) - rankItem.Texture = RankTextures[Math.Abs(2 - gameModeMap.GameMode.CoopDifficultyLevel) + 1]; + rankItem.Texture = RankTextures[Math.Abs(2 - GetCoopDifficultyLevel()) + 1]; else rankItem.Texture = RankTextures[0]; } @@ -747,6 +779,45 @@ protected void ListMaps() LbGameModeMapList_SelectedIndexChanged(); } + protected int GetCoopDifficultyLevel() + { + var gameMode = GameModeMap?.GameMode; + + if (gameMode != null) + { + if (gameMode.UseDifficultyDropDown && ddDifficulty != null && ddDifficulty.SelectedIndex >= 0) + { + return ddDifficulty.Items.Count - ddDifficulty.SelectedIndex - 1; + } + else + { + return gameMode.CoopDifficultyLevel; + } + } + + return 0; + } + + protected virtual void UpdateDifficultyDropdown() + { + bool enabled = GameModeMap != null && GameModeMap.GameMode != null && GameModeMap.GameMode.UseDifficultyDropDown; + + if (ddDifficulty != null) + { + ddDifficulty.AllowDropDown = enabled; + + if (enabled && ddDifficulty.SelectedIndex < 0) + ddDifficulty.SelectedIndex = PreviousSelectedDifficulty; + else if (!enabled) + ddDifficulty.SelectedIndex = -1; + } + + if (lblDifficulty != null) + { + lblDifficulty.TextColor = enabled ? UISettings.ActiveSettings.TextColor : UISettings.ActiveSettings.DisabledItemColor; + } + } + protected abstract int GetDefaultMapRankIndex(GameModeMap gameModeMap); private void LbGameModeMapList_RightClick(object sender, EventArgs e) @@ -1536,10 +1607,10 @@ private PlayerHouseInfo[] WriteSpawnIni() teamStartMappings = PlayerExtraOptionsPanel.GetTeamStartMappings(); } - PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings); + int coopDifficultyLevel = GetCoopDifficultyLevel(); + PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings); IniFile spawnIni = new IniFile(spawnerSettingsFile.FullName); - IniSection settings = new IniSection("Settings"); settings.SetStringValue("Name", ProgramConstants.PLAYERNAME); @@ -1563,6 +1634,7 @@ private PlayerHouseInfo[] WriteSpawnIni() settings.SetBooleanValue("CoachMode", true); if (GetGameType() == GameType.Coop) settings.SetBooleanValue("AutoSurrender", false); + settings.SetIntValue("CoopDifficultyLevel", coopDifficultyLevel); // Used to display difficulty in game loading lobby. spawnIni.AddSection(settings); WriteSpawnIniAdditions(spawnIni); @@ -1587,7 +1659,7 @@ private PlayerHouseInfo[] WriteSpawnIni() GameMode.ApplySpawnIniCode(spawnIni); // Forced options from the game mode Map.ApplySpawnIniCode(spawnIni, Players.Count + AIPlayers.Count, - AIPlayers.Count, GameMode.CoopDifficultyLevel); // Forced options from the map + AIPlayers.Count, coopDifficultyLevel); // Forced options from the map // Player options @@ -1814,6 +1886,23 @@ private void WriteMap(PlayerHouseInfo[] houseInfos) mapIni.SetStringValue("Basic", "OriginalFilename", mapIniFileName); } + if (GameMode.UseDifficultyDropDown && ddDifficulty != null && ddDifficulty.SelectedIndex >= 0) + { + var filePath = string.Empty; + + try + { + filePath = ClientConfiguration.Instance.CoopDifficultyINIPaths[ddDifficulty.SelectedIndex]; + } + catch (IndexOutOfRangeException) + { + Logger.Log($"CoopDifficultyINIPaths does not contain file path for difficulty index {ddDifficulty.SelectedIndex}"); + } + + IniFile difficultyIni = new IniFile(filePath); + MapCodeHelper.ApplyMapCode(mapIni, difficultyIni); + } + foreach (GameLobbyCheckBox checkBox in CheckBoxes) checkBox.ApplyMapCode(mapIni, GameMode); @@ -2315,6 +2404,11 @@ protected virtual void SetMapLabels() lblMapAuthor.Text = "By".L10N("Client:Main:AuthorBy") + " " + Renderer.GetSafeString(Map.Author, lblMapAuthor.FontIndex); lblGameMode.Text = "Game mode:".L10N("Client:Main:GameModeLabel") + " " + GameMode.UIName; lblMapSize.Text = "Size:".L10N("Client:Main:MapSize") + " " + Map.GetSizeString(); + + if (GameMode.UseDifficultyDropDown && ddDifficulty != null && ddDifficulty.SelectedIndex >= 0) + { + lblGameMode.Text += " (" + "Difficulty: ".L10N("Client:Main:DifficultyLabel") + ddDifficulty.SelectedItem.Text + ")"; + } } /// @@ -2333,6 +2427,7 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) { MapPreviewBox.GameModeMap = null; OnGameOptionChanged(); + UpdateDifficultyDropdown(); return; } @@ -2345,6 +2440,8 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) foreach (var checkBox in CheckBoxes) checkBox.AllowChecking = true; + UpdateDifficultyDropdown(); + // We could either pass the CheckBoxes and DropDowns of this class // to the Map and GameMode instances and let them apply their forced // options, or we could do it in this class with helper functions. diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 5bc35d205..5e50ad20f 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -24,7 +24,7 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class LANGameLobby : MultiplayerGameLobby { - private const int GAME_OPTION_SPECIAL_FLAG_COUNT = 5; + private const int GAME_OPTION_SPECIAL_FLAG_COUNT = 6; private const double DROPOUT_TIMEOUT = 20.0; private const double GAME_BROADCAST_INTERVAL = 10.0; @@ -577,6 +577,7 @@ protected override void OnGameOptionChanged() sb.Append(GameMode?.Name ?? string.Empty); sb.Append(FrameSendRate); sb.Append(Convert.ToInt32(RemoveStartingLocations)); + sb.Append(ddDifficulty?.SelectedIndex ?? -1); BroadcastMessage(sb.ToString()); } @@ -760,6 +761,8 @@ private void BroadcastGame() sb.Append(Convert.ToInt32(Locked)); sb.Append(0); // IsLoadedGame sb.Append(Map?.SHA1); + sb.Append(ddDifficulty?.SelectedIndex ?? -1); + GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString())); } @@ -1064,6 +1067,14 @@ private void HandleGameOptionsMessage(string data) AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); } } + + if (GameMode != null && GameMode.UseDifficultyDropDown && ddDifficulty != null) + { + ddDifficulty.SelectedIndex = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 5)], ddDifficulty.SelectedIndex); + + if (ddDifficulty.SelectedIndex >= 0) + AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), "Difficulty".L10N("Client:Main:DifficultyOptionName"), ddDifficulty.SelectedItem.Text)); + } } private void GameHost_HandleReadyRequest(string sender, string autoReady) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index c02778fdc..c1a0fcc68 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -706,6 +706,11 @@ private void HideMapList() btnPickRandomMap.Disable(); btnMapSortAlphabetically.Disable(); + if (ddDifficulty != null) + ddDifficulty.Disable(); + if (lblDifficulty != null) + lblDifficulty.Disable(); + SetMapLabels(); } @@ -736,6 +741,11 @@ private void ShowMapList() ReadINIForControl(lblMapSize); ReadINIForControl(btnMapSortAlphabetically); + if (ddDifficulty != null) + ddDifficulty.Enable(); + if (lblDifficulty != null) + lblDifficulty.Enable(); + SetMapLabels(); } @@ -1062,6 +1072,14 @@ protected override void CopyPlayerDataToUI() } } + protected override void UpdateDifficultyDropdown() + { + base.UpdateDifficultyDropdown(); + + if (!IsHost && ddDifficulty != null) + ddDifficulty.AllowDropDown = false; + } + protected virtual void ClearPingIndicators() { foreach (XNAClientDropDown dd in ddPlayerNames) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index 384802930..e7e760678 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -281,6 +281,7 @@ private void SaveSettings() skirmishSettingsIni.SetStringValue("Settings", "Map", Map.SHA1); skirmishSettingsIni.SetStringValue("Settings", "GameModeMapFilter", ddGameModeMapFilter.SelectedItem?.Text); + skirmishSettingsIni.SetIntValue("Settings", "Difficulty", PreviousSelectedDifficulty); if (ClientConfiguration.Instance.SaveSkirmishGameOptions) { @@ -331,6 +332,10 @@ private void LoadSettings() GameModeMap = gameModeMap; ddGameModeMapFilter.SelectedIndex = ddGameModeMapFilter.Items.FindIndex(i => i.Tag == gameModeMapFilter); + PreviousSelectedDifficulty = skirmishSettingsIni.GetIntValue("Settings", "Difficulty", 0); + + if (GameModeMap.GameMode != null && GameModeMap.GameMode.UseDifficultyDropDown && ddDifficulty != null) + ddDifficulty.SelectedIndex = PreviousSelectedDifficulty; string mapSHA1 = skirmishSettingsIni.GetStringValue("Settings", "Map", string.Empty); diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index a254b05c2..52168b000 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -669,6 +669,7 @@ private void BroadcastGame() sb.Append(Convert.ToInt32(started || Players.Count == SGPlayers.Count)); sb.Append(1); // IsLoadedGame sb.Append(string.Empty); // MapHash + sb.Append(CoopDifficulty); GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString())); } diff --git a/DXMainClient/Domain/Multiplayer/GameMode.cs b/DXMainClient/Domain/Multiplayer/GameMode.cs index 30337921d..3aeef009d 100644 --- a/DXMainClient/Domain/Multiplayer/GameMode.cs +++ b/DXMainClient/Domain/Multiplayer/GameMode.cs @@ -87,13 +87,22 @@ public GameMode(string name) private List> ForcedSpawnIniOptions = new List>(); + /// + /// AI difficulty level for co-op style maps. + /// public int CoopDifficultyLevel { get; set; } + /// + /// Use difficulty dropdown in game lobby instead of for AI difficulty. + /// + public bool UseDifficultyDropDown { get; set; } + public void Initialize() { IniFile forcedOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath)); CoopDifficultyLevel = forcedOptionsIni.GetIntValue(Name, "CoopDifficultyLevel", 0); + UseDifficultyDropDown = forcedOptionsIni.GetBooleanValue(Name, "UseDifficultyDropDown", false); UntranslatedUIName = forcedOptionsIni.GetStringValue(Name, "UIName", Name); UIName = UntranslatedUIName.L10N($"INI:GameModes:{Name}:UIName"); MultiplayerOnly = forcedOptionsIni.GetBooleanValue(Name, "MultiplayerOnly", false); diff --git a/DXMainClient/Domain/Multiplayer/GenericHostedGame.cs b/DXMainClient/Domain/Multiplayer/GenericHostedGame.cs index a0aa63179..5f9082166 100644 --- a/DXMainClient/Domain/Multiplayer/GenericHostedGame.cs +++ b/DXMainClient/Domain/Multiplayer/GenericHostedGame.cs @@ -30,6 +30,8 @@ public abstract class GenericHostedGame: IEquatable public int SkillLevel { get; set; } + public int Difficulty { get; set; } + public virtual bool Equals(GenericHostedGame other) => string.Equals(RoomName, other?.RoomName, StringComparison.InvariantCultureIgnoreCase); } diff --git a/DXMainClient/Domain/Multiplayer/LAN/HostedLANGame.cs b/DXMainClient/Domain/Multiplayer/LAN/HostedLANGame.cs index a2ca4e071..a4a9e4fd3 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/HostedLANGame.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/HostedLANGame.cs @@ -24,7 +24,7 @@ public override int Ping public bool SetDataFromStringArray(GameCollection gc, string[] parameters) { - if (parameters.Length != 10) + if (parameters.Length != 11) { Logger.Log("Ignoring LAN GAME message because of an incorrect number of parameters."); return false; @@ -52,6 +52,7 @@ public bool SetDataFromStringArray(GameCollection gc, string[] parameters) TimeWithoutRefresh = TimeSpan.Zero; RoomName = HostName + "'s Game"; MapHash = parameters[9]; + Difficulty = Conversions.IntFromString(parameters[10], 0); return true; } diff --git a/DXMainClient/Online/FileHashCalculator.cs b/DXMainClient/Online/FileHashCalculator.cs index 56946a5f0..264769e8a 100644 --- a/DXMainClient/Online/FileHashCalculator.cs +++ b/DXMainClient/Online/FileHashCalculator.cs @@ -154,8 +154,21 @@ public void CalculateHashes() } } - // Add the hashes for each checked file from the available translations + // Add coop difficulty INIs unless they were already in one of the paths. + foreach (var path in ClientConfiguration.Instance.CoopDifficultyINIPaths) + { + var fullPath = SafePath.CombineFilePath(ProgramConstants.GamePath, path); + var dir = SafePath.GetDirectory(Path.GetDirectoryName(fullPath)); + if (iniPaths.Any(x => x.FullName.Equals(dir.FullName))) + continue; + + string hash = fh.AddHashForFileIfExists(path, fullPath); + if (!string.IsNullOrEmpty(hash)) + Logger.Log("Hash for " + path + ": " + hash); + } + + // Add the hashes for each checked file from the available translations if (Directory.Exists(ClientConfiguration.Instance.TranslationsFolderPath)) { DirectoryInfo translationsFolderPath = SafePath.GetDirectory(ClientConfiguration.Instance.TranslationsFolderPath);