From 2af2064a761e2e0992cec14e409896cb5a5cad89 Mon Sep 17 00:00:00 2001 From: devo1929 Date: Wed, 11 May 2022 22:31:38 -0400 Subject: [PATCH] Quick match --- ClientCore/ClientConfiguration.cs | 2 + ClientCore/Exceptions/ClientException.cs | 11 + .../Exceptions/ClientRequestException.cs | 14 + ClientGUI/Parser.cs | 8 +- ClientGUI/XNAMessageBox.cs | 7 +- ClientGUI/XNAScrollablePanel.cs | 12 + DXMainClient/DXGUI/Generic/LoadingScreen.cs | 47 ++- DXMainClient/DXGUI/Generic/MainMenu.cs | 26 +- DXMainClient/DXGUI/Generic/TopBar.cs | 5 + .../QuickMatch/QuickMatchLobbyFooterPanel.cs | 37 ++ .../QuickMatch/QuickMatchLobbyPanel.cs | 399 ++++++++++++++++++ .../QuickMatch/QuickMatchLoginPanel.cs | 100 +++++ .../QuickMatch/QuickMatchMapList.cs | 120 ++++++ .../QuickMatch/QuickMatchMapListItem.cs | 148 +++++++ .../QuickMatchMapListItemDropDown.cs | 21 + .../QuickMatch/QuickMatchStatusOverlay.cs | 170 ++++++++ .../QuickMatch/QuickMatchWindow.cs | 92 ++++ DXMainClient/DXMainClient.csproj | 2 + .../EventArgs/QmStatusMessageEventArgs.cs | 17 + .../Events/IQmOverlayLoadingStatusEvent.cs | 6 + .../Events/QmCancelingRequestMatchEvent.cs | 6 + .../Models/Events/QmErrorMessageEvent.cs | 14 + .../QuickMatch/Models/Events/QmEvent.cs | 6 + .../Models/Events/QmLadderMapsEvent.cs | 13 + .../Models/Events/QmLadderSelectedEvent.cs | 11 + .../Models/Events/QmLadderStatsEvent.cs | 11 + .../Events/QmLaddersAndUserAccountsEvent.cs | 16 + .../Models/Events/QmLoadingLadderMapsEvent.cs | 5 + .../Events/QmLoadingLadderStatsEvent.cs | 5 + .../QmLoadingLaddersAndUserAccountsEvent.cs | 5 + .../Models/Events/QmLoggingInEvent.cs | 5 + .../QuickMatch/Models/Events/QmLoginEvent.cs | 6 + .../QuickMatch/Models/Events/QmLogoutEvent.cs | 5 + .../Models/Events/QmRequestResponseEvent.cs | 11 + .../Models/Events/QmRequestingMatchEvent.cs | 13 + .../CnCNet/QuickMatch/Models/QmAuthData.cs | 9 + .../CnCNet/QuickMatch/Models/QmData.cs | 11 + .../CnCNet/QuickMatch/Models/QmLadder.cs | 53 +++ .../CnCNet/QuickMatch/Models/QmLadderMap.cs | 65 +++ .../CnCNet/QuickMatch/Models/QmLadderRules.cs | 75 ++++ .../CnCNet/QuickMatch/Models/QmLadderStats.cs | 26 ++ .../QuickMatch/Models/QmLoginRequest.cs | 13 + .../CnCNet/QuickMatch/Models/QmMap.cs | 19 + .../CnCNet/QuickMatch/Models/QmRequest.cs | 67 +++ .../QuickMatch/Models/QmRequestResponse.cs | 38 ++ .../CnCNet/QuickMatch/Models/QmSettings.cs | 43 ++ .../CnCNet/QuickMatch/Models/QmSide.cs | 19 + .../CnCNet/QuickMatch/Models/QmUserAccount.cs | 22 + .../QuickMatch/Models/QmUserSettings.cs | 11 + .../CnCNet/QuickMatch/QmApiService.cs | 161 +++++++ .../CnCNet/QuickMatch/QmRequestTypes.cs | 9 + .../CnCNet/QuickMatch/QmResponseTypes.cs | 12 + .../CnCNet/QuickMatch/QmService.cs | 226 ++++++++++ .../CnCNet/QuickMatch/QmSettingsService.cs | 110 +++++ .../CnCNet/QuickMatch/QmStrings.cs | 66 +++ .../QuickMatch/QmUserSettingsService.cs | 80 ++++ DXMainClient/Domain/Multiplayer/MapLoader.cs | 11 + Docs/INISystem.md | 238 +++++------ build/AfterPublish.targets | 5 +- 59 files changed, 2625 insertions(+), 140 deletions(-) create mode 100644 ClientCore/Exceptions/ClientException.cs create mode 100644 ClientCore/Exceptions/ClientRequestException.cs create mode 100644 ClientGUI/XNAScrollablePanel.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyFooterPanel.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLoginPanel.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItemDropDown.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusOverlay.cs create mode 100644 DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmStatusMessageEventArgs.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/IQmOverlayLoadingStatusEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmCancelingRequestMatchEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmErrorMessageEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderMapsEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderSelectedEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderStatsEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLaddersAndUserAccountsEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderMapsEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderStatsEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLaddersAndUserAccountsEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoggingInEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoginEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLogoutEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestResponseEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestingMatchEvent.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmAuthData.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmData.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadder.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderMap.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderRules.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStats.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLoginRequest.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmMap.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequest.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequestResponse.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSettings.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSide.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserAccount.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserSettings.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmApiService.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestTypes.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmResponseTypes.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmService.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettingsService.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStrings.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettingsService.cs diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 574c14e0c..5b8997eba 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -252,6 +252,8 @@ public string GetThemePath(string themeName) public string MPMapsIniPath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, "MPMapsPath", SafePath.CombineFilePath("INI", "MPMaps.ini"))); + public string QuickMatchPath => clientDefinitionsIni.GetStringValue(SETTINGS, "QuickMatchPath", "INI/QuickMatch.ini"); + public string KeyboardINI => clientDefinitionsIni.GetStringValue(SETTINGS, "KeyboardINI", "Keyboard.ini"); public int MinimumIngameWidth => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumIngameWidth", 640); diff --git a/ClientCore/Exceptions/ClientException.cs b/ClientCore/Exceptions/ClientException.cs new file mode 100644 index 000000000..dbcaf3b5a --- /dev/null +++ b/ClientCore/Exceptions/ClientException.cs @@ -0,0 +1,11 @@ +using System; + +namespace ClientCore.Exceptions +{ + public class ClientException : Exception + { + public ClientException(string message, Exception innerException = null) : base(message, innerException) + { + } + } +} diff --git a/ClientCore/Exceptions/ClientRequestException.cs b/ClientCore/Exceptions/ClientRequestException.cs new file mode 100644 index 000000000..552e34d66 --- /dev/null +++ b/ClientCore/Exceptions/ClientRequestException.cs @@ -0,0 +1,14 @@ +using System.Net; + +namespace ClientCore.Exceptions +{ + public class ClientRequestException : ClientException + { + public HttpStatusCode? StatusCode { get; } + + public ClientRequestException(string message, HttpStatusCode? statusCode = null) : base(message) + { + StatusCode = statusCode; + } + } +} diff --git a/ClientGUI/Parser.cs b/ClientGUI/Parser.cs index bd23ca5b3..a91a46f0b 100644 --- a/ClientGUI/Parser.cs +++ b/ClientGUI/Parser.cs @@ -24,6 +24,7 @@ using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; +using System.Text.RegularExpressions; namespace ClientGUI { @@ -69,6 +70,9 @@ private XNAControl GetControl(string controlName) if (controlName == primaryControl.Name) return primaryControl; + if (controlName == primaryControl.Parent.Name) + return primaryControl.Parent; + var control = Find(primaryControl.Children, controlName); if (control == null) throw new KeyNotFoundException($"Control '{controlName}' not found while parsing input '{Input}'"); @@ -104,7 +108,7 @@ public void SetPrimaryControl(XNAControl primaryControl) public int GetExprValue(string input, XNAControl parsingControl) { this.parsingControl = parsingControl; - Input = input; + Input = Regex.Replace(input, @"\s", ""); tokenPlace = 0; return GetExprValue(); } @@ -115,8 +119,6 @@ private int GetExprValue() while (true) { - SkipWhitespace(); - if (IsEndOfInput()) return value; diff --git a/ClientGUI/XNAMessageBox.cs b/ClientGUI/XNAMessageBox.cs index 06351c588..c4afa1b0d 100644 --- a/ClientGUI/XNAMessageBox.cs +++ b/ClientGUI/XNAMessageBox.cs @@ -262,7 +262,10 @@ private static void MsgBox_OKClicked(XNAMessageBox messageBox) /// The caption of the message box. /// The description in the message box. /// The XNAMessageBox instance that is created. - public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description) + public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description) + => ShowYesNoDialog(windowManager, caption, description, null); + + public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description, Action yesAction) { var panel = new DarkeningPanel(windowManager); windowManager.AddAndInitializeControl(panel); @@ -274,6 +277,8 @@ public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string panel.AddChild(msgBox); msgBox.YesClickedAction = MsgBox_YesClicked; + if (yesAction != null) + msgBox.YesClickedAction += yesAction; msgBox.NoClickedAction = MsgBox_NoClicked; return msgBox; diff --git a/ClientGUI/XNAScrollablePanel.cs b/ClientGUI/XNAScrollablePanel.cs new file mode 100644 index 000000000..d9dc190ff --- /dev/null +++ b/ClientGUI/XNAScrollablePanel.cs @@ -0,0 +1,12 @@ +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace ClientGUI; + +public class XNAScrollablePanel : XNAPanel +{ + public XNAScrollablePanel(WindowManager windowManager) : base(windowManager) + { + DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index 357eb8c8c..508cd1434 100644 --- a/DXMainClient/DXGUI/Generic/LoadingScreen.cs +++ b/DXMainClient/DXGUI/Generic/LoadingScreen.cs @@ -7,6 +7,7 @@ using DTAClient.DXGUI.Multiplayer; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; +using DTAClient.DXGUI.Multiplayer.QuickMatch; using DTAClient.Online; using DTAConfig; using Microsoft.Xna.Framework; @@ -14,6 +15,7 @@ using System.Threading.Tasks; using Rampastring.Tools; using ClientUpdater; +using Rampastring.XNAUI.XNAControls; using SkirmishLobby = DTAClient.DXGUI.Multiplayer.GameLobby.SkirmishLobby; namespace DTAClient.DXGUI.Generic @@ -79,26 +81,20 @@ private void LogGameClientVersion() private void LoadMaps() { - mapLoader = new MapLoader(); + mapLoader = MapLoader.GetInstance(); mapLoader.LoadMaps(); } private void Finish() { - ProgramConstants.GAME_VERSION = ClientConfiguration.Instance.ModMode ? + ProgramConstants.GAME_VERSION = ClientConfiguration.Instance.ModMode ? "N/A" : Updater.GameVersion; DiscordHandler discordHandler = null; if (!string.IsNullOrEmpty(ClientConfiguration.Instance.DiscordAppId)) discordHandler = new DiscordHandler(WindowManager); - ClientGUICreator.Instance.AddControl(typeof(GameLobbyCheckBox)); - ClientGUICreator.Instance.AddControl(typeof(GameLobbyDropDown)); - ClientGUICreator.Instance.AddControl(typeof(MapPreviewBox)); - ClientGUICreator.Instance.AddControl(typeof(GameLaunchButton)); - ClientGUICreator.Instance.AddControl(typeof(ChatListBox)); - ClientGUICreator.Instance.AddControl(typeof(XNAChatTextBox)); - ClientGUICreator.Instance.AddControl(typeof(PlayerExtraOptionsPanel)); + DeclareCustomControls(); var gameCollection = new GameCollection(); gameCollection.Initialize(); @@ -109,7 +105,7 @@ private void Finish() var cncnetManager = new CnCNetManager(WindowManager, gameCollection, cncnetUserData); var tunnelHandler = new TunnelHandler(WindowManager, cncnetManager); var privateMessageHandler = new PrivateMessageHandler(cncnetManager, cncnetUserData); - + var topBar = new TopBar(WindowManager, cncnetManager, privateMessageHandler); var optionsWindow = new OptionsWindow(WindowManager, gameCollection, topBar); @@ -120,23 +116,26 @@ private void Finish() var cncnetGameLobby = new CnCNetGameLobby(WindowManager, "MultiplayerGameLobby", topBar, cncnetManager, tunnelHandler, gameCollection, cncnetUserData, mapLoader, discordHandler, pmWindow); - var cncnetGameLoadingLobby = new CnCNetGameLoadingLobby(WindowManager, + var cncnetGameLoadingLobby = new CnCNetGameLoadingLobby(WindowManager, topBar, cncnetManager, tunnelHandler, mapLoader.GameModes, gameCollection, discordHandler); - var cncnetLobby = new CnCNetLobby(WindowManager, cncnetManager, + var cncnetLobby = new CnCNetLobby(WindowManager, cncnetManager, cncnetGameLobby, cncnetGameLoadingLobby, topBar, pmWindow, tunnelHandler, gameCollection, cncnetUserData, optionsWindow); var gipw = new GameInProgressWindow(WindowManager); var skirmishLobby = new SkirmishLobby(WindowManager, topBar, mapLoader, discordHandler); + var quickMatchWindow = new QuickMatchWindow(WindowManager); topBar.SetSecondarySwitch(cncnetLobby); - var mainMenu = new MainMenu(WindowManager, skirmishLobby, lanLobby, + var mainMenu = new MainMenu(WindowManager, skirmishLobby, quickMatchWindow, lanLobby, topBar, optionsWindow, cncnetLobby, cncnetManager, discordHandler); WindowManager.AddAndInitializeControl(mainMenu); DarkeningPanel.AddAndInitializeWithControl(WindowManager, skirmishLobby); + DarkeningPanel.AddAndInitializeWithControl(WindowManager, quickMatchWindow); + DarkeningPanel.AddAndInitializeWithControl(WindowManager, cncnetGameLoadingLobby); DarkeningPanel.AddAndInitializeWithControl(WindowManager, cncnetGameLobby); @@ -152,9 +151,11 @@ private void Finish() topBar.SetTertiarySwitch(pmWindow); topBar.SetOptionsWindow(optionsWindow); + topBar.SetQuickMatchWindow(quickMatchWindow); WindowManager.AddAndInitializeControl(gipw); skirmishLobby.Disable(); + quickMatchWindow.Disable(); cncnetLobby.Disable(); cncnetGameLobby.Disable(); cncnetGameLoadingLobby.Disable(); @@ -183,6 +184,26 @@ private void Finish() Cursor.Visible = visibleSpriteCursor; } + private static void DeclareCustomControls() + { + ClientGUICreator.Instance.AddControl(typeof(GameLobbyCheckBox)); + ClientGUICreator.Instance.AddControl(typeof(GameLobbyDropDown)); + ClientGUICreator.Instance.AddControl(typeof(MapPreviewBox)); + ClientGUICreator.Instance.AddControl(typeof(GameLaunchButton)); + ClientGUICreator.Instance.AddControl(typeof(ChatListBox)); + ClientGUICreator.Instance.AddControl(typeof(XNAChatTextBox)); + ClientGUICreator.Instance.AddControl(typeof(XNAScrollBar)); + ClientGUICreator.Instance.AddControl(typeof(XNAPasswordBox)); + ClientGUICreator.Instance.AddControl(typeof(PlayerExtraOptionsPanel)); + ClientGUICreator.Instance.AddControl(typeof(QuickMatchLoginPanel)); + ClientGUICreator.Instance.AddControl(typeof(QuickMatchLobbyPanel)); + ClientGUICreator.Instance.AddControl(typeof(QuickMatchLobbyFooterPanel)); + ClientGUICreator.Instance.AddControl(typeof(QuickMatchMapList)); + ClientGUICreator.Instance.AddControl(typeof(QuickMatchStatusOverlay)); + ClientGUICreator.Instance.AddControl(typeof(XNAClientTabControl)); + ClientGUICreator.Instance.AddControl(typeof(XNAScrollablePanel)); + } + public override void Update(GameTime gameTime) { base.Update(gameTime); diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index b890e5bb8..3fb8470bc 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -5,6 +5,7 @@ using DTAClient.DXGUI.Multiplayer; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; +using DTAClient.DXGUI.Multiplayer.QuickMatch; using DTAClient.Online; using DTAConfig; using Localization; @@ -36,12 +37,20 @@ class MainMenu : XNAWindow, ISwitchable /// /// Creates a new instance of the main menu. /// - public MainMenu(WindowManager windowManager, SkirmishLobby skirmishLobby, - LANLobby lanLobby, TopBar topBar, OptionsWindow optionsWindow, + public MainMenu( + WindowManager windowManager, + SkirmishLobby skirmishLobby, + QuickMatchWindow quickMatchWindow, + LANLobby lanLobby, + TopBar topBar, + OptionsWindow optionsWindow, CnCNetLobby cncnetLobby, - CnCNetManager connectionManager, DiscordHandler discordHandler) : base(windowManager) + CnCNetManager connectionManager, + DiscordHandler discordHandler + ) : base(windowManager) { this.skirmishLobby = skirmishLobby; + this.quickMatchWindow = quickMatchWindow; this.lanLobby = lanLobby; this.topBar = topBar; this.connectionManager = connectionManager; @@ -62,6 +71,8 @@ public MainMenu(WindowManager windowManager, SkirmishLobby skirmishLobby, private SkirmishLobby skirmishLobby; + private QuickMatchWindow quickMatchWindow; + private LANLobby lanLobby; private CnCNetManager connectionManager; @@ -106,6 +117,7 @@ private bool UpdateInProgress private XNAClientButton btnLoadGame; private XNAClientButton btnSkirmish; private XNAClientButton btnCnCNet; + private XNAClientButton btnQuickmatch; private XNAClientButton btnLan; private XNAClientButton btnOptions; private XNAClientButton btnMapEditor; @@ -154,6 +166,11 @@ public override void Initialize() btnCnCNet.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnCnCNet.LeftClick += BtnCnCNet_LeftClick; + btnQuickmatch = new XNAClientButton(WindowManager); + btnQuickmatch.Name = nameof(btnQuickmatch); + btnQuickmatch.LeftClick += BtnQuickmatch_LeftClick; + btnQuickmatch.Disable(); + btnLan = new XNAClientButton(WindowManager); btnLan.Name = nameof(btnLan); btnLan.IdleTexture = AssetLoader.LoadTexture("MainMenu/lan.png"); @@ -225,6 +242,7 @@ public override void Initialize() AddChild(btnLoadGame); AddChild(btnSkirmish); AddChild(btnCnCNet); + AddChild(btnQuickmatch); AddChild(btnLan); AddChild(btnOptions); AddChild(btnMapEditor); @@ -799,6 +817,8 @@ private void BtnLan_LeftClick(object sender, EventArgs e) private void BtnCnCNet_LeftClick(object sender, EventArgs e) => topBar.SwitchToSecondary(); + private void BtnQuickmatch_LeftClick(object sender, EventArgs e) => quickMatchWindow.Enable(); + private void BtnSkirmish_LeftClick(object sender, EventArgs e) { skirmishLobby.Open(); diff --git a/DXMainClient/DXGUI/Generic/TopBar.cs b/DXMainClient/DXGUI/Generic/TopBar.cs index ab8fadcc6..370fb6756 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -10,6 +10,7 @@ using ClientCore; using System.Threading; using DTAClient.Domain.Multiplayer.CnCNet; +using DTAClient.DXGUI.Multiplayer.QuickMatch; using DTAClient.Online.EventArguments; using DTAConfig; using Localization; @@ -53,6 +54,7 @@ PrivateMessageHandler privateMessageHandler private ISwitchable privateMessageSwitch; private OptionsWindow optionsWindow; + private QuickMatchWindow quickMatchWindow; private XNAClientButton btnMainButton; private XNAClientButton btnCnCNetLobby; @@ -107,6 +109,8 @@ public void SetOptionsWindow(OptionsWindow optionsWindow) optionsWindow.EnabledChanged += OptionsWindow_EnabledChanged; } + public void SetQuickMatchWindow(QuickMatchWindow quickMatchWindow) => this.quickMatchWindow = quickMatchWindow; + private void OptionsWindow_EnabledChanged(object sender, EventArgs e) { if (!lanMode) @@ -324,6 +328,7 @@ private void BtnMainButton_LeftClick(object sender, EventArgs e) LastSwitchType = SwitchType.PRIMARY; cncnetLobbySwitch.SwitchOff(); privateMessageSwitch.SwitchOff(); + quickMatchWindow.Disable(); primarySwitches[primarySwitches.Count - 1].SwitchOn(); // HACK warning diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyFooterPanel.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyFooterPanel.cs new file mode 100644 index 000000000..05c30a199 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyFooterPanel.cs @@ -0,0 +1,37 @@ +using System; +using ClientGUI; +using Rampastring.XNAUI; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch; + +public class QuickMatchLobbyFooterPanel : INItializableWindow +{ + private XNAClientButton btnQuickMatch; + private XNAClientButton btnLogout; + private XNAClientButton btnExit; + + public EventHandler LogoutEvent; + + public EventHandler ExitEvent; + + public EventHandler QuickMatchEvent; + + public QuickMatchLobbyFooterPanel(WindowManager windowManager) : base(windowManager) + { + } + + public override void Initialize() + { + IniNameOverride = nameof(QuickMatchLobbyFooterPanel); + base.Initialize(); + + btnLogout = FindChild(nameof(btnLogout)); + btnLogout.LeftClick += (_, _) => LogoutEvent?.Invoke(this, null); + + btnExit = FindChild(nameof(btnExit)); + btnExit.LeftClick += (_, _) => ExitEvent?.Invoke(this, null); + + btnQuickMatch = FindChild(nameof(btnQuickMatch)); + btnQuickMatch.LeftClick += (_, _) => QuickMatchEvent?.Invoke(this, null); + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs new file mode 100644 index 000000000..8c10437f1 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ClientCore.Exceptions; +using ClientGUI; +using DTAClient.Domain.Multiplayer; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; +using Localization; +using Rampastring.Tools; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; +using Timer = System.Timers.Timer; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchLobbyPanel : INItializableWindow + { + private readonly QmService qmService; + private readonly MapLoader mapLoader; + + public event EventHandler Exit; + public event EventHandler LogoutEvent; + + private QuickMatchMapList mapList; + private QuickMatchLobbyFooterPanel footerPanel; + private XNAClientDropDown ddUserAccounts; + private XNAClientDropDown ddNicknames; + private XNAClientDropDown ddSides; + private XNAPanel mapPreviewBox; + private XNAPanel settingsPanel; + private XNAClientButton btnMap; + private XNAClientButton btnSettings; + private XNALabel lblStats; + + private readonly EnhancedSoundEffect matchFoundSoundEffect; + private readonly QmSettings qmSettings; + + public QuickMatchLobbyPanel(WindowManager windowManager) : base(windowManager) + { + qmService = QmService.GetInstance(); + qmService.QmEvent += HandleQmEvent; + + mapLoader = MapLoader.GetInstance(); + + qmSettings = QmSettingsService.GetInstance().GetSettings(); + matchFoundSoundEffect = new EnhancedSoundEffect(qmSettings.MatchFoundSoundFile); + + IniNameOverride = nameof(QuickMatchLobbyPanel); + } + + public override void Initialize() + { + base.Initialize(); + + mapList = FindChild(nameof(mapList)); + mapList.MapSelectedEvent += HandleMapSelectedEventEvent; + + footerPanel = FindChild(nameof(footerPanel)); + footerPanel.ExitEvent += (sender, args) => Exit?.Invoke(sender, args); + footerPanel.LogoutEvent += BtnLogout_LeftClick; + footerPanel.QuickMatchEvent += BtnQuickMatch_LeftClick; + + ddUserAccounts = FindChild(nameof(ddUserAccounts)); + ddUserAccounts.SelectedIndexChanged += UserAccountSelected; + + ddNicknames = FindChild(nameof(ddNicknames)); + + ddSides = FindChild(nameof(ddSides)); + + mapPreviewBox = FindChild(nameof(mapPreviewBox)); + mapPreviewBox.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.CENTERED; + + settingsPanel = FindChild(nameof(settingsPanel)); + + btnMap = FindChild(nameof(btnMap)); + btnMap.LeftClick += (_, _) => EnableRightPanel(mapPreviewBox); + + btnSettings = FindChild(nameof(btnSettings)); + btnSettings.LeftClick += (_, _) => EnableRightPanel(settingsPanel); + + lblStats = FindChild(nameof(lblStats)); + + EnabledChanged += EnabledChangedEvent; + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmLadderMapsEvent e: + HandleLadderMapsEvent(e.LadderMaps); + return; + case QmLadderStatsEvent e: + HandleLadderStatsEvent(e.LadderStats); + return; + case QmLoadingLadderStatsEvent: + HandleLoadingLadderStatsEvent(); + return; + case QmRequestResponseEvent e: + HandleQuickMatchRequestResponseEvent(e.Response); + return; + case QmLaddersAndUserAccountsEvent e: + HandleLoadLadderAndUserAccountsEvent(e); + return; + case QmLadderSelectedEvent e: + HandleLadderSelectedEvent(e.Ladder); + return; + case QmLoginEvent: + Enable(); + return; + case QmLogoutEvent: + HandleLogoutEvent(); + return; + } + } + + private void EnableRightPanel(XNAControl control) + { + foreach (XNAControl parentChild in control.Parent.Children) + parentChild.Disable(); + + control.Enable(); + } + + private void EnabledChangedEvent(object sender, EventArgs e) + { + if (!Enabled) + return; + + LoadLaddersAndUserAccountsAsync(); + } + + private void LoadLaddersAndUserAccountsAsync() + { + qmService.LoadLaddersAndUserAccountsAsync(); + } + + private void BtnQuickMatch_LeftClick(object sender, EventArgs eventArgs) + => RequestQuickMatch(); + + private void RequestQuickMatch() + { + QmRequest matchRequest = GetMatchRequest(); + if (matchRequest == null) + return; + + qmService.RequestMatchAsync(matchRequest); + } + + private void HandleQuickMatchResponse(QmRequestResponse qmRequestResponse) + { + switch (true) + { + case true when qmRequestResponse.IsError: + case true when qmRequestResponse.IsFatal: + HandleQuickMatchErrorResponse(qmRequestResponse); + return; + case true when qmRequestResponse.IsUpdate: + HandleQuickMatchUpdateResponse(qmRequestResponse); + return; + case true when qmRequestResponse.IsSpawn: + HandleQuickMatchSpawnResponse(qmRequestResponse); + return; + case true when qmRequestResponse.IsQuit: + HandleQuickMatchQuitResponse(qmRequestResponse); + return; + case true when qmRequestResponse.IsQuit: + HandleQuickMatchQuitResponse(qmRequestResponse); + return; + case true when qmRequestResponse.IsWait: + HandleQuickMatchWaitResponse(qmRequestResponse); + return; + } + } + + private void HandleQuickMatchSpawnResponse(QmRequestResponse qmRequestResponse) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, "qm spawn"); + } + + private void HandleQuickMatchUpdateResponse(QmRequestResponse qmRequestResponse) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, "qm update"); + } + + private void HandleQuickMatchQuitResponse(QmRequestResponse qmRequestResponse) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, "qm quit"); + } + + private void HandleQuickMatchWaitResponse(QmRequestResponse qmRequestResponse) + { + SoundPlayer.Play(matchFoundSoundEffect); + Thread.Sleep(qmRequestResponse.CheckBack * 1000); + RequestQuickMatch(); + } + + private void HandleQuickMatchErrorResponse(QmRequestResponse qmRequestResponse) + { + Console.WriteLine(); + // qmService.ClearStatus(); + // XNAMessageBox.Show(WindowManager, QuickMatchWindow.ErrorMessageTitle, qmRequestResponse.Message ?? qmRequestResponse.Description ?? QmStrings.UnknownError); + } + + private void HandleQuickMatchRequestResponseEvent(QmRequestResponse response) + => HandleQuickMatchResponse(response); + + private QmRequest GetMatchRequest() + { + QmUserAccount userAccount = GetSelectedUserAccount(); + if (userAccount == null) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, QmStrings.NoLadderSelectedError); + return null; + } + + QmSide side = GetSelectedSide(); + if (side == null) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, QmStrings.NoSideSelectedError); + return null; + } + + return new QmRequest { Ladder = userAccount.Ladder.Abbreviation, PlayerName = userAccount.Username, Side = side.LocalId }; + } + + private QmSide GetSelectedSide() + => ddSides.SelectedItem?.Tag as QmSide; + + private QmUserAccount GetSelectedUserAccount() + => ddUserAccounts.SelectedItem?.Tag as QmUserAccount; + + private void BtnLogout_LeftClick(object sender, EventArgs eventArgs) + { + XNAMessageBox.ShowYesNoDialog(WindowManager, QmStrings.ConfirmationCaption, QmStrings.LogoutConfirmation, box => + { + qmService.Logout(); + LogoutEvent?.Invoke(this, null); + }); + } + + private void UserAccountsUpdated(IEnumerable userAccounts) + { + ddUserAccounts.Items.Clear(); + foreach (QmUserAccount userAccount in userAccounts) + { + ddUserAccounts.AddItem(new XNADropDownItem() { Text = userAccount.Ladder.Name, Tag = userAccount }); + } + + if (ddUserAccounts.Items.Count == 0) + return; + + string cachedLadder = qmService.GetCachedLadder(); + if (!string.IsNullOrEmpty(cachedLadder)) + ddUserAccounts.SelectedIndex = ddUserAccounts.Items.FindIndex(i => (i.Tag as QmUserAccount)?.Ladder.Abbreviation == cachedLadder); + + if (ddUserAccounts.SelectedIndex < 0) + ddUserAccounts.SelectedIndex = 0; + } + + /// + /// Called when the QM service has finished the login process + /// + /// + /// + private void HandleLoadLadderAndUserAccountsEvent(QmLaddersAndUserAccountsEvent e) + => UserAccountsUpdated(e.UserAccounts); + + /// + /// Called when the user has selected a UserAccount from the drop down + /// + /// + /// + private void UserAccountSelected(object sender, EventArgs eventArgs) + { + if (ddUserAccounts.SelectedItem?.Tag is not QmUserAccount selectedUserAccount) + return; + + UpdateNickames(selectedUserAccount); + UpdateSides(selectedUserAccount); + mapList.Clear(); + qmService.SetLadder(selectedUserAccount.Ladder); + } + + private void LoadLadderMapsAsync(QmLadder ladder) => qmService.LoadLadderMapsForAbbrAsync(ladder.Abbreviation); + + private void LoadLadderStatsAsync(QmLadder ladder) => qmService.LoadLadderStatsForAbbrAsync(ladder.Abbreviation); + + /// + /// Update the nicknames drop down + /// + /// + private void UpdateNickames(QmUserAccount selectedUserAccount) + { + ddNicknames.Items.Clear(); + + ddNicknames.AddItem(new XNADropDownItem() { Text = selectedUserAccount.Username, Tag = selectedUserAccount }); + + ddNicknames.SelectedIndex = 0; + } + + /// + /// Update the top Sides dropdown + /// + /// + private void UpdateSides(QmUserAccount selectedUserAccount) + { + ddSides.Items.Clear(); + + var ladder = qmService.GetLadderForId(selectedUserAccount.LadderId); + + foreach (QmSide side in ladder.Sides) + { + ddSides.AddItem(new XNADropDownItem { Text = side.Name, Tag = side }); + } + + if (ddSides.Items.Count > 0) + ddSides.SelectedIndex = 0; + } + + /// + /// Called when the QM service has loaded new ladder maps for the ladder selected + /// + /// + /// + private void HandleLadderMapsEvent(IEnumerable maps) + { + mapList.Clear(); + var ladderMaps = maps?.ToList() ?? new List(); + if (!ladderMaps.Any()) + return; + + if (ddUserAccounts.SelectedItem?.Tag is not QmUserAccount selectedUserAccount) + return; + + var ladder = qmService.GetLadderForId(selectedUserAccount.LadderId); + + mapList.AddItems(ladderMaps.Select(ladderMap => new QuickMatchMapListItem(WindowManager, ladderMap, ladder))); + } + + /// + /// Called when the QM service has loaded new ladder maps for the ladder selected + /// + private void HandleLadderStatsEvent(QmLadderStats stats) + => lblStats.Text = stats == null ? "No stats found..." : $"Players waiting: {stats.QueuedPlayerCount}, Recent matches: {stats.RecentMatchCount}"; + + /// + /// Called when the QM service has loaded new ladder maps for the ladder selected + /// + private void HandleLoadingLadderStatsEvent() => lblStats.Text = QmStrings.LoadingStats; + + private void HandleLadderSelectedEvent(QmLadder ladder) + { + LoadLadderMapsAsync(ladder); + LoadLadderStatsAsync(ladder); + } + + private void HandleMatchedEvent() + { + } + + /// + /// Called when the user selects a map in the list + /// + /// + /// + private void HandleMapSelectedEventEvent(object sender, QmMap qmMap) + { + if (qmMap == null) + return; + + Map map = mapLoader.GetMapForSHA(qmMap.Hash); + + mapPreviewBox.BackgroundTexture = map?.LoadPreviewTexture(); + EnableRightPanel(mapPreviewBox); + } + + private void HandleLogoutEvent() + { + Disable(); + ClearForLogout(); + } + + public void ClearForLogout() + { + ddNicknames.Items.Clear(); + ddNicknames.SelectedIndex = -1; + ddSides.Items.Clear(); + ddSides.SelectedIndex = -1; + ddUserAccounts.Items.Clear(); + ddUserAccounts.SelectedIndex = -1; + mapList.Clear(); + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLoginPanel.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLoginPanel.cs new file mode 100644 index 000000000..99ca5f594 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLoginPanel.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading.Tasks; +using ClientCore.Exceptions; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchLoginPanel : INItializableWindow + { + public event EventHandler Exit; + private const string LoginErrorTitle = "Login Error"; + private readonly QmService qmService; + + private XNATextBox tbEmail; + private XNAPasswordBox tbPassword; + private bool loginInitialized; + + public event EventHandler LoginEvent; + + public QuickMatchLoginPanel(WindowManager windowManager) : base(windowManager) + { + qmService = QmService.GetInstance(); + qmService.QmEvent += HandleQmEvent; + IniNameOverride = nameof(QuickMatchLoginPanel); + } + + public override void Initialize() + { + base.Initialize(); + + XNAClientButton btnLogin; + btnLogin = FindChild(nameof(btnLogin)); + btnLogin.LeftClick += BtnLogin_LeftClick; + + XNAClientButton btnCancel; + btnCancel = FindChild(nameof(btnCancel)); + btnCancel.LeftClick += (_, _) => Exit?.Invoke(this, null); + + tbEmail = FindChild(nameof(tbEmail)); + tbEmail.Text = qmService.GetCachedEmail() ?? string.Empty; + + tbPassword = FindChild(nameof(tbPassword)); + + EnabledChanged += InitLogin; + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmLoginEvent: + Disable(); + return; + case QmLogoutEvent: + Enable(); + return; + } + } + + public void InitLogin(object sender, EventArgs eventArgs) + { + if (!Enabled || loginInitialized) + return; + + if (qmService.IsLoggedIn()) + qmService.RefreshAsync(); + + loginInitialized = true; + } + + private void BtnLogin_LeftClick(object sender, EventArgs eventArgs) + { + if (!ValidateForm()) + return; + + qmService.LoginAsync(tbEmail.Text, tbPassword.Password); + } + + private bool ValidateForm() + { + if (string.IsNullOrEmpty(tbEmail.Text)) + { + XNAMessageBox.Show(WindowManager, "No Email specified", LoginErrorTitle); + return false; + } + + if (string.IsNullOrEmpty(tbPassword.Text)) + { + XNAMessageBox.Show(WindowManager, "No Password specified", LoginErrorTitle); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs new file mode 100644 index 000000000..cb5858caf --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchMapList : INItializableWindow + { + private const int MouseScrollRate = 6; + public const int ItemHeight = 22; + public event EventHandler MapSelectedEvent; + + private XNALabel lblVeto; + private XNALabel lblSides; + private XNALabel lblMaps; + private XNAScrollablePanel mapListPanel; + public XNAScrollBar scrollBar { get; private set; } + + public int VetoX => lblVeto?.X ?? 0; + public int VetoWidth => lblVeto?.Width ?? 0; + public int SidesX => lblSides?.X ?? 0; + public int SidesWidth => lblSides?.Width ?? 0; + public int MapsX => lblMaps?.X ?? 0; + public int MapsWidth => lblMaps?.Width ?? 0; + + public QuickMatchMapList(WindowManager windowManager) : base(windowManager) + { + } + + public override void Initialize() + { + base.Initialize(); + + lblVeto = FindChild(nameof(lblVeto)); + lblSides = FindChild(nameof(lblSides)); + lblMaps = FindChild(nameof(lblMaps)); + scrollBar = FindChild(nameof(scrollBar)); + mapListPanel = FindChild(nameof(mapListPanel)); + + MouseScrolled += OnMouseScrolled; + } + + private void OnMouseScrolled(object sender, EventArgs e) + { + int viewTop = GetNewScrollBarViewTop(); + if (viewTop == scrollBar.ViewTop) + return; + + scrollBar.ViewTop = viewTop; + + foreach (QuickMatchMapListItem quickMatchMapListItem in MapItemChildren.ToList()) + { + quickMatchMapListItem.CloseDropDowns(); + } + } + + public void AddItems(IEnumerable listItems) + { + foreach (QuickMatchMapListItem quickMatchMapListItem in listItems) + AddItem(quickMatchMapListItem); + } + + private void AddItem(QuickMatchMapListItem listItem) + { + listItem.LeftClickMap += MapItem_LeftClick; + listItem.SetParentList(this); + mapListPanel.AddChild(listItem); + } + + public override void Draw(GameTime gameTime) + { + var children = MapItemChildren.ToList(); + scrollBar.Length = children.Count * ItemHeight; + scrollBar.DisplayedPixelCount = mapListPanel.Height - 4; + scrollBar.Refresh(); + for (int i = 0; i < children.Count; i++) + children[i].ClientRectangle = new Rectangle(0, (i * ItemHeight) - scrollBar.ViewTop, Width - scrollBar.ScrollWidth, ItemHeight); + + base.Draw(gameTime); + } + + private int GetNewScrollBarViewTop() + { + int scrollWheelValue = Cursor.ScrollWheelValue; + int viewTop = scrollBar.ViewTop - (scrollWheelValue * MouseScrollRate); + int maxViewTop = scrollBar.Length - scrollBar.DisplayedPixelCount; + + if (viewTop < 0) + viewTop = 0; + else if (viewTop > maxViewTop) + viewTop = maxViewTop; + + return viewTop; + } + + private void MapItem_LeftClick(object sender, EventArgs eventArgs) + { + var selectedItem = sender as QuickMatchMapListItem; + foreach (QuickMatchMapListItem quickMatchMapItem in MapItemChildren) + quickMatchMapItem.Selected = quickMatchMapItem == selectedItem; + + MapSelectedEvent?.Invoke(this, selectedItem?.Map); + } + + public void Clear() + { + foreach (QuickMatchMapListItem child in MapItemChildren.ToList()) + mapListPanel.RemoveChild(child); + } + + private IEnumerable MapItemChildren + => mapListPanel.Children.Select(c => c as QuickMatchMapListItem).Where(i => i != null); + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs new file mode 100644 index 000000000..b919adac5 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs @@ -0,0 +1,148 @@ +using System; +using System.Linq; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchMapListItem : XNAPanel + { + public event EventHandler LeftClickMap; + + private readonly QmLadderMap ladderMap; + private readonly QmLadder ladder; + private XNAClientCheckBox cbVeto; + private QuickMatchMapListItemDropDown ddSide; + private XNAPanel panelMap; + private XNALabel lblMap; + private Color defaultTextColor; + + private XNAPanel topBorder; + private XNAPanel bottomBorder; + private XNAPanel rightBorder; + + private QuickMatchMapList ParentList; + + public QmMap Map => ladderMap.Map; + + private bool selected; + + public int OpenedDownWindowBottom => GetWindowRectangle().Bottom + (ddSide.ItemHeight * ddSide.Items.Count); + + public bool Selected + { + get => selected; + set + { + selected = value; + panelMap.BackgroundTexture = selected ? AssetLoader.CreateTexture(new Color(255, 0, 0), 1, 1) : null; + } + } + + public QuickMatchMapListItem(WindowManager windowManager, QmLadderMap ladderMap, QmLadder ladder) : base(windowManager) + { + this.ladderMap = ladderMap; + this.ladder = ladder; + } + + public override void Initialize() + { + base.Initialize(); + DrawBorders = false; + + topBorder = new XNAPanel(WindowManager); + topBorder.DrawBorders = true; + AddChild(topBorder); + + bottomBorder = new XNAPanel(WindowManager); + bottomBorder.DrawBorders = true; + AddChild(bottomBorder); + + rightBorder = new XNAPanel(WindowManager); + rightBorder.DrawBorders = true; + AddChild(rightBorder); + + cbVeto = new XNAClientCheckBox(WindowManager); + cbVeto.CheckedChanged += CbVeto_CheckChanged; + + ddSide = new QuickMatchMapListItemDropDown(WindowManager); + defaultTextColor = ddSide.TextColor; + AddChild(ddSide); + + panelMap = new XNAPanel(WindowManager); + panelMap.LeftClick += Map_LeftClicked; + panelMap.DrawBorders = false; + AddChild(panelMap); + + lblMap = new XNALabel(WindowManager); + lblMap.LeftClick += Map_LeftClicked; + lblMap.ClientRectangle = new Rectangle(4, 2, panelMap.Width, panelMap.Height); + panelMap.AddChild(lblMap); + AddChild(cbVeto); + + InitUI(); + } + + public void SetParentList(QuickMatchMapList parentList) => ParentList = parentList; + + public override void Draw(GameTime gameTime) + { + ddSide.OpenUp = OpenedDownWindowBottom > ParentList.scrollBar.GetWindowRectangle().Bottom; + + + base.Draw(gameTime); + } + + private void CbVeto_CheckChanged(object sender, EventArgs e) + { + ddSide.TextColor = cbVeto.Checked ? UISettings.ActiveSettings.DisabledItemColor : defaultTextColor; + lblMap.TextColor = cbVeto.Checked ? UISettings.ActiveSettings.DisabledItemColor : defaultTextColor; + ddSide.AllowDropDown = !cbVeto.Checked; + } + + private void Map_LeftClicked(object sender, EventArgs eventArgs) => LeftClickMap?.Invoke(this, EventArgs.Empty); + + private void InitUI() + { + ddSide.Items.Clear(); + foreach (int ladderMapAllowedSideId in ladderMap.AllowedSideIds) + { + var side = ladder.Sides.FirstOrDefault(s => s.LocalId == ladderMapAllowedSideId); + if (side == null) + continue; + + ddSide.AddItem(new XNADropDownItem() + { + Text = side.Name, + Tag = side + }); + } + + if (ddSide.Items.Count > 0) + ddSide.SelectedIndex = 0; + + lblMap.Text = ladderMap.Description; + + cbVeto.ClientRectangle = new Rectangle(ParentList.VetoX, 0, ParentList.VetoWidth, QuickMatchMapList.ItemHeight); + ddSide.ClientRectangle = new Rectangle(ParentList.SidesX, 0, ParentList.SidesWidth, QuickMatchMapList.ItemHeight); + panelMap.ClientRectangle = new Rectangle(ParentList.MapsX, 0, ParentList.MapsWidth, QuickMatchMapList.ItemHeight); + + topBorder.ClientRectangle = new Rectangle(panelMap.X, panelMap.Y, panelMap.Width, 1); + bottomBorder.ClientRectangle = new Rectangle(panelMap.X, panelMap.Bottom, panelMap.Width, 1); + rightBorder.ClientRectangle = new Rectangle(panelMap.Right, panelMap.Y, 1, panelMap.Height); + } + + public bool IsVetoed() => cbVeto.Checked; + + public bool ContainsPointVertical(Point point) => Y < point.Y && Y + Height < point.Y; + + public void CloseDropDowns() + { + ddSide.Close(); + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItemDropDown.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItemDropDown.cs new file mode 100644 index 000000000..83579988c --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItemDropDown.cs @@ -0,0 +1,21 @@ +using ClientGUI; +using Rampastring.XNAUI; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch; + +public class QuickMatchMapListItemDropDown : XNAClientDropDown +{ + public QuickMatchMapListItemDropDown(WindowManager windowManager) : base(windowManager) + { + } + + public override void OnMouseScrolled() + { + // override this to prevent mouse scrolling on the drop down + } + + public void Close() + { + CloseDropDown(); + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusOverlay.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusOverlay.cs new file mode 100644 index 000000000..fefb8da05 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusOverlay.cs @@ -0,0 +1,170 @@ +using System; +using System.Linq; +using System.Timers; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchStatusOverlay : INItializableWindow + { + private int DefaultInternalWidth; + + private XNAPanel statusOverlayBox { get; set; } + + private XNALabel statusMessage { get; set; } + + private XNAClientButton cancelBtn { get; set; } + + private Action cancelAction { get; set; } + + private Type lastQmLoadingEventType { get; set; } + + private string currentMessage { get; set; } + + private readonly Timer loadingMessageTimer; + + private const int loadingMessageAppendLength = 4; + + public QuickMatchStatusOverlay(WindowManager windowManager) : base(windowManager) + { + QmService.GetInstance().QmEvent += HandleQmEvent; + + loadingMessageTimer = new Timer(500); + loadingMessageTimer.Enabled = true; + loadingMessageTimer.AutoReset = true; + loadingMessageTimer.Elapsed += AppendLoadingMessage; + } + + public override void Initialize() + { + base.Initialize(); + + statusOverlayBox = FindChild(nameof(statusOverlayBox)); + DefaultInternalWidth = statusOverlayBox.ClientRectangle.Width; + + statusMessage = FindChild(nameof(statusMessage)); + + cancelBtn = FindChild(nameof(cancelBtn)); + cancelBtn.LeftClick += CancelButton_LeftClick; + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmLoggingInEvent: + HandleLoggingInEvent(); + break; + case QmLoginEvent: + HandleLoginEvent(); + break; + case QmLoadingLaddersAndUserAccountsEvent: + HandleLoadingLaddersAndUserAccountsEvent(); + break; + case QmLaddersAndUserAccountsEvent: + HandleLaddersAndUserAccountsEvent(); + break; + case QmLoadingLadderMapsEvent: + HandleLoadingLadderMapsEvent(); + break; + case QmLadderMapsEvent: + HandleLadderMapsEvent(); + break; + case QmRequestingMatchEvent e: + HandleRequestingMatchEvent(e); + break; + case QmCancelingRequestMatchEvent: + HandleCancelingMatchRequest(); + break; + case QmErrorMessageEvent: + Disable(); + return; + case QmRequestResponseEvent e: + HandleRequestResponseEvent(e); + return; + } + + if (qmEvent is IQmOverlayLoadingStatusEvent) + lastQmLoadingEventType = qmEvent.GetType(); + } + + private void HandleLoggingInEvent() => SetStatus(QmStrings.LoggingInStatus); + + private void HandleLoginEvent() => CloseIfLastEventType(typeof(QmLoggingInEvent)); + + private void HandleLoadingLaddersAndUserAccountsEvent() => SetStatus(QmStrings.LoadingLaddersAndAccountsStatus); + + private void HandleLaddersAndUserAccountsEvent() => CloseIfLastEventType(typeof(QmLoadingLaddersAndUserAccountsEvent)); + + private void HandleLoadingLadderMapsEvent() => SetStatus(QmStrings.LoadingLadderMapsStatus); + + private void HandleLadderMapsEvent() => CloseIfLastEventType(typeof(QmLoadingLadderMapsEvent)); + + private void HandleRequestingMatchEvent(QmRequestingMatchEvent e) => SetStatus(QmStrings.RequestingMatchStatus, e.CancelAction); + + private void HandleRequestResponseEvent(QmRequestResponseEvent e) + { + QmRequestResponse response = e.Response; + switch (true) + { + case true when response.IsWait: + return; // need to keep the overlay open while waiting + default: + CloseIfLastEventType(typeof(QmRequestingMatchEvent), typeof(QmCancelingRequestMatchEvent)); + return; + } + } + + private void HandleCancelingMatchRequest() => SetStatus(QmStrings.CancelingMatchRequestStatus); + + private void CloseIfLastEventType(params Type[] lastEventType) + { + if (lastEventType.Any(t => t == lastQmLoadingEventType)) + Disable(); + } + + private void SetStatus(string message, Action messageCancelAction = null) + { + currentMessage = message; + statusMessage.Text = message; + cancelAction = messageCancelAction; + if (cancelAction != null) + cancelBtn.Enable(); + else + cancelBtn.Disable(); + + ResizeForText(); + Enable(); + } + + private void AppendLoadingMessage(object sender, ElapsedEventArgs elapsedEventArgs) + { + if (string.IsNullOrEmpty(currentMessage)) + return; + + int maxLength = currentMessage.Length + loadingMessageAppendLength; + statusMessage.Text = statusMessage.Text.Length >= maxLength ? statusMessage.Text.Substring(0, currentMessage.Length) : statusMessage.Text + "."; + } + + private void ResizeForText() + { + Vector2 textDimensions = Renderer.GetTextDimensions(statusMessage.Text, statusMessage.FontIndex); + + statusOverlayBox.Width = (int)Math.Max(DefaultInternalWidth, textDimensions.X + loadingMessageAppendLength + 60); + statusOverlayBox.X = (Width / 2) - (statusOverlayBox.Width / 2); + } + + private void CancelButton_LeftClick(object sender, EventArgs eventArgs) + { + Disable(); + statusMessage.Text = string.Empty; + cancelAction?.Invoke(); + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs new file mode 100644 index 000000000..8731c7b3b --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; +using ClientCore.Exceptions; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; +using Timer = System.Timers.Timer; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchWindow : INItializableWindow + { + private readonly QmService qmService; + private readonly QmSettingsService qmSettingsService; + + private QuickMatchLoginPanel loginPanel; + + private QuickMatchLobbyPanel lobbyPanel; + + private XNAPanel headerGameLogo; + + public QuickMatchWindow(WindowManager windowManager) : base(windowManager) + { + qmService = QmService.GetInstance(); + qmService.QmEvent += HandleQmEvent; + qmSettingsService = QmSettingsService.GetInstance(); + } + + public override void Initialize() + { + Name = nameof(QuickMatchWindow); + BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1); + PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; + + base.Initialize(); + + loginPanel = FindChild(nameof(loginPanel)); + loginPanel.Exit += (sender, args) => Disable(); + + lobbyPanel = FindChild(nameof(lobbyPanel)); + lobbyPanel.Exit += (sender, args) => Disable(); + + headerGameLogo = FindChild(nameof(headerGameLogo)); + + WindowManager.CenterControlOnScreen(this); + + EnabledChanged += EnabledChangedEvent; + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmLadderSelectedEvent e: + HandleLadderSelectedEvent(e.Ladder); + return; + case QmErrorMessageEvent e: + HandleErrorMessageEvent(e); + return; + } + } + + private void HandleErrorMessageEvent(QmErrorMessageEvent e) + => XNAMessageBox.Show(WindowManager, e.ErrorTitle, e.ErrorMessage); + + private void HandleLadderSelectedEvent(QmLadder ladder) + { + headerGameLogo.BackgroundTexture = qmSettingsService.GetSettings().GetLadderHeaderLogo(ladder.Abbreviation); + if (headerGameLogo.BackgroundTexture == null) + return; + + // Resize image to ensure proper ratio and spacing from right edge + float imageRatio = (float)headerGameLogo.BackgroundTexture.Width / headerGameLogo.BackgroundTexture.Height; + int newImageWidth = (int)imageRatio * headerGameLogo.Height; + headerGameLogo.ClientRectangle = new Rectangle(headerGameLogo.Parent.Right - newImageWidth - headerGameLogo.Parent.X, headerGameLogo.Y, newImageWidth, headerGameLogo.Height); + } + + private void EnabledChangedEvent(object sender, EventArgs e) + { + if (!Enabled) + return; + + loginPanel.Enable(); + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index b8108e0a4..6e5dcc672 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -43,9 +43,11 @@ + + diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmStatusMessageEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmStatusMessageEventArgs.cs new file mode 100644 index 000000000..3e441eb20 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmStatusMessageEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmStatusMessageEventArgs : EventArgs + { + public string Message { get; } + + public Action CancelAction { get; } + + public QmStatusMessageEventArgs(string message, Action cancelAction = null) + { + Message = message; + CancelAction = cancelAction; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/IQmOverlayLoadingStatusEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/IQmOverlayLoadingStatusEvent.cs new file mode 100644 index 000000000..eba55092b --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/IQmOverlayLoadingStatusEvent.cs @@ -0,0 +1,6 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public interface IQmOverlayLoadingStatusEvent +{ + +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmCancelingRequestMatchEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmCancelingRequestMatchEvent.cs new file mode 100644 index 000000000..90fb8cb40 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmCancelingRequestMatchEvent.cs @@ -0,0 +1,6 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmCancelingRequestMatchEvent : QmEvent +{ + +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmErrorMessageEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmErrorMessageEvent.cs new file mode 100644 index 000000000..58e47bb1a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmErrorMessageEvent.cs @@ -0,0 +1,14 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmErrorMessageEvent : QmEvent +{ + public string ErrorTitle { get; } + + public string ErrorMessage { get; } + + public QmErrorMessageEvent(string errorMessage, string errorTitle = null) + { + ErrorMessage = errorMessage; + ErrorTitle = errorTitle ?? QmStrings.GenericErrorTitle; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmEvent.cs new file mode 100644 index 000000000..2e44336a1 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmEvent.cs @@ -0,0 +1,6 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public abstract class QmEvent +{ + +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderMapsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderMapsEvent.cs new file mode 100644 index 000000000..679ed4c1f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderMapsEvent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLadderMapsEvent : QmEvent +{ + public IEnumerable LadderMaps { get; } + + public QmLadderMapsEvent(IEnumerable qmLadderMaps) + { + LadderMaps = qmLadderMaps; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderSelectedEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderSelectedEvent.cs new file mode 100644 index 000000000..3243e939a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderSelectedEvent.cs @@ -0,0 +1,11 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLadderSelectedEvent : QmEvent +{ + public QmLadder Ladder { get; } + + public QmLadderSelectedEvent(QmLadder qmLadder) + { + Ladder = qmLadder; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderStatsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderStatsEvent.cs new file mode 100644 index 000000000..e645ae13d --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLadderStatsEvent.cs @@ -0,0 +1,11 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLadderStatsEvent : QmEvent +{ + public QmLadderStats LadderStats { get; } + + public QmLadderStatsEvent(QmLadderStats ladderStats) + { + LadderStats = ladderStats; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLaddersAndUserAccountsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLaddersAndUserAccountsEvent.cs new file mode 100644 index 000000000..22faddafd --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLaddersAndUserAccountsEvent.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLaddersAndUserAccountsEvent : QmEvent +{ + public IEnumerable Ladders { get; } + + public IEnumerable UserAccounts { get; } + + public QmLaddersAndUserAccountsEvent(IEnumerable ladders, IEnumerable userAccounts) + { + Ladders = ladders; + UserAccounts = userAccounts; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderMapsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderMapsEvent.cs new file mode 100644 index 000000000..0edbb3904 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderMapsEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLoadingLadderMapsEvent : QmEvent, IQmOverlayLoadingStatusEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderStatsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderStatsEvent.cs new file mode 100644 index 000000000..508dc9d6b --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLadderStatsEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLoadingLadderStatsEvent : QmEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLaddersAndUserAccountsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLaddersAndUserAccountsEvent.cs new file mode 100644 index 000000000..c5ed27ea6 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoadingLaddersAndUserAccountsEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLoadingLaddersAndUserAccountsEvent : QmEvent, IQmOverlayLoadingStatusEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoggingInEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoggingInEvent.cs new file mode 100644 index 000000000..5a2eff542 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoggingInEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLoggingInEvent : QmEvent, IQmOverlayLoadingStatusEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoginEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoginEvent.cs new file mode 100644 index 000000000..42ad67a7c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLoginEvent.cs @@ -0,0 +1,6 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLoginEvent : QmEvent +{ + public string ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLogoutEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLogoutEvent.cs new file mode 100644 index 000000000..a4ffa138f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmLogoutEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmLogoutEvent : QmEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestResponseEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestResponseEvent.cs new file mode 100644 index 000000000..38dcb70ae --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestResponseEvent.cs @@ -0,0 +1,11 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmRequestResponseEvent : QmEvent +{ + public QmRequestResponse Response { get; } + + public QmRequestResponseEvent(QmRequestResponse response) + { + Response = response; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestingMatchEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestingMatchEvent.cs new file mode 100644 index 000000000..7b6955fdf --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/Events/QmRequestingMatchEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; + +public class QmRequestingMatchEvent : QmEvent, IQmOverlayLoadingStatusEvent +{ + public Action CancelAction { get; } + + public QmRequestingMatchEvent(Action cancelAction) + { + CancelAction = cancelAction; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmAuthData.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmAuthData.cs new file mode 100644 index 000000000..d5313ce49 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmAuthData.cs @@ -0,0 +1,9 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmAuthData + { + public string Token { get; set; } + public string Email { get; set; } + public string Name { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmData.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmData.cs new file mode 100644 index 000000000..21afe7308 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmData.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmData + { + public List Ladders { get; set; } + + public List UserAccounts { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadder.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadder.cs new file mode 100644 index 000000000..b13d61719 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadder.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmLadder + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("abbreviation")] + public string Abbreviation { get; set; } + + [JsonProperty("game")] + public string Game { get; set; } + + [JsonProperty("clans_allowed")] + private int clansAllowed { get; set; } + + [JsonIgnore] + public bool ClansAllowed => clansAllowed == 1; + + [JsonProperty("game_object_schema_id")] + public int GameObjectSchemaId { get; set; } + + [JsonProperty("map_pool_id")] + public int MapPoolId { get; set; } + + [JsonProperty("private")] + private int _private { get; set; } + + [JsonIgnore] + public bool IsPrivate => _private == 1; + + [JsonProperty("sides")] + public IEnumerable Sides { get; set; } + + [JsonProperty("vetoes")] + public int VetoesRemaining { get; set; } + + [JsonProperty("allowed_sides")] + public IEnumerable AllowedSideLocalIds { get; set; } + + [JsonProperty("current")] + public string Current { get; set; } + + [JsonProperty("qm_ladder_rules")] + public QmLadderRules LadderRules { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderMap.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderMap.cs new file mode 100644 index 000000000..b07cdc83c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderMap.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmLadderMap + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("map_id")] + public int MapId { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("bit_idx")] + public int BitIndex { get; set; } + + [JsonProperty("valid")] + private int valid { get; set; } + + [JsonIgnore] + public bool IsValid => valid == 1; + + [JsonProperty("spawn_order")] + public string SpawnOrder { get; set; } + + [JsonProperty("team1_spawn_order")] + public string Team1SpawnOrder { get; set; } + + [JsonProperty("team2_spawn_order")] + public string Team2SpawnOrder { get; set; } + + [JsonProperty("allowed_sides")] + public IEnumerable AllowedSideIds { get; set; } + + [JsonProperty("admin_description")] + public string AdminDescription { get; set; } + + [JsonProperty("map_pool_id")] + public int MapPoolId { get; set; } + + [JsonProperty("rejectable")] + private int rejectable { get; set; } + + [JsonIgnore] + public bool IsRejectable => rejectable == 1; + + [JsonProperty("default_reject")] + private int defaultReject { get; set; } + + [JsonIgnore] + public bool IsDefaultReject => defaultReject == 1; + + [JsonProperty("hash")] + public string Hash { get; set; } + + [JsonProperty("map")] + public QmMap Map { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderRules.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderRules.cs new file mode 100644 index 000000000..d62704c67 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderRules.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmLadderRules + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("player_count")] + public int PlayerCount { get; set; } + + [JsonProperty("map_vetoes")] + public int MapVetoes { get; set; } + + [JsonProperty("max_difference")] + public int MaxDifference { get; set; } + + [JsonProperty("all_sides")] + private string allSides { get; set; } + + [JsonIgnore] + public IEnumerable AllSides => allSides?.Split(',').Select(int.Parse) ?? new List(); + + [JsonProperty("allowed_sides")] + private string allowedSides { get; set; } + + [JsonIgnore] + public IEnumerable AllowedSides => allSides?.Split(',').Select(int.Parse) ?? new List(); + + [JsonProperty("bail_time")] + public int BailTime { get; set; } + + [JsonProperty("bail_fps")] + public int BailFps { get; set; } + + [JsonProperty("tier2_rating")] + public int Tier2Rating { get; set; } + + [JsonProperty("rating_per_second")] + public double RatingPerSecond { get; set; } + + [JsonProperty("max_points_difference")] + public int MaxPointsDifference { get; set; } + + [JsonProperty("points_per_second")] + public int PointsPerSecond { get; set; } + + [JsonProperty("use_elo_points")] + private int useEloPoints { get; set; } + + [JsonIgnore] + public bool UseEloPoints => useEloPoints == 1; + + [JsonProperty("wol_k")] + public int WolK { get; set; } + + [JsonProperty("show_map_preview")] + private int showMapPreview { get; set; } + + [JsonIgnore] + public bool ShowMapPreview => showMapPreview == 1; + + [JsonProperty("reduce_map_repeats")] + private int reduceMapRepeats { get; set; } + + [JsonIgnore] + public bool ReduceMapRepeats => reduceMapRepeats == 1; + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStats.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStats.cs new file mode 100644 index 000000000..82f351e02 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStats.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmLadderStats + { + [JsonProperty("recentMatchedPlayers")] + public int RecentMatchedPlayerCount { get; set; } + + [JsonProperty("queuedPlayers")] + public int QueuedPlayerCount { get; set; } + + [JsonProperty("past24hMatches")] + public int Past24HourMatchCount { get; set; } + + [JsonProperty("recentMatches")] + public int RecentMatchCount { get; set; } + + [JsonProperty("activeMatches")] + public int ActiveMatchCount { get; set; } + + [JsonProperty("time")] + public DateTime DateTime { get; set; } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLoginRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLoginRequest.cs new file mode 100644 index 000000000..987ecbde7 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLoginRequest.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QMLoginRequest + { + [JsonProperty("email")] + public string Email { get; set; } + + [JsonProperty("password")] + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmMap.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmMap.cs new file mode 100644 index 000000000..406e2e967 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmMap.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmMap + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("hash")] + public string Hash { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequest.cs new file mode 100644 index 000000000..93ff59727 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequest.cs @@ -0,0 +1,67 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmRequest + { + [JsonProperty("type")] + public string Type { get; set; } = QmRequestTypes.MatchMeUp; + + [JsonProperty("lan_ip")] + public string LanIP { get; set; } + + [JsonProperty("lan_port")] + public string LanPort { get; set; } + + [JsonProperty("ipv6_address")] + public string IPv6Address { get; set; } + + [JsonProperty("ipv6_port")] + public string IPv6Port { get; set; } + + [JsonProperty("ip_address")] + public string IPAddress { get; set; } + + [JsonProperty("ip_port")] + public string IPPort { get; set; } + + [JsonProperty("side")] + public int Side { get; set; } + + [JsonProperty("map_bitfield")] + public string MapBitfield { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } = "2.0"; + + [JsonProperty("platform")] + public string Platform { get; set; } + + [JsonProperty("map_sides")] + public string[] MapSides { get; set; } + + [JsonProperty("ai_dat")] + public string CheatSeen { get; set; } + + [JsonProperty("exe_hash")] + public string ExeHash { get; set; } + + [JsonProperty("ddraw")] + public string DDrawHash { get; set; } + + [JsonProperty("session")] + public string Session { get; set; } + + [JsonIgnore] + public string Ladder { get; set; } + + [JsonIgnore] + public string PlayerName { get; set; } + + private bool IsType(string type) => Type == type; + + public bool IsMatchMeUp() => IsType(QmRequestTypes.MatchMeUp); + + public bool IsQuit() => IsType(QmRequestTypes.Quit); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequestResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequestResponse.cs new file mode 100644 index 000000000..1a0a76f81 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmRequestResponse.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmRequestResponse + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("checkback")] + public int CheckBack { get; set; } + + [JsonProperty("no_sooner_than")] + public int NoSoonerThan { get; set; } + + public bool IsError => IsType(QmResponseTypes.Error); + + public bool IsFatal => IsType(QmResponseTypes.Fatal); + + public bool IsSpawn => IsType(QmResponseTypes.Spawn); + + public bool IsUpdate => IsType(QmResponseTypes.Update); + + public bool IsQuit => IsType(QmResponseTypes.Quit); + + public bool IsWait => IsType(QmResponseTypes.Wait); + + public bool IsSuccessful => !IsError && !IsFatal; + + private bool IsType(string type) => Type == type; + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSettings.cs new file mode 100644 index 000000000..bb68274cb --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSettings.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmSettings + { + public const string DefaultBaseUrl = "https://ladder.cncnet.org"; + public const string DefaultLoginUrl = "/api/v1/auth/login"; + public const string DefaultRefreshUrl = "/api/v1/auth/refresh"; + public const string DefaultServerStatusUrl = "/api/v1/ping"; + public const string DefaultGetUserAccountsUrl = "/api/v1/user/account"; + public const string DefaultGetLaddersUrl = "/api/v1/ladder"; + public const string DefaultGetLadderMapsUrl = "/api/v1/qm/ladder/{0}/maps"; + public const string DefaultGetLadderStatsUrl = "/api/v1/qm/ladder/{0}/stats"; + public const string DefaultQuickMatchUrl = "/api/v1/qm/{0}/{1}"; + + public string BaseUrl { get; set; } = DefaultBaseUrl; + + public string LoginUrl { get; set; } = DefaultLoginUrl; + + public string RefreshUrl { get; set; } = DefaultRefreshUrl; + + public string ServerStatusUrl { get; set; } = DefaultServerStatusUrl; + + public string GetUserAccountsUrl { get; set; } = DefaultGetUserAccountsUrl; + + public string GetLaddersUrl { get; set; } = DefaultGetLaddersUrl; + + public string GetLadderMapsUrlFormat { get; set; } = DefaultGetLadderMapsUrl; + + public string GetLadderStatsUrlFormat { get; set; } = DefaultGetLadderStatsUrl; + + public string QuickMatchUrlFormat { get; set; } = DefaultQuickMatchUrl; + + public string MatchFoundSoundFile { get; set; } + + public IDictionary HeaderLogos = new Dictionary(); + + public Texture2D GetLadderHeaderLogo(string ladder) + => !HeaderLogos.ContainsKey(ladder) ? null : HeaderLogos[ladder]; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSide.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSide.cs new file mode 100644 index 000000000..6810700ff --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSide.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmSide + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("local_id")] + public int LocalId { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserAccount.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserAccount.cs new file mode 100644 index 000000000..2c189cb6e --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserAccount.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmUserAccount + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("username")] + public string Username { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("card_id")] + public int CardId { get; set; } + + [JsonProperty("ladder")] + public QmLadder Ladder { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserSettings.cs new file mode 100644 index 000000000..8bd4374f2 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserSettings.cs @@ -0,0 +1,11 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models +{ + public class QmUserSettings + { + public string Email { get; set; } + + public string Ladder { get; set; } + + public QmAuthData AuthData { get; set; } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmApiService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmApiService.cs new file mode 100644 index 000000000..b1791519c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmApiService.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using ClientCore.Exceptions; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Newtonsoft.Json; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmApiService : IDisposable + { + private HttpClient _httpClient; + private readonly QmSettings qmSettings; + private string _token; + + public QmApiService() + { + qmSettings = QmSettingsService.GetInstance().GetSettings(); + } + + public void SetToken(string token) + { + _token = token; + HttpClient httpClient = GetHttpClient(); + httpClient.DefaultRequestHeaders.Clear(); + if (!string.IsNullOrEmpty(token)) + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_token}"); + } + + public async Task> LoadLadderMapsForAbbrAsync(string ladderAbbreviation) + { + HttpClient httpClient = GetHttpClient(); + string url = string.Format(qmSettings.GetLadderMapsUrlFormat, ladderAbbreviation); + HttpResponseMessage response = await httpClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + throw new ClientException(string.Format(QmStrings.LoadingLadderMapsErrorFormat, response.ReasonPhrase)); + + return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + } + + public async Task LoadLadderStatsForAbbrAsync(string ladderAbbreviation) + { + HttpClient httpClient = GetHttpClient(); + string url = string.Format(qmSettings.GetLadderStatsUrlFormat, ladderAbbreviation); + HttpResponseMessage response = await httpClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + throw new ClientException(string.Format(QmStrings.LoadingLadderStatsErrorFormat, response.ReasonPhrase)); + + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + } + + public async Task> LoadUserAccountsAsync() + { + HttpClient httpClient = GetHttpClient(); + HttpResponseMessage response = await httpClient.GetAsync(qmSettings.GetUserAccountsUrl); + if (!response.IsSuccessStatusCode) + throw new ClientException(string.Format(QmStrings.LoadingUserAccountsErrorFormat, response.ReasonPhrase)); + + return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + } + + public async Task> LoadLaddersAsync() + { + HttpClient httpClient = GetHttpClient(); + HttpResponseMessage response = await httpClient.GetAsync(qmSettings.GetLaddersUrl); + if (!response.IsSuccessStatusCode) + throw new ClientException(string.Format(QmStrings.LoadingLaddersErrorFormat, response.ReasonPhrase)); + + return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + } + + public async Task LoginAsync(string email, string password) + { + HttpClient httpClient = GetHttpClient(); + var postBodyContent = new StringContent(JsonConvert.SerializeObject(new QMLoginRequest() { Email = email, Password = password }), Encoding.Default, "application/json"); + var response = await httpClient.PostAsync(qmSettings.LoginUrl, postBodyContent); + + return await HandleLoginResponse(response, QmStrings.LoggingInUnknownErrorFormat); + } + + public async Task RefreshAsync() + { + HttpClient httpClient = GetHttpClient(); + HttpResponseMessage response = await httpClient.GetAsync(qmSettings.RefreshUrl); + + return await HandleLoginResponse(response, "Error refreshing token: {0}, {1}"); + } + + private async Task HandleLoginResponse(HttpResponseMessage response, string unknownErrorFormat) + { + if (!response.IsSuccessStatusCode) + return await HandleFailedLoginResponse(response, unknownErrorFormat); + + string responseBody = await response.Content.ReadAsStringAsync(); + QmAuthData authData = JsonConvert.DeserializeObject(responseBody); + if (authData == null) + throw new ClientException(responseBody); + + return authData; + } + + private async Task HandleFailedLoginResponse(HttpResponseMessage response, string unknownErrorFormat) + { + string responseBody = await response.Content.ReadAsStringAsync(); + string message; + switch (response.StatusCode) + { + case HttpStatusCode.BadGateway: + message = QmStrings.ServerUnreachableError; + break; + case HttpStatusCode.Unauthorized: + message = QmStrings.InvalidUsernamePasswordError; + break; + default: + message = string.Format(unknownErrorFormat, response.ReasonPhrase, responseBody); + break; + } + + throw new ClientRequestException(message, response.StatusCode); + } + + public bool IsServerAvailable() + { + HttpClient httpClient = GetHttpClient(); + HttpResponseMessage response = httpClient.GetAsync(qmSettings.ServerStatusUrl).Result; + return response.IsSuccessStatusCode; + } + + private HttpClient GetHttpClient() => + _httpClient ??= new HttpClient { BaseAddress = new Uri(qmSettings.BaseUrl), Timeout = TimeSpan.FromSeconds(10) }; + + + public async Task QuickMatchRequestAsync(QmRequest qmRequest) + { + HttpClient httpClient = GetHttpClient(); + string url = string.Format(qmSettings.QuickMatchUrlFormat, qmRequest.Ladder, qmRequest.PlayerName); + HttpResponseMessage response = await httpClient.PostAsync(url, new StringContent(JsonConvert.SerializeObject(qmRequest), Encoding.Default, "application/json")); + + string responseBody = await response.Content.ReadAsStringAsync(); + Logger.Log(responseBody); + QmRequestResponse matchRequestResponse = JsonConvert.DeserializeObject(responseBody); + + if (!response.IsSuccessStatusCode) + throw new ClientException(string.Format(QmStrings.RequestingMatchErrorFormat, response.ReasonPhrase)); + + if (!(matchRequestResponse?.IsSuccessful ?? false)) + throw new ClientException(string.Format(QmStrings.RequestingMatchErrorFormat, matchRequestResponse?.Message ?? matchRequestResponse?.Description ?? "unknown")); + + return matchRequestResponse; + } + + public void Dispose() + { + _httpClient?.Dispose(); + } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestTypes.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestTypes.cs new file mode 100644 index 000000000..dccee908a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestTypes.cs @@ -0,0 +1,9 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public static class QmRequestTypes + { + public const string Quit = "quit"; + public const string Update = "update"; + public const string MatchMeUp = "match me up"; + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmResponseTypes.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmResponseTypes.cs new file mode 100644 index 000000000..6fcdfe7dd --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmResponseTypes.cs @@ -0,0 +1,12 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public static class QmResponseTypes + { + public const string Error = "error"; + public const string Fatal = "fatal"; + public const string Spawn = "spawn"; + public const string Update = "update"; + public const string Quit = "quit"; + public const string Wait = "please wait"; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmService.cs new file mode 100644 index 000000000..8e0c82186 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmService.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ClientCore.Exceptions; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models.Events; +using JWT; +using JWT.Algorithms; +using JWT.Exceptions; +using JWT.Serializers; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; + +public class QmService : IDisposable +{ + private readonly QmUserSettingsService userSettingsService; + private readonly QmApiService apiService; + private readonly QmUserSettings qmUserSettings; + private readonly QmData qmData; + + private static QmService _instance; + + private QmService() + { + userSettingsService = new QmUserSettingsService(); + apiService = new QmApiService(); + qmUserSettings = userSettingsService.GetSettings(); + qmData = new QmData(); + } + + public event EventHandler QmEvent; + + public static QmService GetInstance() => _instance ??= new QmService(); + + public IEnumerable GetUserAccounts() => qmData?.UserAccounts; + + public QmLadder GetLadderForId(int ladderId) => qmData.Ladders.FirstOrDefault(l => l.Id == ladderId); + + public string GetCachedEmail() => qmUserSettings.Email; + + public string GetCachedLadder() => qmUserSettings.Ladder; + + public bool IsServerAvailable() => apiService.IsServerAvailable(); + + /// + /// Login process to cncnet. + /// + /// The email for login. + /// The password for login. + public void LoginAsync(string email, string password) => + ExecuteLoginRequest(async () => + { + QmAuthData authData = await apiService.LoginAsync(email, password); + FinishLogin(authData, email); + }); + + /// + /// Attempts to refresh an existing auth tokenl. + /// + public void RefreshAsync() => + ExecuteLoginRequest(async () => + { + QmAuthData authData = await apiService.RefreshAsync(); + FinishLogin(authData); + }); + + /// + /// Simply clear all auth data from our settings. + /// + public void Logout() + { + ClearAuthData(); + QmEvent?.Invoke(this, new QmLogoutEvent()); + } + + public bool IsLoggedIn() + { + if (qmUserSettings.AuthData == null) + return false; + + try + { + DecodeToken(qmUserSettings.AuthData.Token); + } + catch (TokenExpiredException) + { + Logger.Log(QmStrings.TokenExpiredError); + return false; + } + catch (Exception e) + { + Logger.Log(e.StackTrace); + return false; + } + + apiService.SetToken(qmUserSettings.AuthData.Token); + + return true; + } + + public void SetLadder(QmLadder ladder) + { + qmUserSettings.Ladder = ladder?.Abbreviation; + userSettingsService.SaveSettings(); + QmEvent?.Invoke(this, new QmLadderSelectedEvent(ladder)); + } + + public void LoadLaddersAndUserAccountsAsync() => + ExecuteRequest(new QmLoadingLaddersAndUserAccountsEvent(), async () => + { + Task> loadLaddersTask = apiService.LoadLaddersAsync(); + Task> loadUserAccountsTask = apiService.LoadUserAccountsAsync(); + + await Task.WhenAll(loadLaddersTask, loadUserAccountsTask); + qmData.Ladders = loadLaddersTask.Result.ToList(); + qmData.UserAccounts = loadUserAccountsTask.Result.ToList(); + + if (!qmData.Ladders.Any()) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(QmStrings.NoLaddersFoundError)); + return; + } + + if (!qmData.UserAccounts.Any()) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(QmStrings.NoUserAccountsFoundError)); + return; + } + + QmEvent?.Invoke(this, new QmLaddersAndUserAccountsEvent(qmData.Ladders, qmData.UserAccounts)); + }); + + public void LoadLadderMapsForAbbrAsync(string ladderAbbr) => + ExecuteRequest(new QmLoadingLadderMapsEvent(), async () => + { + IEnumerable ladderMaps = await apiService.LoadLadderMapsForAbbrAsync(ladderAbbr); + QmEvent?.Invoke(this, new QmLadderMapsEvent(ladderMaps)); + }); + + public void LoadLadderStatsForAbbrAsync(string ladderAbbr) => + ExecuteRequest(new QmLoadingLadderStatsEvent(), async () => + { + QmLadderStats ladderStats = await apiService.LoadLadderStatsForAbbrAsync(ladderAbbr); + QmEvent?.Invoke(this, new QmLadderStatsEvent(ladderStats)); + }); + + public void RequestMatchAsync(QmRequest qmRequest) => + ExecuteRequest(new QmRequestingMatchEvent(CancelRequestMatchAsync), async () => + { + QmRequestResponse response = await apiService.QuickMatchRequestAsync(qmRequest); + QmEvent?.Invoke(this, new QmRequestResponseEvent(response)); + }); + + public void Dispose() + { + apiService.Dispose(); + } + + /// + /// We only need to verify the expiration date of the token so that we can refresh or request a new one if it is expired. + /// We do not need to worry about the signature. The API will handle that validation when the token is used. + /// + /// The token to be decoded. + private static void DecodeToken(string token) + { + IJsonSerializer serializer = new JsonNetSerializer(); + IDateTimeProvider provider = new UtcDateTimeProvider(); + ValidationParameters validationParameters = ValidationParameters.Default; + validationParameters.ValidateSignature = false; + IJwtValidator validator = new JwtValidator(serializer, provider, validationParameters); + IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); + IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric + IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); + + decoder.Decode(token, "nokey", verify: true); + } + + private void ClearAuthData() + { + userSettingsService.ClearAuthData(); + userSettingsService.SaveSettings(); + apiService.SetToken(null); + } + + private void CancelRequestMatchAsync() => + ExecuteRequest(new QmCancelingRequestMatchEvent(), async () => + { + var qmRequest = new QmRequest { Type = QmRequestTypes.Quit }; + QmRequestResponse response = await apiService.QuickMatchRequestAsync(qmRequest); + QmEvent?.Invoke(this, new QmRequestResponseEvent(response)); + }); + + private void ExecuteLoginRequest(Func func) => + ExecuteRequest(new QmLoggingInEvent(), async () => + { + await func(); + QmEvent?.Invoke(this, new QmLoginEvent()); + }); + + private void ExecuteRequest(QmEvent qmEvent, Func requestAction) + { + QmEvent?.Invoke(this, qmEvent); + Task.Run(async () => + { + try + { + await requestAction(); + } + catch (Exception e) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent((e as ClientException)?.Message ?? QmStrings.UnknownError)); + } + }); + } + + private void FinishLogin(QmAuthData authData, string email = null) + { + qmUserSettings.AuthData = authData; + qmUserSettings.Email = email ?? qmUserSettings.Email; + userSettingsService.SaveSettings(); + + apiService.SetToken(authData.Token); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettingsService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettingsService.cs new file mode 100644 index 000000000..82ac7fcd2 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettingsService.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ClientCore; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Rampastring.Tools; +using Rampastring.XNAUI; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmSettingsService + { + private static QmSettingsService Instance; + private static readonly string SettingsFile = ClientConfiguration.Instance.QuickMatchPath; + + private const string BasicSectionKey = "Basic"; + private const string SoundsSectionKey = "Sounds"; + private const string HeaderLogosSectionKey = "HeaderLogos"; + + private const string BaseUrlKey = "BaseUrl"; + private const string LoginUrlKey = "LoginUrl"; + private const string RefreshUrlKey = "RefreshUrl"; + private const string ServerStatusUrlKey = "ServerStatusUrl"; + private const string GetUserAccountsUrlKey = "GetUserAccountsUrl"; + private const string GetLaddersUrlKey = "GetLaddersUrl"; + private const string GetLadderMapsUrlKey = "GetLadderMapsUrl"; + + private const string MatchFoundSoundFileKey = "MatchFoundSoundFile"; + + private QmSettings qmSettings; + + private QmSettingsService() + { + } + + public static QmSettingsService GetInstance() => Instance ??= new QmSettingsService(); + + public QmSettings GetSettings() => qmSettings ??= LoadSettings(); + + private static QmSettings LoadSettings() + { + var settings = new QmSettings(); + if (!File.Exists(SettingsFile)) + SaveSettings(settings); // init the settings file + + var iniFile = new IniFile(SettingsFile); + LoadBasicSettings(iniFile, settings); + LoadSoundSettings(iniFile, settings); + LoadHeaderLogoSettings(iniFile, settings); + + return settings; + } + + private static void LoadBasicSettings(IniFile iniFile, QmSettings settings) + { + IniSection basicSection = iniFile.GetSection(BasicSectionKey); + if (basicSection == null) + return; + + settings.BaseUrl = basicSection.GetStringValue(BaseUrlKey, QmSettings.DefaultBaseUrl); + settings.LoginUrl = basicSection.GetStringValue(LoginUrlKey, QmSettings.DefaultLoginUrl); + settings.RefreshUrl = basicSection.GetStringValue(RefreshUrlKey, QmSettings.DefaultRefreshUrl); + settings.ServerStatusUrl = basicSection.GetStringValue(ServerStatusUrlKey, QmSettings.DefaultServerStatusUrl); + settings.GetUserAccountsUrl = basicSection.GetStringValue(GetUserAccountsUrlKey, QmSettings.DefaultGetUserAccountsUrl); + settings.GetLaddersUrl = basicSection.GetStringValue(GetLaddersUrlKey, QmSettings.DefaultGetLaddersUrl); + settings.GetLadderMapsUrlFormat = basicSection.GetStringValue(GetLadderMapsUrlKey, QmSettings.DefaultGetLadderMapsUrl); + } + + private static void LoadSoundSettings(IniFile iniFile, QmSettings settings) + { + IniSection soundsSection = iniFile.GetSection(SoundsSectionKey); + if (soundsSection == null) + return; + + string matchFoundSoundFile = soundsSection.GetStringValue(MatchFoundSoundFileKey, null); + if (matchFoundSoundFile == null) + return; + + matchFoundSoundFile = SafePath.CombineFilePath("Resources", matchFoundSoundFile); + if (File.Exists(matchFoundSoundFile)) + settings.MatchFoundSoundFile = matchFoundSoundFile; + } + + private static void LoadHeaderLogoSettings(IniFile iniFile, QmSettings settings) + { + IniSection headerLogosSection = iniFile.GetSection(HeaderLogosSectionKey); + if (headerLogosSection == null) + return; + + foreach (KeyValuePair keyValuePair in headerLogosSection.Keys.Where(keyValuePair => AssetLoader.AssetExists(keyValuePair.Value))) + settings.HeaderLogos.Add(keyValuePair.Key, AssetLoader.LoadTexture(keyValuePair.Value)); + } + + public static void SaveSettings(QmSettings settings) + { + var iniFile = new IniFile(); + var basicSection = new IniSection(BasicSectionKey); + basicSection.AddKey(BaseUrlKey, settings.BaseUrl); + basicSection.AddKey(LoginUrlKey, settings.LoginUrl); + basicSection.AddKey(RefreshUrlKey, settings.RefreshUrl); + basicSection.AddKey(ServerStatusUrlKey, settings.ServerStatusUrl); + basicSection.AddKey(GetUserAccountsUrlKey, settings.GetUserAccountsUrl); + basicSection.AddKey(GetLaddersUrlKey, settings.GetLaddersUrl); + basicSection.AddKey(GetLadderMapsUrlKey, settings.GetLadderMapsUrlFormat); + + iniFile.AddSection(basicSection); + iniFile.WriteIniFile(SettingsFile); + } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStrings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStrings.cs new file mode 100644 index 000000000..aa882acba --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStrings.cs @@ -0,0 +1,66 @@ +using Localization; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; + +public static class QmStrings +{ + // Error Messages + public static string TokenExpiredError => "QuickMatch token is expired".L10N("QM:Error:TokenExpired"); + + public static string NoLaddersFoundError => "No quick match ladders currently found.".L10N("QM:Error:NoLaddersFound"); + + public static string NoUserAccountsFoundError => "No user accounts found in quick match. Are you registered for this month?".L10N("QM:Error:NoUserAccountsFound"); + + public static string LoadingLadderStatsError => "Error loading ladder stats".L10N("QM:Error:LoadingLadderStats"); + + public static string LoadingLadderMapsError => "Error loading ladder maps".L10N("QM:Error:LoadingLadderMaps"); + + public static string ServerUnreachableError => "Server unreachable".L10N("QM:Error:ServerUnreachable"); + + public static string InvalidUsernamePasswordError => "Invalid username/password".L10N("QM:Error:InvalidUsernamePassword"); + + public static string NoSideSelectedError => "No side selected".L10N("QM:Error:NoSideSelected"); + + public static string NoLadderSelectedError => "No ladder selected".L10N("QM:Error:NoLadderSelected"); + + public static string UnknownError => "Unknown error occurred".L10N("QM:Error:Unknown"); + public static string LoggingInUnknownError => "Error logging in".L10N("QM:Error:LoggingInUnknown"); + + public static string CancelingMatchRequestError => "Error canceling match request".L10N("QM:Error:CancelingMatchRequest"); + + public static string RequestingMatchUnknownError => "Error requesting match".L10N("QM:Error:RequestingMatchUnknown"); + + public static string LoadingLaddersAndAccountsUnknownError => "Error loading ladders and accounts...".L10N("QM:Error:LoadingLaddersAndAccountsUnknown"); + + // Error Messages Formatted + public static string LoadingLadderMapsErrorFormat => "Error loading ladder maps: {0}".L10N("QM:Error:LoadingLadderMapsFormat"); + + public static string LoadingLadderStatsErrorFormat => "Error loading ladder stats: {0}".L10N("QM:Error:LoadingLadderStatsFormat"); + + public static string LoadingUserAccountsErrorFormat => "Error loading user accounts: {0}".L10N("QM:Error:LoadingUserAccountsFormat"); + + public static string LoadingLaddersErrorFormat => "Error loading ladders: {0}".L10N("QM:Error:LoadingLaddersFormat"); + + public static string LoggingInUnknownErrorFormat => "Error logging in: {0}, {1}".L10N("QM:Error:LoggingInUnknownFormat"); + + public static string RequestingMatchErrorFormat => "Error requesting match: {0}".L10N("QM:Error:RequestingMatchFormat"); + + // UI Messages + public static string GenericErrorTitle => "Error".L10N("QM:UI:GenericErrorTitle"); + + public static string LogoutConfirmation => "Are you sure you want to log out?".L10N("QM:UI:LogoutConfirmation"); + + public static string ConfirmationCaption => "Confirmation".L10N("QM:UI:ConfirmationCaption"); + + public static string RequestingMatchStatus => "Requesting match".L10N("QM:UI:RequestingMatchStatus"); + + public static string CancelingMatchRequestStatus => "Canceling match request".L10N("QM:UI:CancelingMatchRequest"); + + public static string LoadingStats => "Loading stats...".L10N("QM:UI:LoadingStats"); + + public static string LoadingLaddersAndAccountsStatus => "Loading ladders and accounts".L10N("QM:UI:LoadingLaddersAndAccountsStatus"); + + public static string LoadingLadderMapsStatus => "Loading ladder maps".L10N("QM:UI:LoadingLadderMapsStatus"); + + public static string LoggingInStatus => "Logging in".L10N("QM:UI:LoggingInStatus"); +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettingsService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettingsService.cs new file mode 100644 index 000000000..816066924 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettingsService.cs @@ -0,0 +1,80 @@ +using System; +using System.IO; +using ClientCore; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Newtonsoft.Json; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmUserSettingsService + { + private static readonly string SettingsFile = $"{ProgramConstants.ClientUserFilesPath}QuickMatchSettings.ini"; + + private const string BasicSectionKey = "Basic"; + private const string AuthDataKey = "AuthData"; + private const string EmailKey = "Email"; + private const string LadderKey = "Ladder"; + + private QmUserSettings qmUserSettings; + + public QmUserSettings GetSettings() => qmUserSettings ??= LoadSettings(); + + private static QmUserSettings LoadSettings() + { + var settings = new QmUserSettings(); + if (!File.Exists(SettingsFile)) + return settings; + + var iniFile = new IniFile(SettingsFile); + LoadBasicSettings(iniFile, settings); + + return settings; + } + + private static void LoadBasicSettings(IniFile iniFile, QmUserSettings settings) + { + IniSection basicSection = iniFile.GetSection(BasicSectionKey); + if (basicSection == null) + return; + + settings.AuthData = GetAuthData(basicSection); + settings.Email = basicSection.GetStringValue(EmailKey, null); + settings.Ladder = basicSection.GetStringValue(LadderKey, null); + } + + private static QmAuthData GetAuthData(IniSection section) + { + if (!section.KeyExists(AuthDataKey)) + return null; + + string authDataValue = section.GetStringValue(AuthDataKey, null); + if (string.IsNullOrEmpty(authDataValue)) + return null; + + try + { + return JsonConvert.DeserializeObject(authDataValue); + } + catch (Exception e) + { + Logger.Log(e.StackTrace); + return null; + } + } + + public void ClearAuthData() => qmUserSettings.AuthData = null; + + public void SaveSettings() + { + var iniFile = new IniFile(); + var basicSection = new IniSection(BasicSectionKey); + basicSection.AddKey(EmailKey, qmUserSettings.Email ?? string.Empty); + basicSection.AddKey(LadderKey, qmUserSettings.Ladder ?? string.Empty); + basicSection.AddKey(AuthDataKey, JsonConvert.SerializeObject(qmUserSettings.AuthData)); + + iniFile.AddSection(basicSection); + iniFile.WriteIniFile(SettingsFile); + } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 67020840f..8f1ab0b7a 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -21,6 +21,8 @@ public class MapLoader private const string GameModeAliasesSection = "GameModeAliases"; private const int CurrentCustomMapCacheVersion = 1; + private static MapLoader Instance; + /// /// List of game modes. /// @@ -46,6 +48,12 @@ public class MapLoader /// private string[] AllowedGameModes = ClientConfiguration.Instance.AllowedCustomGameModes.Split(','); + private MapLoader() + { + } + + public static MapLoader GetInstance() => Instance ?? (Instance = new MapLoader()); + /// /// Loads multiplayer map info asynchonously. /// @@ -331,5 +339,8 @@ private void AddMapToGameModes(Map map, bool enableLogging) } } } + + public Map GetMapForSHA(string sha1) + => GameModeMaps.Select(gmm => gmm.Map).FirstOrDefault(m => m.SHA1 == sha1); } } diff --git a/Docs/INISystem.md b/Docs/INISystem.md index 1269a1974..a40786983 100644 --- a/Docs/INISystem.md +++ b/Docs/INISystem.md @@ -7,8 +7,8 @@ The `[ParserConstants]` section of the `GlobalThemeSettings.ini` file contains c ### Predefined System Constants -`RESOLUTION_WIDTH`: the width of the window when it is initialized -`RESOLUTION_HEIGHT`: the height of the window when it is initialized +`RESOLUTION_WIDTH`: the width of the window when it is initialized +`RESOLUTION_HEIGHT`: the height of the window when it is initialized ### User Defined Constants @@ -22,7 +22,7 @@ $X=MY_EXAMPLE_CONSTANT ``` _NOTE: Constants can only be used in [dynamic control properties](#dynamic-control-properties)_ -## Control properties: +## Control properties: Below lists basic and dynamic control properties. Ordering of properties is important. If there is a property that relies on the size of a control, the properties must set the size of that control first. @@ -31,121 +31,121 @@ Basic control properties cannot use constants #### XNAControl -`X` = `{integer}` the X location of the control -`Y` = `{integer}` the Y location of the control -`Location` = `{comma separated integers}` the X and Y location of the control. -`Width` = `{integer}` the Width of the control -`Height` = `{integer}` the Height of the control -`Size` = `{comma separated integers}` the Width and Height of the control. -`Text` = `{string}` the text to display for the control (ex: buttons, labels, etc...) -`Visible` = `{true/false or yes/no}` whether or not the control should be visible by default -`Enabled` = `{true/false or yes/no}` whether or not the control should be enabled by default -`DistanceFromRightBorder` = `{integer}` the distance of the right edge of this control from the right edge of its parent. This control MUST have a parent. -`DistanceFromBottomBorder` = `{integer}` the distance of the bottom edge of this control from the bottom edge of its parent. This control MUST have a parent. -`FillWidth` = `{integer}` this will set the width of this control to fill the parent/window MINUS this value, starting from the its X position -`FillHeight` = `{integer}` this will set the height of this control to fill the parent/window MINUS this value, starting from the its Y position -`DrawOrder` -`UpdateOrder` -`RemapColor` +`X` = `{integer}` the X location of the control +`Y` = `{integer}` the Y location of the control +`Location` = `{comma separated integers}` the X and Y location of the control. +`Width` = `{integer}` the Width of the control +`Height` = `{integer}` the Height of the control +`Size` = `{comma separated integers}` the Width and Height of the control. +`Text` = `{string}` the text to display for the control (ex: buttons, labels, etc...) +`Visible` = `{true/false or yes/no}` whether or not the control should be visible by default +`Enabled` = `{true/false or yes/no}` whether or not the control should be enabled by default +`DistanceFromRightBorder` = `{integer}` the distance of the right edge of this control from the right edge of its parent. This control MUST have a parent. +`DistanceFromBottomBorder` = `{integer}` the distance of the bottom edge of this control from the bottom edge of its parent. This control MUST have a parent. +`FillWidth` = `{integer}` this will set the width of this control to fill the parent/window MINUS this value, starting from the its X position +`FillHeight` = `{integer}` this will set the height of this control to fill the parent/window MINUS this value, starting from the its Y position +`DrawOrder` +`UpdateOrder` +`RemapColor` #### XNAPanel -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`BorderColor` -`DrawMode` -`AlphaRate` -`BackgroundTexture` -`SolidColorBackgroundTexture` -`DrawBorders` -`Padding` +`BorderColor` +`DrawMode` +`AlphaRate` +`BackgroundTexture` +`SolidColorBackgroundTexture` +`DrawBorders` +`Padding` #### XNAExtraPanel -_(inherits XNAPanel)_ +_(inherits [XNAPanel](#xnapanel))_ -`BackgroundTexture` +`BackgroundTexture` #### XNALabel -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`RemapColor` -`TextColor` -`FontIndex` -`AnchorPoint` -`TextAnchor` -`TextShadowDistance` +`RemapColor` +`TextColor` +`FontIndex` +`AnchorPoint` +`TextAnchor` +`TextShadowDistance` #### XNAButton -_(inherits XNAControl)_ - -`TextColorIdle` -`TextColorHover` -`HoverSoundEffect` -`ClickSoundEffect` -`AdaptiveText` -`AlphaRate` -`FontIndex` -`IdleTexture` -`HoverTexture` -`TextShadowDistance` +_(inherits [XNAControl](#xnacontrol))_ + +`TextColorIdle` +`TextColorHover` +`HoverSoundEffect` +`ClickSoundEffect` +`AdaptiveText` +`AlphaRate` +`FontIndex` +`IdleTexture` +`HoverTexture` +`TextShadowDistance` #### XNAClientButton -_(inherits XNAButton)_ +_(inherits [XNAButton](#xnabutton))_ -`MatchTextureSize` +`MatchTextureSize` #### XNALinkButton -_(inherits XNAClientButton)_ +_(inherits [XNAClientButton](#xnaclientbutton))_ -`URL` -`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines +`URL` +`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines #### XNACheckbox -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`FontIndex` -`IdleColor` -`HighlightColor` -`AlphaRate` -`AllowChecking` -`Checked` +`FontIndex` +`IdleColor` +`HighlightColor` +`AlphaRate` +`AllowChecking` +`Checked` #### XNAClientCheckbox -_(inherits XNACheckBox)_ +_(inherits [XNACheckBox](#xnacheckbox))_ `ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines #### XNADropDown -_(inherits XNAControl)_ - -`OpenUp` -`DropDownTexture` -`DropDownOpenTexture` -`ItemHeight` -`ClickSoundEffect` -`FontIndex` -`BorderColor` -`FocusColor` -`BackColor` -`~~DisabledItemColor~~` -`OptionN` +_(inherits [XNAControl](#xnacontrol))_ + +`OpenUp` +`DropDownTexture` +`DropDownOpenTexture` +`ItemHeight` +`ClickSoundEffect` +`FontIndex` +`BorderColor` +`FocusColor` +`BackColor` +`~~DisabledItemColor~~` +`OptionN` #### XNAClientDropDown -_(inherits XNADropDown)_ +_(inherits [XNADropDown](#xnadropdown))_ -`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines +`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines #### XNATabControl -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`RemapColor` -`TextColor` -`TextColorDisabled` -`RemoveTabIndexN` +`RemapColor` +`TextColor` +`TextColorDisabled` +`RemoveTabIndexN` #### XNATextBox -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`MaximumTextLength` +`MaximumTextLength` ### Basic Control Property Examples ``` @@ -176,49 +176,49 @@ Following controls are only available as children of `XNAOptionsPanel` and deriv ##### SettingCheckBox _(inherits XNAClientCheckBox)_ -`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. -`WriteSettingValue` = `{true/false or yes/no}` enable to write a specific string value to setting INI key instead of the checked state of the checkbox. Defaults to `false`. -`EnabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is checked. -`DisabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is not checked. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. -`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. -`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. +`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. +`WriteSettingValue` = `{true/false or yes/no}` enable to write a specific string value to setting INI key instead of the checked state of the checkbox. Defaults to `false`. +`EnabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is checked. +`DisabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is not checked. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. +`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. ##### FileSettingCheckBox _(inherits XNAClientCheckBox)_ -`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. -`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. -`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. -`CheckAvailability` = `{true/false or yes/no}` if set, whether or not the checkbox can be (un)checked depends on if the files to copy are actually present. Defaults to `false`. -`ResetUnavailableValue` = `{true/false or yes/no}` if set together with `CheckAvailability`, checkbox set to a value that is unavailable will be reset back to `DefaultValue`. Defaults to `false`. -`EnabledFileN` = `{comma-separated strings}` files to copy if checkbox is checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). -`DisabledFileN` = `{comma-separated strings}` files to copy if checkbox is not checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). +`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. +`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. +`CheckAvailability` = `{true/false or yes/no}` if set, whether or not the checkbox can be (un)checked depends on if the files to copy are actually present. Defaults to `false`. +`ResetUnavailableValue` = `{true/false or yes/no}` if set together with `CheckAvailability`, checkbox set to a value that is unavailable will be reset back to `DefaultValue`. Defaults to `false`. +`EnabledFileN` = `{comma-separated strings}` files to copy if checkbox is checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). +`DisabledFileN` = `{comma-separated strings}` files to copy if checkbox is not checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). ##### SettingDropDown _(inherits XNAClientDropDown)_ -`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. -`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_SelectedIndex`. -`WriteSettingValue` = `{true/false or yes/no}` enable to write selected item value to the setting INI key instead of the checked state of the checkbox. Defaults to `false`. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. +`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_SelectedIndex`. +`WriteSettingValue` = `{true/false or yes/no}` enable to write selected item value to the setting INI key instead of the checked state of the checkbox. Defaults to `false`. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. ##### FileSettingDropDown _(inherits XNAClientDropDown)_ -`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. -`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_SelectedIndex`. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. -`ItemXFileN` = `{comma-separated strings}` files to copy when dropdown item X is selected. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). +`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. +`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_SelectedIndex`. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`ItemXFileN` = `{comma-separated strings}` files to copy when dropdown item X is selected. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). ##### Appendix: File Operation Options @@ -234,11 +234,11 @@ Dynamic Control Properties CAN use constants These can ONLY be used in parent controls that inherit the `INItializableWindow` class -`$X` = ``{integer}`` the X location of the control -`$Y` = ``{integer}`` the Y location of the control -`$Width` = ``{integer}`` the Width of the control -`$Height` = ``{integer}`` the Height of the control -`$TextAnchor` +`$X` = ``{integer}`` the X location of the control +`$Y` = ``{integer}`` the Y location of the control +`$Width` = ``{integer}`` the Width of the control +`$Height` = ``{integer}`` the Height of the control +`$TextAnchor` ### Dynamic Control Property Examples ``` diff --git a/build/AfterPublish.targets b/build/AfterPublish.targets index d415b5031..73febddf4 100644 --- a/build/AfterPublish.targets +++ b/build/AfterPublish.targets @@ -24,6 +24,7 @@ + @@ -48,6 +49,7 @@ + @@ -68,6 +70,7 @@ + @@ -117,4 +120,4 @@ - \ No newline at end of file +