diff --git a/ClientCore/Extensions/IniFileExtensions.cs b/ClientCore/Extensions/IniFileExtensions.cs index 56447b781..f4771e368 100644 --- a/ClientCore/Extensions/IniFileExtensions.cs +++ b/ClientCore/Extensions/IniFileExtensions.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +#nullable enable + +using System; +using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using Rampastring.Tools; @@ -26,7 +28,7 @@ public static void RemoveAllKeys(this IniSection iniSection) iniSection.RemoveKey(iniSectionKey.Key); } - public static string[] GetStringListValue(this IniFile iniFile, string section, string key, string defaultValue, char[] separators = null) + public static string[] GetStringListValue(this IniFile iniFile, string section, string key, string defaultValue, char[]? separators = null) { separators ??= [',']; IniSection iniSection = iniFile.GetSection(section); @@ -38,5 +40,18 @@ public static string[] GetStringListValue(this IniFile iniFile, string section, .Where(s => !string.IsNullOrEmpty(s)) .ToArray(); } + + public static string? GetStringValueOrNull(this IniSection section, string key) => + section.KeyExists(key) ? section.GetStringValue(key, string.Empty) : null; + + public static int? GetIntValueOrNull(this IniSection section, string key) => + section.KeyExists(key) ? section.GetIntValue(key, 0) : null; + + public static bool? GetBooleanValueOrNull(this IniSection section, string key) => + section.KeyExists(key) ? section.GetBooleanValue(key, false) : null; + + public static List? GetListValueOrNull(this IniSection section, string key, char separator, Func converter) => + section.KeyExists(key) ? section.GetListValue(key, separator, converter) : null; + } } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index ed8ada41a..9df6119dc 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -791,16 +791,16 @@ private void HandleOptionsRequest(string playerName, int options) if (0 < side && side < SideCount && disallowedSides[side]) return; - if (Map?.CoopInfo != null) + if (GameModeMap?.CoopInfo != null) { - if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) + if (GameModeMap.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) return; - if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) + if (GameModeMap.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) return; } - if (start < 0 || start > Map?.MaxPlayers) + if (!(start == 0 || (GameModeMap?.AllowedStartingLocations?.Contains(start) ?? true))) return; if (team < 0 || team > 4) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 0944a6290..d641f83f2 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -17,6 +17,7 @@ using DTAClient.Online.EventArguments; using ClientCore.Extensions; using TextCopy; +using System.Diagnostics; namespace DTAClient.DXGUI.Multiplayer.GameLobby @@ -498,19 +499,19 @@ protected void ApplyPlayerExtraOptions(string sender, string message) if (PlayerExtraOptionsPanel != null) { - if (playerExtraOptions.IsForceRandomSides != PlayerExtraOptionsPanel.IsForcedRandomSides()) + if (playerExtraOptions.IsForceRandomSides != PlayerExtraOptionsPanel.IsForcedRandomSides) AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomSides, "side selection".L10N("Client:Main:SideAsANoun")); - if (playerExtraOptions.IsForceRandomColors != PlayerExtraOptionsPanel.IsForcedRandomColors()) + if (playerExtraOptions.IsForceRandomColors != PlayerExtraOptionsPanel.IsForcedRandomColors) AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomColors, "color selection".L10N("Client:Main:ColorAsANoun")); - if (playerExtraOptions.IsForceRandomStarts != PlayerExtraOptionsPanel.IsForcedRandomStarts()) + if (playerExtraOptions.IsForceRandomStarts != PlayerExtraOptionsPanel.IsForcedRandomStarts) AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomStarts, "start selection".L10N("Client:Main:StartPositionAsANoun")); - if (playerExtraOptions.IsForceRandomTeams != PlayerExtraOptionsPanel.IsForcedRandomTeams()) - AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomTeams, "team selection".L10N("Client:Main:TeamAsANoun")); + if (playerExtraOptions.IsForceNoTeams != PlayerExtraOptionsPanel.IsForcedNoTeams) + AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceNoTeams, "team selection".L10N("Client:Main:TeamAsANoun")); - if (playerExtraOptions.IsUseTeamStartMappings != PlayerExtraOptionsPanel.IsUseTeamStartMappings()) + if (playerExtraOptions.IsUseTeamStartMappings != PlayerExtraOptionsPanel.IsUseTeamStartMappings) AddPlayerExtraOptionForcedNotice(!playerExtraOptions.IsUseTeamStartMappings, "auto ally".L10N("Client:Main:AutoAllyAsANoun")); } @@ -601,10 +602,10 @@ protected void ListMaps() var gameModeMap = filteredMaps[i]; XNAListBoxItem rankItem = new XNAListBoxItem(); - if (gameModeMap.Map.IsCoop) + if (gameModeMap.IsCoop) { 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 - gameModeMap.CoopDifficultyLevel) + 1]; else rankItem.Texture = RankTextures[0]; } @@ -618,7 +619,7 @@ protected void ListMaps() mapNameItem.Text = Renderer.GetSafeString(mapNameText, lbGameModeMapList.FontIndex); - if ((gameModeMap.Map.MultiplayerOnly || gameModeMap.GameMode.MultiplayerOnly) && !isMultiplayer) + if (gameModeMap.MultiplayerOnly && !isMultiplayer) mapNameItem.TextColor = UISettings.ActiveSettings.DisabledItemColor; mapNameItem.Tag = gameModeMap; @@ -695,7 +696,7 @@ private void MapPreviewBox_ToggleFavorite(object sender, EventArgs e) => protected virtual void ToggleFavoriteMap() { if (GameModeMap != null) - { + { GameModeMap.IsFavorite = UserINISettings.Instance.ToggleFavoriteMap(Map.UntranslatedName, GameMode.Name, GameModeMap.IsFavorite); MapPreviewBox.RefreshFavoriteBtn(); } @@ -799,12 +800,24 @@ private void PickRandomMap() private List GetMapList(int playerCount) { List maps = IsFavoriteMapsSelected() - ? GetFavoriteGameModeMaps().Select(gmm => gmm.Map).ToList() + ? GetFavoriteGameModeMaps().Select(gameModeMap => gameModeMap.Map).ToList() : GameMode?.Maps.ToList() ?? new List(); if (playerCount != 1) { - maps = maps.Where(x => x.MaxPlayers == playerCount).ToList(); + + if (GameMode?.MaxPlayersOverride != null) + { + // MaxPlayers have been overridden in GameMode. This means all maps in the game mode has the same MaxPlayers value + if (playerCount != GameMode.MaxPlayersOverride) + maps = []; + } + else + { + // Maps could have different MaxPlayers values. + maps = maps.Where(x => x.MaxPlayers == playerCount).ToList(); + } + if (maps.Count < 1 && playerCount <= MAX_PLAYER_COUNT) return GetMapList(playerCount + 1); } @@ -1007,19 +1020,37 @@ protected virtual void PlayerExtraOptions_OptionsChanged(object sender, EventArg { var playerExtraOptions = GetPlayerExtraOptions(); - for (int i = 0; i < ddPlayerSides.Length; i++) + for (int i = 0; i < MAX_PLAYER_COUNT; i++) + { + var pInfo = GetPlayerInfoForIndex(i); + + // IsForceRandomSides + if (pInfo != null && playerExtraOptions.IsForceRandomSides) + pInfo.SideId = 0; + EnablePlayerOptionDropDown(ddPlayerSides[i], i, !playerExtraOptions.IsForceRandomSides); - for (int i = 0; i < ddPlayerTeams.Length; i++) - EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceRandomTeams); + // IsForceNoTeams + Debug.Assert(!playerExtraOptions.IsForceNoTeams || !GameModeMap.IsCoop, "Co-ops should not have force no teams enabled."); + if (pInfo != null && playerExtraOptions.IsForceNoTeams) + pInfo.TeamId = 0; + + EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceNoTeams); + + // IsForceRandomColors + if (pInfo != null && playerExtraOptions.IsForceRandomColors) + pInfo.ColorId = 0; - for (int i = 0; i < ddPlayerColors.Length; i++) EnablePlayerOptionDropDown(ddPlayerColors[i], i, !playerExtraOptions.IsForceRandomColors); - for (int i = 0; i < ddPlayerStarts.Length; i++) + // IsForceRandomStarts + if (pInfo != null && playerExtraOptions.IsForceRandomStarts) + pInfo.StartingLocation = 0; + EnablePlayerOptionDropDown(ddPlayerStarts[i], i, !playerExtraOptions.IsForceRandomStarts); + } - UpdateMapPreviewBoxEnabledStatus(); + CopyPlayerDataToUI(); RefreshBtnPlayerExtraOptionsOpenTexture(); } @@ -1028,8 +1059,6 @@ private void EnablePlayerOptionDropDown(XNAClientDropDown clientDropDown, int pl var pInfo = GetPlayerInfoForIndex(playerIndex); var allowOtherPlayerOptionsChange = AllowPlayerOptionsChange() && pInfo != null; clientDropDown.AllowDropDown = enable && (allowOtherPlayerOptionsChange || pInfo?.Name == ProgramConstants.PLAYERNAME); - if (!clientDropDown.AllowDropDown) - clientDropDown.SelectedIndex = clientDropDown.SelectedIndex > 0 ? 0 : clientDropDown.SelectedIndex; } protected PlayerInfo GetPlayerInfoForIndex(int playerIndex) @@ -1224,7 +1253,7 @@ protected void CheckDisallowedSidesForGroup(bool forHumanPlayers) } } - if (Map != null && Map.CoopInfo != null) + if (GameModeMap != null && GameModeMap.CoopInfo != null) { // Disallow spectator @@ -1258,8 +1287,8 @@ protected void CheckDisallowedSidesForGroup(bool forHumanPlayers) /// protected void CheckDisallowedSides() { - CheckDisallowedSidesForGroup(forHumanPlayers:false); - CheckDisallowedSidesForGroup(forHumanPlayers:true); + CheckDisallowedSidesForGroup(forHumanPlayers: false); + CheckDisallowedSidesForGroup(forHumanPlayers: true); } /// @@ -1287,11 +1316,11 @@ protected bool[] GetDisallowedSides() { var returnValue = new bool[SideCount]; - if (Map != null && Map.CoopInfo != null) + if (GameModeMap != null && GameModeMap.CoopInfo != null) { // Co-Op map disallowed side logic - foreach (int disallowedSideIndex in Map.CoopInfo.DisallowedPlayerSides) + foreach (int disallowedSideIndex in GameModeMap.CoopInfo.DisallowedPlayerSides) returnValue[disallowedSideIndex] = true; } @@ -1312,7 +1341,7 @@ protected bool[] GetDisallowedSides() /// and returns the options as an array of PlayerHouseInfos. /// /// An array of PlayerHouseInfos. - protected virtual PlayerHouseInfo[] Randomize(List teamStartMappings) + protected virtual PlayerHouseInfo[] Randomize(List teamStartMappings, Random pseudoRandom) { int totalPlayerCount = Players.Count + AIPlayers.Count; PlayerHouseInfo[] houseInfos = new PlayerHouseInfo[totalPlayerCount]; @@ -1331,9 +1360,9 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa for (int cId = 0; cId < MPColors.Count; cId++) freeColors.Add(cId); - if (Map.CoopInfo != null) + if (GameModeMap.CoopInfo != null) { - foreach (int colorIndex in Map.CoopInfo.DisallowedPlayerColors) + foreach (int colorIndex in GameModeMap.CoopInfo.DisallowedPlayerColors) freeColors.Remove(colorIndex); } @@ -1348,8 +1377,8 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa List freeStartingLocations = new List(); List takenStartingLocations = new List(); - for (int i = 0; i < Map.MaxPlayers; i++) - freeStartingLocations.Add(i); + foreach (int i in GameModeMap.AllowedStartingLocations) + freeStartingLocations.Add(i - 1); for (int i = 0; i < Players.Count; i++) { @@ -1371,8 +1400,6 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa // Randomize options - Random pseudoRandom = new Random(RandomSeed); - for (int i = 0; i < totalPlayerCount; i++) { PlayerInfo pInfo; @@ -1382,18 +1409,21 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa if (i < Players.Count) { pInfo = Players[i]; - disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers:true); + disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers: true); } else { pInfo = AIPlayers[i - Players.Count]; - disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers:false); + disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers: false); } pHouseInfo.RandomizeSide(pInfo, SideCount, pseudoRandom, disallowedSides, RandomSelectors, RandomSelectorCount); pHouseInfo.RandomizeColor(pInfo, freeColors, MPColors, pseudoRandom); - pHouseInfo.RandomizeStart(pInfo, pseudoRandom, freeStartingLocations, takenStartingLocations, teamStartMappings.Any()); + + bool overrideGameRandomLocations = teamStartMappings.Any() + || GameModeMap.AllowedStartingLocations.Max() > GameModeMap.MaxPlayers; // non-sequential AllowedStartingLocations + pHouseInfo.RandomizeStart(pInfo, pseudoRandom, freeStartingLocations, takenStartingLocations, overrideGameRandomLocations); } return houseInfos; @@ -1402,7 +1432,7 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa /// /// Writes spawn.ini. Returns the player house info returned from the randomizer. /// - private PlayerHouseInfo[] WriteSpawnIni() + private PlayerHouseInfo[] WriteSpawnIni(Random pseudoRandom) { Logger.Log("Writing spawn.ini"); @@ -1410,13 +1440,19 @@ private PlayerHouseInfo[] WriteSpawnIni() spawnerSettingsFile.Delete(); - if (Map.IsCoop) + if (GameModeMap.IsCoop) { foreach (PlayerInfo pInfo in Players) + { + Debug.Assert(pInfo.TeamId == 1, "Co-ops should always set TeamId to 1 before lanching the game"); pInfo.TeamId = 1; + } foreach (PlayerInfo pInfo in AIPlayers) + { + Debug.Assert(pInfo.TeamId == 1, "Co-ops should always set TeamId to 1 before lanching the game"); pInfo.TeamId = 1; + } } var teamStartMappings = new List(0); @@ -1425,7 +1461,7 @@ private PlayerHouseInfo[] WriteSpawnIni() teamStartMappings = PlayerExtraOptionsPanel.GetTeamStartMappings(); } - PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings); + PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings, pseudoRandom); IniFile spawnIni = new IniFile(spawnerSettingsFile.FullName); @@ -1476,7 +1512,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, GameModeMap.IsCoop, GameModeMap.CoopInfo, GameModeMap.CoopDifficultyLevel, pseudoRandom, SideCount); // Forced options from the map // Player options @@ -1625,7 +1661,7 @@ protected virtual void WriteSpawnIniAdditions(IniFile iniFile) private void InitializeMatchStatistics(PlayerHouseInfo[] houseInfos) { matchStatistics = new MatchStatistics(ProgramConstants.GAME_VERSION, UniqueGameID, - Map.UntranslatedName, GameMode.UntranslatedUIName, Players.Count, Map.IsCoop); + Map.UntranslatedName, GameMode.UntranslatedUIName, Players.Count, GameModeMap.IsCoop); bool isValidForStar = true; foreach (GameLobbyCheckBox checkBox in CheckBoxes) @@ -1662,7 +1698,7 @@ private void InitializeMatchStatistics(PlayerHouseInfo[] houseInfos) /// /// Writes spawnmap.ini. /// - private void WriteMap(PlayerHouseInfo[] houseInfos) + private void WriteMap(PlayerHouseInfo[] houseInfos, Random pseudoRandom) { FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); @@ -1677,7 +1713,11 @@ private void WriteMap(PlayerHouseInfo[] houseInfos) IniFile globalCodeIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", "Map Code", "GlobalCode.ini")); - MapCodeHelper.ApplyMapCode(mapIni, GameMode.GetMapRulesIniFile()); + foreach (IniFile iniFile in GameMode.GetMapRulesIniFiles(pseudoRandom)) + { + MapCodeHelper.ApplyMapCode(mapIni, iniFile); + } + MapCodeHelper.ApplyMapCode(mapIni, globalCodeIni); if (isMultiplayer) @@ -1744,10 +1784,10 @@ private void CopySupplementalMapFiles(IniFile mapIni) Logger.Log(errorMessage); Logger.Log(ex.ToString()); XNAMessageBox.Show(WindowManager, "Error".L10N("Client:Main:Error"), errorMessage); - + } } - + // Write the supplemental map files to the INI (eventual spawnmap.ini) mapIni.SetStringValue("Basic", "SupplementalFiles", string.Join(",", supplementalFileNames)); } @@ -1796,7 +1836,7 @@ private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] house { if (RemoveStartingLocations) { - if (Map.EnforceMaxPlayers) + if (GameModeMap.EnforceMaxPlayers) return; // All random starting locations given by the game @@ -1902,9 +1942,11 @@ private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] house /// protected virtual void StartGame() { - PlayerHouseInfo[] houseInfos = WriteSpawnIni(); + Random pseudoRandom = new Random(RandomSeed); + + PlayerHouseInfo[] houseInfos = WriteSpawnIni(pseudoRandom); InitializeMatchStatistics(houseInfos); - WriteMap(houseInfos); + WriteMap(houseInfos, pseudoRandom); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; @@ -1994,7 +2036,7 @@ protected virtual void CopyPlayerDataFromUI(object sender, EventArgs e) SideId = Math.Max(ddPlayerSides[cmbId].SelectedIndex, 0), ColorId = Math.Max(ddPlayerColors[cmbId].SelectedIndex, 0), StartingLocation = Math.Max(ddPlayerStarts[cmbId].SelectedIndex, 0), - TeamId = Map != null && Map.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0), + TeamId = Map != null && GameModeMap.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0), IsAI = true }; @@ -2081,8 +2123,8 @@ protected virtual void CopyPlayerDataToUI() ddPlayerTeams[pId].SelectedIndex = pInfo.TeamId; if (GameModeMap != null) { - ddPlayerTeams[pId].AllowDropDown = !playerExtraOptions.IsForceRandomTeams && allowPlayerOptionsChange && !Map.IsCoop && !Map.ForceNoTeams && !GameMode.ForceNoTeams; - ddPlayerStarts[pId].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowPlayerOptionsChange && (Map.IsCoop || !Map.ForceRandomStartLocations && !GameMode.ForceRandomStartLocations); + ddPlayerTeams[pId].AllowDropDown = !playerExtraOptions.IsForceNoTeams && allowPlayerOptionsChange && !GameModeMap.IsCoop && !GameModeMap.ForceNoTeams; + ddPlayerStarts[pId].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowPlayerOptionsChange && !GameModeMap.ForceRandomStartLocations; } } @@ -2115,8 +2157,8 @@ protected virtual void CopyPlayerDataToUI() if (GameModeMap != null) { - ddPlayerTeams[index].AllowDropDown = !playerExtraOptions.IsForceRandomTeams && allowOptionsChange && !Map.IsCoop && !Map.ForceNoTeams && !GameMode.ForceNoTeams; - ddPlayerStarts[index].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowOptionsChange && (Map.IsCoop || !Map.ForceRandomStartLocations && !GameMode.ForceRandomStartLocations); + ddPlayerTeams[index].AllowDropDown = !playerExtraOptions.IsForceNoTeams && allowOptionsChange && !GameModeMap.IsCoop && !GameModeMap.ForceNoTeams; + ddPlayerStarts[index].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowOptionsChange && !GameModeMap.ForceRandomStartLocations; } } @@ -2281,13 +2323,19 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) ddStart.AddItem("???"); - for (int i = 1; i <= Map.MaxPlayers; i++) - ddStart.AddItem(i.ToString()); + int maxLocation = GameModeMap.MaxPlayers == 0 ? 0 : (GameModeMap.AllowedStartingLocations.Max() == GameModeMap.MaxPlayers ? GameModeMap.MaxPlayers : MAX_PLAYER_COUNT); + for (int i = 1; i <= maxLocation; i++) + { + if (GameModeMap.AllowedStartingLocations.Contains(i)) + ddStart.AddItem(i.ToString()); + else + ddStart.AddItem(new XNADropDownItem() { Text = i.ToString(), Selectable = false }); + } } // Check if AI players allowed - bool AIAllowed = !(Map.HumanPlayersOnly || GameMode.HumanPlayersOnly); + bool AIAllowed = !GameModeMap.HumanPlayersOnly; foreach (var ddName in ddPlayerNames) { if (ddName.Items.Count > 3) @@ -2303,18 +2351,18 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) foreach (PlayerInfo pInfo in concatPlayerList) { - if (pInfo.StartingLocation > Map.MaxPlayers || - (!Map.IsCoop && (Map.ForceRandomStartLocations || GameMode.ForceRandomStartLocations))) + if (!GameModeMap.AllowedStartingLocations.Contains(pInfo.StartingLocation) || + GameModeMap.ForceRandomStartLocations) pInfo.StartingLocation = 0; - if (!Map.IsCoop && (Map.ForceNoTeams || GameMode.ForceNoTeams)) + if (!GameModeMap.IsCoop && GameModeMap.ForceNoTeams) pInfo.TeamId = 0; } - if (Map.CoopInfo != null) + if (GameModeMap.CoopInfo != null) { // Co-Op map disallowed color logic - foreach (int disallowedColorIndex in Map.CoopInfo.DisallowedPlayerColors) + foreach (int disallowedColorIndex in GameModeMap.CoopInfo.DisallowedPlayerColors) { if (disallowedColorIndex >= MPColors.Count) continue; @@ -2338,6 +2386,23 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) // Force teams foreach (PlayerInfo pInfo in concatPlayerList) pInfo.TeamId = 1; + + if (PlayerOptionsPanel != null) + { + PlayerExtraOptionsPanel.IsForcedNoTeamsAllowChecking = false; + PlayerExtraOptionsPanel.IsForcedNoTeams = false; + + PlayerExtraOptionsPanel.IsUseTeamStartMappingsAllowChecking = false; + PlayerExtraOptionsPanel.IsUseTeamStartMappings = false; + } + } + else + { + if (PlayerOptionsPanel != null) + { + PlayerExtraOptionsPanel.IsForcedNoTeamsAllowChecking = true; + PlayerExtraOptionsPanel.IsUseTeamStartMappingsAllowChecking = true; + } } OnGameOptionChanged(); @@ -2347,7 +2412,7 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) disableGameOptionUpdateBroadcast = false; - PlayerExtraOptionsPanel?.UpdateForMap(Map); + PlayerExtraOptionsPanel?.UpdateForGameModeMap(GameModeMap); } private void ApplyForcedCheckBoxOptions(List optionList, @@ -2447,14 +2512,14 @@ protected Rank GetRank() return Rank.None; // PvP stars for 2-player and 3-player maps - if (Map.MaxPlayers <= 3) + if (GameModeMap.MaxPlayers <= 3) { List filteredPlayers = Players.Where(p => !IsPlayerSpectator(p)).ToList(); if (AIPlayers.Count > 0) return Rank.None; - if (filteredPlayers.Count != Map.MaxPlayers) + if (filteredPlayers.Count != GameModeMap.MaxPlayers) return Rank.None; int localTeamIndex = localPlayer.TeamId; @@ -2513,7 +2578,7 @@ protected Rank GetRank() // Skirmish! // ********* - if (AIPlayers.Count != Map.MaxPlayers - 1) + if (AIPlayers.Count != GameModeMap.MaxPlayers - 1) return Rank.None; teamMemberCounts[localPlayer.TeamId]++; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index d7f316a46..4710ff644 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -845,16 +845,16 @@ private void HandlePlayerOptionsRequest(string sender, string data) if (side > 0 && side <= SideCount && disallowedSides[side - 1]) return; - if (Map.CoopInfo != null) + if (GameModeMap.CoopInfo != null) { - if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) + if (GameModeMap.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) return; - if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) + if (GameModeMap.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) return; } - if (start < 0 || start > Map.MaxPlayers) + if (!(start == 0 || (GameModeMap?.AllowedStartingLocations?.Contains(start) ?? true))) return; if (team < 0 || team > 4) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs index 31744b247..275c942ca 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs @@ -260,7 +260,7 @@ private void ContextMenu_OptionSelected(int index) { SoundPlayer.Play(sndDropdownSound); - if (GameModeMap.Map.EnforceMaxPlayers) + if (GameModeMap.EnforceMaxPlayers) { foreach (PlayerInfo pInfo in players.Concat(aiPlayers)) { @@ -301,7 +301,7 @@ private void Indicator_LeftClick(object sender, EventArgs e) if (!EnableContextMenu) { - if (GameModeMap.Map.EnforceMaxPlayers) + if (GameModeMap.EnforceMaxPlayers) { foreach (PlayerInfo pInfo in players.Concat(aiPlayers)) { @@ -455,22 +455,25 @@ private void UpdateMap() List startingLocations = GameModeMap.Map.GetStartingLocationPreviewCoords(new Point(previewTexture.Width, previewTexture.Height)); - for (int i = 0; i < startingLocations.Count && i < GameModeMap.Map.MaxPlayers; i++) + for (int i = 0; i < MAX_STARTING_LOCATIONS; i++) { - PlayerLocationIndicator indicator = startingLocationIndicators[i]; + bool showLocation = i < startingLocations.Count && GameModeMap.AllowedStartingLocations.Contains(i + 1); + if (showLocation) + { + PlayerLocationIndicator indicator = startingLocationIndicators[i]; - Point location = new Point( - texturePositionX + (int)(startingLocations[i].X * ratio), - texturePositionY + (int)(startingLocations[i].Y * ratio)); + Point location = new Point( + texturePositionX + (int)(startingLocations[i].X * ratio), + texturePositionY + (int)(startingLocations[i].Y * ratio)); - indicator.SetPosition(location); - indicator.Enabled = true; - indicator.Visible = true; - } - - for (int i = startingLocations.Count; i < MAX_STARTING_LOCATIONS; i++) - { - startingLocationIndicators[i].Disable(); + indicator.SetPosition(location); + indicator.Enabled = true; + indicator.Visible = true; + } + else + { + startingLocationIndicators[i].Disable(); + } } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 289c278b2..69bbb3e98 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -16,6 +16,7 @@ using Microsoft.Xna.Framework.Graphics; using ClientCore.Extensions; using DTAClient.DXGUI.Multiplayer.CnCNet; +using System.Diagnostics; namespace DTAClient.DXGUI.Multiplayer.GameLobby { @@ -801,7 +802,7 @@ protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) return; } - if (Map.EnforceMaxPlayers) + if (GameModeMap.EnforceMaxPlayers) { foreach (PlayerInfo pInfo in Players) { @@ -836,14 +837,14 @@ protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + AIPlayers.Count; - int minPlayers = GameMode.MinPlayersOverride > -1 ? GameMode.MinPlayersOverride : Map.MinPlayers; + int minPlayers = GameModeMap.MinPlayers; if (totalPlayerCount < minPlayers) { InsufficientPlayersNotification(); return; } - if (Map.EnforceMaxPlayers && totalPlayerCount > Map.MaxPlayers) + if (GameModeMap.EnforceMaxPlayers && totalPlayerCount > GameModeMap.MaxPlayers) { TooManyPlayersNotification(); return; @@ -895,7 +896,7 @@ protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) GetReadyNotification(); return; } - + } HostLaunchGame(); @@ -937,19 +938,16 @@ protected virtual void GetReadyNotification() protected virtual void InsufficientPlayersNotification() { - if (GameMode != null && GameMode.MinPlayersOverride > -1) - AddNotice(String.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotification1"), - GameMode.UIName, GameMode.MinPlayersOverride)); - else if (Map != null) - AddNotice(String.Format("Unable to launch game: this map cannot be played with fewer than {0} players.".L10N("Client:Main:InsufficientPlayersNotification2"), - Map.MinPlayers)); + Debug.Assert(GameModeMap != null, "GameModeMap should not be null"); + AddNotice(string.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotification1"), + GameModeMap.ToString(), GameModeMap.MinPlayers)); } protected virtual void TooManyPlayersNotification() { - if (Map != null) - AddNotice(String.Format("Unable to launch game: this map cannot be played with more than {0} players.".L10N("Client:Main:TooManyPlayersNotification"), - Map.MaxPlayers)); + Debug.Assert(GameModeMap != null, "GameModeMap should not be null"); + AddNotice(string.Format("Unable to launch game: {0} cannot be played with more than {1} players.".L10N("Client:Main:TooManyPlayersNotification1"), + GameModeMap.ToString(), GameModeMap.MaxPlayers)); } public virtual void Clear() @@ -1017,7 +1015,8 @@ protected override void CopyPlayerDataToUI() { StatusIndicators[pId].SwitchTexture("error"); } - else */ if (Players[pId].IsInGame) // If player is ingame + else */ + if (Players[pId].IsInGame) // If player is ingame { StatusIndicators[pId].SwitchTexture(PlayerSlotState.InGame); } @@ -1152,10 +1151,10 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) { - if (gameModeMap.Map.MaxPlayers > 3) - return StatisticsManager.Instance.GetCoopRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.Map.MaxPlayers); + if (gameModeMap.MaxPlayers > 3) + return StatisticsManager.Instance.GetCoopRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.MaxPlayers); - if (StatisticsManager.Instance.HasWonMapInPvP(gameModeMap.Map.UntranslatedName, gameModeMap.GameMode.UntranslatedUIName, gameModeMap.Map.MaxPlayers)) + if (StatisticsManager.Instance.HasWonMapInPvP(gameModeMap.Map.UntranslatedName, gameModeMap.GameMode.UntranslatedUIName, gameModeMap.MaxPlayers)) return 2; return -1; @@ -1171,7 +1170,7 @@ protected override void UpdateMapPreviewBoxEnabledStatus() { if (Map != null && GameMode != null) { - bool disablestartlocs = (Map.ForceRandomStartLocations || GameMode.ForceRandomStartLocations || GetPlayerExtraOptions().IsForceRandomStarts); + bool disablestartlocs = GameModeMap.ForceRandomStartLocations || GetPlayerExtraOptions().IsForceRandomStarts; MapPreviewBox.EnableContextMenu = disablestartlocs ? false : IsHost; MapPreviewBox.EnableStartLocationSelection = !disablestartlocs; } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index d6caebf8b..5b7796116 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -109,35 +109,24 @@ private string CheckGameValidity() int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + AIPlayers.Count; - if (GameMode.MultiplayerOnly) + if (GameModeMap.MultiplayerOnly) { - return String.Format("{0} can only be played on CnCNet and LAN.".L10N("Client:Main:GameModeMultiplayerOnly"), - GameMode.UIName); + return string.Format("{0} can only be played on CnCNet and LAN.".L10N("Client:Main:GameModeMultiplayerOnly"), + GameModeMap.ToString()); } - if (GameMode.MinPlayersOverride > -1 && totalPlayerCount < GameMode.MinPlayersOverride) + if (totalPlayerCount < GameModeMap.MinPlayers) { - return String.Format("{0} cannot be played with less than {1} players.".L10N("Client:Main:GameModeInsufficientPlayers"), - GameMode.UIName, GameMode.MinPlayersOverride); + return string.Format("{0} cannot be played with less than {1} players.".L10N("Client:Main:GameModeInsufficientPlayers"), + GameModeMap.ToString(), GameModeMap.MinPlayers); } - if (Map.MultiplayerOnly) + if (GameModeMap.EnforceMaxPlayers) { - return "The selected map can only be played on CnCNet and LAN.".L10N("Client:Main:MapMultiplayerOnly"); - } - - if (totalPlayerCount < Map.MinPlayers) - { - return String.Format("The selected map cannot be played with less than {0} players.".L10N("Client:Main:MapInsufficientPlayers"), - Map.MinPlayers); - } - - if (Map.EnforceMaxPlayers) - { - if (totalPlayerCount > Map.MaxPlayers) + if (totalPlayerCount > GameModeMap.MaxPlayers) { - return String.Format("The selected map cannot be played with more than {0} players.".L10N("Client:Main:MapTooManyPlayers"), - Map.MaxPlayers); + return string.Format("{0} cannot be played with more than {1} players.".L10N("Client:Main:TooManyPlayers"), + GameModeMap.ToString(), GameModeMap.MaxPlayers); } IEnumerable concatList = Players.Concat(AIPlayers); @@ -154,7 +143,7 @@ private string CheckGameValidity() } } - if (Map.IsCoop && Players[0].SideId == ddPlayerSides[0].Items.Count - 1) + if (GameModeMap.IsCoop && Players[0].SideId == ddPlayerSides[0].Items.Count - 1) { return "Co-op missions cannot be spectated. You'll have to show a bit more effort to cheat here.".L10N("Client:Main:CoOpMissionSpectatorPrompt"); } @@ -223,7 +212,7 @@ protected override bool AllowPlayerOptionsChange() protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) { - return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.Map.MaxPlayers); + return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.MaxPlayers); } protected override void GameProcessExited() @@ -369,7 +358,7 @@ private void LoadSettings() //return; } - bool AIAllowed = !(Map.HumanPlayersOnly || GameMode.HumanPlayersOnly); + bool AIAllowed = !GameModeMap.HumanPlayersOnly; foreach (string key in keys) { if (!AIAllowed) break; @@ -469,13 +458,13 @@ private void CheckLoadedPlayerVariableBounds(PlayerInfo pInfo, bool isAIPlayer = } if (pInfo.TeamId < 0 || pInfo.TeamId >= ddPlayerTeams[0].Items.Count || - !Map.IsCoop && (Map.ForceNoTeams || GameMode.ForceNoTeams)) + !GameModeMap.IsCoop && GameModeMap.ForceNoTeams) { pInfo.TeamId = 0; } if (pInfo.StartingLocation < 0 || pInfo.StartingLocation > MAX_PLAYER_COUNT || - !Map.IsCoop && (Map.ForceRandomStartLocations || GameMode.ForceRandomStartLocations)) + GameModeMap.ForceRandomStartLocations) { pInfo.StartingLocation = 0; } @@ -497,7 +486,7 @@ private void InitDefaultSettings() protected override void UpdateMapPreviewBoxEnabledStatus() { - MapPreviewBox.EnableContextMenu = !((Map != null && Map.ForceRandomStartLocations) || (GameMode != null && GameMode.ForceRandomStartLocations) || GetPlayerExtraOptions().IsForceRandomStarts); + MapPreviewBox.EnableContextMenu = !(GameModeMap.ForceRandomStartLocations || GetPlayerExtraOptions().IsForceRandomStarts); MapPreviewBox.EnableStartLocationSelection = MapPreviewBox.EnableContextMenu; } diff --git a/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs b/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs index f014596f2..3d672d2c8 100644 --- a/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs +++ b/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs @@ -20,7 +20,7 @@ public class PlayerExtraOptionsPanel : XNAWindow private readonly string customPresetName = "Custom".L10N("Client:Main:CustomPresetName"); private XNAClientCheckBox chkBoxForceRandomSides; - private XNAClientCheckBox chkBoxForceRandomTeams; + private XNAClientCheckBox chkBoxForceNoTeams; private XNAClientCheckBox chkBoxForceRandomColors; private XNAClientCheckBox chkBoxForceRandomStarts; private XNAClientCheckBox chkBoxUseTeamStartMappings; @@ -32,17 +32,58 @@ public class PlayerExtraOptionsPanel : XNAWindow public EventHandler OptionsChanged; public EventHandler OnClose; - private Map _map; + private GameModeMap _gameModeMap; public PlayerExtraOptionsPanel(WindowManager windowManager) : base(windowManager) { } - public bool IsForcedRandomSides() => chkBoxForceRandomSides.Checked; - public bool IsForcedRandomTeams() => chkBoxForceRandomTeams.Checked; - public bool IsForcedRandomColors() => chkBoxForceRandomColors.Checked; - public bool IsForcedRandomStarts() => chkBoxForceRandomStarts.Checked; - public bool IsUseTeamStartMappings() => chkBoxUseTeamStartMappings.Checked; + public bool IsForcedRandomSides + { + get => chkBoxForceRandomSides.Checked; + set => chkBoxForceRandomSides.Checked = value; + } + + public bool IsForcedNoTeams + { + get => chkBoxForceNoTeams.Checked; + set => chkBoxForceNoTeams.Checked = value; + } + + private bool _isForcedNoTeamsAllowChecking = true; + public bool IsForcedNoTeamsAllowChecking + { + get => _isForcedNoTeamsAllowChecking; + set + { + _isForcedNoTeamsAllowChecking = value; + RefreshChkBoxForceNoTeams_AllowChecking(); + } + } + + public bool IsForcedRandomColors + { + get => chkBoxForceRandomColors.Checked; + set => chkBoxForceRandomColors.Checked = value; + } + + public bool IsForcedRandomStarts + { + get => chkBoxForceRandomStarts.Checked; + set => chkBoxForceRandomStarts.Checked = value; + } + + public bool IsUseTeamStartMappings + { + get => chkBoxUseTeamStartMappings.Checked; + set => chkBoxUseTeamStartMappings.Checked = value; + } + + public bool IsUseTeamStartMappingsAllowChecking + { + get => chkBoxUseTeamStartMappings.AllowChecking; + set => chkBoxUseTeamStartMappings.AllowChecking = value; + } private void Options_Changed(object sender, EventArgs e) => OptionsChanged?.Invoke(sender, e); @@ -58,17 +99,17 @@ private void Mapping_Changed(object sender, EventArgs e) private void ChkBoxUseTeamStartMappings_Changed(object sender, EventArgs e) { RefreshTeamStartMappingsPanel(); - chkBoxForceRandomTeams.Checked = chkBoxForceRandomTeams.Checked || chkBoxUseTeamStartMappings.Checked; - chkBoxForceRandomTeams.AllowChecking = !chkBoxUseTeamStartMappings.Checked; - - // chkBoxForceRandomStarts.Checked = chkBoxForceRandomStarts.Checked || chkBoxUseTeamStartMappings.Checked; - // chkBoxForceRandomStarts.AllowChecking = !chkBoxUseTeamStartMappings.Checked; + chkBoxForceNoTeams.Checked = chkBoxForceNoTeams.Checked || chkBoxUseTeamStartMappings.Checked; + RefreshChkBoxForceNoTeams_AllowChecking(); RefreshPresetDropdown(); Options_Changed(sender, e); } + private void RefreshChkBoxForceNoTeams_AllowChecking() + => chkBoxForceNoTeams.AllowChecking = IsForcedNoTeamsAllowChecking && !chkBoxUseTeamStartMappings.Checked; + private void RefreshTeamStartMappingsPanel() { teamStartMappingsPanel.EnableControls(_isHost && chkBoxUseTeamStartMappings.Checked); @@ -112,11 +153,11 @@ private void RefreshTeamStartMappingPanels() { var teamStartMappingPanel = teamStartMappingPanels[i]; teamStartMappingPanel.ClearSelections(); - if (!IsUseTeamStartMappings()) + if (!IsUseTeamStartMappings) continue; - teamStartMappingPanel.EnableControls(_isHost && chkBoxUseTeamStartMappings.Checked && i < _map?.MaxPlayers); - RefreshTeamStartMappingPresets(_map?.TeamStartMappingPresets); + teamStartMappingPanel.EnableControls(_isHost && chkBoxUseTeamStartMappings.Checked && _gameModeMap != null && _gameModeMap.AllowedStartingLocations.Contains(i + 1)); + RefreshTeamStartMappingPresets(_gameModeMap?.Map?.TeamStartMappingPresets); } } @@ -189,17 +230,17 @@ public override void Initialize() chkBoxForceRandomColors.CheckedChanged += Options_Changed; AddChild(chkBoxForceRandomColors); - chkBoxForceRandomTeams = new XNAClientCheckBox(WindowManager); - chkBoxForceRandomTeams.Name = nameof(chkBoxForceRandomTeams); - chkBoxForceRandomTeams.Text = "Force Random Teams".L10N("Client:Main:ForceRandomTeams"); - chkBoxForceRandomTeams.ClientRectangle = new Rectangle(defaultX, chkBoxForceRandomColors.Bottom + 4, 0, 0); - chkBoxForceRandomTeams.CheckedChanged += Options_Changed; - AddChild(chkBoxForceRandomTeams); + chkBoxForceNoTeams = new XNAClientCheckBox(WindowManager); + chkBoxForceNoTeams.Name = nameof(chkBoxForceNoTeams); + chkBoxForceNoTeams.Text = "Force No Teams".L10N("Client:Main:ForceNoTeams"); + chkBoxForceNoTeams.ClientRectangle = new Rectangle(defaultX, chkBoxForceRandomColors.Bottom + 4, 0, 0); + chkBoxForceNoTeams.CheckedChanged += Options_Changed; + AddChild(chkBoxForceNoTeams); chkBoxForceRandomStarts = new XNAClientCheckBox(WindowManager); chkBoxForceRandomStarts.Name = nameof(chkBoxForceRandomStarts); chkBoxForceRandomStarts.Text = "Force Random Starts".L10N("Client:Main:ForceRandomStarts"); - chkBoxForceRandomStarts.ClientRectangle = new Rectangle(defaultX, chkBoxForceRandomTeams.Bottom + 4, 0, 0); + chkBoxForceRandomStarts.ClientRectangle = new Rectangle(defaultX, chkBoxForceNoTeams.Bottom + 4, 0, 0); chkBoxForceRandomStarts.CheckedChanged += Options_Changed; AddChild(chkBoxForceRandomStarts); @@ -252,17 +293,17 @@ private void BtnHelp_LeftClick(object sender, EventArgs args) "When players are assigned to spawn locations, they will be auto assigned to teams based on these mappings.\n" + "This is best used with random teams and random starts. However, only random teams is required.\n" + "Manually specified starts will take precedence.").L10N("Client:Main:AutoAllyingText1") + "\n\n" + - $"{TeamStartMapping.NO_TEAM} : " + "Block this location from being assigned to a player.".L10N("Client:Main:AutoAllyingTextNoTeam") + "\n" + - $"{TeamStartMapping.RANDOM_TEAM} : " + "Allow a player here, but don't assign a team.".L10N("Client:Main:AutoAllyingTextRandomTeam") + $"{TeamStartMapping.NO_PLAYER} : " + "Block this location from being randomly assigned to a player if there are spare locations.".L10N("Client:Main:AutoAllyingTextNoPlayerV2") + "\n" + + $"{TeamStartMapping.NO_TEAM} : " + "Allow a player here, but don't assign a team.".L10N("Client:Main:AutoAllyingTextNoTeamV2") ); } - public void UpdateForMap(Map map) + public void UpdateForGameModeMap(GameModeMap gameModeMap) { - if (_map == map) + if (_gameModeMap == gameModeMap) return; - _map = map; + _gameModeMap = gameModeMap; RefreshTeamStartMappingPanels(); } @@ -276,7 +317,7 @@ public void EnableControls(bool enable) chkBoxForceRandomSides.InputEnabled = enable; chkBoxForceRandomColors.InputEnabled = enable; chkBoxForceRandomStarts.InputEnabled = enable; - chkBoxForceRandomTeams.InputEnabled = enable; + chkBoxForceNoTeams.InputEnabled = enable; chkBoxUseTeamStartMappings.InputEnabled = enable; teamStartMappingsPanel.EnableControls(enable && chkBoxUseTeamStartMappings.Checked); @@ -285,11 +326,11 @@ public void EnableControls(bool enable) public PlayerExtraOptions GetPlayerExtraOptions() => new PlayerExtraOptions() { - IsForceRandomSides = IsForcedRandomSides(), - IsForceRandomColors = IsForcedRandomColors(), - IsForceRandomStarts = IsForcedRandomStarts(), - IsForceRandomTeams = IsForcedRandomTeams(), - IsUseTeamStartMappings = IsUseTeamStartMappings(), + IsForceRandomSides = IsForcedRandomSides, + IsForceRandomColors = IsForcedRandomColors, + IsForceRandomStarts = IsForcedRandomStarts, + IsForceNoTeams = IsForcedNoTeams, + IsUseTeamStartMappings = IsUseTeamStartMappings, TeamStartMappings = GetTeamStartMappings() }; @@ -297,7 +338,7 @@ public void SetPlayerExtraOptions(PlayerExtraOptions playerExtraOptions) { chkBoxForceRandomSides.Checked = playerExtraOptions.IsForceRandomSides; chkBoxForceRandomColors.Checked = playerExtraOptions.IsForceRandomColors; - chkBoxForceRandomTeams.Checked = playerExtraOptions.IsForceRandomTeams; + chkBoxForceNoTeams.Checked = playerExtraOptions.IsForceNoTeams; chkBoxForceRandomStarts.Checked = playerExtraOptions.IsForceRandomStarts; chkBoxUseTeamStartMappings.Checked = playerExtraOptions.IsUseTeamStartMappings; teamStartMappingsPanel.SetTeamStartMappings(playerExtraOptions.TeamStartMappings); diff --git a/DXMainClient/Domain/Multiplayer/CoopHouseInfo.cs b/DXMainClient/Domain/Multiplayer/CoopHouseInfo.cs index faeaa7bb2..e9e82adf3 100644 --- a/DXMainClient/Domain/Multiplayer/CoopHouseInfo.cs +++ b/DXMainClient/Domain/Multiplayer/CoopHouseInfo.cs @@ -1,4 +1,8 @@ -namespace DTAClient.Domain.Multiplayer +using Rampastring.Tools; +using System.Collections.Generic; +using System; + +namespace DTAClient.Domain.Multiplayer { /// /// Holds information about enemy houses in a co-op map. @@ -26,5 +30,26 @@ public CoopHouseInfo(int side, int color, int startingLocation) /// The starting location waypoint of the enemy house. /// public int StartingLocation; + + public static List GetGenericHouseInfoList(IniSection iniSection, string keyName) + { + var houseList = new List(); + + for (int i = 0; ; i++) + { + string[] houseInfo = iniSection.GetStringValue(keyName + i, string.Empty).Split( + new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (houseInfo.Length == 0) + break; + + int[] info = Conversions.IntArrayFromStringArray(houseInfo); + var chInfo = new CoopHouseInfo(info[0], info[1], info[2]); + + houseList.Add(new CoopHouseInfo(info[0], info[1], info[2])); + } + + return houseList; + } } } diff --git a/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs b/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs index 632d20982..42ebe1935 100644 --- a/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs +++ b/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs @@ -1,8 +1,9 @@ -using Rampastring.Tools; -using System; +#nullable enable using System.Collections.Generic; using System.Text.Json.Serialization; +using Rampastring.Tools; + namespace DTAClient.Domain.Multiplayer { public class CoopMapInfo @@ -19,31 +20,15 @@ public class CoopMapInfo [JsonInclude] public List DisallowedPlayerColors = new List(); - public void SetHouseInfos(IniSection iniSection) - { - EnemyHouses = GetGenericHouseInfo(iniSection, "EnemyHouse"); - AllyHouses = GetGenericHouseInfo(iniSection, "AllyHouse"); - } + public CoopMapInfo() { } - private List GetGenericHouseInfo(IniSection iniSection, string keyName) + public void Initialize(IniSection section) { - var houseList = new List(); - - for (int i = 0; ; i++) - { - string[] houseInfo = iniSection.GetStringValue(keyName + i, string.Empty).Split( - new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - if (houseInfo.Length == 0) - break; - - int[] info = Conversions.IntArrayFromStringArray(houseInfo); - var chInfo = new CoopHouseInfo(info[0], info[1], info[2]); - - houseList.Add(new CoopHouseInfo(info[0], info[1], info[2])); - } - - return houseList; + DisallowedPlayerSides = section.GetListValue("DisallowedPlayerSides", ',', int.Parse); + DisallowedPlayerColors = section.GetListValue("DisallowedPlayerColors", ',', int.Parse); + EnemyHouses = CoopHouseInfo.GetGenericHouseInfoList(section, "EnemyHouse"); + AllyHouses = CoopHouseInfo.GetGenericHouseInfoList(section, "AllyHouse"); } + } } diff --git a/DXMainClient/Domain/Multiplayer/GameMode.cs b/DXMainClient/Domain/Multiplayer/GameMode.cs index 30337921d..5713b12fc 100644 --- a/DXMainClient/Domain/Multiplayer/GameMode.cs +++ b/DXMainClient/Domain/Multiplayer/GameMode.cs @@ -1,15 +1,19 @@ using ClientCore; using ClientCore.Extensions; + using Rampastring.Tools; + using System; using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; namespace DTAClient.Domain.Multiplayer { /// /// A multiplayer game mode. /// - public class GameMode + public class GameMode : GameModeMapBase { public GameMode(string name) { @@ -35,26 +39,6 @@ public GameMode(string name) /// public string UntranslatedUIName { get; private set; } - /// - /// If set, this game mode cannot be played on Skirmish. - /// - public bool MultiplayerOnly { get; private set; } - - /// - /// If set, this game mode cannot be played with AI players. - /// - public bool HumanPlayersOnly { get; private set; } - - /// - /// If set, players are forced to random starting locations on this game mode. - /// - public bool ForceRandomStartLocations { get; private set; } - - /// - /// If set, players are forced to different teams on this game mode. - /// - public bool ForceNoTeams { get; private set; } - /// /// List of side indices players cannot select in this game mode. /// @@ -70,13 +54,17 @@ public GameMode(string name) /// public List DisallowedComputerPlayerSides = new List(); - /// /// Override for minimum amount of players needed to play any map in this game mode. + /// Priority sequences: GameMode.MinPlayersOverride, Map.MinPlayers, GameMode.MinPlayers. /// - public int MinPlayersOverride { get; private set; } = -1; + public int? MinPlayersOverride { get; private set; } + + public int? MaxPlayersOverride { get; private set; } private string mapCodeININame; + private List randomizedMapCodeININames; + private int randomizedMapCodesCount; private string forcedOptionsSection; @@ -87,39 +75,27 @@ public GameMode(string name) private List> ForcedSpawnIniOptions = new List>(); - public int CoopDifficultyLevel { get; set; } - public void Initialize() { IniFile forcedOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath)); + IniSection section = forcedOptionsIni.GetSection(Name); - CoopDifficultyLevel = forcedOptionsIni.GetIntValue(Name, "CoopDifficultyLevel", 0); - UntranslatedUIName = forcedOptionsIni.GetStringValue(Name, "UIName", Name); + UntranslatedUIName = section.GetStringValue("UIName", Name); UIName = UntranslatedUIName.L10N($"INI:GameModes:{Name}:UIName"); - MultiplayerOnly = forcedOptionsIni.GetBooleanValue(Name, "MultiplayerOnly", false); - HumanPlayersOnly = forcedOptionsIni.GetBooleanValue(Name, "HumanPlayersOnly", false); - ForceRandomStartLocations = forcedOptionsIni.GetBooleanValue(Name, "ForceRandomStartLocations", false); - ForceNoTeams = forcedOptionsIni.GetBooleanValue(Name, "ForceNoTeams", false); - MinPlayersOverride = forcedOptionsIni.GetIntValue(Name, "MinPlayersOverride", -1); - forcedOptionsSection = forcedOptionsIni.GetStringValue(Name, "ForcedOptions", string.Empty); - mapCodeININame = forcedOptionsIni.GetStringValue(Name, "MapCodeININame", Name + ".ini"); - - string[] disallowedSides = forcedOptionsIni.GetStringListValue(Name, "DisallowedPlayerSides", string.Empty); - foreach (string sideIndex in disallowedSides) - DisallowedPlayerSides.Add(int.Parse(sideIndex)); + InitializeBaseSettingsFromIniSection(forcedOptionsIni.GetSection(Name), isCustomMap: false); - disallowedSides = forcedOptionsIni - .GetStringListValue(Name, "DisallowedHumanPlayerSides", string.Empty); + MinPlayersOverride = section.GetIntValueOrNull("MinPlayersOverride"); + MaxPlayersOverride = section.GetIntValueOrNull("MaxPlayersOverride"); - foreach (string sideIndex in disallowedSides) - DisallowedHumanPlayerSides.Add(int.Parse(sideIndex)); + forcedOptionsSection = section.GetStringValue("ForcedOptions", string.Empty); + mapCodeININame = section.GetStringValue("MapCodeININame", Name + ".ini"); + randomizedMapCodeININames = section.GetStringValue("RandomizedMapCodeININames", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); + randomizedMapCodesCount = section.GetIntValue("RandomizedMapCodesCount", 1); - disallowedSides = forcedOptionsIni - .GetStringListValue(Name, "DisallowedComputerPlayerSides", string.Empty); - - foreach (string sideIndex in disallowedSides) - DisallowedComputerPlayerSides.Add(int.Parse(sideIndex)); + DisallowedPlayerSides = section.GetListValue("DisallowedPlayerSides", ',', int.Parse); + DisallowedHumanPlayerSides = section.GetListValue("DisallowedHumanPlayerSides", ',', int.Parse); + DisallowedComputerPlayerSides = section.GetListValue("DisallowedComputerPlayerSides", ',', int.Parse); ParseForcedOptions(forcedOptionsIni); @@ -174,9 +150,23 @@ public void ApplySpawnIniCode(IniFile spawnIni) spawnIni.SetStringValue("Settings", key.Key, key.Value); } - public IniFile GetMapRulesIniFile() + public List GetMapRulesIniFiles(Random pseudoRandom) { - return new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, mapCodeININame)); + var mapRules = new List() { new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, mapCodeININame)) }; + if (randomizedMapCodeININames.Count == 0) + return mapRules; + + Dictionary randomOrder = new(); + foreach (string name in randomizedMapCodeININames) + { + randomOrder[name] = pseudoRandom.Next(); + } + + mapRules.AddRange( + from iniName in randomizedMapCodeININames.OrderBy(x => randomOrder[x]).Take(randomizedMapCodesCount) + select new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, iniName))); + + return mapRules; } protected bool Equals(GameMode other) => string.Equals(Name, other?.Name, StringComparison.InvariantCultureIgnoreCase); diff --git a/DXMainClient/Domain/Multiplayer/GameModeMap.cs b/DXMainClient/Domain/Multiplayer/GameModeMap.cs index b28d2e791..88b8d8de3 100644 --- a/DXMainClient/Domain/Multiplayer/GameModeMap.cs +++ b/DXMainClient/Domain/Multiplayer/GameModeMap.cs @@ -1,14 +1,31 @@ -namespace DTAClient.Domain.Multiplayer +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using ClientCore.Extensions; + +namespace DTAClient.Domain.Multiplayer { /// /// An instance of a Map in a given GameMode /// - public class GameModeMap + public record GameModeMap : IGameModeMap { - public GameMode GameMode { get; } - public Map Map { get; } - public bool IsFavorite { get; set; } + public required GameMode GameMode { get; init; } + public required Map Map { get; init; } + public bool IsFavorite { get; set; } = false; + public GameModeMap() { } + [SetsRequiredMembers] + public GameModeMap(GameMode gameMode, Map map) + { + GameMode = gameMode; + Map = map; + } + + [SetsRequiredMembers] public GameModeMap(GameMode gameMode, Map map, bool isFavorite) { GameMode = gameMode; @@ -16,17 +33,56 @@ public GameModeMap(GameMode gameMode, Map map, bool isFavorite) IsFavorite = isFavorite; } - protected bool Equals(GameModeMap other) => Equals(GameMode, other.GameMode) && Equals(Map, other.Map); + public string ToUntranslatedUIString() => $"{Map.UntranslatedName} - {GameMode.UntranslatedUIName}"; + + public string ToUIString() => $"{Map.Name} - {GameMode.UIName}"; + + public override string ToString() => ToUIString(); - public override int GetHashCode() + public List AllowedStartingLocations { - unchecked + get { - var hashCode = (GameMode != null ? GameMode.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Map != null ? Map.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ IsFavorite.GetHashCode(); - return hashCode; + var ret = Map.AllowedStartingLocations ?? GameMode.AllowedStartingLocations ?? Enumerable.Range(1, MaxPlayers).ToList(); + + if (ret.Count != MaxPlayers) + throw new Exception(string.Format("The number of AllowedStartingLocations does not equal to MaxPlayer.".L10N("Client:Main:InvalidAllowedStartingLocationsCount"))); + + return ret; } } + + public int CoopDifficultyLevel => + Map.CoopDifficultyLevel ?? GameMode.CoopDifficultyLevel ?? 0; + + public CoopMapInfo? CoopInfo => + Map.CoopInfo ?? GameMode.CoopInfo ?? null; + + public bool EnforceMaxPlayers => + Map.EnforceMaxPlayers ?? GameMode.EnforceMaxPlayers ?? false; + + public bool ForceNoTeams => + Map.ForceNoTeams ?? GameMode.ForceNoTeams ?? false; + + public bool ForceRandomStartLocations => + Map.ForceRandomStartLocations ?? GameMode.ForceRandomStartLocations ?? false; + + public bool HumanPlayersOnly => + Map.HumanPlayersOnly ?? GameMode.HumanPlayersOnly ?? false; + + public bool IsCoop => + Map.IsCoop ?? GameMode.IsCoop ?? false; + + public int MaxPlayers => + // Note: GameLobbyBase.GetMapList() assumes the priority. + // If you have modified the expression here, you should also update GameLobbyBase.GetMapList(). + GameMode.MaxPlayersOverride ?? Map.MaxPlayers ?? GameMode.MaxPlayers ?? 0; + + public int MinPlayers => + GameMode.MinPlayersOverride ?? Map.MinPlayers ?? GameMode.MinPlayers ?? 0; + + public bool MultiplayerOnly => + Map.MultiplayerOnly ?? GameMode.MultiplayerOnly ?? false; + } } diff --git a/DXMainClient/Domain/Multiplayer/GameModeMapBase.cs b/DXMainClient/Domain/Multiplayer/GameModeMapBase.cs new file mode 100644 index 000000000..7c1b812ba --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/GameModeMapBase.cs @@ -0,0 +1,138 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; + +using ClientCore; +using ClientCore.Extensions; + +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer +{ + public abstract class GameModeMapBase + { + public const int MAX_PLAYERS = 8; + + /// + /// The maximum amount of players supported by the map or a game mode (such as a 2v2 mode). + /// + [JsonInclude] + public int? MaxPlayers { get; private set; } + + /// + /// The minimum amount of players supported by the map or a game mode. + /// + [JsonInclude] + public int? MinPlayers { get; private set; } + + /// + /// Whether to use MaxPlayers for limiting the player count of the map or a game mode. + /// If false (which is the default), MaxPlayers is only used for randomizing + /// players to starting waypoints. + /// + [JsonInclude] + public bool? EnforceMaxPlayers { get; private set; } + + /// + /// The allowed starting locations for this map or game mode. + /// + [JsonInclude] + public List? AllowedStartingLocations { get; private set; } + + /// + /// Controls if the map is meant for a co-operation game mode + /// (enables briefing logic and forcing options, among others). + /// + [JsonInclude] + public bool? IsCoop { get; private set; } + + /// + /// Contains co-op information. + /// + [JsonInclude] + public CoopMapInfo? CoopInfo { get; private set; } + + [JsonInclude] + public int? CoopDifficultyLevel { get; set; } + + /// + /// If set, this map cannot be played on Skirmish. + /// + [JsonInclude] + public bool? MultiplayerOnly { get; private set; } + + /// + /// If set, this map cannot be played with AI players. + /// + [JsonInclude] + public bool? HumanPlayersOnly { get; private set; } + + /// + /// If set, players are forced to random starting locations on this map. + /// + [JsonInclude] + public bool? ForceRandomStartLocations { get; private set; } + + /// + /// If set, players are forced to different teams on this map. + /// + [JsonInclude] + public bool? ForceNoTeams { get; private set; } + + protected void InitializeBaseSettingsFromIniSection(IniSection section, bool isCustomMap) + { + // MinPlayers + MinPlayers = section.GetIntValueOrNull(isCustomMap ? "MinPlayer" : "MinPlayers"); + + // MaxPlayers + if (isCustomMap) + MaxPlayers = section.GetIntValueOrNull("ClientMaxPlayer") ?? section.GetIntValueOrNull("MaxPlayer"); + else + MaxPlayers = section.GetIntValueOrNull("MaxPlayers"); + + // EnforceMaxPlayers + EnforceMaxPlayers = section.GetBooleanValueOrNull("EnforceMaxPlayers"); + + // AllowedStartingLocations + List? rawAllowedStartingLocations = section.GetListValueOrNull("AllowedStartingLocations", ',', int.Parse); + + if (rawAllowedStartingLocations != null && rawAllowedStartingLocations.Count > 0) + { + // In configuration files, the number starts from 0. While in the code, the number starts from 1. + AllowedStartingLocations = rawAllowedStartingLocations.Select(x => x + 1).Distinct().OrderBy(x => x).ToList(); + + if (AllowedStartingLocations.Max() > MAX_PLAYERS || AllowedStartingLocations.Min() <= 0) + throw new Exception(string.Format("Invalid AllowedStartingLocations {0}".L10N("Client:Main:InvalidAllowedStartingLocations"), string.Join(", ", rawAllowedStartingLocations))); + } + + // IsCoop + IsCoop = section.GetBooleanValueOrNull("IsCoopMission"); + + // CoopInfo + if (IsCoop ?? false) + { + CoopInfo = new CoopMapInfo(); + CoopInfo.Initialize(section); + } + + // MultiplayerOnly + MultiplayerOnly = section.GetBooleanValueOrNull(isCustomMap ? "ClientMultiplayerOnly" : "MultiplayerOnly"); + + // HumanPlayersOnly + HumanPlayersOnly = section.GetBooleanValueOrNull("HumanPlayersOnly"); + + // ForceRandomStartLocations + ForceRandomStartLocations = section.GetBooleanValueOrNull("ForceRandomStartLocations"); + + // ForceNoTeams + ForceNoTeams = section.GetBooleanValueOrNull("ForceNoTeams"); + + // CoopDifficultyLevel + CoopDifficultyLevel = section.GetIntValueOrNull("CoopDifficultyLevel"); + } + + } +} diff --git a/DXMainClient/Domain/Multiplayer/IGameModeMap.cs b/DXMainClient/Domain/Multiplayer/IGameModeMap.cs new file mode 100644 index 000000000..3bf5e5469 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/IGameModeMap.cs @@ -0,0 +1,20 @@ +#nullable enable +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer +{ + public interface IGameModeMap + { + List AllowedStartingLocations { get; } + int CoopDifficultyLevel { get; } + CoopMapInfo? CoopInfo { get; } + bool EnforceMaxPlayers { get; } + bool ForceNoTeams { get; } + bool ForceRandomStartLocations { get; } + bool HumanPlayersOnly { get; } + bool IsCoop { get; } + int MaxPlayers { get; } + int MinPlayers { get; } + bool MultiplayerOnly { get; } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index 6f58a783f..39b00ae02 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -12,8 +12,6 @@ using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Point = Microsoft.Xna.Framework.Point; -using Utilities = Rampastring.Tools.Utilities; -using static System.Collections.Specialized.BitVector32; using System.Diagnostics; using System.Text; using ClientCore.PlatformShim; @@ -40,10 +38,8 @@ public ExtraMapPreviewTexture(string textureName, Point point, int level, bool t /// /// A multiplayer map. /// - public class Map + public class Map : GameModeMapBase { - private const int MAX_PLAYERS = 8; - [JsonConstructor] public Map(string baseFilePath) : this(baseFilePath, true) @@ -72,33 +68,6 @@ public Map(string baseFilePath, bool isCustomMap) /// public string UntranslatedName { get; private set; } - /// - /// The maximum amount of players supported by the map. - /// - [JsonInclude] - public int MaxPlayers { get; private set; } - - /// - /// The minimum amount of players supported by the map. - /// - [JsonInclude] - public int MinPlayers { get; private set; } - - /// - /// Whether to use MaxPlayers for limiting the player count of the map. - /// If false (which is the default), MaxPlayers is only used for randomizing - /// players to starting waypoints. - /// - [JsonInclude] - public bool EnforceMaxPlayers { get; private set; } - - /// - /// Controls if the map is meant for a co-operation game mode - /// (enables briefing logic and forcing options, among others). - /// - [JsonInclude] - public bool IsCoop { get; private set; } - /// /// If set, this map won't be automatically transferred over CnCNet when /// a player doesn't have it. @@ -106,12 +75,6 @@ public Map(string baseFilePath, bool isCustomMap) [JsonIgnore] public bool Official { get; private set; } - /// - /// Contains co-op information. - /// - [JsonInclude] - public CoopMapInfo CoopInfo { get; private set; } - /// /// The briefing of the map. /// @@ -125,10 +88,10 @@ public Map(string baseFilePath, bool isCustomMap) public string Author { get; private set; } /// - /// The calculated SHA1 of the map. + /// The calculated SHA1 hash of the map. /// [JsonIgnore] - public string SHA1 { get; private set; } + public string SHA1 { get; private set; } = null; /// /// The path to the map file. @@ -149,37 +112,6 @@ public Map(string baseFilePath, bool isCustomMap) [JsonInclude] public string PreviewPath { get; private set; } - /// - /// If set, this map cannot be played on Skirmish. - /// - [JsonInclude] - public bool MultiplayerOnly { get; private set; } - - /// - /// If set, this map cannot be played with AI players. - /// - [JsonInclude] - public bool HumanPlayersOnly { get; private set; } - - /// - /// If set, players are forced to random starting locations on this map. - /// - [JsonInclude] - public bool ForceRandomStartLocations { get; private set; } - - /// - /// If set, players are forced to different teams on this map. - /// - [JsonInclude] - public bool ForceNoTeams { get; private set; } - - /// - /// The name of an extra INI file in INI\Map Code\ that should be - /// embedded into this map's INI code when a game is started. - /// - [JsonInclude] - public string ExtraININame { get; private set; } - /// /// The game modes that the map is listed for. /// @@ -274,12 +206,19 @@ public void CalculateSHA() [JsonIgnore] private List> ForcedSpawnIniOptions = new List>(0); + /// + /// The name of an extra INI file in INI\Map Code\ that should be + /// embedded into this map's INI code when a game is started. + /// + [JsonInclude] + public string ExtraININame { get; private set; } + /// /// This is used to load a map from the MPMaps.ini (default name) file. /// /// The configuration file for the multiplayer maps. /// True if loading the map succeeded, otherwise false. - public bool SetInfoFromMpMapsINI(IniFile iniFile) + public bool InitializeFromMpMapsINI(IniFile iniFile) { try { @@ -297,10 +236,6 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) Author = section.GetStringValue("Author", "Unknown author"); GameModes = section.GetStringValue("GameModes", "Default").Split(','); - MinPlayers = section.GetIntValue("MinPlayers", 0); - MaxPlayers = section.GetIntValue("MaxPlayers", 0); - EnforceMaxPlayers = section.GetBooleanValue("EnforceMaxPlayers", false); - FileInfo mapFile = SafePath.GetFile(BaseFilePath); PreviewPath = SafePath.CombineFilePath(SafePath.GetDirectory(mapFile.FullName).Parent.FullName[ProgramConstants.GamePath.Length..], FormattableString.Invariant($"{section.GetStringValue("PreviewImage", mapFile.Name)}.png")); @@ -309,16 +244,14 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) .L10N($"INI:Maps:{BaseFilePath}:Briefing"); CalculateSHA(); - IsCoop = section.GetBooleanValue("IsCoopMission", false); + + InitializeBaseSettingsFromIniSection(section, isCustomMap: false); + Credits = section.GetIntValue("Credits", -1); UnitCount = section.GetIntValue("UnitCount", -1); NeutralHouseColor = section.GetIntValue("NeutralColor", -1); SpecialHouseColor = section.GetIntValue("SpecialColor", -1); - MultiplayerOnly = section.GetBooleanValue("MultiplayerOnly", false); - HumanPlayersOnly = section.GetBooleanValue("HumanPlayersOnly", false); - ForceRandomStartLocations = section.GetBooleanValue("ForceRandomStartLocations", false); - ForceNoTeams = section.GetBooleanValue("ForceNoTeams", false); - ExtraININame = section.GetStringValue("ExtraININame", string.Empty); + string bases = section.GetStringValue("Bases", string.Empty); if (!string.IsNullOrEmpty(bases)) { @@ -362,24 +295,6 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) i++; } - if (IsCoop) - { - CoopInfo = new CoopMapInfo(); - string[] disallowedSides = section.GetStringValue("DisallowedPlayerSides", string.Empty).Split( - new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (string sideIndex in disallowedSides) - CoopInfo.DisallowedPlayerSides.Add(int.Parse(sideIndex, CultureInfo.InvariantCulture)); - - string[] disallowedColors = section.GetStringValue("DisallowedPlayerColors", string.Empty).Split( - new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (string colorIndex in disallowedColors) - CoopInfo.DisallowedPlayerColors.Add(int.Parse(colorIndex, CultureInfo.InvariantCulture)); - - CoopInfo.SetHouseInfos(section); - } - if (MainClientConstants.USE_ISOMETRIC_CELLS) { localSize = section.GetStringValue("LocalSize", "0,0,0,0").Split(','); @@ -431,6 +346,8 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) ParseSpawnIniOptions(iniFile, fsioSection); } + ExtraININame = section.GetStringValueOrNull("ExtraININame"); + return true; } catch (Exception ex) @@ -524,7 +441,7 @@ private IniFile GetCustomMapIniFile() /// Loads map information from a TS/RA2 map INI file. /// Returns true if successful, otherwise false. /// - public bool SetInfoFromCustomMap() + public bool InitializeFromCustomMap() { if (!File.Exists(customMapFilePath)) return false; @@ -558,25 +475,19 @@ public bool SetInfoFromCustomMap() GameModes[i] = gameMode.Substring(0, 1).ToUpperInvariant() + gameMode.Substring(1); } - MinPlayers = 0; - if (basicSection.KeyExists("ClientMaxPlayer")) - MaxPlayers = basicSection.GetIntValue("ClientMaxPlayer", 0); - else - MaxPlayers = basicSection.GetIntValue("MaxPlayer", 0); - EnforceMaxPlayers = basicSection.GetBooleanValue("EnforceMaxPlayers", true); Briefing = basicSection.GetStringValue("Briefing", string.Empty) .FromIniString(); + CalculateSHA(); - IsCoop = basicSection.GetBooleanValue("IsCoopMission", false); + + InitializeBaseSettingsFromIniSection(basicSection, isCustomMap: true); + Credits = basicSection.GetIntValue("Credits", -1); UnitCount = basicSection.GetIntValue("UnitCount", -1); NeutralHouseColor = basicSection.GetIntValue("NeutralColor", -1); SpecialHouseColor = basicSection.GetIntValue("SpecialColor", -1); - HumanPlayersOnly = basicSection.GetBooleanValue("HumanPlayersOnly", false); - ForceRandomStartLocations = basicSection.GetBooleanValue("ForceRandomStartLocations", false); - ForceNoTeams = basicSection.GetBooleanValue("ForceNoTeams", false); + PreviewPath = Path.ChangeExtension(customMapFilePath[ProgramConstants.GamePath.Length..], ".png"); - MultiplayerOnly = basicSection.GetBooleanValue("ClientMultiplayerOnly", false); string bases = basicSection.GetStringValue("Bases", string.Empty); if (!string.IsNullOrEmpty(bases)) @@ -584,22 +495,6 @@ public bool SetInfoFromCustomMap() Bases = Convert.ToInt32(Conversions.BooleanFromString(bases, false)); } - if (IsCoop) - { - CoopInfo = new CoopMapInfo(); - string[] disallowedSides = iniFile.GetStringListValue("Basic", "DisallowedPlayerSides", string.Empty); - - foreach (string sideIndex in disallowedSides) - CoopInfo.DisallowedPlayerSides.Add(int.Parse(sideIndex, CultureInfo.InvariantCulture)); - - string[] disallowedColors = iniFile.GetStringListValue("Basic", "DisallowedPlayerColors", string.Empty); - - foreach (string colorIndex in disallowedColors) - CoopInfo.DisallowedPlayerColors.Add(int.Parse(colorIndex, CultureInfo.InvariantCulture)); - - CoopInfo.SetHouseInfos(basicSection); - } - localSize = iniFile.GetStringValue("Map", "LocalSize", "0,0,0,0").Split(','); actualSize = iniFile.GetStringValue("Map", "Size", "0,0,0,0").Split(','); @@ -631,6 +526,8 @@ public bool SetInfoFromCustomMap() ParseForcedOptions(iniFile, "ForcedOptions"); ParseSpawnIniOptions(iniFile, "ForcedSpawnIniOptions"); + ExtraININame = basicSection.GetStringValueOrNull("ExtraININame"); + return true; } catch @@ -728,7 +625,7 @@ public IniFile GetMapIni() } public void ApplySpawnIniCode(IniFile spawnIni, int totalPlayerCount, - int aiPlayerCount, int coopDifficultyLevel) + int aiPlayerCount, bool isCoop, CoopMapInfo coopInfo, int coopDifficultyLevel, Random pseudoRandom, int sideCount) { foreach (KeyValuePair key in ForcedSpawnIniOptions) spawnIni.SetStringValue("Settings", key.Key, key.Value); @@ -742,16 +639,18 @@ public void ApplySpawnIniCode(IniFile spawnIni, int totalPlayerCount, int neutralHouseIndex = totalPlayerCount + 1; int specialHouseIndex = totalPlayerCount + 2; - if (IsCoop) + if (isCoop) { - var allyHouses = CoopInfo.AllyHouses; - var enemyHouses = CoopInfo.EnemyHouses; + int NextRandomSide() => pseudoRandom.Next(0, sideCount); + + var allyHouses = coopInfo.AllyHouses; + var enemyHouses = coopInfo.EnemyHouses; int multiId = totalPlayerCount + 1; foreach (var houseInfo in allyHouses.Concat(enemyHouses)) { spawnIni.SetIntValue("HouseHandicaps", "Multi" + multiId, coopDifficultyLevel); - spawnIni.SetIntValue("HouseCountries", "Multi" + multiId, houseInfo.Side); + spawnIni.SetIntValue("HouseCountries", "Multi" + multiId, houseInfo.Side == -1 ? NextRandomSide() : houseInfo.Side); spawnIni.SetIntValue("HouseColors", "Multi" + multiId, houseInfo.Color); spawnIni.SetIntValue("SpawnLocations", "Multi" + multiId, houseInfo.StartingLocation); @@ -922,7 +821,11 @@ private static Point GetIsoTilePixelCoord(int isoTileX, int isoTileY, string[] a return new Point(pixelX, pixelY); } - protected bool Equals(Map other) => string.Equals(SHA1, other?.SHA1, StringComparison.InvariantCultureIgnoreCase); + protected bool Equals(Map other) + { + Debug.Assert(other?.SHA1 != null || SHA1 != null); + return string.Equals(SHA1, other?.SHA1, StringComparison.InvariantCultureIgnoreCase); + } public override int GetHashCode() => SHA1 != null ? SHA1.GetHashCode() : 0; } diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 9c2679d8c..a34d544bf 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -21,7 +21,15 @@ public class MapLoader private const string MultiMapsSection = "MultiMaps"; private const string GameModesSection = "GameModes"; private const string GameModeAliasesSection = "GameModeAliases"; - private const int CurrentCustomMapCacheVersion = 1; + + /// + /// Version identifier for the cache. + /// Increment this version number to invalidate cached data. You should do this if: + /// (a) Map class gains new members, or + /// (b) Map parsing logic changes in ways that could produce different results + /// + private const int CurrentCustomMapCacheVersion = 2; + private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { IncludeFields = true }; /// @@ -110,7 +118,7 @@ private void LoadMultiMaps(IniFile mpMapsIni) var map = new Map(mapFilePathValue, false); - if (!map.SetInfoFromMpMapsINI(mpMapsIni)) + if (!map.InitializeFromMpMapsINI(mpMapsIni)) continue; maps.Add(map); @@ -183,7 +191,7 @@ private void LoadCustomMaps() .Replace(Path.AltDirectorySeparatorChar, '/'), true); map.CalculateSHA(); localMapSHAs.Add(map.SHA1); - if (!customMapCache.ContainsKey(map.SHA1) && map.SetInfoFromCustomMap()) + if (!customMapCache.ContainsKey(map.SHA1) && map.InitializeFromCustomMap()) customMapCache.TryAdd(map.SHA1, map); })); } @@ -272,7 +280,7 @@ public Map LoadCustomMap(string mapPath, out string resultMessage) var map = new Map(mapPath, true); - if (map.SetInfoFromCustomMap()) + if (map.InitializeFromCustomMap()) { foreach (GameMode gm in GameModes) { diff --git a/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs b/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs index e3a34d210..005f0dbf5 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs @@ -20,7 +20,7 @@ public class PlayerExtraOptions public bool IsForceRandomSides { get; set; } public bool IsForceRandomColors { get; set; } - public bool IsForceRandomTeams { get; set; } + public bool IsForceNoTeams { get; set; } public bool IsForceRandomStarts { get; set; } public bool IsUseTeamStartMappings { get; set; } public List TeamStartMappings { get; set; } = new List(); @@ -50,7 +50,7 @@ public override string ToString() var stringBuilder = new StringBuilder(); stringBuilder.Append(IsForceRandomSides ? "1" : "0"); stringBuilder.Append(IsForceRandomColors ? "1" : "0"); - stringBuilder.Append(IsForceRandomTeams ? "1" : "0"); + stringBuilder.Append(IsForceNoTeams ? "1" : "0"); stringBuilder.Append(IsForceRandomStarts ? "1" : "0"); stringBuilder.Append(IsUseTeamStartMappings ? "1" : "0"); stringBuilder.Append(MESSAGE_SEPARATOR); @@ -73,7 +73,7 @@ public static PlayerExtraOptions FromMessage(string message) { IsForceRandomSides = boolParts[0] == '1', IsForceRandomColors = boolParts[1] == '1', - IsForceRandomTeams = boolParts[2] == '1', + IsForceNoTeams = boolParts[2] == '1', IsForceRandomStarts = boolParts[3] == '1', IsUseTeamStartMappings = boolParts[4] == '1', TeamStartMappings = TeamStartMapping.FromListString(parts[1]) @@ -85,7 +85,7 @@ public bool IsDefault() var defaultPLayerExtraOptions = new PlayerExtraOptions(); return IsForceRandomColors == defaultPLayerExtraOptions.IsForceRandomColors && IsForceRandomStarts == defaultPLayerExtraOptions.IsForceRandomStarts && - IsForceRandomTeams == defaultPLayerExtraOptions.IsForceRandomTeams && + IsForceNoTeams == defaultPLayerExtraOptions.IsForceNoTeams && IsForceRandomSides == defaultPLayerExtraOptions.IsForceRandomSides && IsUseTeamStartMappings == defaultPLayerExtraOptions.IsUseTeamStartMappings; } diff --git a/DXMainClient/Domain/Multiplayer/TeamStartMapping.cs b/DXMainClient/Domain/Multiplayer/TeamStartMapping.cs index 47e585a36..b0c9cb280 100644 --- a/DXMainClient/Domain/Multiplayer/TeamStartMapping.cs +++ b/DXMainClient/Domain/Multiplayer/TeamStartMapping.cs @@ -9,9 +9,9 @@ public class TeamStartMapping { private const char LIST_SEPARATOR = ','; - public const string NO_TEAM = "x"; - public const string RANDOM_TEAM = "-"; - public static readonly List TEAMS = new List() { NO_TEAM, RANDOM_TEAM }.Concat(ProgramConstants.TEAMS).ToList(); + public const string NO_PLAYER = "x"; + public const string NO_TEAM = "-"; + public static readonly List TEAMS = new List() { NO_PLAYER, NO_TEAM }.Concat(ProgramConstants.TEAMS).ToList(); [JsonInclude] [JsonPropertyName("t")] @@ -34,7 +34,7 @@ public class TeamStartMapping public int StartingWaypoint => Start - 1; [JsonIgnore] - public bool IsBlock => Team == NO_TEAM; + public bool IsBlock => Team == NO_PLAYER; /// /// Write these out in a delimited list.