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/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index 357eb8c8c..124986220 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,24 @@ 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(QuickMatchMapList)); + ClientGUICreator.Instance.AddControl(typeof(QuickMatchStatusMessageWindow)); + ClientGUICreator.Instance.AddControl(typeof(XNAClientTabControl)); + } + 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/QuickMatchLobbyPanel.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs new file mode 100644 index 000000000..dd8f30fa2 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs @@ -0,0 +1,437 @@ +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 Localization; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchLobbyPanel : QuickMatchPanel + { + private const int TAB_WIDTH = 133; + + private readonly QmService qmService; + private readonly MapLoader mapLoader; + + public event EventHandler LogoutEvent; + + private QuickMatchMapList mapList; + private XNAClientButton btnQuickMatch; + private XNAClientButton btnLogout; + private XNAClientButton btnExit; + private XNAClientDropDown ddUserAccounts; + private XNAClientDropDown ddNicknames; + private XNAClientDropDown ddSides; + private XNAPanel mapPreviewBox; + private XNAPanel settingsPanel; + private XNAClientTabControl tabPanel; + + private bool requestingMatchStatus; + private readonly EnhancedSoundEffect matchFoundSoundEffect; + private readonly QmSettings qmSettings; + + public QuickMatchLobbyPanel(WindowManager windowManager) : base(windowManager) + { + qmService = QmService.GetInstance(); + qmService.UserAccountsEvent += UserAccountsEvent; + qmService.LadderMapsEvent += LadderMapsEvent; + qmService.MatchedEvent += MatchedEvent; + qmService.QmRequestEvent += QmRequestResponseEvent; + + mapLoader = MapLoader.GetInstance(); + + qmSettings = QmSettingsService.GetInstance().GetSettings(); + matchFoundSoundEffect = new EnhancedSoundEffect(qmSettings.MatchFoundSoundFile); + } + + public override void Initialize() + { + Name = nameof(QuickMatchLobbyPanel); + + base.Initialize(); + + mapList = FindChild(nameof(QuickMatchMapList)); + mapList.MapSelected += MapSelected; + + btnLogout = FindChild(nameof(btnLogout)); + btnLogout.LeftClick += BtnLogout_LeftClick; + + btnExit = FindChild(nameof(btnExit)); + btnExit.LeftClick += Exit_Click; + + btnQuickMatch = FindChild(nameof(btnQuickMatch)); + btnQuickMatch.LeftClick += 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)); + settingsPanel.Disable(); + + tabPanel = FindChild(nameof(tabPanel)); + tabPanel.AddTab("Map".L10N("QM:Tabs:Map"), TAB_WIDTH); + tabPanel.AddTab("Settings".L10N("QM:Tabs:Settings"), TAB_WIDTH); + tabPanel.SelectedIndexChanged += TabSelected; + + EnabledChanged += EnabledChangedEvent; + } + + private void EnabledChangedEvent(object sender, EventArgs e) + { + if (!Enabled) + return; + + FetchLaddersAndUserAccountsAsync(); + } + + private void FetchLaddersAndUserAccountsAsync() + { + qmService.ShowStatus("Fetching ladders and accounts..."); + Task.Run(async () => + { + try + { + var qmData = await qmService.FetchLaddersAndUserAccountsAsync(); + qmService.ClearStatus(); + UserAccountsUpdated(qmData.UserAccounts); + } + catch (Exception e) + { + string error = (e as ClientException)?.Message ?? "Error fetching ladders and accounts..."; + ShowError(error); + qmService.ClearStatus(); + } + }); + } + + private void TabSelected(object sender, EventArgs eventArgs) + { + switch (tabPanel.SelectedTab) + { + case 0: + mapPreviewBox.Enable(); + settingsPanel.Disable(); + return; + case 1: + mapPreviewBox.Disable(); + settingsPanel.Enable(); + return; + } + } + + private void BtnQuickMatch_LeftClick(object sender, EventArgs eventArgs) + => RequestQuickMatch(); + + private void RequestQuickMatch() + { + var matchRequest = GetMatchRequest(); + if (matchRequest == null) + return; + + StartRequestingMatchStatus(); + Task.Run(async () => + { + try + { + HandleQuickMatchResponse(await qmService.QuickMatchAsync(matchRequest)); + } + catch (Exception e) + { + string message = (e as ClientException)?.Message ?? "Error requesting match"; + requestingMatchStatus = false; + ShowError(message); + qmService.ClearStatus(); + } + }); + } + + private void StartRequestingMatchStatus() + { + if (requestingMatchStatus) + return; + + const string baseMessage = "Requesting match"; + string message = baseMessage; + int maxLength = baseMessage.Length + 4; + requestingMatchStatus = true; + Task.Run(() => + { + while (requestingMatchStatus) + { + message = message.Length > maxLength ? message.Substring(0, baseMessage.Length) : message + "."; + qmService.ShowStatus(message); + Thread.Sleep(500); + } + }); + } + + 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) + { + requestingMatchStatus = false; + ShowError("qm spawn"); + } + + private void HandleQuickMatchUpdateResponse(QmRequestResponse qmRequestResponse) + { + requestingMatchStatus = false; + ShowError("qm update"); + } + + private void HandleQuickMatchQuitResponse(QmRequestResponse qmRequestResponse) + { + requestingMatchStatus = false; + ShowError("qm quit"); + } + + private void HandleQuickMatchWaitResponse(QmRequestResponse qmRequestResponse) + { + SoundPlayer.Play(matchFoundSoundEffect); + Thread.Sleep(qmRequestResponse.CheckBack * 1000); + RequestQuickMatch(); + } + + private void HandleQuickMatchErrorResponse(QmRequestResponse qmRequestResponse) + { + qmService.ClearStatus(); + ShowError(qmRequestResponse.Message ?? qmRequestResponse.Description); + requestingMatchStatus = false; + } + + private void QmRequestResponseEvent(object sender, QmRequestEventArgs args) + { + QmRequestResponse response = args.Response; + if (!response.IsSuccessful) + { + XNAMessageBox.Show(WindowManager, "Error", response.Message ?? response.Description ?? "Unknown error occurred."); + return; + } + + XNAMessageBox.Show(WindowManager, "test", response.Type); + } + + private QmRequest GetMatchRequest() + { + var userAccount = GetSelectedUserAccount(); + if (userAccount == null) + { + XNAMessageBox.Show(WindowManager, "Error", "No ladder selected"); + return null; + } + + var side = GetSelectedSide(); + if (side == null) + { + XNAMessageBox.Show(WindowManager, "Error", "No side selected"); + return null; + } + + return new QmRequest() + { + Ladder = userAccount.Ladder.Abbreviation, + PlayerName = userAccount.Username, + Side = side.Id + }; + } + + 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, "Confirmation", "Are you sure you want to log out?", 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 UserAccountsEvent(object sender, QmUserAccountsEventArgs qmUserAccountsEventArgs) + => UserAccountsUpdated(qmUserAccountsEventArgs.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 QmUserAccount selectedUserAccount)) + return; + + UpdateNickames(selectedUserAccount); + UpdateSides(selectedUserAccount); + mapList.Clear(); + qmService.SetLadder(selectedUserAccount.Ladder.Abbreviation); + + FetchLadderMapsForAbbrAsync(selectedUserAccount.Ladder.Abbreviation); + } + + private void FetchLadderMapsForAbbrAsync(string ladderAbbr) + { + qmService.ShowStatus("Fetching ladder maps..."); + Task.Run(async () => + { + try + { + await qmService.FetchLadderMapsForAbbrAsync(ladderAbbr); + qmService.ClearStatus(); + } + catch (Exception e) + { + string message = (e as ClientException)?.Message ?? "Error fetching ladder maps"; + ShowError(message); + } + }); + } + + /// + /// 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 fetched new ladder maps for the ladder selected + /// + /// + /// + private void LadderMapsEvent(object sender, QmLadderMapsEventArgs qmLadderMapsEventArgs) + { + mapList.Clear(); + var ladderMaps = qmLadderMapsEventArgs?.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))); + } + + private void MatchedEvent(object sender, EventArgs eventArgs) + { + } + + /// + /// Called when the user selects a map in the list + /// + /// + /// + private void MapSelected(object sender, QmMapSelectedEventArgs qmMapSelectedEventArgs) + { + if (qmMapSelectedEventArgs?.Map == null) + return; + + var map = mapLoader.GetMapForSHA(qmMapSelectedEventArgs.Map.Hash); + + mapPreviewBox.BackgroundTexture = map?.LoadPreviewTexture(); + } + } +} \ 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..39c85a0af --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLoginPanel.cs @@ -0,0 +1,105 @@ +using System; +using System.Threading.Tasks; +using ClientCore.Exceptions; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchLoginPanel : QuickMatchPanel + { + 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(); + } + + public override void Initialize() + { + Name = nameof(QuickMatchLoginPanel); + + base.Initialize(); + + XNAClientButton btnLogin; + btnLogin = FindChild(nameof(btnLogin)); + btnLogin.LeftClick += BtnLogin_LeftClick; + + XNAClientButton btnCancel; + btnCancel = FindChild(nameof(btnCancel)); + btnCancel.LeftClick += Exit_Click; + + tbEmail = FindChild(nameof(tbEmail)); + tbEmail.Text = qmService.GetCachedEmail() ?? string.Empty; + + tbPassword = FindChild(nameof(tbPassword)); + + EnabledChanged += InitLogin; + } + + public void InitLogin(object sender, EventArgs eventArgs) + { + if (!Enabled || loginInitialized) + return; + + if (qmService.IsLoggedIn()) + LoginAsync(qmService.RefreshAsync); + + loginInitialized = true; + } + + private void LoginAsync(Func qmServiceLoginAction) + { + qmService.ShowStatus("Logging in..."); + Task.Run(async () => + { + try + { + await qmServiceLoginAction(); + qmService.ClearStatus(); + LoginEvent?.Invoke(this, null); + } + catch (Exception e) + { + string message = (e as ClientException)?.Message ?? "Error logging in"; + qmService.ClearStatus(); + ShowError(message, LoginErrorTitle); + } + }); + } + + private void BtnLogin_LeftClick(object sender, EventArgs eventArgs) + { + if (!ValidateForm()) + return; + + LoginAsync(async () => await qmService.LoginAsync(tbEmail.Text, tbPassword.Password)); + } + + private bool ValidateForm() + { + if (string.IsNullOrEmpty(tbEmail.Text)) + { + ShowError("No Email specified", LoginErrorTitle); + return false; + } + + if (string.IsNullOrEmpty(tbPassword.Text)) + { + ShowError("No Password specified", LoginErrorTitle); + return false; + } + + return true; + } + } +} diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs new file mode 100644 index 000000000..313c9f436 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchMapList : XNAPanel + { + private const int MouseScrollRate = 6; + public const int ItemHeight = 22; + public event EventHandler MapSelected; + + private XNALabel lblVeto; + private XNALabel lblSides; + private XNALabel lblMaps; + private XNAPanel panelMaps; + private XNAScrollBar scrollBar; + + 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(); + + const int vetoGap = 16; + const int scrollBarWidth = 18; + + lblVeto = new XNALabel(WindowManager); + lblVeto.Text = "Veto"; + lblVeto.ClientRectangle = new Rectangle(0, 0, 18, 18); + AddChild(lblVeto); + + lblSides = new XNALabel(WindowManager); + lblSides.Text = "Sides"; + lblSides.ClientRectangle = new Rectangle(lblVeto.Right + vetoGap, 0, 100, 18); + AddChild(lblSides); + + lblMaps = new XNALabel(WindowManager); + lblMaps.Text = "Maps"; + lblMaps.ClientRectangle = new Rectangle(lblSides.Right, 0, Width - lblVeto.Width - lblSides.Width - vetoGap - scrollBarWidth, 18); + AddChild(lblMaps); + + panelMaps = new XNAPanel(WindowManager); + panelMaps.DrawBorders = false; + panelMaps.ClientRectangle = new Rectangle(0, lblVeto.Bottom, Width, Height - lblVeto.Height); + panelMaps.DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; + + scrollBar = new XNAScrollBar(WindowManager); + scrollBar.ClientRectangle = new Rectangle(panelMaps.Width - 18, 0, scrollBarWidth, panelMaps.Height); + scrollBar.Scrolled += ScrollBarScrolled; + panelMaps.AddChild(scrollBar); + + AddChild(panelMaps); + + MouseScrolled += OnMouseScrolled; + } + + private void OnMouseScrolled(object sender, EventArgs e) + { + int viewTop = GetNewScrollBarViewTop(); + if (viewTop == scrollBar.ViewTop) + return; + + scrollBar.ViewTop = viewTop; + RefreshScrollbar(); + RefreshItemLocations(); + } + + public void AddItems(IEnumerable listItems) + { + foreach (QuickMatchMapListItem quickMatchMapListItem in listItems) + AddItem(quickMatchMapListItem); + } + + private void AddItem(QuickMatchMapListItem listItem) + { + listItem.ClientRectangle = new Rectangle(0, MapItemCount * ItemHeight, Width - scrollBar.ScrollWidth, ItemHeight); + listItem.LeftClickMap += MapItem_LeftClick; + panelMaps.AddChild(listItem); + + listItem.SetLocations( + new Rectangle(lblVeto.X, 0, lblVeto.Width, ItemHeight), + new Rectangle(lblSides.X, 0, lblSides.Width, ItemHeight), + new Rectangle(lblMaps.X, 0, lblMaps.Width, ItemHeight) + ); + RefreshScrollbar(); + RefreshItemOpenUp(listItem); + } + + private void RefreshItemLocations() + { + int index = 0; + foreach (QuickMatchMapListItem quickMatchMapItem in MapItemChildren) + { + quickMatchMapItem.Y = (index++ * ItemHeight) - scrollBar.ViewTop; + RefreshItemOpenUp(quickMatchMapItem); + } + } + + private void RefreshItemOpenUp(QuickMatchMapListItem quickMatchMapListItem) + => quickMatchMapListItem.SetOpenUp(quickMatchMapListItem.OpenedDownWindowBottom > scrollBar.GetWindowRectangle().Bottom); + + private void ScrollBarScrolled(object sender, EventArgs eventArgs) => RefreshItemLocations(); + + 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; + } + + public void RefreshScrollbar() + { + scrollBar.Length = MapItemChildren.Count() * ItemHeight; + scrollBar.DisplayedPixelCount = panelMaps.Height - 4; + scrollBar.Refresh(); + } + + private void MapItem_LeftClick(object sender, EventArgs eventArgs) + { + var selectedItem = sender as QuickMatchMapListItem; + foreach (QuickMatchMapListItem quickMatchMapItem in MapItemChildren) + quickMatchMapItem.Selected = quickMatchMapItem == selectedItem; + + MapSelected?.Invoke(this, new QmMapSelectedEventArgs(selectedItem?.Map)); + } + + public void Clear() + { + foreach (QuickMatchMapListItem child in MapItemChildren.ToList()) + panelMaps.RemoveChild(child); + + RefreshScrollbar(); + } + + private int MapItemCount => MapItemChildren.Count(); + + private IEnumerable MapItemChildren + => panelMaps.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..3eb941043 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs @@ -0,0 +1,129 @@ +using System; +using System.Linq; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +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 XNAClientDropDown ddSide; + private XNAPanel panelMap; + private XNALabel lblMap; + private Color defaultTextColor; + + private XNAPanel topBorder; + private XNAPanel bottomBorder; + + public QmMap Map => ladderMap.Map; + + private bool selected; + + 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); + + cbVeto = new XNAClientCheckBox(WindowManager); + cbVeto.CheckedChanged += CbVeto_CheckChanged; + + ddSide = new XNAClientDropDown(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(); + } + + 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; + } + + public void SetLocations(Rectangle vetoR, Rectangle ddSideR, Rectangle mapR) + { + cbVeto.ClientRectangle = vetoR; + ddSide.ClientRectangle = ddSideR; + panelMap.ClientRectangle = mapR; + + topBorder.ClientRectangle = new Rectangle(panelMap.X, panelMap.Y, panelMap.Width, 1); + bottomBorder.ClientRectangle = new Rectangle(panelMap.X, panelMap.Bottom, panelMap.Width, 1); + } + + 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 = 1; + + lblMap.Text = ladderMap.Description; + } + + public void SetOpenUp(bool openUp) => ddSide.OpenUp = openUp; + + public bool IsVetoed() => cbVeto.Checked; + + public int OpenedDownWindowBottom => GetWindowRectangle().Bottom + (ddSide.ItemHeight * ddSide.Items.Count); + + public bool ContainsPointVertical(Point point) => Y < point.Y && Y + Height < point.Y; + } +} diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchPanel.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchPanel.cs new file mode 100644 index 000000000..6a4ac9b6c --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchPanel.cs @@ -0,0 +1,20 @@ +using System; +using ClientGUI; +using Rampastring.XNAUI; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public abstract class QuickMatchPanel : INItializableWindow + { + public event EventHandler Exit; + + protected QuickMatchPanel(WindowManager windowManager) : base(windowManager) + { + } + + protected void Exit_Click(object sender, EventArgs args) => Exit?.Invoke(sender, args); + + protected void ShowError(string error, string title = "Error") + => XNAMessageBox.Show(WindowManager, title, error); + } +} diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusMessageWindow.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusMessageWindow.cs new file mode 100644 index 000000000..479120e8d --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusMessageWindow.cs @@ -0,0 +1,59 @@ +using System; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchStatusMessageWindow : INItializableWindow + { + private int DefaultInternalWidth; + + private XNAPanel statusWindowInternal { get; set; } + + private XNALabel statusMessage { get; set; } + + private Action buttonAction { get; set; } + + public QuickMatchStatusMessageWindow(WindowManager windowManager) : base(windowManager) + { + } + + public override void Initialize() + { + base.Initialize(); + + statusWindowInternal = FindChild(nameof(statusWindowInternal)); + DefaultInternalWidth = statusWindowInternal.ClientRectangle.Width; + + statusMessage = FindChild(nameof(statusMessage)); + } + + public void Show(string message) => Show(new QmStatusMessageEventArgs(message)); + + public void Show(QmStatusMessageEventArgs statusMessageEventArgs) + { + statusMessage.Text = statusMessageEventArgs.Message; + buttonAction = statusMessageEventArgs.ButtonAction; + + ResizeForText(); + Enable(); + } + + private void ResizeForText() + { + var textDimensions = Renderer.GetTextDimensions(statusMessage.Text, statusMessage.FontIndex); + + statusWindowInternal.Width = (int)Math.Max(DefaultInternalWidth, textDimensions.X + 60); + statusWindowInternal.X = (Width / 2) - (statusWindowInternal.Width / 2); + } + + private void Button_LeftClick(object sender, EventArgs eventArgs) + { + Disable(); + buttonAction?.Invoke(); + } + } +} diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs new file mode 100644 index 000000000..b39bebb9d --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs @@ -0,0 +1,80 @@ +using System; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchWindow : INItializableWindow + { + private readonly QmService qmService; + + private QuickMatchLoginPanel loginPanel; + private QuickMatchLobbyPanel lobbyPanel; + private QuickMatchStatusMessageWindow statusWindow; + + public QuickMatchWindow(WindowManager windowManager) : base(windowManager) + { + qmService = QmService.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(QuickMatchLoginPanel)); + loginPanel.LoginEvent += LoginEvent; + loginPanel.Exit += (sender, args) => Disable(); + + lobbyPanel = FindChild(nameof(QuickMatchLobbyPanel)); + lobbyPanel.LogoutEvent += LogoutEvent; + lobbyPanel.Exit += (sender, args) => Disable(); + + statusWindow = FindChild(nameof(statusWindow)); + + WindowManager.CenterControlOnScreen(this); + + qmService.StatusMessageEvent += StatusMessageEvent; + + EnabledChanged += EnabledChangedEvent; + } + + private void EnabledChangedEvent(object sender, EventArgs e) + { + if (!Enabled) + return; + + loginPanel.Enable(); + } + + private void StatusMessageEvent(object sender, QmStatusMessageEventArgs qmStatusMessageEventArgs) + { + if (string.IsNullOrEmpty(qmStatusMessageEventArgs?.Message)) + { + statusWindow.Disable(); + return; + } + + statusWindow.Show(qmStatusMessageEventArgs); + } + + private void LoginEvent(object sender, EventArgs args) + { + lobbyPanel.Enable(); + loginPanel.Disable(); + } + + private void LogoutEvent(object sender, EventArgs args) + { + loginPanel.Enable(); + lobbyPanel.Disable(); + qmService.ClearStatus(); + } + } +} 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/QmFetchDataEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmFetchDataEventArgs.cs new file mode 100644 index 000000000..8ec934c40 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmFetchDataEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmFetchDataEventArgs : EventArgs + { + public bool IsSuccess { get; set; } + + public QmFetchDataEventArgs(bool isSuccess) + { + IsSuccess = isSuccess; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLadderMapsEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLadderMapsEventArgs.cs new file mode 100644 index 000000000..f8f153001 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLadderMapsEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmLadderMapsEventArgs : EventArgs + { + public IEnumerable Maps { get; set; } + + public QmLadderMapsEventArgs(IEnumerable maps) + { + Maps = maps; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLaddersEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLaddersEventArgs.cs new file mode 100644 index 000000000..7378acaf9 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLaddersEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmLaddersEventArgs : EventArgs + { + public readonly IEnumerable Ladders; + + public QmLaddersEventArgs(IEnumerable ladders) + { + Ladders = ladders; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLoginEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLoginEventArgs.cs new file mode 100644 index 000000000..3eb171bce --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLoginEventArgs.cs @@ -0,0 +1,8 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmLoginEventArgs : EventArgs + { + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLogoutEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLogoutEventArgs.cs new file mode 100644 index 000000000..5c1ecf4b8 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmLogoutEventArgs.cs @@ -0,0 +1,6 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmLogoutEventArgs : QmLoginEventArgs + { + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmMapSelectedEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmMapSelectedEventArgs.cs new file mode 100644 index 000000000..898b0d05b --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmMapSelectedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmMapSelectedEventArgs : EventArgs + { + public QmMap Map { get; set; } + + public QmMapSelectedEventArgs(QmMap map) + { + Map = map; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmRequestEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmRequestEventArgs.cs new file mode 100644 index 000000000..276ac8bd4 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmRequestEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmRequestEventArgs : EventArgs + { + public readonly QmRequestResponse Response; + + public QmRequestEventArgs(QmRequestResponse qmRequestResponse) + { + Response = qmRequestResponse; + } + } +} 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..888d370dd --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmStatusMessageEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmStatusMessageEventArgs : EventArgs + { + public string Message { get; } + + public string ButtonText { get; } + + public Action ButtonAction { get; } + + public QmStatusMessageEventArgs(string message, string buttonText = null, Action buttonAction = null) + { + Message = message; + ButtonText = buttonText; + ButtonAction = buttonAction; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmUserAccountsEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmUserAccountsEventArgs.cs new file mode 100644 index 000000000..74a505d2a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/EventArgs/QmUserAccountsEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmUserAccountsEventArgs : EventArgs + { + public readonly IEnumerable UserAccounts; + + public QmUserAccountsEventArgs(IEnumerable userAccounts) + { + UserAccounts = userAccounts; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmApiService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmApiService.cs new file mode 100644 index 000000000..14d451391 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmApiService.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using ClientCore.Exceptions; +using Newtonsoft.Json; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmApiService + { + private HttpClient _httpClient; + private readonly QmSettings qmSettings; + private string _token; + + public QmApiService() + { + qmSettings = QmSettingsService.GetInstance().GetSettings(); + } + + public void SetToken(string token) + { + _token = token; + } + + public async Task> FetchLadderMapsForAbbrAsync(string ladderAbbreviation) + { + var httpClient = GetAuthenticatedClient(); + string url = string.Format(qmSettings.GetLadderMapsUrl, ladderAbbreviation); + var response = await httpClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + throw new ClientException($"Error fetching ladder maps: {response.ReasonPhrase}"); + + return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + } + + public async Task> FetchUserAccountsAsync() + { + var httpClient = GetAuthenticatedClient(); + var response = await httpClient.GetAsync(qmSettings.GetUserAccountsUrl); + if (!response.IsSuccessStatusCode) + throw new ClientException($"Error fetching user accounts: {response.ReasonPhrase}"); + + return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + } + + public async Task> FetchLaddersAsync() + { + var httpClient = GetAuthenticatedClient(); + var response = await httpClient.GetAsync(qmSettings.GetLaddersUrl); + if (!response.IsSuccessStatusCode) + throw new ClientException($"Error fetching ladders: {response.ReasonPhrase}"); + + return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + } + + public async Task LoginAsync(string email, string password) + { + var 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, "Error logging in: {0}, {1}"); + } + + public async Task RefreshAsync() + { + var httpClient = GetAuthenticatedClient(); + var 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(); + var authData = JsonConvert.DeserializeObject(responseBody); + if (authData == null) + throw new ClientException(responseBody); + + _token = authData.Token; + 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 = "Server unreachable"; + break; + case HttpStatusCode.Unauthorized: + message = "Invalid username/password"; + break; + default: + message = string.Format(unknownErrorFormat, response.ReasonPhrase, responseBody); + break; + } + + throw new ClientRequestException(message, response.StatusCode); + } + + + public bool IsServerAvailable() + { + var httpClient = GetAuthenticatedClient(); + var response = httpClient.GetAsync(qmSettings.ServerStatusUrl).Result; + return response.IsSuccessStatusCode; + } + + private HttpClient GetHttpClient() => + _httpClient ?? (_httpClient = new HttpClient + { + BaseAddress = new Uri(qmSettings.BaseUrl), + Timeout = TimeSpan.FromSeconds(10) + }); + + private HttpClient GetAuthenticatedClient() + { + var httpClient = GetHttpClient(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_token}"); + return httpClient; + } + + public async Task QuickMatchAsync(QmRequest qmRequest) + { + var httpClient = GetAuthenticatedClient(); + string url = string.Format(qmSettings.QuickMatchUrl, qmRequest.Ladder, qmRequest.PlayerName); + var response = await httpClient.PostAsync(url, new StringContent(JsonConvert.SerializeObject(qmRequest), Encoding.Default, "application/json")); + + string responseBody = await response.Content.ReadAsStringAsync(); + Logger.Log(responseBody); + var matchRequestResponse = JsonConvert.DeserializeObject(responseBody); + + if (!response.IsSuccessStatusCode) + throw new ClientException($"Error requesting match: {response.ReasonPhrase}"); + + if (!matchRequestResponse.IsSuccessful) + throw new ClientException($"Error requesting match: {matchRequestResponse.Message ?? matchRequestResponse.Description ?? "unknown"}"); + + return matchRequestResponse; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmAuthData.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmAuthData.cs new file mode 100644 index 000000000..a3296117a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmAuthData.cs @@ -0,0 +1,9 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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/QmData.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmData.cs new file mode 100644 index 000000000..1288b9762 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmData.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmData + { + public List Ladders { get; set; } + + public List UserAccounts { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLadder.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLadder.cs new file mode 100644 index 000000000..0420c7207 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLadder.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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 Vetoes { 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/QmLadderMap.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLadderMap.cs new file mode 100644 index 000000000..97d006046 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLadderMap.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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/QmLadderRules.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLadderRules.cs new file mode 100644 index 000000000..29c63205c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLadderRules.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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/QmLoginEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginEvent.cs new file mode 100644 index 000000000..5a8da2060 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmLoginEvent : EventArgs + { + // public QmLoginEventStatusEnum Status { get; set; } + + public string Error { get; set; } + + // public bool IsSuccess => Status == QmLoginEventStatusEnum.Success; + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginEventStatusEnum.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginEventStatusEnum.cs new file mode 100644 index 000000000..42a19f38a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginEventStatusEnum.cs @@ -0,0 +1,13 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public enum QmLoginEventStatusEnum2 + { + Unknown, + Success, + Unauthorized, + Logout, + NoUserAccounts, + FailedRefresh, + FailedDataFetch, + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginRequest.cs new file mode 100644 index 000000000..2553a343c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmLoginRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QMLoginRequest + { + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("password")] + public string Password { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmMap.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmMap.cs new file mode 100644 index 000000000..1e86dbc52 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmMap.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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/QmRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequest.cs new file mode 100644 index 000000000..2234c0f1a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequest.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestResponse.cs new file mode 100644 index 000000000..454649990 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestResponse.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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/QmRequestTypes.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmRequestTypes.cs new file mode 100644 index 000000000..7f5c88da3 --- /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..f0f0f6b68 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmResponseTypes.cs @@ -0,0 +1,17 @@ +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"; + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmService.cs new file mode 100644 index 000000000..05430698d --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmService.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ClientCore.Exceptions; +using JWT; +using JWT.Algorithms; +using JWT.Exceptions; +using JWT.Serializers; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmService + { + private readonly QmUserSettingsService userSettingsService; + private readonly QmApiService apiService; + private readonly QmUserSettings qmUserSettings; + private readonly QmData qmData; + + private static QmService Instance; + + public event EventHandler StatusMessageEvent; + + public event EventHandler FetchDataEvent; + + public event EventHandler LadderMapsEvent; + + public event EventHandler MatchedEvent; + + public event EventHandler UserAccountsEvent; + + public event EventHandler LaddersEvent; + + public event EventHandler QmRequestEvent; + + private QmService() + { + userSettingsService = new QmUserSettingsService(); + apiService = new QmApiService(); + qmUserSettings = userSettingsService.LoadSettings(); + qmData = new QmData(); + } + + public static QmService GetInstance() => Instance ?? (Instance = new QmService()); + + /// + /// Login process to cncnet + /// + /// + /// + public async Task LoginAsync(string email, string password) + { + var authData = await apiService.LoginAsync(email, password); + FinishLogin(authData, false, email); + } + + /// + /// Attempts to refresh an existing auth token + /// + public async Task RefreshAsync() + { + try + { + var authData = await apiService.RefreshAsync(); + FinishLogin(authData, false); + } + catch (Exception) + { + ClearAuthData(); + throw; + } + } + + /// + /// Simply clear all auth data from our settings + /// + public void Logout() => ClearAuthData(); + + private void ClearAuthData() + { + userSettingsService.ClearAuthData(); + userSettingsService.SaveSettings(); + } + + 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(); + + public bool IsLoggedIn() + { + if (qmUserSettings.AuthData == null) + return false; + + try + { + DecodeToken(qmUserSettings.AuthData.Token); + } + catch (TokenExpiredException) + { + Logger.Log("QuickMatch token is expired"); + return false; + } + catch (Exception e) + { + Logger.Log(e.StackTrace); + return false; + } + + apiService.SetToken(qmUserSettings.AuthData.Token); + + return true; + } + + public void SetLadder(string ladder) + { + qmUserSettings.Ladder = ladder; + userSettingsService.SaveSettings(); + } + + public async Task FetchLaddersAndUserAccountsAsync() + { + var fetchLaddersTask = apiService.FetchLaddersAsync(); + var fetchUserAccountsTask = apiService.FetchUserAccountsAsync(); + + await Task.WhenAll(fetchLaddersTask, fetchUserAccountsTask); + qmData.Ladders = fetchLaddersTask.Result.ToList(); + qmData.UserAccounts = fetchUserAccountsTask.Result.ToList(); + + if (!qmData.Ladders.Any()) + throw new ClientException("No quick match ladders currently found."); + + if (!qmData.UserAccounts.Any()) + throw new ClientException("No user accounts found in quick match. Are you registered for this month?"); + + return qmData; + } + + public async Task FetchLadderMapsForAbbrAsync(string ladderAbbr) + { + var ladderMaps = await apiService.FetchLadderMapsForAbbrAsync(ladderAbbr); + + LadderMapsEvent?.Invoke(this, new QmLadderMapsEventArgs(ladderMaps)); + } + + private void FinishLogin(QmAuthData authData, bool refresh, string email = null) + { + qmUserSettings.AuthData = authData; + qmUserSettings.Email = email ?? qmUserSettings.Email; + userSettingsService.SaveSettings(); + } + + /// + /// 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. + /// + /// + 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, "nosecret", verify: true); + } + + public async Task QuickMatchAsync(QmRequest qmRequest) + => await apiService.QuickMatchAsync(qmRequest); + + public void ShowStatus(string statusMessage, string buttonText = null, Action buttonAction = null) + => StatusMessageEvent?.Invoke(this, new QmStatusMessageEventArgs(statusMessage, buttonText, buttonAction)); + + public void ClearStatus() + => StatusMessageEvent?.Invoke(this, null); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettings.cs new file mode 100644 index 000000000..b8c196edf --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettings.cs @@ -0,0 +1,25 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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 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 GetLadderMapsUrl { get; set; } = DefaultGetLadderMapsUrl; + public string QuickMatchUrl { get; set; } = DefaultQuickMatchUrl; + + public string MatchFoundSoundFile { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettingsService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettingsService.cs new file mode 100644 index 000000000..13d8ed57e --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSettingsService.cs @@ -0,0 +1,82 @@ +using System.IO; +using ClientCore; +using Rampastring.Tools; + +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 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 ?? (Instance = new QmSettingsService()); + + public QmSettings GetSettings() => qmSettings ?? LoadSettings(); + + private QmSettings LoadSettings() + { + qmSettings = new QmSettings(); + if (!File.Exists(SettingsFile)) + SaveSettings(); // init the settings file + + var iniFile = new IniFile(SettingsFile); + var basicSection = iniFile.GetSection(BasicSectionKey); + if (basicSection == null) + return qmSettings; + + qmSettings.BaseUrl = basicSection.GetStringValue(BaseUrlKey, QmSettings.DefaultBaseUrl); + qmSettings.LoginUrl = basicSection.GetStringValue(LoginUrlKey, QmSettings.DefaultLoginUrl); + qmSettings.RefreshUrl = basicSection.GetStringValue(RefreshUrlKey, QmSettings.DefaultRefreshUrl); + qmSettings.ServerStatusUrl = basicSection.GetStringValue(ServerStatusUrlKey, QmSettings.DefaultServerStatusUrl); + qmSettings.GetUserAccountsUrl = basicSection.GetStringValue(GetUserAccountsUrlKey, QmSettings.DefaultGetUserAccountsUrl); + qmSettings.GetLaddersUrl = basicSection.GetStringValue(GetLaddersUrlKey, QmSettings.DefaultGetLaddersUrl); + qmSettings.GetLadderMapsUrl = basicSection.GetStringValue(GetLadderMapsUrlKey, QmSettings.DefaultGetLadderMapsUrl); + + var soundsSection = iniFile.GetSection(SoundsSectionKey); + if (soundsSection == null) + return qmSettings; + + string matchFoundSoundFile = soundsSection.GetStringValue(MatchFoundSoundFileKey, null); + if (File.Exists(matchFoundSoundFile)) + qmSettings.MatchFoundSoundFile = matchFoundSoundFile; + + return qmSettings; + } + + + public void SaveSettings() + { + var iniFile = new IniFile(); + var basicSection = new IniSection(BasicSectionKey); + basicSection.AddKey(BaseUrlKey, qmSettings.BaseUrl); + basicSection.AddKey(LoginUrlKey, qmSettings.LoginUrl); + basicSection.AddKey(RefreshUrlKey, qmSettings.RefreshUrl); + basicSection.AddKey(ServerStatusUrlKey, qmSettings.ServerStatusUrl); + basicSection.AddKey(GetUserAccountsUrlKey, qmSettings.GetUserAccountsUrl); + basicSection.AddKey(GetLaddersUrlKey, qmSettings.GetLaddersUrl); + basicSection.AddKey(GetLadderMapsUrlKey, qmSettings.GetLadderMapsUrl); + + iniFile.AddSection(basicSection); + iniFile.WriteIniFile(SettingsFile); + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSide.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSide.cs new file mode 100644 index 000000000..4e3615872 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmSide.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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/QmStatusMessage.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStatusMessage.cs new file mode 100644 index 000000000..a37bf27f7 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStatusMessage.cs @@ -0,0 +1,9 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmStatusMessage : EventArgs + { + public string Message { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStatusMessageButton.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStatusMessageButton.cs new file mode 100644 index 000000000..d96d1b0a7 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmStatusMessageButton.cs @@ -0,0 +1,17 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmStatusMessageButton + { + public string Text { get; } + + public Action Action { get; } + + public QmStatusMessageButton(string message, Action action) + { + Text = message; + Action = action; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserAccount.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserAccount.cs new file mode 100644 index 000000000..19ee47e7a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserAccount.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + 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/QmUserSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettings.cs new file mode 100644 index 000000000..6f71b2995 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettings.cs @@ -0,0 +1,9 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch +{ + public class QmUserSettings + { + public string Email { get; set; } + public string Ladder { get; set; } + public QmAuthData AuthData { get; set; } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettingsService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettingsService.cs new file mode 100644 index 000000000..73cddad18 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/QmUserSettingsService.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using ClientCore; +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 LoadSettings() + { + if (qmUserSettings != null) + return qmUserSettings; + + qmUserSettings = new QmUserSettings(); + if (!File.Exists(SettingsFile)) + return qmUserSettings; + + var iniFile = new IniFile(SettingsFile); + var basicSection = iniFile.GetSection(BasicSectionKey); + if (basicSection == null) + return qmUserSettings; + + qmUserSettings.AuthData = GetAuthData(basicSection); + qmUserSettings.Email = basicSection.GetStringValue(EmailKey, null); + qmUserSettings.Ladder = basicSection.GetStringValue(LadderKey, null); + + return qmUserSettings; + } + + 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) ?? string.Empty); + + iniFile.AddSection(basicSection); + iniFile.WriteIniFile(SettingsFile); + } + } +} 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 ```