diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 25b8069ad..d80fb6a05 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -20,12 +20,14 @@ public class ClientConfiguration private const string CLIENT_SETTINGS = "DTACnCNetClient.ini"; private const string GAME_OPTIONS = "GameOptions.ini"; private const string CLIENT_DEFS = "ClientDefinitions.ini"; + private const string NETWORK_DEFS = "NetworkDefinitions.ini"; private static ClientConfiguration _instance; private IniFile gameOptions_ini; private IniFile DTACnCNetClient_ini; private IniFile clientDefinitionsIni; + private IniFile networkDefinitionsIni; protected ClientConfiguration() { @@ -36,7 +38,7 @@ protected ClientConfiguration() FileInfo clientDefinitionsFile = SafePath.GetFile(baseResourceDirectory.FullName, CLIENT_DEFS); - if (clientDefinitionsFile is null) + if (!(clientDefinitionsFile?.Exists ?? false)) throw new FileNotFoundException($"Couldn't find {CLIENT_DEFS} at {baseResourceDirectory}. Please verify that you're running the client from the correct directory."); clientDefinitionsIni = new IniFile(clientDefinitionsFile.FullName); @@ -44,6 +46,8 @@ protected ClientConfiguration() DTACnCNetClient_ini = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), CLIENT_SETTINGS)); gameOptions_ini = new IniFile(SafePath.CombineFilePath(baseResourceDirectory.FullName, GAME_OPTIONS)); + + networkDefinitionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), NETWORK_DEFS)); } /// @@ -99,7 +103,7 @@ public void RefreshSettings() public string AltUIBackgroundColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "AltUIBackgroundColor", "196,196,196"); - public string WindowBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "WindowBorderColor", "128,128,128"); + public string WindowBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "WindowBorderColor", "128,128,128"); public string PanelBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "PanelBorderColor", "255,255,255"); @@ -385,6 +389,37 @@ public IEnumerable SupplementalMapFileExtensions #endregion + #region Network definitions + + public string CnCNetTunnelListURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetTunnelListURL", "http://cncnet.org/master-list"); + + public string CnCNetPlayerCountURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetPlayerCountURL", "http://api.cncnet.org/status"); + + public string CnCNetMapDBDownloadURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetMapDBDownloadURL", "http://mapdb.cncnet.org"); + + public string CnCNetMapDBUploadURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetMapDBUploadURL", "http://mapdb.cncnet.org/upload"); + + public bool DisableDiscordIntegration => networkDefinitionsIni.GetBooleanValue(SETTINGS, "DisableDiscordIntegration", false); + + public List IRCServers => GetIRCServers(); + + #endregion + + public List GetIRCServers() + { + List servers = []; + + IniSection serversSection = networkDefinitionsIni.GetSection("IRCServers"); + if (serversSection != null) + foreach ((_, string value) in serversSection.Keys) + if (!string.IsNullOrWhiteSpace(value)) + servers.Add(value); + + return servers; + } + + public bool DiscordIntegrationGloballyDisabled => string.IsNullOrWhiteSpace(DiscordAppId) || DisableDiscordIntegration; + public OSVersion GetOperatingSystemVersion() { #if NETFRAMEWORK diff --git a/DTAConfig/OptionPanels/CnCNetOptionsPanel.cs b/DTAConfig/OptionPanels/CnCNetOptionsPanel.cs index 1ba5e8756..13ce2142f 100644 --- a/DTAConfig/OptionPanels/CnCNetOptionsPanel.cs +++ b/DTAConfig/OptionPanels/CnCNetOptionsPanel.cs @@ -140,7 +140,7 @@ private void InitOptions() chkConnectOnStartup.Bottom + 12, 0, 0); chkDiscordIntegration.Text = "Show detailed game info in Discord status".L10N("Client:DTAConfig:DiscordStatus"); - if (String.IsNullOrEmpty(ClientConfiguration.Instance.DiscordAppId)) + if (ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) { chkDiscordIntegration.AllowChecking = false; chkDiscordIntegration.Checked = false; @@ -317,7 +317,7 @@ public override void Load() chkSkipLoginWindow.Checked = IniSettings.SkipConnectDialog; chkPersistentMode.Checked = IniSettings.PersistentMode; - chkDiscordIntegration.Checked = !String.IsNullOrEmpty(ClientConfiguration.Instance.DiscordAppId) + chkDiscordIntegration.Checked = !ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled && IniSettings.DiscordIntegration; chkAllowGameInvitesFromFriendsOnly.Checked = IniSettings.AllowGameInvitesFromFriendsOnly; @@ -352,7 +352,7 @@ public override bool Save() IniSettings.SkipConnectDialog.Value = chkSkipLoginWindow.Checked; IniSettings.PersistentMode.Value = chkPersistentMode.Checked; - if (!String.IsNullOrEmpty(ClientConfiguration.Instance.DiscordAppId)) + if (!ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) { IniSettings.DiscordIntegration.Value = chkDiscordIntegration.Checked; } diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 795c55a66..8c0749a38 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -410,7 +410,7 @@ private void SettingsSaved(object sender, EventArgs e) if (!connectionManager.IsConnected) ProgramConstants.PLAYERNAME = UserINISettings.Instance.PlayerName; - if (UserINISettings.Instance.DiscordIntegration) + if (UserINISettings.Instance.DiscordIntegration && !ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) discordHandler.Connect(); else discordHandler.Disconnect(); diff --git a/DXMainClient/Domain/DiscordHandler.cs b/DXMainClient/Domain/DiscordHandler.cs index b87e2bbfa..7fc292a93 100644 --- a/DXMainClient/Domain/DiscordHandler.cs +++ b/DXMainClient/Domain/DiscordHandler.cs @@ -48,7 +48,7 @@ public RichPresence CurrentPresence /// public DiscordHandler() { - if (!UserINISettings.Instance.DiscordIntegration || string.IsNullOrEmpty(ClientConfiguration.Instance.DiscordAppId)) + if (!UserINISettings.Instance.DiscordIntegration || ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) return; InitializeClient(); diff --git a/DXMainClient/Domain/MainClientConstants.cs b/DXMainClient/Domain/MainClientConstants.cs index b35e5346c..f5f05ec76 100644 --- a/DXMainClient/Domain/MainClientConstants.cs +++ b/DXMainClient/Domain/MainClientConstants.cs @@ -12,7 +12,7 @@ namespace DTAClient.Domain { public static class MainClientConstants { - public const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; + public static string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; public static string GAME_NAME_LONG = "CnCNet Client"; public static string GAME_NAME_SHORT = "CnCNet"; @@ -92,6 +92,8 @@ public static void Initialize() CREDITS_URL = clientConfiguration.CreditsURL; + CNCNET_TUNNEL_LIST_URL = clientConfiguration.CnCNetTunnelListURL; + USE_ISOMETRIC_CELLS = clientConfiguration.UseIsometricCells; TDRA_WAYPOINT_COEFFICIENT = clientConfiguration.WaypointCoefficient; MAP_CELL_SIZE_X = clientConfiguration.MapCellSizeX; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 0bf357b90..b417dd1d9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -50,10 +50,15 @@ private static int GetCnCNetPlayerCount() { try { + // Don't fetch the player count if it is explicitly disabled + // For example, the official CnCNet server might be unavailable/unstable in a country with Internet censorship, + // which causes lags in the splash screen. In the worst case, say if packets are dropped, it waits until timeouts --- 30 seconds + if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetPlayerCountURL)) + return -1; + WebClient client = new WebClient(); + Stream data = client.OpenRead(ClientConfiguration.Instance.CnCNetPlayerCountURL); - Stream data = client.OpenRead("http://api.cncnet.org/status"); - string info = string.Empty; using (StreamReader reader = new StreamReader(data)) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index 7dfa4e8a2..99fb6905d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -36,8 +36,6 @@ public static class MapSharer private static readonly object locker = new object(); - private const string MAPDB_URL = "http://mapdb.cncnet.org/upload"; - /// /// Adds a map into the CnCNet map upload queue. /// @@ -78,8 +76,14 @@ private static void Upload(object mapAndGame) Logger.Log("MapSharer: Starting upload of " + map.BaseFilePath); - bool success = false; - string message = MapUpload(MAPDB_URL, map, myGameId, out success); + if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetMapDBUploadURL)) + { + Logger.Log("MapSharer: Upload URL is not configured."); + MapUploadFailed?.Invoke(null, new MapEventArgs(map)); + return; + } + + string message = MapUpload(ClientConfiguration.Instance.CnCNetMapDBUploadURL, map, myGameId, out bool success); if (success) { @@ -380,12 +384,22 @@ private static string DownloadMain(string sha1, string myGame, string mapName, o using (TWebClient webClient = new TWebClient()) { + // TODO enable proxy support for some users webClient.Proxy = null; + if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetMapDBDownloadURL)) + { + success = false; + Logger.Log("MapSharer: Download URL is not configured."); + return null; + } + + string url = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}.zip", ClientConfiguration.Instance.CnCNetMapDBDownloadURL, myGame, sha1); + try { - Logger.Log("MapSharer: Downloading URL: " + "http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip"); - webClient.DownloadFile("http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip", destinationFile.FullName); + Logger.Log($"MapSharer: Downloading URL: {url}"); + webClient.DownloadFile(url, destinationFile.FullName); } catch (Exception ex) { @@ -449,6 +463,7 @@ class TWebClient : WebClient public TWebClient() { + // TODO enable proxy support for some users this.Proxy = null; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 929a193b3..d569e017b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -143,48 +143,78 @@ private Task PingCurrentTunnelAsync(bool checkTunnelList = false) }); } - /// - /// Downloads and parses the list of CnCNet tunnels. - /// - /// A list of tunnel servers. - private List RefreshTunnels() - { - FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); - - List returnValue = new List(); + private bool OnlineTunnelDataAvailable => !string.IsNullOrWhiteSpace(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + private bool OfflineTunnelDataAvailable => SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache").Exists; + private byte[] GetRawTunnelDataOnline() + { WebClient client = new WebClient(); + return client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + } - byte[] data; + private byte[] GetRawTunnelDataOffline() + { + FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); + return File.ReadAllBytes(tunnelCacheFile.FullName); + } + private byte[] GetRawTunnelData(int retryCount = 2) + { Logger.Log("Fetching tunnel server info."); - try - { - data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); - } - catch (Exception ex) + if (OnlineTunnelDataAvailable) { - Logger.Log("Error when downloading tunnel server info: " + ex.ToString()); - Logger.Log("Retrying."); - try + for (int i = 0; i < retryCount; i++) { - data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); - } - catch - { - if (!tunnelCacheFile.Exists) + try { - Logger.Log("Tunnel cache file doesn't exist!"); - return returnValue; + byte[] data = GetRawTunnelDataOnline(); + return data; } - else + catch (Exception ex) { - Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); - data = File.ReadAllBytes(tunnelCacheFile.FullName); + Logger.Log("Error when downloading tunnel server info: " + ex.Message); + if (i < retryCount - 1) + Logger.Log("Retrying."); + else + Logger.Log("Fetching tunnel server list failed."); } } } + else + { + // Don't fetch the latest tunnel list if it is explicitly disabled + // For example, the official CnCNet server might be unavailable/unstable in a country with Internet censorship, + // where players might either establish a substitute server or manually distribute the tunnel cache file + Logger.Log("Fetching tunnel server list online is disabled."); + } + + if (OfflineTunnelDataAvailable) + { + Logger.Log("Using cached tunnel data."); + byte[] data = GetRawTunnelDataOffline(); + return data; + } + else + Logger.Log("Tunnel cache file doesn't exist!"); + + return null; + } + + + /// + /// Downloads and parses the list of CnCNet tunnels. + /// + /// A list of tunnel servers. + private List RefreshTunnels() + { + List returnValue = new List(); + + FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); + + byte[] data = GetRawTunnelData(); + if (data is null) + return returnValue; string convertedData = Encoding.Default.GetString(data); @@ -234,6 +264,7 @@ private List RefreshTunnels() } } + Logger.Log($"Successfully refreshed tunnel cache with {returnValue.Count} servers."); return returnValue; } diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 3eed41359..15ad88087 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -31,28 +31,32 @@ public Connection(IConnectionManager connectionManager) IConnectionManager connectionManager; + private static IList _servers = null; /// /// The list of CnCNet / GameSurge IRC servers to connect to. /// - private static readonly IList Servers = new List + private static IList Servers { - new Server("Burstfire.UK.EU.GameSurge.net", "GameSurge London, UK", new int[3] { 6667, 6668, 7000 }), - new Server("ColoCrossing.IL.US.GameSurge.net", "GameSurge Chicago, IL", new int[5] { 6660, 6666, 6667, 6668, 6669 }), - new Server("Gameservers.NJ.US.GameSurge.net", "GameSurge Newark, NJ", new int[7] { 6665, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("Krypt.CA.US.GameSurge.net", "GameSurge Santa Ana, CA", new int[4] { 6666, 6667, 6668, 6669 }), - new Server("NuclearFallout.WA.US.GameSurge.net", "GameSurge Seattle, WA", new int[2] { 6667, 5960 }), - new Server("Portlane.SE.EU.GameSurge.net", "GameSurge Stockholm, Sweden", new int[5] { 6660, 6666, 6667, 6668, 6669 }), - new Server("Prothid.NY.US.GameSurge.Net", "GameSurge NYC, NY", new int[7] { 5960, 6660, 6666, 6667, 6668, 6669, 6697 }), - new Server("TAL.DE.EU.GameSurge.net", "GameSurge Wuppertal, Germany", new int[5] { 6660, 6666, 6667, 6668, 6669 }), - new Server("208.167.237.120", "GameSurge IP 208.167.237.120", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("192.223.27.109", "GameSurge IP 192.223.27.109", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("108.174.48.100", "GameSurge IP 108.174.48.100", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("208.146.35.105", "GameSurge IP 208.146.35.105", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("195.8.250.180", "GameSurge IP 195.8.250.180", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("91.217.189.76", "GameSurge IP 91.217.189.76", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("195.68.206.250", "GameSurge IP 195.68.206.250", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("irc.gamesurge.net", "GameSurge", new int[1] { 6667 }), - }.AsReadOnly(); + get + { + if (_servers is not null) + return _servers; + + IEnumerable serversList; + if (ClientConfiguration.Instance.IRCServers.Count > 0) + serversList = ClientConfiguration.Instance.IRCServers; + else + { + // fallback to the hardcoded servers list + serversList = [ + "irc.gamesurge.net|GameSurge|6667,6660,6666,6668,6669", + ]; + } + + _servers = serversList.Select(Server.Deserialize).ToList(); + return _servers; + } + } bool _isConnected = false; public bool IsConnected diff --git a/DXMainClient/Online/Server.cs b/DXMainClient/Online/Server.cs index e603b8135..96cc3c51b 100644 --- a/DXMainClient/Online/Server.cs +++ b/DXMainClient/Online/Server.cs @@ -1,4 +1,6 @@ -namespace DTAClient.Online +using System; + +namespace DTAClient.Online { /// /// A struct containing information on an IRC server. @@ -15,5 +17,21 @@ public Server(string host, string name, int[] ports) public string Host; public string Name; public int[] Ports; + + public string Serialize() => FormattableString.Invariant($"{Host}|{Name}|{string.Join(",", Ports)}"); + + public static Server Deserialize(string serialized) + { + string[] parts = serialized.Split('|'); + string host = parts[0]; + string name = parts[1]; + string[] portStrings = parts[2].Split(','); + int[] ports = new int[portStrings.Length]; + + for (int i = 0; i < portStrings.Length; i++) + ports[i] = int.Parse(portStrings[i]); + + return new Server(host, name, ports); + } } } diff --git a/DXMainClient/Resources/DTA/NetworkDefinitions.ini b/DXMainClient/Resources/DTA/NetworkDefinitions.ini new file mode 100644 index 000000000..7d4490937 --- /dev/null +++ b/DXMainClient/Resources/DTA/NetworkDefinitions.ini @@ -0,0 +1,9 @@ +[Settings] +CnCNetTunnelListURL=http://cncnet.org/master-list +CnCNetPlayerCountURL=http://api.cncnet.org/status +CnCNetMapDBDownloadURL=http://mapdb.cncnet.org +CnCNetMapDBUploadURL=http://mapdb.cncnet.org/upload +DisableDiscordIntegration=False + +[IRCServers] +1=irc.gamesurge.net|GameSurge|6667,6660,6666,6668,6669 \ No newline at end of file