From 053f634dd7a8eb416b3a720d1fe15513042a8387 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 16 May 2025 21:41:17 +0800 Subject: [PATCH 01/26] Rework partial GameModeMap and GameLobby codes --- .../Extensions/RampastringToolsExtensions.cs | 25 +++ .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 8 +- .../Multiplayer/GameLobby/GameLobbyBase.cs | 188 ++++++++++++------ .../Multiplayer/GameLobby/LANGameLobby.cs | 8 +- .../Multiplayer/GameLobby/MapPreviewBox.cs | 33 +-- .../GameLobby/MultiplayerGameLobby.cs | 35 ++-- .../Multiplayer/GameLobby/SkirmishLobby.cs | 43 ++-- .../Multiplayer/PlayerExtraOptionsPanel.cs | 109 ++++++---- .../Domain/Multiplayer/CoopHouseInfo.cs | 27 ++- .../Domain/Multiplayer/CoopMapInfo.cs | 35 +--- DXMainClient/Domain/Multiplayer/GameMode.cs | 92 ++++----- .../Domain/Multiplayer/GameModeMap.cs | 80 ++++++-- .../Domain/Multiplayer/GameModeMapBase.cs | 138 +++++++++++++ .../Domain/Multiplayer/IGameModeMap.cs | 20 ++ DXMainClient/Domain/Multiplayer/Map.cs | 173 ++++------------ DXMainClient/Domain/Multiplayer/MapLoader.cs | 6 +- .../Domain/Multiplayer/PlayerExtraOptions.cs | 8 +- .../Domain/Multiplayer/TeamStartMapping.cs | 8 +- 18 files changed, 633 insertions(+), 403 deletions(-) create mode 100644 ClientCore/Extensions/RampastringToolsExtensions.cs create mode 100644 DXMainClient/Domain/Multiplayer/GameModeMapBase.cs create mode 100644 DXMainClient/Domain/Multiplayer/IGameModeMap.cs diff --git a/ClientCore/Extensions/RampastringToolsExtensions.cs b/ClientCore/Extensions/RampastringToolsExtensions.cs new file mode 100644 index 000000000..5e518b041 --- /dev/null +++ b/ClientCore/Extensions/RampastringToolsExtensions.cs @@ -0,0 +1,25 @@ +#nullable enable +using System; +using System.Collections.Generic; + +using Rampastring.Tools; + +namespace ClientCore.Extensions +{ + public static class RampastringToolsExtensions + { + 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 cb6a09b65..b8437e210 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -756,16 +756,16 @@ private void HandleOptionsRequest(string playerName, int options) 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/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 8e25487b2..ed16cfcee 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 { @@ -479,19 +480,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")); } @@ -559,10 +560,10 @@ protected void ListMaps() } 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]; } @@ -576,7 +577,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; @@ -654,7 +655,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(); } @@ -758,12 +759,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); } @@ -966,19 +979,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(); } @@ -987,8 +1018,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) @@ -1183,7 +1212,7 @@ protected void CheckDisallowedSidesForGroup(bool forHumanPlayers) } } - if (Map != null && Map.CoopInfo != null) + if (GameModeMap != null && GameModeMap.CoopInfo != null) { // Disallow spectator @@ -1217,8 +1246,8 @@ protected void CheckDisallowedSidesForGroup(bool forHumanPlayers) /// protected void CheckDisallowedSides() { - CheckDisallowedSidesForGroup(forHumanPlayers:false); - CheckDisallowedSidesForGroup(forHumanPlayers:true); + CheckDisallowedSidesForGroup(forHumanPlayers: false); + CheckDisallowedSidesForGroup(forHumanPlayers: true); } /// @@ -1246,11 +1275,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; } @@ -1271,7 +1300,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 random) { int totalPlayerCount = Players.Count + AIPlayers.Count; PlayerHouseInfo[] houseInfos = new PlayerHouseInfo[totalPlayerCount]; @@ -1290,9 +1319,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); } @@ -1307,8 +1336,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++) { @@ -1330,8 +1359,6 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa // Randomize options - Random random = new Random(RandomSeed); - for (int i = 0; i < totalPlayerCount; i++) { PlayerInfo pInfo; @@ -1341,12 +1368,12 @@ 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, random, disallowedSides, RandomSelectors, RandomSelectorCount); @@ -1361,7 +1388,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 random) { Logger.Log("Writing spawn.ini"); @@ -1369,13 +1396,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); @@ -1384,7 +1417,7 @@ private PlayerHouseInfo[] WriteSpawnIni() teamStartMappings = PlayerExtraOptionsPanel.GetTeamStartMappings(); } - PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings); + PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings, random); IniFile spawnIni = new IniFile(spawnerSettingsFile.FullName); @@ -1435,7 +1468,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, random, SideCount); // Forced options from the map // Player options @@ -1584,7 +1617,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) @@ -1621,7 +1654,7 @@ private void InitializeMatchStatistics(PlayerHouseInfo[] houseInfos) /// /// Writes spawnmap.ini. /// - private void WriteMap(PlayerHouseInfo[] houseInfos) + private void WriteMap(PlayerHouseInfo[] houseInfos, Random random) { FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); @@ -1636,7 +1669,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(random)) + { + MapCodeHelper.ApplyMapCode(mapIni, iniFile); + } + MapCodeHelper.ApplyMapCode(mapIni, globalCodeIni); if (isMultiplayer) @@ -1703,10 +1740,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)); } @@ -1755,7 +1792,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 @@ -1861,9 +1898,11 @@ private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] house /// protected virtual void StartGame() { - PlayerHouseInfo[] houseInfos = WriteSpawnIni(); + Random random = new Random(RandomSeed); + + PlayerHouseInfo[] houseInfos = WriteSpawnIni(random); InitializeMatchStatistics(houseInfos); - WriteMap(houseInfos); + WriteMap(houseInfos, random); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; @@ -1953,7 +1992,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 }; @@ -2040,8 +2079,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; } } @@ -2074,8 +2113,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; } } @@ -2230,13 +2269,19 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) ddStart.AddItem("???"); - for (int i = 1; i <= Map.MaxPlayers; i++) - ddStart.AddItem(i.ToString()); + int maxLocation = 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) @@ -2252,18 +2297,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; @@ -2281,6 +2326,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(); @@ -2290,7 +2352,7 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) disableGameOptionUpdateBroadcast = false; - PlayerExtraOptionsPanel?.UpdateForMap(Map); + PlayerExtraOptionsPanel?.UpdateForGameModeMap(GameModeMap); } private void ApplyForcedCheckBoxOptions(List optionList, @@ -2390,14 +2452,14 @@ protected int 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; @@ -2456,7 +2518,7 @@ protected int 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 574ed73ba..1c6b5382c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -841,16 +841,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 6e25c36a1..19efddd0e 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 { @@ -799,7 +800,7 @@ protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) return; } - if (Map.EnforceMaxPlayers) + if (GameModeMap.EnforceMaxPlayers) { foreach (PlayerInfo pInfo in Players) { @@ -834,14 +835,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; @@ -893,7 +894,7 @@ protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) GetReadyNotification(); return; } - + } HostLaunchGame(); @@ -935,19 +936,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() @@ -1015,7 +1013,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); } @@ -1150,10 +1149,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; @@ -1169,7 +1168,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 25849273d..1a16ec935 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -106,35 +106,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); @@ -151,7 +140,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"); } @@ -220,7 +209,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() @@ -366,7 +355,7 @@ private void LoadSettings() //return; } - bool AIAllowed = !(Map.HumanPlayersOnly || GameMode.HumanPlayersOnly); + bool AIAllowed = !GameModeMap.HumanPlayersOnly; foreach (string key in keys) { if (!AIAllowed) break; @@ -466,13 +455,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; } @@ -494,7 +483,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..4521e7e37 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 => chkBoxForceNoTeams.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 assigned to a player.".L10N("Client:Main:AutoAllyingTextNoPlayer") + "\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..1130b58f2 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 60af545f9..d3ec4424f 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,43 +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 - .GetStringValue(Name, "DisallowedPlayerSides", string.Empty) - .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (string sideIndex in disallowedSides) - DisallowedPlayerSides.Add(int.Parse(sideIndex)); + InitializeBaseSettingsFromIniSection(forcedOptionsIni.GetSection(Name)); - disallowedSides = forcedOptionsIni - .GetStringValue(Name, "DisallowedHumanPlayerSides", string.Empty) - .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + 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 - .GetStringValue(Name, "DisallowedComputerPlayerSides", string.Empty) - .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - 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); @@ -178,9 +150,23 @@ public void ApplySpawnIniCode(IniFile spawnIni) spawnIni.SetStringValue("Settings", key.Key, key.Value); } - public IniFile GetMapRulesIniFile() + public List GetMapRulesIniFiles(Random random) { - 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] = random.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..9a3035568 --- /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) + { + // MinPlayers + MinPlayers = section.GetIntValueOrNull("MinPlayers"); + + // MaxPlayers + if (section.KeyExists("ClientMaxPlayer")) + MaxPlayers = section.GetIntValueOrNull("ClientMaxPlayer"); + else + MaxPlayers = section.GetIntValueOrNull("MaxPlayers"); + + // EnforceMaxPlayers + EnforceMaxPlayers = section.GetBooleanValueOrNull("EnforceMaxPlayers"); + + // AllowedStartingLocations + List? rawAllowedStartingLocations = section.GetListValueOrNull("AllowedStartingLocations", ',', int.Parse); + + if (rawAllowedStartingLocations != null) + { + // 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("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 872f95124..4c0ebd316 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; @@ -38,10 +36,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) @@ -70,33 +66,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. @@ -104,12 +73,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. /// @@ -123,10 +86,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. @@ -147,37 +110,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. /// @@ -272,12 +204,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 { @@ -295,10 +234,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")); @@ -307,16 +242,14 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) .L10N($"INI:Maps:{BaseFilePath}:Briefing"); CalculateSHA(); - IsCoop = section.GetBooleanValue("IsCoopMission", false); + + InitializeBaseSettingsFromIniSection(section); + 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)) { @@ -360,24 +293,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(','); @@ -429,6 +344,8 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) ParseSpawnIniOptions(iniFile, fsioSection); } + ExtraININame = section.GetStringValueOrNull("ExtraININame"); + return true; } catch (Exception ex) @@ -522,7 +439,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; @@ -556,25 +473,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); + 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)) @@ -582,24 +493,6 @@ public bool SetInfoFromCustomMap() Bases = Convert.ToInt32(Conversions.BooleanFromString(bases, false)); } - if (IsCoop) - { - CoopInfo = new CoopMapInfo(); - string[] disallowedSides = iniFile.GetStringValue("Basic", "DisallowedPlayerSides", string.Empty).Split( - new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (string sideIndex in disallowedSides) - CoopInfo.DisallowedPlayerSides.Add(int.Parse(sideIndex, CultureInfo.InvariantCulture)); - - string[] disallowedColors = iniFile.GetStringValue("Basic", "DisallowedPlayerColors", string.Empty).Split( - new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - 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 +524,8 @@ public bool SetInfoFromCustomMap() ParseForcedOptions(iniFile, "ForcedOptions"); ParseSpawnIniOptions(iniFile, "ForcedSpawnIniOptions"); + ExtraININame = basicSection.GetStringValueOrNull("ExtraININame"); + return true; } catch @@ -728,7 +623,7 @@ public IniFile GetMapIni() } public void ApplySpawnIniCode(IniFile spawnIni, int totalPlayerCount, - int aiPlayerCount, int coopDifficultyLevel) + int aiPlayerCount, bool isCoop, CoopMapInfo coopInfo, int coopDifficultyLevel, Random random, int sideCount) { foreach (KeyValuePair key in ForcedSpawnIniOptions) spawnIni.SetStringValue("Settings", key.Key, key.Value); @@ -742,16 +637,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() => random.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 +819,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 474c16ed7..d78bd3ad0 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -110,7 +110,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 +183,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 +272,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. From 629a5dfdac1f4e0b4ba4de0f0590bc08a7a70ba7 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 16 May 2025 21:45:43 +0800 Subject: [PATCH 02/26] Update AutoAllyingTextNoPlayerV2 --- DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs b/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs index 4521e7e37..30c6621f1 100644 --- a/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs +++ b/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs @@ -293,7 +293,7 @@ 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_PLAYER} : " + "Block this location from being assigned to a player.".L10N("Client:Main:AutoAllyingTextNoPlayer") + "\n" + + $"{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") ); } From c3dd0fbff634e27ecafbe9f8b9fb540419f5bcfe Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 16 May 2025 21:49:16 +0800 Subject: [PATCH 03/26] Update PlayerExtraOptionsPanel.cs --- DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs b/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs index 30c6621f1..3d672d2c8 100644 --- a/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs +++ b/DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs @@ -41,7 +41,7 @@ public PlayerExtraOptionsPanel(WindowManager windowManager) : base(windowManager public bool IsForcedRandomSides { get => chkBoxForceRandomSides.Checked; - set => chkBoxForceNoTeams.Checked = value; + set => chkBoxForceRandomSides.Checked = value; } public bool IsForcedNoTeams From f926d0e50b8d882d46412ae24a0cee16fd0a5aa0 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 16 May 2025 22:04:05 +0800 Subject: [PATCH 04/26] Update MultiplayerGameLobby.cs --- .../DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 19efddd0e..1197c1452 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -944,7 +944,7 @@ protected virtual void InsufficientPlayersNotification() protected virtual void TooManyPlayersNotification() { 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"), + AddNotice(string.Format("Unable to launch game: {0} cannot be played with more than {1} players.".L10N("Client:Main:TooManyPlayersNotification1"), GameModeMap.ToString(), GameModeMap.MaxPlayers)); } From e7fd6ca07822f994bf3bdb291614f1c3f4ed4534 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 30 May 2025 00:04:37 +0800 Subject: [PATCH 05/26] random -> pseudoRandom --- .../DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs | 16 ++++++++-------- DXMainClient/Domain/Multiplayer/Map.cs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 4ca92fd2c..cbb68f7e1 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1395,7 +1395,7 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa /// /// Writes spawn.ini. Returns the player house info returned from the randomizer. /// - private PlayerHouseInfo[] WriteSpawnIni(Random random) + private PlayerHouseInfo[] WriteSpawnIni(Random pseudoRandom) { Logger.Log("Writing spawn.ini"); @@ -1424,7 +1424,7 @@ private PlayerHouseInfo[] WriteSpawnIni(Random random) teamStartMappings = PlayerExtraOptionsPanel.GetTeamStartMappings(); } - PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings, random); + PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings, pseudoRandom); IniFile spawnIni = new IniFile(spawnerSettingsFile.FullName); @@ -1475,7 +1475,7 @@ private PlayerHouseInfo[] WriteSpawnIni(Random random) GameMode.ApplySpawnIniCode(spawnIni); // Forced options from the game mode Map.ApplySpawnIniCode(spawnIni, Players.Count + AIPlayers.Count, - AIPlayers.Count, GameModeMap.IsCoop, GameModeMap.CoopInfo, GameModeMap.CoopDifficultyLevel, random, SideCount); // Forced options from the map + AIPlayers.Count, GameModeMap.IsCoop, GameModeMap.CoopInfo, GameModeMap.CoopDifficultyLevel, pseudoRandom, SideCount); // Forced options from the map // Player options @@ -1661,7 +1661,7 @@ private void InitializeMatchStatistics(PlayerHouseInfo[] houseInfos) /// /// Writes spawnmap.ini. /// - private void WriteMap(PlayerHouseInfo[] houseInfos, Random random) + private void WriteMap(PlayerHouseInfo[] houseInfos, Random pseudoRandom) { FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); @@ -1676,7 +1676,7 @@ private void WriteMap(PlayerHouseInfo[] houseInfos, Random random) IniFile globalCodeIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", "Map Code", "GlobalCode.ini")); - foreach (IniFile iniFile in GameMode.GetMapRulesIniFiles(random)) + foreach (IniFile iniFile in GameMode.GetMapRulesIniFiles(pseudoRandom)) { MapCodeHelper.ApplyMapCode(mapIni, iniFile); } @@ -1905,11 +1905,11 @@ private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] house /// protected virtual void StartGame() { - Random random = new Random(RandomSeed); + Random pseudoRandom = new Random(RandomSeed); - PlayerHouseInfo[] houseInfos = WriteSpawnIni(random); + PlayerHouseInfo[] houseInfos = WriteSpawnIni(pseudoRandom); InitializeMatchStatistics(houseInfos); - WriteMap(houseInfos, random); + WriteMap(houseInfos, pseudoRandom); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index 4c0ebd316..c6fb2c796 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -623,7 +623,7 @@ public IniFile GetMapIni() } public void ApplySpawnIniCode(IniFile spawnIni, int totalPlayerCount, - int aiPlayerCount, bool isCoop, CoopMapInfo coopInfo, int coopDifficultyLevel, Random random, int sideCount) + int aiPlayerCount, bool isCoop, CoopMapInfo coopInfo, int coopDifficultyLevel, Random pseudoRandom, int sideCount) { foreach (KeyValuePair key in ForcedSpawnIniOptions) spawnIni.SetStringValue("Settings", key.Key, key.Value); @@ -639,7 +639,7 @@ public void ApplySpawnIniCode(IniFile spawnIni, int totalPlayerCount, if (isCoop) { - int NextRandomSide() => random.Next(0, sideCount); + int NextRandomSide() => pseudoRandom.Next(0, sideCount); var allyHouses = coopInfo.AllyHouses; var enemyHouses = coopInfo.EnemyHouses; From dd4ca90aa2358711819936b4965994af377a6929 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 30 May 2025 00:05:58 +0800 Subject: [PATCH 06/26] random -> pseudoRandom --- DXMainClient/Domain/Multiplayer/GameMode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/GameMode.cs b/DXMainClient/Domain/Multiplayer/GameMode.cs index d3ec4424f..2261d3b4d 100644 --- a/DXMainClient/Domain/Multiplayer/GameMode.cs +++ b/DXMainClient/Domain/Multiplayer/GameMode.cs @@ -150,7 +150,7 @@ public void ApplySpawnIniCode(IniFile spawnIni) spawnIni.SetStringValue("Settings", key.Key, key.Value); } - public List GetMapRulesIniFiles(Random random) + public List GetMapRulesIniFiles(Random pseudoRandom) { var mapRules = new List() { new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, mapCodeININame)) }; if (randomizedMapCodeININames.Count == 0) @@ -159,7 +159,7 @@ public List GetMapRulesIniFiles(Random random) Dictionary randomOrder = new(); foreach (string name in randomizedMapCodeININames) { - randomOrder[name] = random.Next(); + randomOrder[name] = pseudoRandom.Next(); } mapRules.AddRange( From 50a0d01be94b81d5022b2a10c3c41a9d459504cb Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 27 Jun 2025 22:45:06 +0800 Subject: [PATCH 07/26] Fix rework gamemodemap --- .../DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs | 2 +- DXMainClient/Domain/Multiplayer/GameMode.cs | 2 +- DXMainClient/Domain/Multiplayer/GameModeMapBase.cs | 12 ++++++------ DXMainClient/Domain/Multiplayer/Map.cs | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index cbb68f7e1..c8d39fbdb 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -2276,7 +2276,7 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) ddStart.AddItem("???"); - int maxLocation = GameModeMap.AllowedStartingLocations.Max() == GameModeMap.MaxPlayers ? GameModeMap.MaxPlayers : MAX_PLAYER_COUNT; + 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)) diff --git a/DXMainClient/Domain/Multiplayer/GameMode.cs b/DXMainClient/Domain/Multiplayer/GameMode.cs index 2261d3b4d..5713b12fc 100644 --- a/DXMainClient/Domain/Multiplayer/GameMode.cs +++ b/DXMainClient/Domain/Multiplayer/GameMode.cs @@ -83,7 +83,7 @@ public void Initialize() UntranslatedUIName = section.GetStringValue("UIName", Name); UIName = UntranslatedUIName.L10N($"INI:GameModes:{Name}:UIName"); - InitializeBaseSettingsFromIniSection(forcedOptionsIni.GetSection(Name)); + InitializeBaseSettingsFromIniSection(forcedOptionsIni.GetSection(Name), isCustomMap: false); MinPlayersOverride = section.GetIntValueOrNull("MinPlayersOverride"); MaxPlayersOverride = section.GetIntValueOrNull("MaxPlayersOverride"); diff --git a/DXMainClient/Domain/Multiplayer/GameModeMapBase.cs b/DXMainClient/Domain/Multiplayer/GameModeMapBase.cs index 9a3035568..7c1b812ba 100644 --- a/DXMainClient/Domain/Multiplayer/GameModeMapBase.cs +++ b/DXMainClient/Domain/Multiplayer/GameModeMapBase.cs @@ -82,14 +82,14 @@ public abstract class GameModeMapBase [JsonInclude] public bool? ForceNoTeams { get; private set; } - protected void InitializeBaseSettingsFromIniSection(IniSection section) + protected void InitializeBaseSettingsFromIniSection(IniSection section, bool isCustomMap) { // MinPlayers - MinPlayers = section.GetIntValueOrNull("MinPlayers"); + MinPlayers = section.GetIntValueOrNull(isCustomMap ? "MinPlayer" : "MinPlayers"); // MaxPlayers - if (section.KeyExists("ClientMaxPlayer")) - MaxPlayers = section.GetIntValueOrNull("ClientMaxPlayer"); + if (isCustomMap) + MaxPlayers = section.GetIntValueOrNull("ClientMaxPlayer") ?? section.GetIntValueOrNull("MaxPlayer"); else MaxPlayers = section.GetIntValueOrNull("MaxPlayers"); @@ -99,7 +99,7 @@ protected void InitializeBaseSettingsFromIniSection(IniSection section) // AllowedStartingLocations List? rawAllowedStartingLocations = section.GetListValueOrNull("AllowedStartingLocations", ',', int.Parse); - if (rawAllowedStartingLocations != null) + 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(); @@ -119,7 +119,7 @@ protected void InitializeBaseSettingsFromIniSection(IniSection section) } // MultiplayerOnly - MultiplayerOnly = section.GetBooleanValueOrNull("MultiplayerOnly"); + MultiplayerOnly = section.GetBooleanValueOrNull(isCustomMap ? "ClientMultiplayerOnly" : "MultiplayerOnly"); // HumanPlayersOnly HumanPlayersOnly = section.GetBooleanValueOrNull("HumanPlayersOnly"); diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index c6fb2c796..77cc707c4 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -243,7 +243,7 @@ public bool InitializeFromMpMapsINI(IniFile iniFile) CalculateSHA(); - InitializeBaseSettingsFromIniSection(section); + InitializeBaseSettingsFromIniSection(section, isCustomMap: false); Credits = section.GetIntValue("Credits", -1); UnitCount = section.GetIntValue("UnitCount", -1); @@ -478,7 +478,7 @@ public bool InitializeFromCustomMap() CalculateSHA(); - InitializeBaseSettingsFromIniSection(basicSection); + InitializeBaseSettingsFromIniSection(basicSection, isCustomMap: true); Credits = basicSection.GetIntValue("Credits", -1); UnitCount = basicSection.GetIntValue("UnitCount", -1); From 0890c430dd8aaa6c7363e014810411bf34d700d6 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Wed, 28 May 2025 21:41:18 +0800 Subject: [PATCH 08/26] Update migration documents --- Docs/Migration.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Docs/Migration.md b/Docs/Migration.md index c2a745cf2..46282c408 100644 --- a/Docs/Migration.md +++ b/Docs/Migration.md @@ -5,9 +5,7 @@ This document lists all the breaking changes and how to address them. Each secti ## 2.12.0 -- The client now has unified different builds among game types. The game type must be defined in the `ClientDefinitions.ini` file. Please specify `ClientGameType` in `[Settings]` section of the `ClientDefinitions.ini` file, e.g., `ClientGameType=Ares`. See [this file](https://github.com/CnCNet/xna-cncnet-client/blob/0554d7974cb741170c881116568144265e6cbabb/ClientCore/Enums/ClientType.cs) for a list of available values. - -- The `trbScrollRate` component in `GameOptionsPanel` was mistakenly named as `trbClientVolume` in the INI configuration file from the beginning. This has been fixed. No action is needed unless this component was explicitly modified in your configuration (rare). +- The client now has unified different builds among game types. The game type must be defined in the `ClientDefinitions.ini` file. Please specify `ClientGameType` in `[Settings]` section of the `ClientDefinitions.ini` file, e.g., `ClientGameType=Ares`. See [this file](https://github.com/CnCNet/xna-cncnet-client/blob/master/ClientCore/Enums/ClientType.cs) for a list of available values on the latest stable version. ## 2.11.0.0 and earlier From 0aea75aec4adcbfcf743331fcbc976ab954b42ee Mon Sep 17 00:00:00 2001 From: SadPencil Date: Wed, 28 May 2025 22:00:45 +0800 Subject: [PATCH 09/26] Update migration documents for recent changes --- Docs/Migration.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Docs/Migration.md b/Docs/Migration.md index 46282c408..62ddfda46 100644 --- a/Docs/Migration.md +++ b/Docs/Migration.md @@ -5,7 +5,19 @@ This document lists all the breaking changes and how to address them. Each secti ## 2.12.0 -- The client now has unified different builds among game types. The game type must be defined in the `ClientDefinitions.ini` file. Please specify `ClientGameType` in `[Settings]` section of the `ClientDefinitions.ini` file, e.g., `ClientGameType=Ares`. See [this file](https://github.com/CnCNet/xna-cncnet-client/blob/master/ClientCore/Enums/ClientType.cs) for a list of available values on the latest stable version. +- The client now has unified different builds among game types. The game type must be defined in the `ClientDefinitions.ini` file. Please specify `ClientGameType` in `[Settings]` section of the `ClientDefinitions.ini` file, e.g., `ClientGameType=Ares`. See [this file](https://github.com/CnCNet/xna-cncnet-client/blob/0554d7974cb741170c881116568144265e6cbabb/ClientCore/Enums/ClientType.cs) for a list of available values on the latest stable version. + +- The `trbScrollRate` component in `GameOptionsPanel` was mistakenly named as `trbClientVolume` in the INI configuration file from the beginning. This has been fixed. No action is needed unless this component was explicitly modified in your configuration (rare). + +## 2.11.7.0 +- The new [map preview in game preview feature](https://github.com/CnCNet/xna-cncnet-client/pull/611) introduces a new texture (in `Resources` folder and/or theme subfolders): + - `noMapPreview.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/d658c28b0ee4538701f5c3ec0bcf7d06ce3720e8/DXMainClient/Resources/DTA/noMapPreview.png) + +## 2.11.6.0 +- The new [preferred skill level feature](https://github.com/CnCNet/xna-cncnet-client/pull/598) introduces the following new textures (in `Resources` folder and/or theme subfolders): + - `skillLevel1.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/95d43c27ca383c6addb4c33ab437d3dee0b735cf/DXMainClient/Resources/DTA/skillLevel1.png) + - `skillLevel2.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/95d43c27ca383c6addb4c33ab437d3dee0b735cf/DXMainClient/Resources/DTA/skillLevel2.png) + - `skillLevel3.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/95d43c27ca383c6addb4c33ab437d3dee0b735cf/DXMainClient/Resources/DTA/skillLevel3.png) ## 2.11.0.0 and earlier @@ -23,6 +35,8 @@ This document lists all the breaking changes and how to address them. Each secti - To support launching the game on Linux the file defined as `UnixGameExecutableName` (defaults to `wine-dta.sh`) in `ClientDefinitions.ini` must be set up correctly. E.g. for launching a game with wine the file could contain `wine gamemd-spawn.exe $*` where `gamemd-spawn.exe` is replaced with the game executable. Note that users might need to execute `chmod +x wine-dta.sh` once to allow it to be launched. +- Due to the unification of client builds, it is now required to define client behavior depending on the game using the `ClientGameType` key in the `ClientDefinitions.ini` file in the `Settings` section. + - The use of `*.cur` mouse cursor files is not supported on the cross-platform `UniversalGL` build. To ensure the intended cursor is shown instead of a missing texture (pink square) all themes need to contain a `cursor.png` file. Existing `*.cur` files will still be used by the Windows-only builds. - The MonoGame MCGB editor will convert the MainMenuTheme to `MainMenuTheme.wma` when publishing for MonoGame WindowsDX. MonoGame DesktopGL only supports the `*.ogg` format. To ensure the MainMenuTheme is available on both the WindowsDX & DesktopGL client versions you need to manually convert and add the missing ogg format file to each theme. Each theme should then contain both `MainMenuTheme.wma` and `MainMenuTheme.ogg` files. The client will then switch out the correct MainMenuTheme format at runtime. From 3d0aecc5be069d599243e9422ade81c529cd3bdb Mon Sep 17 00:00:00 2001 From: SadPencil Date: Wed, 28 May 2025 22:32:02 +0800 Subject: [PATCH 10/26] Replace doc URLs with relative ones --- Docs/Migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/Migration.md b/Docs/Migration.md index 62ddfda46..9510420bf 100644 --- a/Docs/Migration.md +++ b/Docs/Migration.md @@ -5,7 +5,7 @@ This document lists all the breaking changes and how to address them. Each secti ## 2.12.0 -- The client now has unified different builds among game types. The game type must be defined in the `ClientDefinitions.ini` file. Please specify `ClientGameType` in `[Settings]` section of the `ClientDefinitions.ini` file, e.g., `ClientGameType=Ares`. See [this file](https://github.com/CnCNet/xna-cncnet-client/blob/0554d7974cb741170c881116568144265e6cbabb/ClientCore/Enums/ClientType.cs) for a list of available values on the latest stable version. +- The client now has unified different builds among game types. The game type must be defined in the `ClientDefinitions.ini` file. Please specify `ClientGameType` in `[Settings]` section of the `ClientDefinitions.ini` file, e.g., `ClientGameType=Ares`. See [this file](https://github.com/CnCNet/xna-cncnet-client/blob/0554d7974cb741170c881116568144265e6cbabb/ClientCore/Enums/ClientType.cs) for a list of available values. - The `trbScrollRate` component in `GameOptionsPanel` was mistakenly named as `trbClientVolume` in the INI configuration file from the beginning. This has been fixed. No action is needed unless this component was explicitly modified in your configuration (rare). From 871438b96aac9b856c72a59383aac8134dff0c98 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Thu, 29 May 2025 00:18:18 +0800 Subject: [PATCH 11/26] Remove unnecessary migration document items --- Docs/Migration.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Docs/Migration.md b/Docs/Migration.md index 9510420bf..c2a745cf2 100644 --- a/Docs/Migration.md +++ b/Docs/Migration.md @@ -9,16 +9,6 @@ This document lists all the breaking changes and how to address them. Each secti - The `trbScrollRate` component in `GameOptionsPanel` was mistakenly named as `trbClientVolume` in the INI configuration file from the beginning. This has been fixed. No action is needed unless this component was explicitly modified in your configuration (rare). -## 2.11.7.0 -- The new [map preview in game preview feature](https://github.com/CnCNet/xna-cncnet-client/pull/611) introduces a new texture (in `Resources` folder and/or theme subfolders): - - `noMapPreview.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/d658c28b0ee4538701f5c3ec0bcf7d06ce3720e8/DXMainClient/Resources/DTA/noMapPreview.png) - -## 2.11.6.0 -- The new [preferred skill level feature](https://github.com/CnCNet/xna-cncnet-client/pull/598) introduces the following new textures (in `Resources` folder and/or theme subfolders): - - `skillLevel1.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/95d43c27ca383c6addb4c33ab437d3dee0b735cf/DXMainClient/Resources/DTA/skillLevel1.png) - - `skillLevel2.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/95d43c27ca383c6addb4c33ab437d3dee0b735cf/DXMainClient/Resources/DTA/skillLevel2.png) - - `skillLevel3.png`, available [here](https://github.com/CnCNet/xna-cncnet-client/blob/95d43c27ca383c6addb4c33ab437d3dee0b735cf/DXMainClient/Resources/DTA/skillLevel3.png) - ## 2.11.0.0 and earlier - `CustomSettingFileCheckBox` and `CustomSettingFileDropDown` have been renamed to simply `FileSettingCheckBox` and `FileSettingDropDown`. This requires adjusting the control names in `OptionsWindow.ini`. `FileSettingCheckBox` has a fallback to legacy behaviour if the control has any files defined with `FileX`. @@ -35,8 +25,6 @@ This document lists all the breaking changes and how to address them. Each secti - To support launching the game on Linux the file defined as `UnixGameExecutableName` (defaults to `wine-dta.sh`) in `ClientDefinitions.ini` must be set up correctly. E.g. for launching a game with wine the file could contain `wine gamemd-spawn.exe $*` where `gamemd-spawn.exe` is replaced with the game executable. Note that users might need to execute `chmod +x wine-dta.sh` once to allow it to be launched. -- Due to the unification of client builds, it is now required to define client behavior depending on the game using the `ClientGameType` key in the `ClientDefinitions.ini` file in the `Settings` section. - - The use of `*.cur` mouse cursor files is not supported on the cross-platform `UniversalGL` build. To ensure the intended cursor is shown instead of a missing texture (pink square) all themes need to contain a `cursor.png` file. Existing `*.cur` files will still be used by the Windows-only builds. - The MonoGame MCGB editor will convert the MainMenuTheme to `MainMenuTheme.wma` when publishing for MonoGame WindowsDX. MonoGame DesktopGL only supports the `*.ogg` format. To ensure the MainMenuTheme is available on both the WindowsDX & DesktopGL client versions you need to manually convert and add the missing ogg format file to each theme. Each theme should then contain both `MainMenuTheme.wma` and `MainMenuTheme.ogg` files. The client will then switch out the correct MainMenuTheme format at runtime. From 0374a0d6ccf3ab7983bf1ba5f2d87337c66ed5b9 Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Thu, 29 May 2025 20:25:09 +0300 Subject: [PATCH 12/26] Improve `ChatListBox` link handling (#734) * Improve `ChatListBox` link handling * Update message box text * Add trusted links feature * Add `gamesurge.net` and `dronebl.org` to the trusted links regexp * Update msgbox text Co-authored-by: Metadorius * Remove link from class field * Add `HardcodeTrustedLinksRegExp` * Refactor naming * Improve regexps * Add mentioning in docs * Rework user-defined allowed links * Add `https://mapdb.cncnet.org` to the always trusted links * Add suggested changes to the doc * Remove MMB links handler * Rework PR * Fix typo in DroneBL domain * Fix bug and improve performance * Replace the regex with Uri.Host property * Allow subdomains * Correct subdomain determination * Correct subdomain determination * Apply suggestions from code review Co-authored-by: Kerbiter * Update the indent --------- Co-authored-by: Metadorius Co-authored-by: SadPencil --- ClientCore/ClientConfiguration.cs | 4 ++ DXMainClient/DXGUI/Multiplayer/ChatListBox.cs | 53 +++++++++++++++++-- Docs/INISystem.md | 16 ++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index e7e15b2e9..592dd8476 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -260,6 +260,10 @@ public void RefreshSettings() public string StatisticsLogFileName => clientDefinitionsIni.GetStringValue(SETTINGS, "StatisticsLogFileName", "DTA.LOG"); + public string[] TrustedDomains => clientDefinitionsIni.GetStringValue(SETTINGS, "TrustedDomains", string.Empty).Split(','); + + public string[] AlwaysTrustedDomains = {"cncnet.org", "gamesurge.net", "dronebl.org", "discord.com", "youtube.com", "youtu.be"}; + public (string Name, string Path) GetThemeInfoFromIndex(int themeIndex) => clientDefinitionsIni.GetStringValue("Themes", themeIndex.ToString(), ",").Split(',').AsTuple2(); /// diff --git a/DXMainClient/DXGUI/Multiplayer/ChatListBox.cs b/DXMainClient/DXGUI/Multiplayer/ChatListBox.cs index 65936b6a6..e6bc20dd8 100644 --- a/DXMainClient/DXGUI/Multiplayer/ChatListBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/ChatListBox.cs @@ -5,6 +5,9 @@ using System; using ClientCore; using ClientCore.Extensions; +using ClientGUI; +using System.Linq; +using Rampastring.Tools; namespace DTAClient.DXGUI.Multiplayer { @@ -24,11 +27,53 @@ private void ChatListBox_DoubleLeftClick(object sender, EventArgs e) if (SelectedIndex < 0 || SelectedIndex >= Items.Count) return; - var link = Items[SelectedIndex].Text?.GetLink(); + // Get the clicked link + string link = Items[SelectedIndex].Text?.GetLink(); if (link == null) return; - ProcessLauncher.StartShellProcess(link); + // Determine if the link is trusted + bool isTrusted = false; + try + { + string domain = new Uri(link).Host; + var trustedDomains = ClientConfiguration.Instance.TrustedDomains.Concat(ClientConfiguration.Instance.AlwaysTrustedDomains); + isTrusted = trustedDomains.Contains(domain, StringComparer.InvariantCultureIgnoreCase) + || trustedDomains.Any(trustedDomain => domain.EndsWith("." + trustedDomain, StringComparison.InvariantCultureIgnoreCase)); + } + catch (Exception ex) + { + isTrusted = false; + Logger.Log($"Error in parsing the URL \"{link}\": {ex.ToString()}"); + } + + if (isTrusted) + { + ProcessLink(link); + return; + } + + // Show the warning if the link is not trusted + var msgBox = new XNAMessageBox(WindowManager, + "Open Link Confirmation".L10N("Client:Main:OpenLinkConfirmationTitle"), + """ + You're about to open a link shared in chat. + + Please note that this link hasn't been verified, + and CnCNet is not responsible for its content. + + Would you like to open the following link in your browser? + """.L10N("Client:Main:OpenLinkConfirmationText") + + Environment.NewLine + Environment.NewLine + link, + XNAMessageBoxButtons.YesNo); + msgBox.YesClickedAction = (msgBox) => ProcessLink(link); + msgBox.Show(); + } + + private void ProcessLink(string link) + { + if (link != null) + ProcessLauncher.StartShellProcess(link); } public void AddMessage(string message) @@ -49,7 +94,7 @@ public void AddMessage(ChatMessage message) Selectable = true, Tag = message }; - + if (message.SenderName == null) { listBoxItem.Text = Renderer.GetSafeString(string.Format("[{0}] {1}", @@ -61,7 +106,7 @@ public void AddMessage(ChatMessage message) listBoxItem.Text = Renderer.GetSafeString(string.Format("[{0}] {1}: {2}", message.DateTime.ToShortTimeString(), message.SenderName, message.Message), FontIndex); } - + AddItem(listBoxItem); if (LastIndex >= Items.Count - 2) diff --git a/Docs/INISystem.md b/Docs/INISystem.md index 01c9ebd60..fd4ea6148 100644 --- a/Docs/INISystem.md +++ b/Docs/INISystem.md @@ -505,3 +505,19 @@ Children of [XNAWindow](https://github.com/CnCNet/xna-cncnet-client/blob/develop RandomBackgroundTextures= ; comma-separated list of strings, ; paths of files to use randomly as BackgroundTexture ``` + +# Global Config Files + +## [ClientDefinition](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientCore/ClientConfiguration.cs) +> [!NOTE] +> _TODO work in progress_ + +The `ClientDefinitions.ini` file defines the client's global settings, including the game type, recommended resolutions and the executable file used to launch the game. + +In `ClientDefinitions.ini`: +```ini +[Settings] +TrustedDomains= ; comma-separated list of strings, + ; domain names to match links and prevent the message box from appearing before they open by default browser + ; example: cncnet.org,github.com,moddb.com +``` From dcd73d1d4ed8a5fc70283f4a081e5d9ecf276850 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 30 May 2025 01:27:20 +0800 Subject: [PATCH 13/26] Add AssemblyTitle to the main client executable (#739) --- DXMainClient/DXMainClient.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index aa7c922ff..576eba97d 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -6,7 +6,8 @@ true false false - CnCNet Main Client Library + CnCNet Main Client + CnCNet Client DTAClient clienticon.ico SystemAware From 662e8af45a2ee26636708dacd39937a3a3112292 Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Sat, 31 May 2025 12:51:52 +0300 Subject: [PATCH 14/26] Fix bug when YR client shows missing required files text for like Ares client (#741) --- DXMainClient/DXGUI/Generic/MainMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 96523ebed..78004eb17 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -438,7 +438,7 @@ private void CheckRequiredFiles() if (absentFiles.Count > 0) { string description = string.Empty; - if (ClientConfiguration.Instance.ClientGameType == ClientType.YR) + if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares) { description = ("You are missing Yuri's Revenge files that are required\n" + "to play this mod! Yuri's Revenge mods are not standalone,\n" + From c486f9d19d872b427c65a66cdf74c91789d11dec Mon Sep 17 00:00:00 2001 From: SadPencil Date: Sun, 1 Jun 2025 05:11:01 +0800 Subject: [PATCH 15/26] Fix UTF-8 with BOM is introduced in the generated `spawnmap.ini` file (#738) * Fix UTF-8 BOM is introduced in the generated spawnmap.ini file * Provide UTF-8 without BOM encoding into EncodingExt class --- ClientCore/FileHelper.cs | 14 ++++++++++---- ClientCore/PlatformShim/EncodingExt.cs | 2 ++ .../DXGUI/Multiplayer/GameLobby/MapCodeHelper.cs | 15 +++++++++++++-- DXMainClient/Domain/Multiplayer/Map.cs | 9 +++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/ClientCore/FileHelper.cs b/ClientCore/FileHelper.cs index 19e401b32..d72a655f4 100644 --- a/ClientCore/FileHelper.cs +++ b/ClientCore/FileHelper.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using ClientCore.Extensions; +using ClientCore.PlatformShim; using Rampastring.Tools; @@ -91,17 +92,22 @@ public static void CreateHardLinkFromSource(string source, string destination, b } } - public static Encoding GetEncoding(string filename) + public static Encoding GetEncoding(string filename, float minimalConfidence = 0.5f) { - Encoding encoding = new UTF8Encoding(false); + Encoding encoding = EncodingExt.UTF8NoBOM; using (FileStream fs = File.OpenRead(filename)) { Ude.CharsetDetector cdet = new Ude.CharsetDetector(); cdet.Feed(fs); cdet.DataEnd(); - if (cdet.Charset != null) - encoding = Encoding.GetEncoding(cdet.Charset); + if (cdet.Charset != null && cdet.Confidence > minimalConfidence) + { + Encoding detectedEncoding = Encoding.GetEncoding(cdet.Charset); + + if (detectedEncoding is not UTF8Encoding and not ASCIIEncoding) + encoding = detectedEncoding; + } } return encoding; diff --git a/ClientCore/PlatformShim/EncodingExt.cs b/ClientCore/PlatformShim/EncodingExt.cs index b18f5c8b7..44c2befc7 100644 --- a/ClientCore/PlatformShim/EncodingExt.cs +++ b/ClientCore/PlatformShim/EncodingExt.cs @@ -16,4 +16,6 @@ static EncodingExt() /// ANSI doesn't mean a specific codepage, it means the default non-Unicode codepage which can be changed from Control Panel. /// public static Encoding ANSI { get; } + + public static Encoding UTF8NoBOM { get; } = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); } \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapCodeHelper.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapCodeHelper.cs index 069620fbc..4b085ce62 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapCodeHelper.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapCodeHelper.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; + using ClientCore; +using ClientCore.PlatformShim; + using DTAClient.Domain.Multiplayer; + using Rampastring.Tools; namespace DTAClient.DXGUI.Multiplayer.GameLobby @@ -17,14 +22,20 @@ public static class MapCodeHelper /// Currently selected gamemode, if set. public static void ApplyMapCode(IniFile mapIni, string customIniPath, GameMode gameMode) { - IniFile associatedIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, customIniPath)); + string associatedIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, customIniPath); + Encoding associatedIniEncoding = ClientConfiguration.Instance.ClientGameType == ClientCore.Enums.ClientType.TS ? FileHelper.GetEncoding(associatedIniPath) : EncodingExt.UTF8NoBOM; + IniFile associatedIni = new IniFile(associatedIniPath, associatedIniEncoding); string extraIniName = null; if (gameMode != null) extraIniName = associatedIni.GetStringValue("GameModeIncludes", gameMode.Name, null); associatedIni.EraseSectionKeys("GameModeIncludes"); ApplyMapCode(mapIni, associatedIni); if (!String.IsNullOrEmpty(extraIniName)) - ApplyMapCode(mapIni, new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, extraIniName))); + { + string extraIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, extraIniName); + Encoding extraIniEncoding = ClientConfiguration.Instance.ClientGameType == ClientCore.Enums.ClientType.TS ? FileHelper.GetEncoding(extraIniPath) : EncodingExt.UTF8NoBOM; + ApplyMapCode(mapIni, new IniFile(extraIniPath, extraIniEncoding)); + } } /// diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index 77cc707c4..8d464bf57 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -14,6 +14,7 @@ using Point = Microsoft.Xna.Framework.Point; using System.Diagnostics; using System.Text; +using ClientCore.PlatformShim; namespace DTAClient.Domain.Multiplayer { @@ -607,15 +608,15 @@ public Texture2D LoadPreviewTexture() public IniFile GetMapIni() { - Encoding encoding = FileHelper.GetEncoding(CompleteFilePath); + Encoding mapIniEncoding = ClientConfiguration.Instance.ClientGameType == ClientCore.Enums.ClientType.TS ? FileHelper.GetEncoding(CompleteFilePath) : EncodingExt.UTF8NoBOM; - var mapIni = new IniFile(CompleteFilePath, encoding); + var mapIni = new IniFile(CompleteFilePath, mapIniEncoding); if (!string.IsNullOrEmpty(ExtraININame)) { string extraIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", "Map Code", ExtraININame); - encoding = ClientConfiguration.Instance.ClientGameType == ClientCore.Enums.ClientType.TS ? FileHelper.GetEncoding(extraIniPath) : new UTF8Encoding(false); - var extraIni = new IniFile(extraIniPath, encoding); + Encoding extraIniEncoding = ClientConfiguration.Instance.ClientGameType == ClientCore.Enums.ClientType.TS ? FileHelper.GetEncoding(extraIniPath) : EncodingExt.UTF8NoBOM; + var extraIni = new IniFile(extraIniPath, extraIniEncoding); IniFile.ConsolidateIniFiles(mapIni, extraIni); } From c0ef06e911fee3ea1491ac79be4897f2dc40505b Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Wed, 4 Jun 2025 19:48:17 +0300 Subject: [PATCH 16/26] Add `discord.gg` as trusted domain (#745) --- ClientCore/ClientConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 592dd8476..cb06b16a8 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -262,7 +262,7 @@ public void RefreshSettings() public string[] TrustedDomains => clientDefinitionsIni.GetStringValue(SETTINGS, "TrustedDomains", string.Empty).Split(','); - public string[] AlwaysTrustedDomains = {"cncnet.org", "gamesurge.net", "dronebl.org", "discord.com", "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(); From fb8e3cf34f814412bab4296e84e8bbec8e8ba02b Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Wed, 4 Jun 2025 19:49:35 +0300 Subject: [PATCH 17/26] Refactor the ranks in `GameLobbyBase` class (#713) * Refactoring the ranks from private constants to enum * Replace `enum` with `class` to reduce `(int)` calls * Move ranks to `GameLobbyBase` * Replace enum with struct to reduce `(int)` and `(Rank)` casts * Replace struct with record * Make `rank` field private --- .../Multiplayer/GameLobby/GameLobbyBase.cs | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index c8d39fbdb..7235cc5ef 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -27,6 +27,22 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby /// public abstract class GameLobbyBase : INItializableWindow { + protected record Rank + { + private readonly int rank; + + public static readonly Rank None = 0; + public static readonly Rank Easy = 1; + public static readonly Rank Medium = 2; + public static readonly Rank Hard = 3; + + private Rank(int rank) => this.rank = rank; + + public static implicit operator int(Rank value) => value.rank; + + public static implicit operator Rank(int value) => new Rank(value); + } + protected const int MAX_PLAYER_COUNT = 8; protected const int PLAYER_OPTION_VERTICAL_MARGIN = 12; protected const int PLAYER_OPTION_HORIZONTAL_MARGIN = 3; @@ -38,11 +54,6 @@ public abstract class GameLobbyBase : INItializableWindow private readonly string FavoriteMapsLabel = "Favorite Maps".L10N("Client:Main:FavoriteMaps"); - private const int RANK_NONE = 0; - private const int RANK_EASY = 1; - private const int RANK_MEDIUM = 2; - private const int RANK_HARD = 3; - /// /// Creates a new instance of the game lobby base. /// @@ -2410,27 +2421,27 @@ protected GameType GetGameType() return GameType.TeamGame; } - protected int GetRank() + protected Rank GetRank() { if (GameMode == null || Map == null) - return RANK_NONE; + return Rank.None; foreach (GameLobbyCheckBox checkBox in CheckBoxes) { if ((checkBox.MapScoringMode == CheckBoxMapScoringMode.DenyWhenChecked && checkBox.Checked) || (checkBox.MapScoringMode == CheckBoxMapScoringMode.DenyWhenUnchecked && !checkBox.Checked)) { - return RANK_NONE; + return Rank.None; } } PlayerInfo localPlayer = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); if (localPlayer == null) - return RANK_NONE; + return Rank.None; if (IsPlayerSpectator(localPlayer)) - return RANK_NONE; + return Rank.None; // These variables are used by both the skirmish and multiplayer code paths int[] teamMemberCounts = new int[5]; @@ -2456,7 +2467,7 @@ protected int GetRank() if (isMultiplayer) { if (Players.Count == 1) - return RANK_NONE; + return Rank.None; // PvP stars for 2-player and 3-player maps if (GameModeMap.MaxPlayers <= 3) @@ -2464,42 +2475,42 @@ protected int GetRank() List filteredPlayers = Players.Where(p => !IsPlayerSpectator(p)).ToList(); if (AIPlayers.Count > 0) - return RANK_NONE; + return Rank.None; if (filteredPlayers.Count != GameModeMap.MaxPlayers) - return RANK_NONE; + return Rank.None; int localTeamIndex = localPlayer.TeamId; if (localTeamIndex > 0 && filteredPlayers.Count(p => p.TeamId == localTeamIndex) > 1) - return RANK_NONE; + return Rank.None; - return RANK_HARD; + return Rank.Hard; } // Coop stars for maps with 4 or more players // See the code in StatisticsManager.GetRankForCoopMatch for the conditions if (Players.Find(p => IsPlayerSpectator(p)) != null) - return RANK_NONE; + return Rank.None; if (AIPlayers.Count == 0) - return RANK_NONE; + return Rank.None; if (Players.Find(p => p.TeamId != localPlayer.TeamId) != null) - return RANK_NONE; + return Rank.None; if (Players.Find(p => p.TeamId == 0) != null) - return RANK_NONE; + return Rank.None; if (AIPlayers.Find(p => p.TeamId == 0) != null) - return RANK_NONE; + return Rank.None; teamMemberCounts[localPlayer.TeamId] += Players.Count; if (lowestEnemyAILevel < highestAllyAILevel) { // Check that the player's AI allies aren't stronger - return RANK_NONE; + return Rank.None; } // Check that all teams have at least as many players @@ -2514,7 +2525,7 @@ protected int GetRank() if (teamMemberCounts[i] > 0) { if (teamMemberCounts[i] < allyCount) - return RANK_NONE; + return Rank.None; } } @@ -2526,14 +2537,14 @@ protected int GetRank() // ********* if (AIPlayers.Count != GameModeMap.MaxPlayers - 1) - return RANK_NONE; + return Rank.None; teamMemberCounts[localPlayer.TeamId]++; if (lowestEnemyAILevel < highestAllyAILevel) { // Check that the player's AI allies aren't stronger - return RANK_NONE; + return Rank.None; } if (localPlayer.TeamId > 0) @@ -2550,7 +2561,7 @@ protected int GetRank() if (teamMemberCounts[i] > 0) { if (teamMemberCounts[i] < allyCount) - return RANK_NONE; + return Rank.None; } } @@ -2569,7 +2580,7 @@ protected int GetRank() } if (!pass) - return RANK_NONE; + return Rank.None; } return lowestEnemyAILevel + 1; From 78b8c8cd70a47f4117ec953506a742df74a1b381 Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:16:06 +0300 Subject: [PATCH 18/26] Merge YR branch changes (add inactive host check, fix disallowed sides not working with multiple random selectors, favorite maps storage improvements) (#744) * Add DisallowJoiningIncompatibleGames to ClientConfiguration * #465 non host cannot select America/Cuba with random selectors and disallowed side settings. * Move favorite maps to its own section * #502 - players cannot select Germany faction with "No Yuri/No France" * Warn host and eventually close if inactive * Event handler null checks * Check for enabled on inactive host kick timer * Check for "default" updateconfig.ini file to copy * Proper onmousemove, stop timer after lobby leave * Consolidate inactive timer check, move logic into containing class * build on yr/** branches * Skip inactive timer for password games * Rewords privacy notice as per #427 (#459) * post merge fixes * Update version text * Disable #cncnet channel from dropdowon * Bump version * Turn off development mode for yr/develop branch * Update commit hash * Bump xna version * Bump version * Change XNA client version info * Add steam * Update commit hash * Update client version * Update client version * Update client version 2.12.0, develop/80d08c66 * Update client version 2.12.2, develop/0718826 * Add abscent fields * Remove `yr/**` branches from build workflow * Refactor title and text for inactive host warning message to be translatable * Remove `yr/develop` code from `ClientCore/CnCNet5/GameCollections.cs` * Refactor `CnCNetLobby.cs` * Revert changes in `Directory.Build.targets` * Remove `updateconfig.default.ini` feature that never used * Refactor `GameHostInactiveCheck.cs` * Refactor handling `gameHostInactiveCheck` * Refactor `disallowedSides` code * Refactor `UserINISettings.cs` * Rename `dttmStart` to `startTime` * Rename `GameHostInactiveCheck` to `GameHostInactiveChecker` * Refactoring `IsGameFiltersApplied` * Fix spacing * Remove extra space * Another spacing fix * Move `random` lower --------- Co-authored-by: devo1929 Co-authored-by: Grant Bartlett Co-authored-by: SadPencil --- ClientCore/ClientConfiguration.cs | 8 +- ClientCore/ClientCore.csproj | 2 +- ClientCore/Extensions/IniFileExtensions.cs | 26 ++++++ ClientCore/Settings/UserINISettings.cs | 84 ++++++++++++++----- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 2 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 37 +++++++- .../GameLobby/GameHostInactiveChecker.cs | 74 ++++++++++++++++ DXMainClient/DXMainClient.csproj | 2 +- 8 files changed, 210 insertions(+), 25 deletions(-) create mode 100644 ClientCore/Extensions/IniFileExtensions.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/GameLobby/GameHostInactiveChecker.cs diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index cb06b16a8..33f60925c 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -360,6 +360,12 @@ private List ParseTranslationGameFiles() public string AllowedCustomGameModes => clientDefinitionsIni.GetStringValue(SETTINGS, "AllowedCustomGameModes", "Standard,Custom Map"); + public int InactiveHostWarningMessageSeconds => clientDefinitionsIni.GetIntValue(SETTINGS, "InactiveHostWarningMessageSeconds", 0); + + public int InactiveHostKickSeconds => clientDefinitionsIni.GetIntValue(SETTINGS, "InactiveHostKickSeconds", 0) + InactiveHostWarningMessageSeconds; + + public bool InactiveHostKickEnabled => InactiveHostWarningMessageSeconds > 0 && InactiveHostKickSeconds > 0; + public string SkillLevelOptions => clientDefinitionsIni.GetStringValue(SETTINGS, "SkillLevelOptions", "Any,Beginner,Intermediate,Pro"); public int DefaultSkillLevelIndex => clientDefinitionsIni.GetIntValue(SETTINGS, "DefaultSkillLevelIndex", 0); @@ -382,7 +388,7 @@ public string GetGameExecutableName() public bool DisplayPlayerCountInTopBar => clientDefinitionsIni.GetBooleanValue(SETTINGS, "DisplayPlayerCountInTopBar", false); /// - /// The name of the executable in the main game directory that selects + /// The name of the executable in the main game directory that selects /// the correct main client executable. /// For example, DTA.exe in case of DTA. /// diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 7db5a65b4..9ad0ea8f2 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/ClientCore/Extensions/IniFileExtensions.cs b/ClientCore/Extensions/IniFileExtensions.cs new file mode 100644 index 000000000..b2547d6da --- /dev/null +++ b/ClientCore/Extensions/IniFileExtensions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Rampastring.Tools; + +namespace ClientCore.Extensions +{ + public static class IniFileExtensions + { + public static IniSection GetOrAddSection(this IniFile iniFile, string sectionName) + { + var section = iniFile.GetSection(sectionName); + if (section != null) + return section; + + section = new IniSection(sectionName); + iniFile.AddSection(section); + return section; + } + + public static void RemoveAllKeys(this IniSection iniSection) + { + var keys = new List>(iniSection.Keys); + foreach (KeyValuePair iniSectionKey in keys) + iniSection.RemoveKey(iniSectionKey.Key); + } + } +} diff --git a/ClientCore/Settings/UserINISettings.cs b/ClientCore/Settings/UserINISettings.cs index d03302347..d2edb3d9e 100644 --- a/ClientCore/Settings/UserINISettings.cs +++ b/ClientCore/Settings/UserINISettings.cs @@ -2,7 +2,9 @@ using Rampastring.Tools; using System; using System.Collections.Generic; +using System.Linq; using ClientCore.Enums; +using ClientCore.Extensions; namespace ClientCore { @@ -16,6 +18,7 @@ public class UserINISettings public const string AUDIO = "Audio"; public const string COMPATIBILITY = "Compatibility"; public const string GAME_FILTERS = "GameFilters"; + private const string FAVORITE_MAPS = "FavoriteMaps"; private const bool DEFAULT_SHOW_FRIENDS_ONLY_GAMES = false; private const bool DEFAULT_HIDE_LOCKED_GAMES = false; @@ -125,7 +128,7 @@ protected UserINISettings(IniFile iniFile) HideIncompatibleGames = new BoolSetting(iniFile, GAME_FILTERS, "HideIncompatibleGames", DEFAULT_HIDE_INCOMPATIBLE_GAMES); MaxPlayerCount = new IntRangeSetting(iniFile, GAME_FILTERS, "MaxPlayerCount", DEFAULT_MAX_PLAYER_COUNT, 2, 8); - FavoriteMaps = new StringListSetting(iniFile, OPTIONS, "FavoriteMaps", new List()); + LoadFavoriteMaps(iniFile); } public IniFile SettingsIni { get; private set; } @@ -256,7 +259,7 @@ protected UserINISettings(IniFile iniFile) public BoolSetting GenerateOnlyNewValuesInTranslationStub { get; private set; } - public StringListSetting FavoriteMaps { get; private set; } + public List FavoriteMaps { get; private set; } public void SetValue(string section, string key, string value) => SettingsIni.SetStringValue(section, key, value); @@ -277,16 +280,14 @@ public int GetValue(string section, string key, int defaultValue) => SettingsIni.GetIntValue(section, key, defaultValue); public bool IsGameFollowed(string gameName) - { - return SettingsIni.GetBooleanValue("Channels", gameName, false); - } + => SettingsIni.GetBooleanValue("Channels", gameName, false); public bool ToggleFavoriteMap(string mapName, string gameModeName, bool isFavorite) { if (string.IsNullOrEmpty(mapName)) return isFavorite; - var favoriteMapKey = FavoriteMapKey(mapName, gameModeName); + string favoriteMapKey = FavoriteMapKey(mapName, gameModeName); isFavorite = IsFavoriteMap(mapName, gameModeName); if (isFavorite) FavoriteMaps.Remove(favoriteMapKey); @@ -295,22 +296,43 @@ public bool ToggleFavoriteMap(string mapName, string gameModeName, bool isFavori Instance.SaveSettings(); + WriteFavoriteMaps(); + return !isFavorite; } + private void LoadFavoriteMaps(IniFile iniFile) + { + FavoriteMaps = new List(); + bool legacyMapsLoaded = LoadLegacyFavoriteMaps(iniFile); + var favoriteMapsSection = SettingsIni.GetOrAddSection(FAVORITE_MAPS); + foreach (KeyValuePair keyValuePair in favoriteMapsSection.Keys) + FavoriteMaps.Add(keyValuePair.Value); + + if (legacyMapsLoaded) + WriteFavoriteMaps(); + } + + private void WriteFavoriteMaps() + { + var favoriteMapsSection = SettingsIni.GetOrAddSection(FAVORITE_MAPS); + favoriteMapsSection.RemoveAllKeys(); + for (int i = 0; i < FavoriteMaps.Count; i++) + favoriteMapsSection.AddKey(i.ToString(), FavoriteMaps[i]); + + SaveSettings(); + } + /// /// Checks if a specified map name and game mode name belongs to the favorite map list. /// /// The name of the map. /// The name of the game mode - public bool IsFavoriteMap(string nameName, string gameModeName) => FavoriteMaps.Value.Contains(FavoriteMapKey(nameName, gameModeName)); + public bool IsFavoriteMap(string nameName, string gameModeName) => FavoriteMaps.Contains(FavoriteMapKey(nameName, gameModeName)); private string FavoriteMapKey(string nameName, string gameModeName) => $"{nameName}:{gameModeName}"; - public void ReloadSettings() - { - SettingsIni.Reload(); - } + public void ReloadSettings() => SettingsIni.Reload(); public void ApplyDefaults() { @@ -332,13 +354,11 @@ public void SaveSettings() } public bool IsGameFiltersApplied() - { - return ShowFriendGamesOnly.Value != DEFAULT_SHOW_FRIENDS_ONLY_GAMES || - HideLockedGames.Value != DEFAULT_HIDE_LOCKED_GAMES || - HidePasswordedGames.Value != DEFAULT_HIDE_PASSWORDED_GAMES || - HideIncompatibleGames.Value != DEFAULT_HIDE_INCOMPATIBLE_GAMES || - MaxPlayerCount.Value != DEFAULT_MAX_PLAYER_COUNT; - } + => ShowFriendGamesOnly.Value != DEFAULT_SHOW_FRIENDS_ONLY_GAMES + || HideLockedGames.Value != DEFAULT_HIDE_LOCKED_GAMES + || HidePasswordedGames.Value != DEFAULT_HIDE_PASSWORDED_GAMES + || HideIncompatibleGames.Value != DEFAULT_HIDE_INCOMPATIBLE_GAMES + || MaxPlayerCount.Value != DEFAULT_MAX_PLAYER_COUNT; public void ResetGameFilters() { @@ -348,5 +368,31 @@ public void ResetGameFilters() HidePasswordedGames.Value = DEFAULT_HIDE_PASSWORDED_GAMES; MaxPlayerCount.Value = DEFAULT_MAX_PLAYER_COUNT; } + + /// + /// Used to remove old sections/keys to avoid confusion when viewing the ini file directly. + /// + private void CleanUpLegacySettings() + => SettingsIni.GetSection(GAME_FILTERS).RemoveKey("SortAlpha"); + + /// + /// Previously, favorite maps were stored under a single key under the [Options] section. + /// This attempts to read in that legacy key. + /// + /// + /// Whether or not legacy favorites were loaded. + private bool LoadLegacyFavoriteMaps(IniFile iniFile) + { + var legacyFavoriteMaps = new StringListSetting(iniFile, OPTIONS, FAVORITE_MAPS, new List()); + if (!legacyFavoriteMaps.Value?.Any() ?? true) + return false; + + foreach (string favoriteMapKey in legacyFavoriteMaps.Value) + FavoriteMaps.Add(favoriteMapKey); + + // remove the old key + iniFile.GetSection(OPTIONS).RemoveKey(FAVORITE_MAPS); + return true; + } } -} \ No newline at end of file +} diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 64b850ae8..8d746956f 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -1194,7 +1194,7 @@ private void ConnectionManager_WelcomeMessageReceived(object sender, EventArgs e tbChatInput.Enabled = true; Channel cncnetChannel = connectionManager.FindChannel("#cncnet"); - cncnetChannel.Join(); + cncnetChannel?.Join(); string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID); connectionManager.FindChannel(localGameChatChannelName).Join(); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 994276118..9df6119dc 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -61,6 +61,8 @@ Random random this.cncnetUserData = cncnetUserData; this.pmWindow = pmWindow; this.random = random; + + gameHostInactiveChecker = ClientConfiguration.Instance.InactiveHostKickEnabled? new GameHostInactiveChecker(WindowManager) : null; ctcpCommandHandlers = new CommandHandlerBase[] { @@ -118,6 +120,8 @@ Random random private CnCNetManager connectionManager; private string localGame; + private readonly GameHostInactiveChecker gameHostInactiveChecker; + private GameCollection gameCollection; private CnCNetUserData cncnetUserData; private readonly PrivateMessagingWindow pmWindow; @@ -176,6 +180,12 @@ public override void Initialize() IniNameOverride = nameof(CnCNetGameLobby); base.Initialize(); + if (gameHostInactiveChecker != null) + { + MouseMove += (sender, args) => gameHostInactiveChecker.Reset(); + gameHostInactiveChecker.CloseEvent += GameHostInactiveChecker_CloseEvent; + } + btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; @@ -245,6 +255,7 @@ public void SetUp(Channel channel, bool isHost, int playerLimit, RandomSeed = random.Next(); RefreshMapSelectionUI(); btnChangeTunnel.Enable(); + StartInactiveCheck(); } else { @@ -264,6 +275,18 @@ public void SetUp(Channel channel, bool isHost, int playerLimit, private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) => UpdatePing(); + private void GameHostInactiveChecker_CloseEvent(object sender, EventArgs e) => LeaveGameLobby(); + + public void StartInactiveCheck() + { + if (isCustomPassword) + return; + + gameHostInactiveChecker?.Start(); + } + + public void StopInactiveCheck() => gameHostInactiveChecker?.Stop(); + public void OnJoined() { FileHashCalculator fhc = new FileHashCalculator(); @@ -406,6 +429,7 @@ public void LeaveGameLobby() { if (IsHost) { + StopInactiveCheck(); closed = true; BroadcastGame(); } @@ -755,9 +779,16 @@ private void HandleOptionsRequest(string playerName, int options) if (color < 0 || color > MPColors.Count) return; - var disallowedSides = GetDisallowedSides(); + // Disallowed sides from client, maps, or game modes do not take random selectors into account + // So, we need to insert "false" for each random at the beginning of this list AFTER getting them + // from client, maps, or game modes. + var randomDisallowedSides = new List(RandomSelectorCount); + for (int i = 0; i < RandomSelectorCount; i++) + randomDisallowedSides.Add(false); + + var disallowedSides = randomDisallowedSides.Concat(GetDisallowedSides()).ToArray(); - if (side > 0 && side <= SideCount && disallowedSides[side - 1]) + if (0 < side && side < SideCount && disallowedSides[side]) return; if (GameModeMap?.CoopInfo != null) @@ -1259,6 +1290,7 @@ protected void ResetGameState() CopyPlayerDataToUI(); BroadcastPlayerOptions(); BroadcastPlayerExtraOptions(); + StartInactiveCheck(); if (Players.Count < playerLimit) UnlockGame(true); @@ -1334,6 +1366,7 @@ protected override void StartGame() HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME); } + StopInactiveCheck(); channel.SendCTCPMessage("STRTD", QueuedMessageType.SYSTEM_MESSAGE, 20); base.StartGame(); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameHostInactiveChecker.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameHostInactiveChecker.cs new file mode 100644 index 000000000..1ceadb6c7 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameHostInactiveChecker.cs @@ -0,0 +1,74 @@ +using System; +using System.Timers; +using ClientCore; +using ClientCore.Extensions; +using ClientGUI; +using Rampastring.XNAUI; + +namespace DTAClient.DXGUI.Multiplayer.GameLobby +{ + public class GameHostInactiveChecker + { + private readonly WindowManager windowManager; + private readonly Timer timer; + private bool isWarningShown; + private DateTime startTime; + private static int WarningSeconds => ClientConfiguration.Instance.InactiveHostWarningMessageSeconds; + private static int CloseSeconds => ClientConfiguration.Instance.InactiveHostKickSeconds; + + public event EventHandler CloseEvent; + + public GameHostInactiveChecker(WindowManager windowManager) + { + this.windowManager = windowManager; + timer = new Timer(); + timer.AutoReset = true; + timer.Interval = 1000; + timer.Elapsed += TimerOnElapsed; + } + + private void TimerOnElapsed(object sender, ElapsedEventArgs e) + { + double secondsElapsed = (DateTime.UtcNow - startTime).TotalSeconds; + + if (secondsElapsed > WarningSeconds && !isWarningShown) + ShowWarning(); + + if (secondsElapsed > CloseSeconds) + SendCloseEvent(); + } + + public void Start() + { + Reset(); + timer.Start(); + } + + public void Reset() + { + startTime = DateTime.UtcNow; + isWarningShown = false; + } + + public void Stop() => timer.Stop(); + + private void SendCloseEvent() + { + Stop(); + CloseEvent?.Invoke(this, null); + } + + private void ShowWarning() + { + isWarningShown = true; + XNAMessageBox hostInactiveWarningMessageBox = new XNAMessageBox( + windowManager, + "Are you still here?".L10N("Client:Main:InactiveHostWarningTitle"), + "Your game may be closed due to inactivity.".L10N("Client:Main:InactiveHostWarningText"), + XNAMessageBoxButtons.OK + ); + hostInactiveWarningMessageBox.OKClickedAction = box => Reset(); + hostInactiveWarningMessageBox.Show(); + } + } +} diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index 576eba97d..3cd687c2c 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -52,4 +52,4 @@ Always - \ No newline at end of file + From 722f08025e4af4efc9855f49cb2148905abe46d3 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Thu, 12 Jun 2025 05:18:09 +0800 Subject: [PATCH 19/26] Fix broken screenshot processing for Ares (#747) --- DXMainClient/DXGUI/Generic/GameInProgressWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs index 370be80ef..b82d4aa03 100644 --- a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs @@ -165,7 +165,7 @@ private void HandleGameProcessExited() DateTime dtn = DateTime.Now; - if (ClientConfiguration.Instance.ClientGameType == ClientType.YR) + if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares) { Task.Factory.StartNew(ProcessScreenshots); From b6336adb47dbacddd0764a3c1153e089f1682412 Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:18:28 +0300 Subject: [PATCH 20/26] Update XNAUI to 2.7.10 version (#746) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5db473627..13c9d2529 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ true - 2.7.8 + 2.7.10 8.0.0 From 4d97ac172cb42a4feb0cbfa6be8f3c9b2ae64117 Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:22:15 +0300 Subject: [PATCH 21/26] Add issue templates (#722) * Add issue templates * Adjust template * Replace link to `#xna-client-chat` --- .github/ISSUE_TEMPLATE/bug-report.yml | 91 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature-request.yml | 18 +++++ 3 files changed, 114 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..8cc3ed234 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,91 @@ +name: Bug Report +description: Open an issue to ask for a XNA Client bug to be fixed. +title: "Your bug report title here" +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + > [!WARNING] + > Before posting an issue, please read the **checklist at the bottom**. + + Thanks for taking the time to fill out this bug report! If you need real-time help, join us on the [C&C Mod Haven Discord](https://discord.gg/Smv4JC8BUG) server in the __#xna-client-chat__ channel. + + Please make sure you follow these instructions and fill in every question with as much detail as possible. + + - type: textarea + id: description + attributes: + label: Description + description: | + Write a detailed description telling us what the issue is, and if/when the bug occurs. + validations: + required: true + + - type: input + id: xna-client-version + attributes: + label: XNA Client Version + description: | + What version of XNA Client are you using? Please provide a link to the exact XNA Client build used, especially if it's not a release build. + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps To Reproduce + description: | + Tell us how to reproduce this issue so the developer(s) can reproduce the bug. + value: | + 1. + 2. + 3. + ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behaviour + description: | + Tell us what should happen. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behaviour + description: | + Tell us what actually happens instead. + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Context + description: | + Attach additional files or links to content related to the bug report here, like: + - images/gifs/videos to illustrate the bug; + - files or ini configs that are needed to reproduce the bug; + - a debug log, crash dump and exception file (mandatory if you're submitting a crash report). + + - type: checkboxes + id: checks + attributes: + label: Checklist + description: Please read and ensure you followed all the following options. + options: + - label: The issue happens on the **latest official** version of XNA Client and wasn't fixed yet. + required: true + - label: I agree to elaborate the details if requested and provide thorough testing if the bugfix is implemented. + required: true + - label: I added a very descriptive title to this issue. + required: true + - label: I used the GitHub search and read the issue list to find a similar issue and didn't find it. + required: true + - label: I have attached as much information as possible *(screenshots, gifs, videos, client logs, etc)*. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..87bc1f4dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Official channels on C&C Mod Haven + url: https://discord.gg/Smv4JC8BUG + about: If you want to discuss something with us without filing an issue. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..7ba112694 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,18 @@ +name: Feature Request +description: Open an issue to ask for a XNA Client feature to be implemented. +title: "Your feature request title here" +labels: ["feature"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! If you need real-time help, join us on the [C&C Mod Haven Discord](https://discord.gg/Smv4JC8BUG) server in the __#xna-client-chat__ channel. + + - type: textarea + id: description + attributes: + label: Description + description: | + Write a detailed description telling us what the feature you want to be implemented in the XNA Client. + validations: + required: true From 28c448000ead0ed1bd537fb1c39445135ffd3558 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Wed, 18 Jun 2025 11:15:52 +0800 Subject: [PATCH 22/26] Fix `LoadOrSaveGameOptionPresetWindow` cannot be customized via an ini file (#748) --- .../Multiplayer/CnCNet/LoadOrSaveGameOptionPresetWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/LoadOrSaveGameOptionPresetWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/LoadOrSaveGameOptionPresetWindow.cs index cacc52549..f4237b7b7 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/LoadOrSaveGameOptionPresetWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/LoadOrSaveGameOptionPresetWindow.cs @@ -132,9 +132,10 @@ public LoadOrSaveGameOptionPresetWindow(WindowManager windowManager) : base(wind public override void Initialize() { + Name = "LoadOrSaveGameOptionPresetWindow"; PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1); - + base.Initialize(); } From c5d7ca6acd736b891e0e47e85d611c456a350638 Mon Sep 17 00:00:00 2001 From: mah_boi <61310813+MahBoiDeveloper@users.noreply.github.com> Date: Wed, 18 Jun 2025 23:04:38 +0300 Subject: [PATCH 23/26] Implement `GetStringListValue` and rework get list values from `IniFile` class (#749) * Implement `GetListValue` and rework get list values from IniFile class * Rename `GetListValue` to `GetStringListValue` * Update ClientCore/Extensions/IniFileExtensions.cs Co-authored-by: Kerbiter * Rework `GetStringListValue` * Refactor `Updater.cs` Co-authored-by: Kerbiter --------- Co-authored-by: Kerbiter --- ClientCore/ClientConfiguration.cs | 22 ++++++++++--------- ClientCore/Extensions/IniFileExtensions.cs | 16 ++++++++++++++ .../INIProcessing/IniPreprocessInfoStore.cs | 4 ++-- ClientUpdater/CustomComponent.cs | 2 +- ClientUpdater/Updater.cs | 7 +++--- DXMainClient/DXGUI/Generic/LoadingScreen.cs | 6 +---- .../Multiplayer/GameLobby/GameLobbyBase.cs | 2 +- .../GameLobby/GameLobbyDropDown.cs | 2 +- .../Domain/Multiplayer/CoopMapInfo.cs | 4 ++-- .../Domain/Multiplayer/MapPreviewExtractor.cs | 3 ++- .../Domain/Multiplayer/MultiplayerColor.cs | 2 +- 11 files changed, 43 insertions(+), 27 deletions(-) diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 33f60925c..ab81bb15d 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; -using Rampastring.Tools; using System.IO; using System.Linq; using System.Runtime.InteropServices; -using ClientCore.I18N; -using ClientCore.Extensions; + using ClientCore.Enums; +using ClientCore.Extensions; +using ClientCore.I18N; + +using Rampastring.Tools; namespace ClientCore { @@ -212,8 +214,8 @@ public void RefreshSettings() public int MaximumRenderHeight => clientDefinitionsIni.GetIntValue(SETTINGS, "MaximumRenderHeight", 800); - public string[] RecommendedResolutions => clientDefinitionsIni.GetStringValue(SETTINGS, "RecommendedResolutions", - $"{MinimumRenderWidth}x{MinimumRenderHeight},{MaximumRenderWidth}x{MaximumRenderHeight}").Split(','); + public string[] RecommendedResolutions => clientDefinitionsIni.GetStringListValue(SETTINGS, "RecommendedResolutions", + $"{MinimumRenderWidth}x{MinimumRenderHeight},{MaximumRenderWidth}x{MaximumRenderHeight}"); public string WindowTitle => clientDefinitionsIni.GetStringValue(SETTINGS, "WindowTitle", string.Empty) .L10N("INI:ClientDefinitions:WindowTitle"); @@ -260,7 +262,7 @@ public void RefreshSettings() public string StatisticsLogFileName => clientDefinitionsIni.GetStringValue(SETTINGS, "StatisticsLogFileName", "DTA.LOG"); - public string[] TrustedDomains => clientDefinitionsIni.GetStringValue(SETTINGS, "TrustedDomains", string.Empty).Split(','); + 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"}; @@ -324,7 +326,7 @@ private List ParseTranslationGameFiles() continue; string value = clientDefinitionsIni.GetStringValue(TRANSLATIONS, key, string.Empty); - string[] parts = value.Split(','); + string[] parts = clientDefinitionsIni.GetStringListValue(TRANSLATIONS, key, string.Empty); // fail explicitly if the syntax is wrong if (parts.Length is < 2 or > 3 @@ -372,7 +374,7 @@ private List ParseTranslationGameFiles() public string GetGameExecutableName() { - string[] exeNames = clientDefinitionsIni.GetStringValue(SETTINGS, "GameExecutableNames", "Game.exe").Split(','); + string[] exeNames = clientDefinitionsIni.GetStringListValue(SETTINGS, "GameExecutableNames", "Game.exe"); return exeNames[0]; } @@ -405,12 +407,12 @@ public string GetGameExecutableName() /// /// List of files that are not distributed but required to play. /// - public string[] RequiredFiles => clientDefinitionsIni.GetStringValue(SETTINGS, "RequiredFiles", String.Empty).Split(','); + public string[] RequiredFiles => clientDefinitionsIni.GetStringListValue(SETTINGS, "RequiredFiles", String.Empty); /// /// List of files that can interfere with the mod functioning. /// - public string[] ForbiddenFiles => clientDefinitionsIni.GetStringValue(SETTINGS, "ForbiddenFiles", String.Empty).Split(','); + public string[] ForbiddenFiles => clientDefinitionsIni.GetStringListValue(SETTINGS, "ForbiddenFiles", String.Empty); /// /// The main map file extension that is read by the client. diff --git a/ClientCore/Extensions/IniFileExtensions.cs b/ClientCore/Extensions/IniFileExtensions.cs index b2547d6da..56447b781 100644 --- a/ClientCore/Extensions/IniFileExtensions.cs +++ b/ClientCore/Extensions/IniFileExtensions.cs @@ -1,4 +1,7 @@ using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + using Rampastring.Tools; namespace ClientCore.Extensions @@ -22,5 +25,18 @@ public static void RemoveAllKeys(this IniSection iniSection) foreach (KeyValuePair iniSectionKey in keys) iniSection.RemoveKey(iniSectionKey.Key); } + + public static string[] GetStringListValue(this IniFile iniFile, string section, string key, string defaultValue, char[] separators = null) + { + separators ??= [',']; + IniSection iniSection = iniFile.GetSection(section); + + return (iniSection?.GetStringValue(key, defaultValue) ?? defaultValue) + .Split(separators) + .ToList() + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); + } } } diff --git a/ClientCore/INIProcessing/IniPreprocessInfoStore.cs b/ClientCore/INIProcessing/IniPreprocessInfoStore.cs index b299e604a..cebe84fc3 100644 --- a/ClientCore/INIProcessing/IniPreprocessInfoStore.cs +++ b/ClientCore/INIProcessing/IniPreprocessInfoStore.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using ClientCore.Extensions; namespace ClientCore.INIProcessing { @@ -50,8 +51,7 @@ public void Load() var keys = iniFile.GetSectionKeys(ProcessedINIsSection); foreach (string key in keys) { - string[] values = iniFile.GetStringValue(ProcessedINIsSection, key, string.Empty).Split( - new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + string[] values = iniFile.GetStringListValue(ProcessedINIsSection, key, string.Empty); if (values.Length != 3) { diff --git a/ClientUpdater/CustomComponent.cs b/ClientUpdater/CustomComponent.cs index 5fab635e2..5cd0968b6 100644 --- a/ClientUpdater/CustomComponent.cs +++ b/ClientUpdater/CustomComponent.cs @@ -212,7 +212,7 @@ private async Task DoDownloadComponentAsync(CancellationToken cancellationToken) } var version = new IniFile(versionFileName); - string[] tmp = version.GetStringValue("AddOns", ININame, string.Empty).Split(','); + string[] tmp = version.GetStringListValue("AddOns", ININame, string.Empty); Updater.GetArchiveInfo(version, LocalPath, out string archiveID, out int archiveSize); UpdaterFileInfo info = Updater.CreateFileInfo(finalFileName, tmp[0], Conversions.IntFromString(tmp[1], 0), archiveID, archiveSize); diff --git a/ClientUpdater/Updater.cs b/ClientUpdater/Updater.cs index 6e3d21956..fef691f10 100644 --- a/ClientUpdater/Updater.cs +++ b/ClientUpdater/Updater.cs @@ -32,6 +32,7 @@ namespace ClientUpdater; using System.Threading.Tasks; using ClientUpdater.Compression; +using ClientCore.Extensions; using Rampastring.Tools; @@ -257,11 +258,11 @@ public static void CheckLocalFileVersions() if (sectionKeys != null) { + char[] separator = new char[] { ',' }; foreach (string str in sectionKeys) { - char[] separator = new char[] { ',' }; - string[] strArray = file.GetStringValue("FileVersions", str, string.Empty).Split(separator); - string[] strArrayArch = file.GetStringValue("ArchivedFiles", str, string.Empty).Split(separator); + string[] strArray = file.GetStringListValue("FileVersions", str, string.Empty, separator); + string[] strArrayArch = file.GetStringListValue("ArchivedFiles", str, string.Empty, separator); bool archiveAvailable = strArrayArch is { Length: >= 2 }; if (strArray.Length >= 2) diff --git a/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index 5403edb70..df0fc0a97 100644 --- a/DXMainClient/DXGUI/Generic/LoadingScreen.cs +++ b/DXMainClient/DXGUI/Generic/LoadingScreen.cs @@ -82,11 +82,7 @@ protected override void GetINIAttributes(IniFile iniFile) { base.GetINIAttributes(iniFile); - randomTextures = iniFile.GetStringValue(Name, "RandomBackgroundTextures", string.Empty) - .Split(',') - .Select(s => s.Trim()) - .Where(s => !string.IsNullOrEmpty(s)) - .ToList(); + randomTextures = iniFile.GetStringListValue(Name, "RandomBackgroundTextures", string.Empty).ToList(); if (randomTextures.Count == 0) return; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 7235cc5ef..d174e3501 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1076,7 +1076,7 @@ private void GetRandomSelectors(List selectorNames, List selector List randomSides = new List(); try { - string[] tmp = GameOptionsIni.GetStringValue("RandomSelectors", randomSelector, string.Empty).Split(','); + string[] tmp = GameOptionsIni.GetStringListValue("RandomSelectors", randomSelector, string.Empty); randomSides = Array.ConvertAll(tmp, int.Parse).Distinct().ToList(); randomSides.RemoveAll(x => (x >= SideCount || x < 0)); } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs index ff99dc45b..6caf8c942 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs @@ -61,7 +61,7 @@ static string Localize(XNAControl control, string attributeName, string defaultV { case "Items": string[] items = value.Split(','); - string[] itemLabels = iniFile.GetStringValue(Name, "ItemLabels", "").Split(','); + string[] itemLabels = iniFile.GetStringListValue(Name, "ItemLabels", ""); for (int i = 0; i < items.Length; i++) { bool hasLabel = itemLabels.Length > i && !string.IsNullOrEmpty(itemLabels[i]); diff --git a/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs b/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs index 1130b58f2..42ebe1935 100644 --- a/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs +++ b/DXMainClient/Domain/Multiplayer/CoopMapInfo.cs @@ -24,8 +24,8 @@ public CoopMapInfo() { } public void Initialize(IniSection section) { - DisallowedPlayerSides = section.GetListValue("DisallowedPlayerSides", ',', int.Parse); - DisallowedPlayerColors = section.GetListValue("DisallowedPlayerColors", ',', int.Parse); + 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/MapPreviewExtractor.cs b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs index 04f857d8a..664b1d4a9 100644 --- a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs +++ b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs @@ -5,6 +5,7 @@ using System.IO.Compression; using System.Text; using ClientCore; +using ClientCore.Extensions; using Rampastring.Tools; using lzo.net; using SixLabors.ImageSharp; @@ -43,7 +44,7 @@ public static Image ExtractMapPreview(IniFile mapIni) return null; } - string[] previewSizes = mapIni.GetStringValue("Preview", "Size", "").Split(','); + string[] previewSizes = mapIni.GetStringListValue("Preview", "Size", string.Empty); int previewWidth = previewSizes.Length > 3 ? Conversions.IntFromString(previewSizes[2], -1) : -1; int previewHeight = previewSizes.Length > 3 ? Conversions.IntFromString(previewSizes[3], -1) : -1; diff --git a/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs b/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs index e23204cc1..2510c5824 100644 --- a/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs +++ b/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs @@ -55,7 +55,7 @@ public static List LoadColors() foreach (string key in colorKeys) { - string[] values = gameOptionsIni.GetStringValue("MPColors", key, "255,255,255,0").Split(','); + string[] values = gameOptionsIni.GetStringListValue("MPColors", key, "255,255,255,0"); try { From 4cd232be49dfce36158d38890db67282703b839c Mon Sep 17 00:00:00 2001 From: SadPencil Date: Sat, 28 Jun 2025 19:51:46 +0800 Subject: [PATCH 24/26] RampastringToolsExtensions -> IniFileExtensions --- ClientCore/Extensions/IniFileExtensions.cs | 21 +++++++++++++--- .../Extensions/RampastringToolsExtensions.cs | 25 ------------------- 2 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 ClientCore/Extensions/RampastringToolsExtensions.cs 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/ClientCore/Extensions/RampastringToolsExtensions.cs b/ClientCore/Extensions/RampastringToolsExtensions.cs deleted file mode 100644 index 5e518b041..000000000 --- a/ClientCore/Extensions/RampastringToolsExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; - -using Rampastring.Tools; - -namespace ClientCore.Extensions -{ - public static class RampastringToolsExtensions - { - 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; - - - } -} From 37c83b601019b4839dbc18695ac5e617737baf2c Mon Sep 17 00:00:00 2001 From: SadPencil Date: Sat, 28 Jun 2025 20:32:16 +0800 Subject: [PATCH 25/26] Increase CurrentCustomMapCacheVersion --- DXMainClient/Domain/Multiplayer/MapLoader.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 337c95864..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 }; /// From 1d9a0dc40a4b36af7d4a108a705a561ef79010f6 Mon Sep 17 00:00:00 2001 From: SadPencil Date: Fri, 1 Aug 2025 16:45:44 +0800 Subject: [PATCH 26/26] Fix unexpected random location on AllowedStartingLocations --- DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 626dda3ef..d641f83f2 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1420,7 +1420,10 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa 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;