diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 9f1f58e1b..0ca785266 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -254,6 +254,10 @@ public string GetThemePath(string themeName) public string MPMapsIniPath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, "MPMapsPath", SafePath.CombineFilePath("INI", "MPMaps.ini"))); + public string ApiIniPath => clientDefinitionsIni.GetStringValue(SETTINGS, "QuickMatchPath", "INI/API.ini"); + + public string QuickMatchIniPath => 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/Enums/ProgressBarModeEnum.cs b/ClientCore/Enums/ProgressBarModeEnum.cs new file mode 100644 index 000000000..b36b60a2e --- /dev/null +++ b/ClientCore/Enums/ProgressBarModeEnum.cs @@ -0,0 +1,9 @@ +using System.ComponentModel; + +namespace ClientCore.Enums; + +public enum ProgressBarModeEnum +{ + Determinate = 0, + Indeterminate = 1 +} \ No newline at end of file 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/GameProcessLogic.cs b/ClientGUI/GameProcessLogic.cs index 319f95323..104145f69 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -71,7 +71,7 @@ public static void StartGameProcess(WindowManager windowManager) SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "TI.LOG"); SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "TS.LOG"); - GameProcessStarting?.Invoke(); + // GameProcessStarting?.Invoke(); if (UserINISettings.Instance.WindowedMode && UseQres && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -139,7 +139,7 @@ public static void StartGameProcess(WindowManager windowManager) } } - GameProcessStarted?.Invoke(); + // GameProcessStarted?.Invoke(); Logger.Log("Waiting for qres.dat or " + gameExecutableName + " to exit."); } 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/XNAClientDropDown.cs b/ClientGUI/XNAClientDropDown.cs index 18e94686d..270294bbe 100644 --- a/ClientGUI/XNAClientDropDown.cs +++ b/ClientGUI/XNAClientDropDown.cs @@ -9,6 +9,8 @@ public class XNAClientDropDown : XNADropDown { public ToolTip ToolTip { get; set; } + public bool DisabledMouseScroll { get; set; } + public XNAClientDropDown(WindowManager windowManager) : base(windowManager) { } @@ -46,6 +48,16 @@ public override void OnMouseLeftDown() UpdateToolTipBlock(); } + public override void OnMouseScrolled() + { + if (DisabledMouseScroll) + return; + + base.OnMouseScrolled(); + } + + public void Close() => CloseDropDown(); + protected override void CloseDropDown() { base.CloseDropDown(); @@ -60,4 +72,4 @@ protected void UpdateToolTipBlock() ToolTip.Blocked = true; } } -} +} \ No newline at end of file diff --git a/ClientGUI/XNAClientProgressBar.cs b/ClientGUI/XNAClientProgressBar.cs new file mode 100644 index 000000000..587cc2156 --- /dev/null +++ b/ClientGUI/XNAClientProgressBar.cs @@ -0,0 +1,85 @@ +using System; +using ClientCore.Enums; +using Microsoft.Xna.Framework; +using Rampastring.Tools; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace ClientGUI; + +public class XNAClientProgressBar : XNAProgressBar +{ + public int Speed { get; set; } = 4; + + public double WidthRatio { get; set; } = 0.25; + + public ProgressBarModeEnum ProgressBarMode { get; set; } + + private int _left { get; set; } + + public XNAClientProgressBar(WindowManager windowManager) : base(windowManager) + { + } + + public override void Update(GameTime gameTime) + { + _left = (_left + Speed) % Width; + + base.Update(gameTime); + } + + public override void Draw(GameTime gameTime) + { + switch (ProgressBarMode) + { + case ProgressBarModeEnum.Indeterminate: + DrawIndeterminateMode(gameTime); + return; + case ProgressBarModeEnum.Determinate: + default: + base.Draw(gameTime); + return; + } + } + + public void DrawIndeterminateMode(GameTime gameTime) + { + Rectangle wrect = RenderRectangle(); + int filledWidth = (int)(wrect.Width * WidthRatio); + + for (int i = 0; i < BorderWidth; i++) + { + var rect = new Rectangle(wrect.X + i, wrect.Y + i, wrect.Width - i, wrect.Height - i); + + Renderer.DrawRectangle(rect, BorderColor); + } + + Renderer.FillRectangle(new Rectangle(wrect.X + BorderWidth, wrect.Y + BorderWidth, wrect.Width - BorderWidth * 2, wrect.Height - BorderWidth * 2), UnfilledColor); + + if (_left + filledWidth > wrect.Width - BorderWidth * 2) + { + Renderer.FillRectangle(new Rectangle(wrect.X + BorderWidth, wrect.Y + BorderWidth, (_left + filledWidth) - (wrect.Width - (BorderWidth * 2)), wrect.Height - BorderWidth * 2), FilledColor); + } + + Renderer.FillRectangle(new Rectangle(wrect.X + BorderWidth + _left, wrect.Y + BorderWidth, Math.Min(filledWidth, wrect.Width - (BorderWidth * 2) - _left), wrect.Height - BorderWidth * 2), FilledColor); + } + + public override void ParseAttributeFromINI(IniFile iniFile, string key, string value) + { + switch (key) + { + case "WidthRatio": + WidthRatio = double.Parse(value); + return; + case "ProgressBarMode": + ProgressBarMode = (ProgressBarModeEnum)Enum.Parse(typeof(ProgressBarModeEnum), value); + return; + case "Speed": + Speed = int.Parse(value); + return; + default: + base.ParseAttributeFromINI(iniFile, key, value); + return; + } + } +} \ No newline at end of file diff --git a/ClientGUI/XNAMessageBox.cs b/ClientGUI/XNAMessageBox.cs index 06351c588..c4afa1b0d 100644 --- a/ClientGUI/XNAMessageBox.cs +++ b/ClientGUI/XNAMessageBox.cs @@ -262,7 +262,10 @@ private static void MsgBox_OKClicked(XNAMessageBox messageBox) /// The caption of the message box. /// The description in the message box. /// The XNAMessageBox instance that is created. - public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description) + public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description) + => ShowYesNoDialog(windowManager, caption, description, null); + + public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description, Action yesAction) { var panel = new DarkeningPanel(windowManager); windowManager.AddAndInitializeControl(panel); @@ -274,6 +277,8 @@ public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string panel.AddChild(msgBox); msgBox.YesClickedAction = MsgBox_YesClicked; + if (yesAction != null) + msgBox.YesClickedAction += yesAction; msgBox.NoClickedAction = MsgBox_NoClicked; return msgBox; diff --git a/ClientGUI/XNAScrollablePanel.cs b/ClientGUI/XNAScrollablePanel.cs new file mode 100644 index 000000000..d9dc190ff --- /dev/null +++ b/ClientGUI/XNAScrollablePanel.cs @@ -0,0 +1,12 @@ +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace ClientGUI; + +public class XNAScrollablePanel : XNAPanel +{ + public XNAScrollablePanel(WindowManager windowManager) : base(windowManager) + { + DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; + } +} \ No newline at end of file diff --git a/DXClient.sln b/DXClient.sln index 303801c36..3d8b1da5e 100644 --- a/DXClient.sln +++ b/DXClient.sln @@ -24,6 +24,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build\WinForms.props = build\WinForms.props EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DXMainClient.Tests", "DXMainClient.Tests\DXMainClient.Tests.csproj", "{A29B49BC-82DA-4CC7-948E-8497717EC1D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution AresUniversalGLDebug|Any CPU = AresUniversalGLDebug|Any CPU @@ -1024,6 +1026,198 @@ Global {2CD114E3-C30F-4A95-9B4A-455C2C8F82E5}.YRWindowsXNARelease|x64.ActiveCfg = YRWindowsXNARelease|x64 {2CD114E3-C30F-4A95-9B4A-455C2C8F82E5}.YRWindowsXNARelease|x86.ActiveCfg = YRWindowsXNARelease|x86 {2CD114E3-C30F-4A95-9B4A-455C2C8F82E5}.YRWindowsXNARelease|x86.Build.0 = YRWindowsXNARelease|x86 + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresUniversalGLRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsDXRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsGLRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNADebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.AresWindowsXNARelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSUniversalGLRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsDXRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsGLRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNADebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.TSWindowsXNARelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRUniversalGLRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsDXRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLDebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsGLRelease|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNADebug|x86.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|Any CPU.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|Any CPU.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|ARM64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|ARM64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|x64.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|x64.Build.0 = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|x86.ActiveCfg = Debug|Any CPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8}.YRWindowsXNARelease|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DXMainClient.Tests/DXMainClient.Tests.csproj b/DXMainClient.Tests/DXMainClient.Tests.csproj new file mode 100644 index 000000000..a2c7f2e28 --- /dev/null +++ b/DXMainClient.Tests/DXMainClient.Tests.csproj @@ -0,0 +1,75 @@ + + + + + Debug + AnyCPU + {A29B49BC-82DA-4CC7-948E-8497717EC1D8} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + DXMainClient.Tests + DXMainClient.Tests + v4.8 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + {97458c1e-2e6c-4c5c-93c7-16a6712802e9} + DXMainClient + + + + + + + + Always + + + Always + + + + + + diff --git a/DXMainClient.Tests/Properties/AssemblyInfo.cs b/DXMainClient.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..aecc474a5 --- /dev/null +++ b/DXMainClient.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DXMainClient.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DXMainClient.Tests")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("A29B49BC-82DA-4CC7-948E-8497717EC1D8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DXMainClient.Tests/QuickMatch/QmRequestResponseConverterTests.cs b/DXMainClient.Tests/QuickMatch/QmRequestResponseConverterTests.cs new file mode 100644 index 000000000..55303c667 --- /dev/null +++ b/DXMainClient.Tests/QuickMatch/QmRequestResponseConverterTests.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace DXMainClient.Tests.QuickMatch +{ + [TestFixture] + public class QmRequestResponseConverterTests + { + [Test] + public void Test_SpawnRead() + { + string json = GetSpawnJson(); + + QmSpawnResponse response = JsonConvert.DeserializeObject(json); + + Assert.IsInstanceOf(response); + } + + [Test] + public void Test_SpawnWrite() + { + string jsonIn = GetSpawnJson(); + + QmSpawnResponse response = JsonConvert.DeserializeObject(jsonIn); + + string jsonOut = JsonConvert.SerializeObject(response); + + Console.WriteLine(jsonOut); + } + + [Test] + public void Test_PleaseWaitRead() + { + string json = GetPleaseWaitJson(); + + QmWaitResponse response = JsonConvert.DeserializeObject(json); + + Assert.IsInstanceOf(response); + } + + private static string GetSpawnJson() => GetJson("qm_spawn_response"); + + private static string GetPleaseWaitJson() => GetJson("qm_please_wait_response"); + + private static string GetJson(string filename) => File.ReadAllText($"TestData/QuickMatch/QmResponses/{filename}.json"); + } +} \ No newline at end of file diff --git a/DXMainClient.Tests/TestData/QuickMatch/QmResponses/qm_please_wait_response.json b/DXMainClient.Tests/TestData/QuickMatch/QmResponses/qm_please_wait_response.json new file mode 100644 index 000000000..36f913e5f --- /dev/null +++ b/DXMainClient.Tests/TestData/QuickMatch/QmResponses/qm_please_wait_response.json @@ -0,0 +1,5 @@ +{ + "type": "please wait", + "checkback": 10, + "no_sooner_than": 5 +} diff --git a/DXMainClient.Tests/TestData/QuickMatch/QmResponses/qm_spawn_response.json b/DXMainClient.Tests/TestData/QuickMatch/QmResponses/qm_spawn_response.json new file mode 100644 index 000000000..8bf7b37fa --- /dev/null +++ b/DXMainClient.Tests/TestData/QuickMatch/QmResponses/qm_spawn_response.json @@ -0,0 +1,50 @@ +{ + "type": "spawn", + "gameID": 1001, + "spawn": { + "SpawnLocations": { + "Multi2": -1, + "Multi1": -1 + }, + "Settings": { + "UIMapName": "Dawn of Peril", + "MapHash": "3e1a6efe2370f1b26419ece66672b52bf98dc131", + "Seed": 101, + "GameID": 101, + "WOLGameID": 101, + "Host": "No", + "IsSpectator": "No", + "Name": "TestMe", + "Port": 1000, + "Side": 0, + "Color": 1, + "GameSpeed": "0", + "Credits": "10000", + "UnitCount": "0", + "Superweapons": "Yes", + "Tournament": "1", + "ShortGame": "Yes", + "Bases": "Yes", + "MCVRedeploy": "Yes", + "MultipleFactory": "Yes", + "Crates": "No", + "GameMode": "Battle", + "FrameSendRate": "4", + "DisableSWvsYuri": "Yes" + }, + "Other1": { + "Name": "TestOther1", + "Side": 0, + "Color": 0, + "Ip": "12.34.56.78", + "Port": 1000, + "IPv6": "", + "PortV6": 0, + "LanIP": "", + "LanPort": 1000 + } + }, + "client": { + "show_map_preview": 1 + } +} diff --git a/DXMainClient.Tests/packages.config b/DXMainClient.Tests/packages.config new file mode 100644 index 000000000..c108d442f --- /dev/null +++ b/DXMainClient.Tests/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index 491eda938..711eecccb 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -12,10 +12,13 @@ using ClientGUI; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; using DTAClient.DXGUI.Multiplayer; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; +using DTAClient.DXGUI.Multiplayer.QuickMatch; using DTAClient.Online; +using DTAClient.Services; using DTAConfig; using DTAConfig.Settings; using Microsoft.Extensions.DependencyInjection; @@ -197,7 +200,12 @@ private IServiceProvider BuildServiceProvider(WindowManager windowManager) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // singleton xna controls - same instance on each request services @@ -215,7 +223,13 @@ private IServiceProvider BuildServiceProvider(WindowManager windowManager) .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() - .AddSingletonXnaControl(); + .AddSingletonXnaControl() + .AddSingletonXnaControl() + .AddSingletonXnaControl() + .AddSingletonXnaControl() + .AddSingletonXnaControl() + .AddSingletonXnaControl() + .AddSingletonXnaControl(); // transient xna controls - new instance on each request services @@ -234,8 +248,12 @@ private IServiceProvider BuildServiceProvider(WindowManager windowManager) .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() + .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() + .AddTransientXnaControl() + .AddTransientXnaControl() + .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 435bf7a15..43742c4c4 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Threading; using ClientUpdater; +using DTAClient.DXGUI.Multiplayer.QuickMatch; namespace DTAClient.DXGUI.Generic { @@ -49,7 +50,8 @@ public MainMenu( CnCNetGameLobby cnCNetGameLobby, PrivateMessagingPanel privateMessagingPanel, PrivateMessagingWindow privateMessagingWindow, - GameInProgressWindow gameInProgressWindow + GameInProgressWindow gameInProgressWindow, + QuickMatchWindow quickMatchWindow ) : base(windowManager) { this.lanLobby = lanLobby; @@ -64,6 +66,7 @@ GameInProgressWindow gameInProgressWindow this.privateMessagingPanel = privateMessagingPanel; this.privateMessagingWindow = privateMessagingWindow; this.gameInProgressWindow = gameInProgressWindow; + this.quickMatchWindow = quickMatchWindow; this.cncnetLobby.UpdateCheck += CncnetLobby_UpdateCheck; isMediaPlayerAvailable = IsMediaPlayerAvailable(); } @@ -92,6 +95,7 @@ GameInProgressWindow gameInProgressWindow private readonly PrivateMessagingPanel privateMessagingPanel; private readonly PrivateMessagingWindow privateMessagingWindow; private readonly GameInProgressWindow gameInProgressWindow; + private readonly QuickMatchWindow quickMatchWindow; private XNAMessageBox firstRunMessageBox; @@ -133,6 +137,7 @@ private bool UpdateInProgress private XNAClientButton btnStatistics; private XNAClientButton btnCredits; private XNAClientButton btnExtras; + private XNAClientButton btnQuickmatch; /// /// Initializes the main menu's controls. @@ -176,6 +181,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"); @@ -247,6 +257,7 @@ public override void Initialize() AddChild(btnLoadGame); AddChild(btnSkirmish); AddChild(btnCnCNet); + AddChild(btnQuickmatch); AddChild(btnLan); AddChild(btnOptions); AddChild(btnMapEditor); @@ -553,12 +564,14 @@ public void PostInit() DarkeningPanel.AddAndInitializeWithControl(WindowManager, cnCNetGameLobby); DarkeningPanel.AddAndInitializeWithControl(WindowManager, cncnetLobby); DarkeningPanel.AddAndInitializeWithControl(WindowManager, lanLobby); + DarkeningPanel.AddAndInitializeWithControl(WindowManager, quickMatchWindow); optionsWindow.SetTopBar(topBar); DarkeningPanel.AddAndInitializeWithControl(WindowManager, optionsWindow); WindowManager.AddAndInitializeControl(privateMessagingPanel); privateMessagingPanel.AddChild(privateMessagingWindow); topBar.SetTertiarySwitch(privateMessagingWindow); topBar.SetOptionsWindow(optionsWindow); + topBar.SetQuickMatchWindow(quickMatchWindow); WindowManager.AddAndInitializeControl(gameInProgressWindow); skirmishLobby.Disable(); @@ -568,6 +581,7 @@ public void PostInit() lanLobby.Disable(); privateMessagingWindow.Disable(); optionsWindow.Disable(); + quickMatchWindow.Disable(); WindowManager.AddAndInitializeControl(topBar); topBar.AddPrimarySwitchable(this); @@ -849,6 +863,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 2a309493e..8e6ad4e8d 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -10,6 +10,7 @@ using ClientCore; using System.Threading; using DTAClient.Domain.Multiplayer.CnCNet; +using DTAClient.DXGUI.Multiplayer.QuickMatch; using DTAClient.Online.EventArguments; using DTAConfig; using Localization; @@ -53,6 +54,7 @@ PrivateMessageHandler privateMessageHandler private ISwitchable privateMessageSwitch; private OptionsWindow optionsWindow; + private QuickMatchWindow quickMatchWindow; private XNAClientButton btnMainButton; private XNAClientButton btnCnCNetLobby; @@ -107,6 +109,8 @@ public void SetOptionsWindow(OptionsWindow optionsWindow) optionsWindow.EnabledChanged += OptionsWindow_EnabledChanged; } + public void SetQuickMatchWindow(QuickMatchWindow quickMatchWindow) => this.quickMatchWindow = quickMatchWindow; + private void OptionsWindow_EnabledChanged(object sender, EventArgs e) { if (!lanMode) @@ -324,6 +328,7 @@ private void BtnMainButton_LeftClick(object sender, EventArgs e) LastSwitchType = SwitchType.PRIMARY; cncnetLobbySwitch.SwitchOff(); privateMessageSwitch.SwitchOff(); + quickMatchWindow.Disable(); primarySwitches[primarySwitches.Count - 1].SwitchOn(); // HACK warning diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyFooterPanel.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyFooterPanel.cs new file mode 100644 index 000000000..05c30a199 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyFooterPanel.cs @@ -0,0 +1,37 @@ +using System; +using ClientGUI; +using Rampastring.XNAUI; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch; + +public class QuickMatchLobbyFooterPanel : INItializableWindow +{ + private XNAClientButton btnQuickMatch; + private XNAClientButton btnLogout; + private XNAClientButton btnExit; + + public EventHandler LogoutEvent; + + public EventHandler ExitEvent; + + public EventHandler QuickMatchEvent; + + public QuickMatchLobbyFooterPanel(WindowManager windowManager) : base(windowManager) + { + } + + public override void Initialize() + { + IniNameOverride = nameof(QuickMatchLobbyFooterPanel); + base.Initialize(); + + btnLogout = FindChild(nameof(btnLogout)); + btnLogout.LeftClick += (_, _) => LogoutEvent?.Invoke(this, null); + + btnExit = FindChild(nameof(btnExit)); + btnExit.LeftClick += (_, _) => ExitEvent?.Invoke(this, null); + + btnQuickMatch = FindChild(nameof(btnQuickMatch)); + btnQuickMatch.LeftClick += (_, _) => QuickMatchEvent?.Invoke(this, null); + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs new file mode 100644 index 000000000..7aeb09bfb --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLobbyPanel.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ClientGUI; +using DTAClient.Domain.Multiplayer; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchLobbyPanel : INItializableWindow + { + private readonly QmService qmService; + private readonly MapLoader mapLoader; + + public event EventHandler Exit; + + public event EventHandler LogoutEvent; + + private QuickMatchMapList mapList; + private QuickMatchLobbyFooterPanel footerPanel; + private XNAClientDropDown ddUserAccounts; + private XNAClientDropDown ddNicknames; + private XNAClientDropDown ddSides; + private XNAPanel mapPreviewBox; + private XNAPanel settingsPanel; + private XNAClientButton btnMap; + private XNAClientButton btnSettings; + private XNALabel lblStats; + + private readonly EnhancedSoundEffect matchFoundSoundEffect; + private readonly QmSettings qmSettings; + + public QuickMatchLobbyPanel(WindowManager windowManager, MapLoader mapLoader, QmService qmService, QmSettingsService qmSettingsService) : base(windowManager) + { + this.qmService = qmService; + this.qmService.QmEvent += HandleQmEvent; + + this.mapLoader = mapLoader; + + qmSettings = qmSettingsService.GetSettings(); + matchFoundSoundEffect = new EnhancedSoundEffect(qmSettings.MatchFoundSoundFile); + + IniNameOverride = nameof(QuickMatchLobbyPanel); + } + + public override void Initialize() + { + base.Initialize(); + + mapList = FindChild(nameof(mapList)); + mapList.MapSelectedEvent += HandleMapSelectedEventEvent; + mapList.MapSideSelectedEvent += HandleMapSideSelectedEvent; + + footerPanel = FindChild(nameof(footerPanel)); + footerPanel.ExitEvent += (sender, args) => Exit?.Invoke(sender, args); + footerPanel.LogoutEvent += BtnLogout_LeftClick; + footerPanel.QuickMatchEvent += BtnQuickMatch_LeftClick; + + ddUserAccounts = FindChild(nameof(ddUserAccounts)); + ddUserAccounts.SelectedIndexChanged += HandleUserAccountSelected; + + ddNicknames = FindChild(nameof(ddNicknames)); + + ddSides = FindChild(nameof(ddSides)); + ddSides.SelectedIndexChanged += HandleSideSelected; + ddSides.DisabledMouseScroll = true; + + mapPreviewBox = FindChild(nameof(mapPreviewBox)); + mapPreviewBox.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.CENTERED; + + settingsPanel = FindChild(nameof(settingsPanel)); + + btnMap = FindChild(nameof(btnMap)); + btnMap.LeftClick += (_, _) => EnableRightPanel(mapPreviewBox); + + btnSettings = FindChild(nameof(btnSettings)); + btnSettings.LeftClick += (_, _) => EnableRightPanel(settingsPanel); + + lblStats = FindChild(nameof(lblStats)); + + EnabledChanged += EnabledChangedEvent; + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (true) + { + case true when qmEvent is QmLadderMapsEvent e: + HandleLadderMapsEvent(e.LadderMaps); + return; + case true when qmEvent is QmLadderStatsEvent e: + HandleLadderStatsEvent(e.LadderStats); + return; + case true when qmEvent is QmLoadingLadderStatsEvent: + HandleLoadingLadderStatsEvent(); + return; + case true when qmEvent is QmLaddersAndUserAccountsEvent e: + HandleLoadLadderAndUserAccountsEvent(e); + return; + case true when qmEvent is QmUserAccountSelectedEvent e: + HandleUserAccountSelected(e.UserAccount); + return; + case true when qmEvent is QmLoginEvent: + Enable(); + return; + case true when qmEvent is QmLogoutEvent: + HandleLogoutEvent(); + return; + case true when qmEvent is QmResponseEvent e && e.Response.IsSuccess && e.Response.Request is QmReadyRequest: + GameProcessLogic.GameProcessExited += () => { }; + GameProcessLogic.StartGameProcess(WindowManager); + break; + } + } + + private void EnableRightPanel(XNAControl control) + { + foreach (XNAControl parentChild in control.Parent.Children) + parentChild.Disable(); + + control.Enable(); + } + + private void EnabledChangedEvent(object sender, EventArgs e) + { + if (!Enabled) + return; + + LoadLaddersAndUserAccountsAsync(); + } + + private void LoadLaddersAndUserAccountsAsync() + { + qmService.LoadLaddersAndUserAccountsAsync(); + } + + private void BtnQuickMatch_LeftClick(object sender, EventArgs eventArgs) + => RequestQuickMatch(); + + private void RequestQuickMatch() + { + QmRequest matchRequest = CreateMatchRequest(); + if (matchRequest == null) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, QmStrings.UnableToCreateMatchRequestDataError); + return; + } + + qmService.RequestMatchAsync(); + } + + // private void HandleQuickMatchSpawnResponse(QmResponse qmResponse) + // { + // XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, "qm spawn"); + // } + + private QmMatchRequest CreateMatchRequest() + { + QmUserAccount userAccount = GetSelectedUserAccount(); + if (userAccount == null) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, QmStrings.NoLadderSelectedError); + return null; + } + + QmSide side = GetSelectedSide(); + if (side == null) + { + XNAMessageBox.Show(WindowManager, QmStrings.GenericErrorTitle, QmStrings.NoSideSelectedError); + return null; + } + + if (side.IsRandom) + side = GetRandomSide(); + + return new QmMatchRequest { Side = side.LocalId }; + } + + private QmSide GetRandomSide() + { + int randomIndex = new Random().Next(0, ddSides.Items.Count - 2); // account for "Random" + return ddSides.Items.Select(i => i.Tag as QmSide).ElementAt(randomIndex); + } + + private QmSide GetSelectedSide() + => ddSides.SelectedItem?.Tag as QmSide; + + private QmUserAccount GetSelectedUserAccount() + => ddUserAccounts.SelectedItem?.Tag as QmUserAccount; + + private void BtnLogout_LeftClick(object sender, EventArgs eventArgs) + { + XNAMessageBox.ShowYesNoDialog(WindowManager, QmStrings.ConfirmationCaption, QmStrings.LogoutConfirmation, box => + { + qmService.Logout(); + LogoutEvent?.Invoke(this, null); + }); + } + + private void UserAccountsUpdated(IEnumerable userAccounts) + { + ddUserAccounts.Items.Clear(); + foreach (QmUserAccount userAccount in userAccounts) + { + ddUserAccounts.AddItem(new XNADropDownItem() { Text = userAccount.Ladder.Name, Tag = userAccount }); + } + + if (ddUserAccounts.Items.Count == 0) + return; + + string cachedLadder = qmService.GetCachedLadder(); + if (!string.IsNullOrEmpty(cachedLadder)) + ddUserAccounts.SelectedIndex = ddUserAccounts.Items.FindIndex(i => (i.Tag as QmUserAccount)?.Ladder.Abbreviation == cachedLadder); + + if (ddUserAccounts.SelectedIndex < 0) + ddUserAccounts.SelectedIndex = 0; + } + + /// + /// Called when the QM service has finished the login process + /// + /// + /// + private void HandleLoadLadderAndUserAccountsEvent(QmLaddersAndUserAccountsEvent e) + => UserAccountsUpdated(e.UserAccounts); + + private void HandleSideSelected(object sender, EventArgs eventArgs) + => qmService.SetMasterSide(GetSelectedSide()); + + /// + /// Called when the user has selected a UserAccount from the drop down + /// + /// + /// + private void HandleUserAccountSelected(object sender, EventArgs eventArgs) + { + if (ddUserAccounts.SelectedItem?.Tag is not QmUserAccount selectedUserAccount) + return; + + UpdateNickames(selectedUserAccount); + UpdateSides(selectedUserAccount); + mapList.Clear(); + qmService.SetUserAccount(selectedUserAccount); + } + + private void LoadLadderMapsAsync(QmLadder ladder) => qmService.LoadLadderMapsForAbbrAsync(ladder.Abbreviation); + + private void LoadLadderStatsAsync(QmLadder ladder) => qmService.LoadLadderStatsForAbbrAsync(ladder.Abbreviation); + + /// + /// Update the nicknames drop down + /// + /// + private void UpdateNickames(QmUserAccount selectedUserAccount) + { + ddNicknames.Items.Clear(); + + ddNicknames.AddItem(new XNADropDownItem() { Text = selectedUserAccount.Username, Tag = selectedUserAccount }); + + ddNicknames.SelectedIndex = 0; + } + + /// + /// Update the top Sides dropdown + /// + /// + private void UpdateSides(QmUserAccount selectedUserAccount) + { + ddSides.Items.Clear(); + + QmLadder ladder = qmService.GetLadderForId(selectedUserAccount.LadderId); + IEnumerable sides = ladder.Sides.Append(QmSide.CreateRandomSide()); + + foreach (QmSide side in sides) + { + ddSides.AddItem(new XNADropDownItem { Text = side.Name, Tag = side }); + } + + if (ddSides.Items.Count > 0) + ddSides.SelectedIndex = 0; + } + + /// + /// Called when the QM service has loaded new ladder maps for the ladder selected + /// + /// + /// + private void HandleLadderMapsEvent(IEnumerable maps) + { + mapList.Clear(); + var ladderMaps = maps?.ToList() ?? new List(); + if (!ladderMaps.Any()) + return; + + if (ddUserAccounts.SelectedItem?.Tag is not QmUserAccount selectedUserAccount) + return; + + var ladder = qmService.GetLadderForId(selectedUserAccount.LadderId); + + mapList.AddItems(ladderMaps.Select(ladderMap => new QuickMatchMapListItem(WindowManager, ladderMap, ladder))); + } + + /// + /// Called when the QM service has loaded new ladder maps for the ladder selected + /// + private void HandleLadderStatsEvent(QmLadderStats stats) + => lblStats.Text = stats == null ? "No stats found..." : $"Players waiting: {stats.QueuedPlayerCount}, Recent matches: {stats.RecentMatchCount}"; + + /// + /// Called when the QM service has loaded new ladder maps for the ladder selected + /// + private void HandleLoadingLadderStatsEvent() => lblStats.Text = QmStrings.LoadingStats; + + private void HandleUserAccountSelected(QmUserAccount userAccount) + { + mapPreviewBox.BackgroundTexture = null; + LoadLadderMapsAsync(userAccount.Ladder); + LoadLadderStatsAsync(userAccount.Ladder); + } + + private void HandleMatchedEvent() + { + } + + /// + /// Called when the user selects a map in the list + /// + /// + /// + private void HandleMapSelectedEventEvent(object sender, QmLadderMap qmLadderMap) + { + if (qmLadderMap == null) + return; + + Map map = mapLoader.GetMapForSHA(qmLadderMap.Hash); + + mapPreviewBox.BackgroundTexture = map?.LoadPreviewTexture(); + EnableRightPanel(mapPreviewBox); + } + + private void HandleMapSideSelectedEvent(object sender, IEnumerable mapSides) + { + qmService.SetMapSides(mapSides); + } + + private void HandleLogoutEvent() + { + Disable(); + ClearUISelections(); + } + + public void ClearUISelections() + { + ddNicknames.Items.Clear(); + ddNicknames.SelectedIndex = -1; + ddSides.Items.Clear(); + ddSides.SelectedIndex = -1; + ddUserAccounts.Items.Clear(); + ddUserAccounts.SelectedIndex = -1; + mapList.Clear(); + mapPreviewBox.BackgroundTexture = null; + } + } +} \ 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..9e8689225 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchLoginPanel.cs @@ -0,0 +1,100 @@ +using System; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchLoginPanel : INItializableWindow + { + public event EventHandler Exit; + + private const string LoginErrorTitle = "Login Error"; + + private readonly QmService qmService; + + private XNATextBox tbEmail; + private XNAPasswordBox tbPassword; + + public event EventHandler LoginEvent; + + public QuickMatchLoginPanel(WindowManager windowManager, QmService qmService) : base(windowManager) + { + this.qmService = qmService; + this.qmService.QmEvent += HandleQmEvent; + IniNameOverride = nameof(QuickMatchLoginPanel); + } + + public override void Initialize() + { + base.Initialize(); + + XNAClientButton btnLogin; + btnLogin = FindChild(nameof(btnLogin)); + btnLogin.LeftClick += (_, _) => Login(); + + XNAClientButton btnCancel; + btnCancel = FindChild(nameof(btnCancel)); + btnCancel.LeftClick += (_, _) => Exit?.Invoke(this, null); + + tbEmail = FindChild(nameof(tbEmail)); + tbEmail.Text = qmService.GetCachedEmail() ?? string.Empty; + tbEmail.EnterPressed += (_, _) => Login(); + + tbPassword = FindChild(nameof(tbPassword)); + tbPassword.EnterPressed += (_, _) => Login(); + + EnabledChanged += InitLogin; + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmLoginEvent: + Disable(); + return; + case QmLogoutEvent: + Enable(); + return; + } + } + + public void InitLogin(object sender, EventArgs eventArgs) + { + if (!Enabled) + return; + + if (qmService.HasToken()) + qmService.RefreshAsync(); + } + + private void Login() + { + if (!ValidateForm()) + return; + + qmService.LoginAsync(tbEmail.Text, tbPassword.Password); + } + + private bool ValidateForm() + { + if (string.IsNullOrEmpty(tbEmail.Text)) + { + XNAMessageBox.Show(WindowManager, LoginErrorTitle, "No Email specified"); + return false; + } + + if (string.IsNullOrEmpty(tbPassword.Text)) + { + XNAMessageBox.Show(WindowManager, LoginErrorTitle, "No Password specified"); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs new file mode 100644 index 000000000..252763719 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapList.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchMapList : INItializableWindow + { + private const int MouseScrollRate = 6; + public const int ItemHeight = 22; + + public event EventHandler MapSelectedEvent; + + public event EventHandler> MapSideSelectedEvent; + + private XNALabel lblVeto; + private XNALabel lblSides; + private XNALabel lblMaps; + private XNAScrollablePanel mapListPanel; + private readonly QmService qmService; + + public XNAScrollBar scrollBar { get; private set; } + + private QmSide masterQmSide { get; set; } + + public int VetoX => lblVeto?.X ?? 0; + + public int VetoWidth => lblVeto?.Width ?? 0; + + public int SidesX => lblSides?.X ?? 0; + + public int SidesWidth => lblSides?.Width ?? 0; + + public int MapsX => lblMaps?.X ?? 0; + + public int MapsWidth => lblMaps?.Width ?? 0; + + public QuickMatchMapList(WindowManager windowManager, QmService qmService) : base(windowManager) + { + this.qmService = qmService; + } + + public override void Initialize() + { + base.Initialize(); + + lblVeto = FindChild(nameof(lblVeto)); + lblSides = FindChild(nameof(lblSides)); + lblMaps = FindChild(nameof(lblMaps)); + scrollBar = FindChild(nameof(scrollBar)); + mapListPanel = FindChild(nameof(mapListPanel)); + + MouseScrolled += OnMouseScrolled; + + qmService.QmEvent += HandleQmEvent; + } + + public void AddItems(IEnumerable listItems) + { + foreach (QuickMatchMapListItem quickMatchMapListItem in listItems) + AddItem(quickMatchMapListItem); + } + + public override void Draw(GameTime gameTime) + { + var children = MapItemChildren.ToList(); + scrollBar.Length = children.Count * ItemHeight; + scrollBar.DisplayedPixelCount = mapListPanel.Height - 4; + scrollBar.Refresh(); + for (int i = 0; i < children.Count; i++) + children[i].ClientRectangle = new Rectangle(0, (i * ItemHeight) - scrollBar.ViewTop, Width - scrollBar.ScrollWidth, ItemHeight); + + base.Draw(gameTime); + } + + public void SetMasterSide(QmSide qmSide) + { + masterQmSide = qmSide; + foreach (QuickMatchMapListItem quickMatchMapListItem in MapItemChildren) + quickMatchMapListItem.SetMasterSide(masterQmSide); + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmMasterSideSelected e: + SetMasterSide(e.Side); + return; + } + } + + private void OnMouseScrolled(object sender, EventArgs e) + { + int viewTop = GetNewScrollBarViewTop(); + if (viewTop == scrollBar.ViewTop) + return; + + scrollBar.ViewTop = viewTop; + + foreach (QuickMatchMapListItem quickMatchMapListItem in MapItemChildren.ToList()) + { + quickMatchMapListItem.CloseDropDowns(); + } + } + + private void AddItem(QuickMatchMapListItem listItem) + { + listItem.LeftClickMap += MapItem_LeftClick; + listItem.SideSelected += (_, _) => MapSideSelected(); + listItem.SetParentList(this); + listItem.SetMasterSide(masterQmSide); + mapListPanel.AddChild(listItem); + } + + private void MapSideSelected() + => MapSideSelectedEvent?.Invoke(this, MapItemChildren.Select(c => c.GetSelectedSide())); + + private int GetNewScrollBarViewTop() + { + int scrollWheelValue = Cursor.ScrollWheelValue; + int viewTop = scrollBar.ViewTop - (scrollWheelValue * MouseScrollRate); + int maxViewTop = scrollBar.Length - scrollBar.DisplayedPixelCount; + + if (viewTop < 0) + viewTop = 0; + else if (viewTop > maxViewTop) + viewTop = maxViewTop; + + return viewTop; + } + + private void MapItem_LeftClick(object sender, EventArgs eventArgs) + { + var selectedItem = sender as QuickMatchMapListItem; + foreach (QuickMatchMapListItem quickMatchMapItem in MapItemChildren) + quickMatchMapItem.Selected = quickMatchMapItem == selectedItem; + + MapSelectedEvent?.Invoke(this, selectedItem?.LadderMap); + } + + public void Clear() + { + foreach (QuickMatchMapListItem child in MapItemChildren.ToList()) + mapListPanel.RemoveChild(child); + } + + private IEnumerable MapItemChildren + => mapListPanel.Children.Select(c => c as QuickMatchMapListItem).Where(i => i != null); + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs new file mode 100644 index 000000000..25aab492f --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchMapListItem.cs @@ -0,0 +1,166 @@ +using System; +using System.Linq; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchMapListItem : XNAPanel + { + public event EventHandler LeftClickMap; + + public event EventHandler SideSelected; + + public readonly QmLadderMap LadderMap; + private readonly QmLadder ladder; + private XNAClientCheckBox cbVeto; + private XNAClientDropDown ddSide; + private XNAPanel panelMap; + private XNALabel lblMap; + private Color defaultTextColor; + private QmSide masterQmSide; + + private XNAPanel topBorder; + private XNAPanel bottomBorder; + private XNAPanel rightBorder; + + private QuickMatchMapList ParentList; + + private bool selected; + + public int OpenedDownWindowBottom => GetWindowRectangle().Bottom + (ddSide.ItemHeight * ddSide.Items.Count); + + public bool Selected + { + get => selected; + set + { + selected = value; + panelMap.BackgroundTexture = selected ? AssetLoader.CreateTexture(new Color(255, 0, 0), 1, 1) : null; + } + } + + public QuickMatchMapListItem(WindowManager windowManager, QmLadderMap ladderMap, QmLadder ladder) : base(windowManager) + { + LadderMap = ladderMap; + this.ladder = ladder; + } + + public override void Initialize() + { + base.Initialize(); + DrawBorders = false; + + topBorder = new XNAPanel(WindowManager); + topBorder.DrawBorders = true; + AddChild(topBorder); + + bottomBorder = new XNAPanel(WindowManager); + bottomBorder.DrawBorders = true; + AddChild(bottomBorder); + + rightBorder = new XNAPanel(WindowManager); + rightBorder.DrawBorders = true; + AddChild(rightBorder); + + cbVeto = new XNAClientCheckBox(WindowManager); + cbVeto.CheckedChanged += CbVeto_CheckChanged; + + ddSide = new XNAClientDropDown(WindowManager); + ddSide.DisabledMouseScroll = true; + defaultTextColor = ddSide.TextColor; + ddSide.SelectedIndexChanged += Side_Selected; + AddChild(ddSide); + + panelMap = new XNAPanel(WindowManager); + panelMap.LeftClick += Map_LeftClicked; + panelMap.DrawBorders = false; + AddChild(panelMap); + + lblMap = new XNALabel(WindowManager); + lblMap.LeftClick += Map_LeftClicked; + lblMap.ClientRectangle = new Rectangle(4, 2, panelMap.Width, panelMap.Height); + panelMap.AddChild(lblMap); + AddChild(cbVeto); + + InitUI(); + } + + public void SetMasterSide(QmSide qmSide) + { + masterQmSide = qmSide; + + if (!(ddSide?.Items.Any() ?? false)) + return; + + ddSide.SelectedIndex = masterQmSide == null ? 0 : ddSide.SelectedIndex = ddSide.Items.FindIndex(i => ((QmSide)i.Tag).Name == qmSide.Name); + } + + public void SetParentList(QuickMatchMapList parentList) => ParentList = parentList; + + public int GetSelectedSide() => ddSide.SelectedIndex; + + public override void Draw(GameTime gameTime) + { + ddSide.OpenUp = OpenedDownWindowBottom > ParentList.scrollBar.GetWindowRectangle().Bottom; + + base.Draw(gameTime); + } + + private void CbVeto_CheckChanged(object sender, EventArgs e) + { + ddSide.TextColor = cbVeto.Checked ? UISettings.ActiveSettings.DisabledItemColor : defaultTextColor; + lblMap.TextColor = cbVeto.Checked ? UISettings.ActiveSettings.DisabledItemColor : defaultTextColor; + ddSide.AllowDropDown = !cbVeto.Checked; + } + + private void Side_Selected(object sender, EventArgs e) => SideSelected?.Invoke(this, ddSide.SelectedItem?.Tag as QmSide); + + 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) + { + QmSide side = ladder.Sides.FirstOrDefault(s => s.LocalId == ladderMapAllowedSideId); + if (side == null) + continue; + + ddSide.AddItem(new XNADropDownItem() { Text = side.Name, Tag = side }); + } + + var randomSide = QmSide.CreateRandomSide(); + ddSide.AddItem(new XNADropDownItem() { Text = randomSide.Name, Tag = randomSide }); + + if (ddSide.Items.Count > 0) + ddSide.SelectedIndex = 0; + + if (masterQmSide != null) + SetMasterSide(masterQmSide); + + lblMap.Text = LadderMap.Description; + + cbVeto.ClientRectangle = new Rectangle(ParentList.VetoX, 0, ParentList.VetoWidth, QuickMatchMapList.ItemHeight); + ddSide.ClientRectangle = new Rectangle(ParentList.SidesX, 0, ParentList.SidesWidth, QuickMatchMapList.ItemHeight); + panelMap.ClientRectangle = new Rectangle(ParentList.MapsX, 0, ParentList.MapsWidth, QuickMatchMapList.ItemHeight); + + topBorder.ClientRectangle = new Rectangle(panelMap.X, panelMap.Y, panelMap.Width, 1); + bottomBorder.ClientRectangle = new Rectangle(panelMap.X, panelMap.Bottom, panelMap.Width, 1); + rightBorder.ClientRectangle = new Rectangle(panelMap.Right, panelMap.Y, 1, panelMap.Height); + } + + public bool IsVetoed() => cbVeto.Checked; + + public bool ContainsPointVertical(Point point) => Y < point.Y && Y + Height < point.Y; + + public void CloseDropDowns() + { + ddSide.Close(); + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusOverlay.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusOverlay.cs new file mode 100644 index 000000000..dd2690439 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchStatusOverlay.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Timers; +using ClientCore.Enums; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using Microsoft.Xna.Framework; +using Rampastring.XNAUI; +using Rampastring.XNAUI.XNAControls; + +namespace DTAClient.DXGUI.Multiplayer.QuickMatch +{ + public class QuickMatchStatusOverlay : INItializableWindow + { + private int DefaultInternalWidth; + + private const int BUTTON_WIDTH = 92; + private const int BUTTON_GAP = 10; + private const int BUTTON_HEIGHT = 21; + + private XNAPanel statusOverlayBox { get; set; } + + private XNALabel statusMessage { get; set; } + + private XNAPanel pnlButtons { get; set; } + + private Type lastQmLoadingEventType { get; set; } + + private string currentMessage { get; set; } + + private int matchupFoundConfirmTimeLeft { get; set; } + + private QmMatchFoundTimer matchupFoundConfirmTimer { get; set; } + + + private XNAClientProgressBar progressBar; + + private readonly QmService qmService; + private readonly QmSettings qmSettings; + + public QuickMatchStatusOverlay(WindowManager windowManager, QmService qmService, QmSettingsService qmSettingsService) : base(windowManager) + { + this.qmService = qmService; + this.qmService.QmEvent += HandleQmEvent; + qmSettings = qmSettingsService.GetSettings(); + + matchupFoundConfirmTimer = new QmMatchFoundTimer(); + matchupFoundConfirmTimer.SetElapsedAction(ReduceMatchupFoundConfirmTimeLeft); + } + + public override void Initialize() + { + base.Initialize(); + + statusOverlayBox = FindChild(nameof(statusOverlayBox)); + DefaultInternalWidth = statusOverlayBox.ClientRectangle.Width; + + statusMessage = FindChild(nameof(statusMessage)); + + pnlButtons = FindChild(nameof(pnlButtons)); + + progressBar = FindChild(nameof(progressBar)); + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmLoggingInEvent: + HandleLoggingInEvent(); + break; + case QmLoginEvent: + HandleLoginEvent(); + break; + case QmLoadingLaddersAndUserAccountsEvent: + HandleLoadingLaddersAndUserAccountsEvent(); + break; + case QmLaddersAndUserAccountsEvent: + HandleLaddersAndUserAccountsEvent(); + break; + case QmLoadingLadderMapsEvent: + HandleLoadingLadderMapsEvent(); + break; + case QmLadderMapsEvent: + HandleLadderMapsEvent(); + break; + case QmRequestingMatchEvent e: + HandleRequestingMatchEvent(e); + break; + case QmCancelingRequestMatchEvent: + HandleCancelingMatchRequest(); + break; + case QmErrorMessageEvent: + Disable(); + return; + case QmResponseEvent e: + HandleRequestResponseEvent(e); + return; + } + + if (qmEvent is IQmOverlayStatusEvent) + lastQmLoadingEventType = qmEvent.GetType(); + } + + private void HandleLoggingInEvent() => SetStatus(QmStrings.LoggingInStatus); + + private void HandleLoginEvent() => CloseIfLastEventType(typeof(QmLoggingInEvent)); + + private void HandleLoadingLaddersAndUserAccountsEvent() => SetStatus(QmStrings.LoadingLaddersAndAccountsStatus); + + private void HandleLaddersAndUserAccountsEvent() => CloseIfLastEventType(typeof(QmLoadingLaddersAndUserAccountsEvent)); + + private void HandleLoadingLadderMapsEvent() => SetStatus(QmStrings.LoadingLadderMapsStatus); + + private void HandleLadderMapsEvent() => CloseIfLastEventType(typeof(QmLoadingLadderMapsEvent)); + + private void HandleRequestingMatchEvent(QmRequestingMatchEvent e) => SetStatus(QmStrings.RequestingMatchStatus, e.CancelAction); + + private void HandleRequestResponseEvent(QmResponseEvent e) + { + QmResponseMessage responseMessage = e.Response.Data; + switch (true) + { + case true when responseMessage is QmWaitResponse: + return; // need to keep the overlay open while waiting + case true when responseMessage is QmSpawnResponse spawnResponse: + HandleSpawnResponseEvent(spawnResponse); + return; + default: + CloseIfLastEventType(typeof(QmRequestingMatchEvent), typeof(QmCancelingRequestMatchEvent)); + return; + } + } + + private void HandleSpawnResponseEvent(QmSpawnResponse spawnResponse) + { + int interval = matchupFoundConfirmTimer.GetInterval(); + int ratio = 1000 / interval; + int max = qmSettings.MatchFoundWaitSeconds * interval / ratio; + progressBar.Maximum = max; + progressBar.Value = max; + var actions = new List> + { + new(QmStrings.MatchupFoundConfirmYes, () => AcceptMatchAsync(spawnResponse.Spawn)), + new(QmStrings.MatchupFoundConfirmNo, () => RejectMatchAsync(spawnResponse.Spawn)) + }; + SetStatus(QmStrings.MatchupFoundConfirmMsg, actions, ProgressBarModeEnum.Determinate); + matchupFoundConfirmTimer.SetSpawn(spawnResponse.Spawn); + matchupFoundConfirmTimer.Start(); + } + + private void AcceptMatchAsync(QmSpawn spawn) + { + Disable(); + matchupFoundConfirmTimer.Stop(); + qmService.AcceptMatchAsync(spawn); + } + + private void RejectMatchAsync(QmSpawn spawn) + { + Disable(); + matchupFoundConfirmTimer.Stop(); + qmService.RejectMatchAsync(spawn); + } + + private void HandleCancelingMatchRequest() => SetStatus(QmStrings.CancelingMatchRequestStatus); + + private void CloseIfLastEventType(params Type[] lastEventType) + { + if (lastEventType.Any(t => t == lastQmLoadingEventType)) + Disable(); + } + + private void ReduceMatchupFoundConfirmTimeLeft() + { + progressBar.Value -= 1; + + if (progressBar.Value != 0) + return; + + matchupFoundConfirmTimer.Stop(); + Disable(); + RejectMatchAsync(matchupFoundConfirmTimer.Spawn); + } + + private void SetStatus(string message, Tuple button) + => SetStatus(message, new List> { button }); + + private void SetStatus(string message, IEnumerable> buttons = null, ProgressBarModeEnum progressBarMode = ProgressBarModeEnum.Indeterminate) + { + currentMessage = message; + statusMessage.Text = message; + progressBar.ProgressBarMode = progressBarMode; + + ResizeForText(); + AddButtons(buttons); + Enable(); + } + + private void ResizeForText() + { + Vector2 textDimensions = Renderer.GetTextDimensions(statusMessage.Text, statusMessage.FontIndex); + + statusOverlayBox.Width = (int)Math.Max(DefaultInternalWidth, textDimensions.X + 60); + statusOverlayBox.X = (Width / 2) - (statusOverlayBox.Width / 2); + } + + private void AddButtons(IEnumerable> buttons = null) + { + foreach (XNAControl xnaControl in pnlButtons.Children.ToList()) + pnlButtons.RemoveChild(xnaControl); + + if (buttons == null) + return; + + var buttonDefinitions = buttons.ToList(); + int fullWidth = (BUTTON_WIDTH * buttonDefinitions.Count) + (BUTTON_GAP * (buttonDefinitions.Count - 1)); + int startX = (statusOverlayBox.Width / 2) - (fullWidth / 2); + + for (int i = 0; i < buttonDefinitions.Count; i++) + { + Tuple buttonDefinition = buttonDefinitions[i]; + var button = new XNAClientButton(WindowManager); + button.Text = buttonDefinition.Item1; + button.LeftClick += (_, _) => buttonDefinition.Item2(); + button.ClientRectangle = new Rectangle(startX + (i * BUTTON_WIDTH) + (i * (buttonDefinitions.Count - 1) * BUTTON_GAP), 0, BUTTON_WIDTH, BUTTON_HEIGHT); + pnlButtons.AddChild(button); + } + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs new file mode 100644 index 000000000..5951e4bb6 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/QuickMatch/QuickMatchWindow.cs @@ -0,0 +1,91 @@ +using System; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; +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 readonly QmSettingsService qmSettingsService; + + private QuickMatchLoginPanel loginPanel; + + private QuickMatchLobbyPanel lobbyPanel; + + private XNAPanel headerGameLogo; + + public QuickMatchWindow(WindowManager windowManager, QmService qmService, QmSettingsService qmSettingsService) : base(windowManager) + { + this.qmService = qmService; + this.qmService.QmEvent += HandleQmEvent; + this.qmSettingsService = qmSettingsService; + } + + public override void Initialize() + { + Name = nameof(QuickMatchWindow); + BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1); + PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; + + base.Initialize(); + + loginPanel = FindChild(nameof(loginPanel)); + loginPanel.Exit += (sender, args) => Disable(); + + lobbyPanel = FindChild(nameof(lobbyPanel)); + lobbyPanel.Exit += (sender, args) => Disable(); + + headerGameLogo = FindChild(nameof(headerGameLogo)); + + WindowManager.CenterControlOnScreen(this); + + EnabledChanged += EnabledChangedEvent; + } + + private void HandleQmEvent(object sender, QmEvent qmEvent) + { + switch (qmEvent) + { + case QmUserAccountSelectedEvent e: + HandleUserAccountSelected(e.UserAccount); + return; + case QmErrorMessageEvent e: + HandleErrorMessageEvent(e); + return; + } + } + + private void HandleErrorMessageEvent(QmErrorMessageEvent e) + => XNAMessageBox.Show(WindowManager, e.ErrorTitle, e.ErrorMessage); + + private void HandleUserAccountSelected(QmUserAccount userAccount) + { + headerGameLogo.BackgroundTexture = qmSettingsService.GetSettings().GetLadderHeaderLogo(userAccount.Ladder.Abbreviation); + if (headerGameLogo.BackgroundTexture == null) + return; + + // Resize image to ensure proper ratio and spacing from right edge + float imageRatio = (float)headerGameLogo.BackgroundTexture.Width / headerGameLogo.BackgroundTexture.Height; + int newImageWidth = (int)imageRatio * headerGameLogo.Height; + headerGameLogo.ClientRectangle = new Rectangle(headerGameLogo.Parent.Right - newImageWidth - headerGameLogo.Parent.X, headerGameLogo.Y, newImageWidth, headerGameLogo.Height); + } + + private void EnabledChangedEvent(object sender, EventArgs e) + { + if (!Enabled) + { + loginPanel.Disable(); + lobbyPanel.Disable(); + return; + } + + loginPanel.Enable(); + } + } +} \ No newline at end of file diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index e6a35201d..0bca7da1f 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -47,12 +47,86 @@ + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + @@ -62,6 +136,9 @@ ..\References\DiscordRPC.dll + + + diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/ApiSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/ApiSettings.cs new file mode 100644 index 000000000..cbc52dbb7 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/ApiSettings.cs @@ -0,0 +1,32 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet; + +public class ApiSettings +{ + public const string DefaultBaseUrl = "https://ladder.cncnet.org"; + public const string DefaultLoginUrl = "/api/v1/auth/login"; + public const string DefaultRefreshUrl = "/api/v1/auth/refresh"; + public const string DefaultServerStatusUrl = "/api/v1/ping"; + public const string DefaultGetUserAccountsUrl = "/api/v1/user/account"; + public const string DefaultGetLaddersUrl = "/api/v1/ladder"; + public const string DefaultGetLadderMapsUrl = "/api/v1/qm/ladder/{0}/maps"; + public const string DefaultGetLadderStatsUrl = "/api/v1/qm/ladder/{0}/stats"; + public const string DefaultQuickMatchUrl = "/api/v1/qm/{0}/{1}"; + + public string BaseUrl { get; set; } = DefaultBaseUrl; + + public string LoginUrl { get; set; } = DefaultLoginUrl; + + public string RefreshUrl { get; set; } = DefaultRefreshUrl; + + public string ServerStatusUrl { get; set; } = DefaultServerStatusUrl; + + public string GetUserAccountsUrl { get; set; } = DefaultGetUserAccountsUrl; + + public string GetLaddersUrl { get; set; } = DefaultGetLaddersUrl; + + public string GetLadderMapsUrlFormat { get; set; } = DefaultGetLadderMapsUrl; + + public string GetLadderStatsUrlFormat { get; set; } = DefaultGetLadderStatsUrl; + + public string QuickMatchUrlFormat { get; set; } = DefaultQuickMatchUrl; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Converters/QmRequestResponseConverter.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Converters/QmRequestResponseConverter.cs new file mode 100644 index 000000000..14377e5b9 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Converters/QmRequestResponseConverter.cs @@ -0,0 +1,40 @@ +using System; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Converters; + +/// +/// The response from the Ladder api for a match request can come back in a few different response types: +/// +/// +public class QmRequestResponseConverter : JsonConverter +{ + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, QmResponseMessage value, JsonSerializer serializer) => throw new NotImplementedException(); + + public override QmResponseMessage ReadJson(JsonReader reader, Type objectType, QmResponseMessage existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var token = JObject.Load(reader); + string responseType = token[QmResponseMessage.TypeKey]?.ToString(); + + if (responseType == null) + return null; + + Type subType = QmResponseMessage.GetSubType(responseType); + + existingValue ??= Activator.CreateInstance(subType) as QmResponseMessage; + + if (existingValue == null) + return null; + + using JsonReader subReader = token.CreateReader(); + serializer.Populate(subReader, existingValue); + + return existingValue; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Converters/QmRequestSpawnResponseSpawnConverter.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Converters/QmRequestSpawnResponseSpawnConverter.cs new file mode 100644 index 000000000..45d25726c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Converters/QmRequestSpawnResponseSpawnConverter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Converters; + +public class QmRequestSpawnResponseSpawnConverter : JsonConverter +{ + private const int MaxOtherSpawns = 7; + + public override void WriteJson(JsonWriter writer, QmSpawn value, JsonSerializer serializer) + { + var obj = new JObject(); + + List others = value?.Others?.ToList() ?? new List(); + for (int i = 0; i < others.Count; i++) + obj.Add($"Other{i + 1}", JObject.FromObject(others[i])); + + obj.WriteTo(writer); + } + + public override QmSpawn ReadJson(JsonReader reader, Type objectType, QmSpawn existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var token = JObject.Load(reader); + + existingValue ??= new QmSpawn(); + + // populate base properties that require no specific conversions + using JsonReader subReader = token.CreateReader(); + serializer.Populate(subReader, existingValue); + + var others = new List(); + for (int i = 1; i <= MaxOtherSpawns; i++) + { + JToken otherN = token[$"Other{i}"]; + if (otherN == null) + break; + + others.Add(JsonConvert.DeserializeObject(otherN.ToString())); + } + + if (others.Any()) + existingValue.Others = others; + + return existingValue; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/IQmOverlayStatusEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/IQmOverlayStatusEvent.cs new file mode 100644 index 000000000..16e3caf9f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/IQmOverlayStatusEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public interface IQmOverlayStatusEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmCancelingRequestMatchEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmCancelingRequestMatchEvent.cs new file mode 100644 index 000000000..6c7d3fd51 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmCancelingRequestMatchEvent.cs @@ -0,0 +1,6 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmCancelingRequestMatchEvent : QmEvent +{ + +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmErrorMessageEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmErrorMessageEvent.cs new file mode 100644 index 000000000..c3ba69eef --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmErrorMessageEvent.cs @@ -0,0 +1,16 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmErrorMessageEvent : QmEvent +{ + public string ErrorTitle { get; } + + public string ErrorMessage { get; } + + public QmErrorMessageEvent(string errorMessage, string errorTitle = null) + { + ErrorMessage = errorMessage; + ErrorTitle = errorTitle ?? QmStrings.GenericErrorTitle; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmEvent.cs new file mode 100644 index 000000000..01fe05a01 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public abstract class QmEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLadderMapsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLadderMapsEvent.cs new file mode 100644 index 000000000..ae691225a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLadderMapsEvent.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLadderMapsEvent : QmEvent +{ + public IEnumerable LadderMaps { get; } + + public QmLadderMapsEvent(IEnumerable qmLadderMaps) + { + LadderMaps = qmLadderMaps; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLadderStatsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLadderStatsEvent.cs new file mode 100644 index 000000000..e8cf1ca56 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLadderStatsEvent.cs @@ -0,0 +1,13 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLadderStatsEvent : QmEvent +{ + public QmLadderStats LadderStats { get; } + + public QmLadderStatsEvent(QmLadderStats ladderStats) + { + LadderStats = ladderStats; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLaddersAndUserAccountsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLaddersAndUserAccountsEvent.cs new file mode 100644 index 000000000..cdb9ec57a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLaddersAndUserAccountsEvent.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLaddersAndUserAccountsEvent : QmEvent +{ + public IEnumerable Ladders { get; } + + public IEnumerable UserAccounts { get; } + + public QmLaddersAndUserAccountsEvent(IEnumerable ladders, IEnumerable userAccounts) + { + Ladders = ladders; + UserAccounts = userAccounts; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLadderMapsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLadderMapsEvent.cs new file mode 100644 index 000000000..a12be102f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLadderMapsEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLoadingLadderMapsEvent : QmEvent, IQmOverlayStatusEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLadderStatsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLadderStatsEvent.cs new file mode 100644 index 000000000..85763c007 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLadderStatsEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLoadingLadderStatsEvent : QmEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLaddersAndUserAccountsEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLaddersAndUserAccountsEvent.cs new file mode 100644 index 000000000..4b04214ca --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoadingLaddersAndUserAccountsEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLoadingLaddersAndUserAccountsEvent : QmEvent, IQmOverlayStatusEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoggingInEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoggingInEvent.cs new file mode 100644 index 000000000..39a4f6cd9 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoggingInEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLoggingInEvent : QmEvent, IQmOverlayStatusEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoginEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoginEvent.cs new file mode 100644 index 000000000..bac4cdbd3 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLoginEvent.cs @@ -0,0 +1,6 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLoginEvent : QmEvent +{ + public string ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLogoutEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLogoutEvent.cs new file mode 100644 index 000000000..7de212156 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmLogoutEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmLogoutEvent : QmEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmMasterSideSelected.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmMasterSideSelected.cs new file mode 100644 index 000000000..71a2bc92b --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmMasterSideSelected.cs @@ -0,0 +1,13 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmMasterSideSelected : QmEvent +{ + public readonly QmSide Side; + + public QmMasterSideSelected(QmSide side) + { + Side = side; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmNotReadyRequestMatchEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmNotReadyRequestMatchEvent.cs new file mode 100644 index 000000000..76f8f4609 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmNotReadyRequestMatchEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmNotReadyRequestMatchEvent : QmEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmReadyRequestMatchEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmReadyRequestMatchEvent.cs new file mode 100644 index 000000000..a8f5257ed --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmReadyRequestMatchEvent.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmReadyRequestMatchEvent : QmEvent +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmRequestingMatchEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmRequestingMatchEvent.cs new file mode 100644 index 000000000..36f3d0591 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmRequestingMatchEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmRequestingMatchEvent : QmEvent, IQmOverlayStatusEvent +{ + public Tuple CancelAction { get; } + + public QmRequestingMatchEvent(Action cancelAction) + { + CancelAction = new Tuple("Cancel", cancelAction); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmResponseEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmResponseEvent.cs new file mode 100644 index 000000000..14551edfe --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmResponseEvent.cs @@ -0,0 +1,14 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmResponseEvent : QmEvent +{ + public QmResponse Response { get; } + + public QmResponseEvent(QmResponse response) + { + Response = response; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmUserAccountSelectedEvent.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmUserAccountSelectedEvent.cs new file mode 100644 index 000000000..87a687f48 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Events/QmUserAccountSelectedEvent.cs @@ -0,0 +1,13 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; + +public class QmUserAccountSelectedEvent : QmEvent +{ + public QmUserAccount UserAccount { get; } + + public QmUserAccountSelectedEvent(QmUserAccount userAccount) + { + UserAccount = userAccount; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmAuthData.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmAuthData.cs new file mode 100644 index 000000000..e52154268 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmAuthData.cs @@ -0,0 +1,10 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmAuthData +{ + public string Token { get; set; } + + public string Email { get; set; } + + public string Name { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmData.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmData.cs new file mode 100644 index 000000000..8c005f948 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmData.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmData +{ + public List Ladders { get; set; } + + public List UserAccounts { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadder.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadder.cs new file mode 100644 index 000000000..189475e4f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadder.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmLadder +{ + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("abbreviation")] + public string Abbreviation { get; set; } + + [JsonProperty("game")] + public string Game { get; set; } + + [JsonProperty("clans_allowed")] + private int clansAllowed { get; set; } + + [JsonIgnore] + public bool ClansAllowed => clansAllowed == 1; + + [JsonProperty("game_object_schema_id")] + public int GameObjectSchemaId { get; set; } + + [JsonProperty("map_pool_id")] + public int? MapPoolId { get; set; } + + [JsonProperty("private")] + private int _private { get; set; } + + [JsonIgnore] + public bool IsPrivate => _private == 1; + + [JsonProperty("sides")] + public IEnumerable Sides { get; set; } + + [JsonProperty("vetoes")] + public int VetoesRemaining { get; set; } + + [JsonProperty("allowed_sides")] + public IEnumerable AllowedSideLocalIds { get; set; } + + [JsonProperty("current")] + public string Current { get; set; } + + [JsonProperty("qm_ladder_rules")] + public QmLadderRules LadderRules { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderMap.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderMap.cs new file mode 100644 index 000000000..87da585ec --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderMap.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmLadderMap +{ + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("map_id")] + public int MapId { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("bit_idx")] + public int BitIndex { get; set; } + + [JsonProperty("valid")] + private int valid { get; set; } + + [JsonIgnore] + public bool IsValid => valid == 1; + + [JsonProperty("spawn_order")] + public string SpawnOrder { get; set; } + + [JsonProperty("team1_spawn_order")] + public string Team1SpawnOrder { get; set; } + + [JsonProperty("team2_spawn_order")] + public string Team2SpawnOrder { get; set; } + + [JsonProperty("allowed_sides")] + public IEnumerable AllowedSideIds { get; set; } + + [JsonProperty("admin_description")] + public string AdminDescription { get; set; } + + [JsonProperty("map_pool_id")] + public int? MapPoolId { get; set; } + + [JsonProperty("rejectable")] + private int rejectable { get; set; } + + [JsonIgnore] + public bool IsRejectable => rejectable == 1; + + [JsonProperty("default_reject")] + private int defaultReject { get; set; } + + [JsonIgnore] + public bool IsDefaultReject => defaultReject == 1; + + [JsonProperty("hash")] + public string Hash { get; set; } + + [JsonProperty("map")] + public QmMap Map { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderRules.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderRules.cs new file mode 100644 index 000000000..63084b101 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderRules.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmLadderRules +{ + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("player_count")] + public int PlayerCount { get; set; } + + [JsonProperty("map_vetoes")] + public int MapVetoes { get; set; } + + [JsonProperty("max_difference")] + public int MaxDifference { get; set; } + + [JsonProperty("all_sides")] + private string allSides { get; set; } + + [JsonIgnore] + public IEnumerable AllSides => allSides?.Split(',').Select(int.Parse) ?? new List(); + + [JsonProperty("allowed_sides")] + private string allowedSides { get; set; } + + [JsonIgnore] + public IEnumerable AllowedSides => allSides?.Split(',').Select(int.Parse) ?? new List(); + + [JsonProperty("bail_time")] + public int BailTime { get; set; } + + [JsonProperty("bail_fps")] + public int BailFps { get; set; } + + [JsonProperty("tier2_rating")] + public int Tier2Rating { get; set; } + + [JsonProperty("rating_per_second")] + public decimal RatingPerSecond { get; set; } + + [JsonProperty("max_points_difference")] + public int MaxPointsDifference { get; set; } + + [JsonProperty("points_per_second")] + public decimal 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; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStats.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStats.cs new file mode 100644 index 000000000..04c5a2f5b --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStats.cs @@ -0,0 +1,25 @@ +using System; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmLadderStats +{ + [JsonProperty("recentMatchedPlayers")] + public int RecentMatchedPlayerCount { get; set; } + + [JsonProperty("queuedPlayers")] + public int QueuedPlayerCount { get; set; } + + [JsonProperty("past24hMatches")] + public int Past24HourMatchCount { get; set; } + + [JsonProperty("recentMatches")] + public int RecentMatchCount { get; set; } + + [JsonProperty("activeMatches")] + public int ActiveMatchCount { get; set; } + + [JsonProperty("time")] + public QmLadderStatsTime Time { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStatsTime.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStatsTime.cs new file mode 100644 index 000000000..abd99bed1 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLadderStatsTime.cs @@ -0,0 +1,16 @@ +using System; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmLadderStatsTime +{ + [JsonProperty("date")] + public DateTime Date { get; set; } + + [JsonProperty("timezone_type")] + public int TimezoneType { get; set; } + + [JsonProperty("timezone")] + public string Timezone { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLoginRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLoginRequest.cs new file mode 100644 index 000000000..78ab29adc --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmLoginRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QMLoginRequest +{ + [JsonProperty("email")] + public string Email { get; set; } + + [JsonProperty("password")] + public string Password { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmMap.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmMap.cs new file mode 100644 index 000000000..a88df41f0 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmMap.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmMap +{ + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("hash")] + public string Hash { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmResponseMessage.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmResponseMessage.cs new file mode 100644 index 000000000..c4ca9072b --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmResponseMessage.cs @@ -0,0 +1,36 @@ +using System; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Converters; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +[JsonConverter(typeof(QmRequestResponseConverter))] +public class QmResponseMessage +{ + public const string TypeKey = "type"; + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + + [JsonIgnore] + public bool IsSuccessful => this is not QmErrorResponse; + + public static Type GetSubType(string type) + { + return type switch + { + QmResponseTypes.Wait => typeof(QmWaitResponse), + QmResponseTypes.Spawn => typeof(QmSpawnResponse), + QmResponseTypes.Error => typeof(QmErrorResponse), + QmResponseTypes.Fatal => typeof(QmFatalResponse), + QmResponseTypes.Update => typeof(QmUpdateResponse), + QmResponseTypes.Quit => typeof(QmQuitResponse), + _ => null + }; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSettings.cs new file mode 100644 index 000000000..bdfbbeca8 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSettings.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmSettings +{ + public const int DefaultMatchFoundWaitSeconds = 20; + + public string MatchFoundSoundFile { get; set; } + + public List AllowedLadders { get; set; } = new(); + + public int MatchFoundWaitSeconds { get; set; } = DefaultMatchFoundWaitSeconds; + + public IDictionary HeaderLogos = new Dictionary(); + + public Texture2D GetLadderHeaderLogo(string ladder) + => !HeaderLogos.ContainsKey(ladder) ? null : HeaderLogos[ladder]; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSide.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSide.cs new file mode 100644 index 000000000..7307584f9 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSide.cs @@ -0,0 +1,24 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmSide +{ + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("local_id")] + public int LocalId { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonIgnore] + public bool IsRandom => Name == QmStrings.RandomSideName; + + public static QmSide CreateRandomSide() => new() { Name = QmStrings.RandomSideName }; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawn.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawn.cs new file mode 100644 index 000000000..ca7bf261c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawn.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Converters; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +[JsonConverter(typeof(QmRequestSpawnResponseSpawnConverter))] +public class QmSpawn +{ + [JsonProperty("SpawnLocations")] + public IDictionary SpawnLocations { get; set; } + + [JsonProperty("Settings")] + public QmSpawnSettings Settings { get; set; } + + /// + /// This is NOT part of the typical JSON that is used to serialize/deserialize this class. + /// + /// The typical JSON contains explicit properties of "Other1", "Other2", up to "Other7". + /// Rather than having an explicit property in this class for each one, we use the + /// to read/write out each property + /// into the list you see below. + /// + [JsonIgnore] + public List Others { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawnOther.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawnOther.cs new file mode 100644 index 000000000..3ce0f79cc --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawnOther.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmSpawnOther +{ + private const string DEFAULT_IP = "127.0.0.1"; + + [JsonProperty("Name")] + public string Name { get; set; } + + [JsonProperty("Side")] + public int Side { get; set; } + + [JsonProperty("Color")] + public int Color { get; set; } + + [JsonProperty("Ip")] + public string Ip + { + get => _ip ?? DEFAULT_IP; + set => _ip = value; + } + + [JsonProperty("Port")] + public int Port { get; set; } + + [JsonProperty("IPv6")] + public string IPv6 { get; set; } + + [JsonProperty("PortV6")] + public int PortV6 { get; set; } + + [JsonProperty("LanIP")] + public string LanIP + { + get => _lanIp ?? DEFAULT_IP; + set => _lanIp = value; + } + + [JsonProperty("LanPort")] + public int LanPort { get; set; } + + private string _ip { get; set; } + + private string _lanIp { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawnSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawnSettings.cs new file mode 100644 index 000000000..9aa60de7d --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmSpawnSettings.cs @@ -0,0 +1,52 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmSpawnSettings +{ + public string UIMapName { get; set; } + + public string MapHash { get; set; } + + public int Seed { get; set; } + + public int GameID { get; set; } + + public int WOLGameID { get; set; } + + public string Host { get; set; } + + public string IsSpectator { get; set; } + + public string Name { get; set; } + + public int Port { get; set; } + + public int Side { get; set; } + + public int Color { get; set; } + + public string GameSpeed { get; set; } + + public string Credits { get; set; } + + public string UnitCount { get; set; } + + public string SuperWeapons { get; set; } + + public string Tournament { get; set; } + + public string ShortGame { get; set; } + + public string Bases { get; set; } + + public string MCVRedeploy { get; set; } + + public string MultipleFactory { get; set; } + + public string Crates { get; set; } + + public string GameMode { get; set; } + + public string FrameSendRate { get; set; } + + public string DisableSWvsYuri { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserAccount.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserAccount.cs new file mode 100644 index 000000000..bf74c0de2 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserAccount.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmUserAccount +{ + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("username")] + public string Username { get; set; } + + [JsonProperty("ladder_id")] + public int LadderId { get; set; } + + [JsonProperty("card_id")] + public int CardId { get; set; } + + [JsonProperty("ladder")] + public QmLadder Ladder { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserSettings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserSettings.cs new file mode 100644 index 000000000..5ba2f698d --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Models/QmUserSettings.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using ClientCore; +using Newtonsoft.Json; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +public class QmUserSettings +{ + private static readonly string SettingsFile = $"{ProgramConstants.ClientUserFilesPath}QuickMatchSettings.ini"; + + private const string BasicSectionKey = "Basic"; + private const string AuthSectionKey = "Auth"; + private const string AuthDataKey = "AuthData"; + private const string EmailKey = "Email"; + private const string LadderKey = "Ladder"; + private const string SideKey = "Side"; + + public string Email { get; set; } + + public string Ladder { get; set; } + + public int? SideId { get; set; } + + public QmAuthData AuthData { get; set; } + + private QmUserSettings() + { + } + + public static QmUserSettings Load() + { + var settings = new QmUserSettings(); + if (!File.Exists(SettingsFile)) + return settings; + + var iniFile = new IniFile(SettingsFile); + LoadAuthSettings(iniFile, settings); + LoadBasicSettings(iniFile, settings); + + return settings; + } + + public void ClearAuthData() => AuthData = null; + + public void Save() + { + var iniFile = new IniFile(); + var authSection = new IniSection(AuthSectionKey); + authSection.AddKey(EmailKey, Email ?? string.Empty); + authSection.AddKey(AuthDataKey, JsonConvert.SerializeObject(AuthData)); + + var basicSection = new IniSection(BasicSectionKey); + basicSection.AddKey(LadderKey, Ladder ?? string.Empty); + basicSection.AddKey(SideKey, SideId?.ToString() ?? string.Empty); + + iniFile.AddSection(authSection); + iniFile.AddSection(basicSection); + iniFile.WriteIniFile(SettingsFile); + } + + private static void LoadAuthSettings(IniFile iniFile, QmUserSettings settings) + { + IniSection authSection = iniFile.GetSection(AuthSectionKey); + if (authSection == null) + return; + + settings.AuthData = GetAuthData(authSection); + settings.Email = authSection.GetStringValue(EmailKey, null); + } + + private static void LoadBasicSettings(IniFile iniFile, QmUserSettings settings) + { + IniSection basicSection = iniFile.GetSection(BasicSectionKey); + if (basicSection == null) + return; + + settings.Ladder = basicSection.GetStringValue(LadderKey, null); + int sideId = basicSection.GetIntValue(SideKey, -1); + if (sideId != -1) + settings.SideId = sideId; + } + + 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.ToString()); + return null; + } + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmMatchRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmMatchRequest.cs new file mode 100644 index 000000000..6ef34737d --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmMatchRequest.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; + +public class QmMatchRequest : QmRequest +{ + [JsonProperty("lan_ip")] + public string LanIP { get; set; } + + [JsonProperty("lan_port")] + public int LanPort { get; set; } + + [JsonProperty("ipv6_address")] + public string IPv6Address { get; set; } + + [JsonProperty("ipv6_port")] + public int IPv6Port { get; set; } + + [JsonProperty("ip_address")] + public string IPAddress { get; set; } + + [JsonProperty("ip_port")] + public int IPPort { get; set; } + + [JsonProperty("side")] + public int Side { get; set; } + + [JsonProperty("map_bitfield")] + public string MapBitfield { get; set; } + + [JsonProperty("platform")] + public string Platform { get; set; } + + [JsonProperty("map_sides")] + public IEnumerable MapSides { get; set; } + + [JsonProperty("ai_dat")] + public bool 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; } + + public QmMatchRequest() + { + Type = QmRequestTypes.MatchMeUp; + MapBitfield = int.MaxValue.ToString(); + Platform = "win32"; + Session = string.Empty; + DDrawHash = "8a00ba609f7d030c67339e1f555199bdb4054b67"; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmNotReadyRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmNotReadyRequest.cs new file mode 100644 index 000000000..a5b55bc08 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmNotReadyRequest.cs @@ -0,0 +1,9 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; + +public class QmNotReadyRequest : QmUpdateRequest +{ + public QmNotReadyRequest(int seed) : base(seed) + { + Status = "NotReady"; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmQuitRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmQuitRequest.cs new file mode 100644 index 000000000..01b817f38 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmQuitRequest.cs @@ -0,0 +1,11 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; + +public class QmQuitRequest : QmRequest +{ + public QmQuitRequest() + { + Type = QmRequestTypes.Quit; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmReadyRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmReadyRequest.cs new file mode 100644 index 000000000..89d8d28d8 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmReadyRequest.cs @@ -0,0 +1,10 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; + +public class QmReadyRequest : QmUpdateRequest +{ + + public QmReadyRequest(int seed) : base (seed) + { + Status = "Ready"; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmRequest.cs new file mode 100644 index 000000000..be5891def --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmRequest.cs @@ -0,0 +1,16 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; + +public class QmRequest +{ + [JsonIgnore] + public string Url { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } = QmService.QmVersion; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmUpdateRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmUpdateRequest.cs new file mode 100644 index 000000000..0f4726e6f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Requests/QmUpdateRequest.cs @@ -0,0 +1,19 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; + +public abstract class QmUpdateRequest : QmRequest +{ + [JsonProperty("status")] + public string Status { get; protected set; } + + [JsonProperty("seed")] + public int Seed { get; set; } + + protected QmUpdateRequest(int seed) + { + Type = QmRequestTypes.Update; + Seed = seed; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmErrorResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmErrorResponse.cs new file mode 100644 index 000000000..effb400d0 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmErrorResponse.cs @@ -0,0 +1,7 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +public class QmErrorResponse : QmResponseMessage +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmFatalResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmFatalResponse.cs new file mode 100644 index 000000000..42030e0d8 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmFatalResponse.cs @@ -0,0 +1,5 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +public class QmFatalResponse : QmErrorResponse +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmQuitResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmQuitResponse.cs new file mode 100644 index 000000000..ba97486f9 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmQuitResponse.cs @@ -0,0 +1,7 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +public class QmQuitResponse : QmResponseMessage +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmResponse.cs new file mode 100644 index 000000000..7c068d802 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmResponse.cs @@ -0,0 +1,56 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Converters; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; +using Newtonsoft.Json; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +public class QmResponse +{ + public QmRequest Request { get; } + + public T Data + { + get => _Data ?? GetData(); + set => _Data = value; + } + + private T _Data; + + private HttpResponseMessage Response { get; } + + public QmResponse(QmRequest request = null, HttpResponseMessage response = null) + { + Request = request; + Response = response; + } + + public bool IsSuccess => Response?.IsSuccessStatusCode ?? false; + + public string ReasonPhrase => Response?.ReasonPhrase; + + public HttpStatusCode StatusCode => Response?.StatusCode ?? HttpStatusCode.InternalServerError; + + public Task ReadContentAsStringAsync() => Response?.Content.ReadAsStringAsync(); + + private T GetData() + { + try + { + return JsonConvert.DeserializeObject(ReadContentAsStringAsync().Result); + } + catch (Exception e) + { + Logger.Log(e.ToString()); + return default; + } + } +} + +public class QmResponse : QmResponse +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmSpawnResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmSpawnResponse.cs new file mode 100644 index 000000000..47a058d4a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmSpawnResponse.cs @@ -0,0 +1,10 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +public class QmSpawnResponse : QmResponseMessage +{ + [JsonProperty("spawn")] + public QmSpawn Spawn { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmUpdateResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmUpdateResponse.cs new file mode 100644 index 000000000..1e4dbaebd --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmUpdateResponse.cs @@ -0,0 +1,7 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +public class QmUpdateResponse : QmResponseMessage +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmWaitResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmWaitResponse.cs new file mode 100644 index 000000000..3828afb96 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Responses/QmWaitResponse.cs @@ -0,0 +1,13 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; + +public class QmWaitResponse : QmResponseMessage +{ + [JsonProperty("checkback")] + public int CheckBack { get; set; } + + [JsonProperty("no_sooner_than")] + public int NoSoonerThan { get; set; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/MockApiService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/MockApiService.cs new file mode 100644 index 000000000..6fea83a6f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/MockApiService.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using DTAClient.Services; +using Newtonsoft.Json; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; + +public class MockApiService : ApiService +{ + public override async Task>> LoadLadderMapsForAbbrAsync(string ladderAbbreviation) => LoadMockData>>($"qm_ladder_maps_{ladderAbbreviation}_response.json"); + + public override async Task> LoadLadderStatsForAbbrAsync(string ladderAbbreviation) => LoadMockData>("qm_ladder_stats_response.json"); + + public override async Task>> LoadUserAccountsAsync() => LoadMockData>>("qm_user_accounts_response.json"); + + public override async Task>> LoadLaddersAsync() => LoadMockData>>("qm_ladders_response.json"); + + public override async Task> LoginAsync(string email, string password) => LoadMockData>("qm_login_response.json"); + + public override async Task> RefreshAsync() => LoadMockData>("qm_login_response.json"); + + public override async Task> QuickMatchRequestAsync(string ladder, string playerName, QmRequest qmRequest) + { + return true switch + { + true when qmRequest.Type == QmRequestTypes.Quit => LoadMockData>("qm_find_match_quit_response.json"), + true when qmRequest.Type == QmRequestTypes.MatchMeUp => LoadMockData>("qm_find_match_spawn_response.json"), + // true when qmRequest.Type ==QmRequestTypes.Update && updateRequest?.Status == QmUpdateRequestStatuses.Ready => LoadMockData("qm_find_match_please_wait_response.json"), + _ => new QmResponse { Data = new QmUpdateResponse { Message = "default response" } } + }; + } + + private static T LoadMockData(string mockDataFileName) + { + string content = File.ReadAllText($"MockData/QuickMatch/{mockDataFileName}"); + + return JsonConvert.DeserializeObject(content); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmService.cs new file mode 100644 index 000000000..a15ba2466 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmService.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using System.Timers; +using ClientCore; +using ClientCore.Exceptions; +using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Events; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; +using DTAClient.Online; +using DTAClient.Services; +using JWT; +using JWT.Algorithms; +using JWT.Exceptions; +using JWT.Serializers; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; + +public class QmService : IDisposable +{ + public const string QmVersion = "2.0"; + + private readonly QmUserSettingsService qmUserSettingsService; + private readonly ApiService apiService; + private readonly SpawnService spawnService; + private readonly QmSettingsService qmSettingsService; + + private readonly QmUserSettings qmUserSettings; + private readonly QmSettings qmSettings; + private readonly QmData qmData; + + private readonly Timer retryRequestmatchTimer; + private QmUserAccount userAccount; + private IEnumerable mapSides; + + public QmService( + QmSettingsService qmSettingsService, + QmUserSettingsService qmUserSettingsService, + ApiService apiService, + SpawnService spawnService + ) + { + this.qmUserSettingsService = qmUserSettingsService; + this.apiService = apiService; + this.spawnService = spawnService; + this.qmSettingsService = qmSettingsService; + + qmUserSettings = this.qmUserSettingsService.GetSettings(); + qmSettings = this.qmSettingsService.GetSettings(); + qmData = new QmData(); + + retryRequestmatchTimer = new Timer(); + retryRequestmatchTimer.AutoReset = false; + retryRequestmatchTimer.Elapsed += (_, _) => RetryRequestMatchAsync(); + } + + public event EventHandler QmEvent; + + 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; + + /// + /// Login process to cncnet. + /// + /// The email for login. + /// The password for login. + public void LoginAsync(string email, string password) => + ExecuteLoginRequest(async () => + { + QmResponse response = await apiService.LoginAsync(email, password); + return FinishLogin(response, email); + }); + + /// + /// Attempts to refresh an existing auth tokenl. + /// + public void RefreshAsync() => + ExecuteLoginRequest(async () => + { + QmResponse response = await apiService.RefreshAsync(); + return FinishLogin(response); + }); + + /// + /// Simply clear all auth data from our settings. + /// + public void Logout() + { + ClearAuthData(); + QmEvent?.Invoke(this, new QmLogoutEvent()); + } + + public bool HasToken() + { + if (qmUserSettings.AuthData == null) + return false; + + try + { + DecodeToken(qmUserSettings.AuthData.Token); + } + catch (TokenExpiredException) + { + Logger.Log(QmStrings.TokenExpiredError); + return false; + } + catch (Exception e) + { + Logger.Log(e.ToString()); + return false; + } + + apiService.SetToken(qmUserSettings.AuthData.Token); + + return true; + } + + public void SetUserAccount(QmUserAccount userAccount) + { + string laddAbbr = userAccount?.Ladder?.Abbreviation; + this.userAccount = userAccount; + qmUserSettings.Ladder = laddAbbr; + qmUserSettingsService.SaveSettings(); + QmEvent?.Invoke(this, new QmUserAccountSelectedEvent(userAccount)); + } + + public void SetMasterSide(QmSide side) + { + qmUserSettings.SideId = side?.LocalId ?? -1; + qmUserSettingsService.SaveSettings(); + QmEvent?.Invoke(this, new QmMasterSideSelected(side)); + } + + public void SetMapSides(IEnumerable mapSides) + { + this.mapSides = mapSides; + } + + public void LoadLaddersAndUserAccountsAsync() => + ExecuteLoadingRequest(new QmLoadingLaddersAndUserAccountsEvent(), async () => + { + Task>> loadLaddersTask = apiService.LoadLaddersAsync(); + Task>> loadUserAccountsTask = apiService.LoadUserAccountsAsync(); + + await Task.WhenAll(loadLaddersTask, loadUserAccountsTask); + + QmResponse> loadLaddersResponse = loadLaddersTask.Result; + if (!loadLaddersResponse.IsSuccess) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(string.Format(QmStrings.LoadingUserAccountsErrorFormat, loadLaddersResponse.ReasonPhrase))); + return; + } + + QmResponse> loadUserAccountsReponse = loadUserAccountsTask.Result; + if (!loadUserAccountsReponse.IsSuccess) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(string.Format(QmStrings.LoadingUserAccountsErrorFormat, loadUserAccountsReponse.ReasonPhrase))); + return; + } + + qmData.Ladders = loadLaddersTask.Result.Data.ToList(); + qmData.UserAccounts = loadUserAccountsTask.Result.Data + .Where(ua => qmSettings.AllowedLadders.Contains(ua.Ladder.Game)) + .GroupBy(ua => ua.Id) // remove possible duplicates + .Select(g => g.First()) + .OrderBy(ua => ua.Ladder.Name) + .ToList(); + + if (!qmData.Ladders.Any()) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(QmStrings.NoLaddersFoundError)); + return; + } + + if (!qmData.UserAccounts.Any()) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(QmStrings.NoUserAccountsFoundError)); + return; + } + + QmEvent?.Invoke(this, new QmLaddersAndUserAccountsEvent(qmData.Ladders, qmData.UserAccounts)); + }); + + public void LoadLadderMapsForAbbrAsync(string ladderAbbr) => + ExecuteLoadingRequest(new QmLoadingLadderMapsEvent(), async () => + { + QmResponse> ladderMapsResponse = await apiService.LoadLadderMapsForAbbrAsync(ladderAbbr); + if (!ladderMapsResponse.IsSuccess) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(string.Format(QmStrings.LoadingLadderMapsErrorFormat, ladderMapsResponse.ReasonPhrase))); + return; + } + + QmEvent?.Invoke(this, new QmLadderMapsEvent(ladderMapsResponse.Data)); + }); + + public void LoadLadderStatsForAbbrAsync(string ladderAbbr) => + ExecuteLoadingRequest(new QmLoadingLadderStatsEvent(), async () => + { + QmResponse ladderStatsResponse = await apiService.LoadLadderStatsForAbbrAsync(ladderAbbr); + + if (!ladderStatsResponse.IsSuccess) + { + QmEvent?.Invoke(this, new QmErrorMessageEvent(string.Format(QmStrings.LoadingLadderStatsErrorFormat, ladderStatsResponse.ReasonPhrase))); + return; + } + + QmEvent?.Invoke(this, new QmLadderStatsEvent(ladderStatsResponse.Data)); + }); + + /// + /// This is called when the user clicks the button to begin searching for a match. + /// + public void RequestMatchAsync() => + ExecuteLoadingRequest(new QmRequestingMatchEvent(CancelRequestMatchAsync), async () => + { + QmResponse response = await apiService.QuickMatchRequestAsync(userAccount.Ladder.Abbreviation, userAccount.Username, GetMatchRequest()); + HandleQuickMatchResponse(response); + }); + + /// + /// This is called when the user clicks the "I'm Ready" button in the match found dialog. + /// + /// Spawn settings from the API. + public void AcceptMatchAsync(QmSpawn spawn) + { + ExecuteLoadingRequest(new QmReadyRequestMatchEvent(), async () => + { + spawnService.WriteSpawnInfo(spawn); + retryRequestmatchTimer.Stop(); + var readyRequest = new QmReadyRequest(spawn.Settings.Seed); + QmResponse response = await apiService.QuickMatchRequestAsync(userAccount.Ladder.Abbreviation, userAccount.Username, readyRequest); + HandleQuickMatchResponse(response); + }); + } + + /// + /// This is called when the user clicks the "Cancel" button in the match found dialog. + /// + /// Spawn settings from the API. + public void RejectMatchAsync(QmSpawn spawn) + { + ExecuteLoadingRequest(new QmNotReadyRequestMatchEvent(), async () => + { + retryRequestmatchTimer.Stop(); + var notReadyRequest = new QmNotReadyRequest(spawn.Settings.Seed); + QmResponse response = await apiService.QuickMatchRequestAsync(userAccount.Ladder.Abbreviation, userAccount.Username, notReadyRequest); + HandleQuickMatchResponse(response); + }); + CancelRequestMatchAsync(); + } + + public void Dispose() + { + apiService.Dispose(); + } + + private QmMatchRequest GetMatchRequest() + { + if (userAccount == null) + throw new ClientException("No user account selected"); + + if (userAccount.Ladder == null) + throw new ClientException("No user account ladder selected"); + + if (!qmUserSettings.SideId.HasValue) + throw new ClientException("No side selected"); + + var fileHashCalculator = new FileHashCalculator(); + + return new QmMatchRequest() + { + IPv6Address = string.Empty, + IPAddress = "98.111.198.94", + IPPort = 51144, + LanIP = "192.168.86.200", + LanPort = 51144, + Side = qmUserSettings.SideId.Value, + MapSides = mapSides, + ExeHash = fileHashCalculator.GetCompleteHash() + }; + } + + private void RetryRequestMatchAsync() => + RequestMatchAsync(); + + private void HandleQuickMatchResponse(QmResponse qmResponse) + { + switch (true) + { + case true when qmResponse.Data is QmWaitResponse waitResponse: + retryRequestmatchTimer.Interval = waitResponse.CheckBack * 1000; + retryRequestmatchTimer.Start(); + break; + } + + QmEvent?.Invoke(this, new QmResponseEvent(qmResponse)); + } + + /// + /// We only need to verify the expiration date of the token so that we can refresh or request a new one if it is expired. + /// We do not need to worry about the signature. The API will handle that validation when the token is used. + /// + /// The token to be decoded. + private static void DecodeToken(string token) + { + IJsonSerializer serializer = new JsonNetSerializer(); + IDateTimeProvider provider = new UtcDateTimeProvider(); + ValidationParameters validationParameters = ValidationParameters.Default; + validationParameters.ValidateSignature = false; + IJwtValidator validator = new JwtValidator(serializer, provider, validationParameters); + IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); + IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric + IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); + + decoder.Decode(token, "nokey", verify: true); + } + + private void ClearAuthData() + { + qmUserSettingsService.ClearAuthData(); + qmUserSettingsService.SaveSettings(); + apiService.SetToken(null); + } + + private void CancelRequestMatchAsync() => + ExecuteLoadingRequest(new QmCancelingRequestMatchEvent(), async () => + { + retryRequestmatchTimer.Stop(); + QmResponse response = await apiService.QuickMatchRequestAsync(userAccount.Ladder.Abbreviation, userAccount.Username, new QmQuitRequest()); + QmEvent?.Invoke(this, new QmResponseEvent(response)); + }); + + private void ExecuteLoginRequest(Func> loginFunction) => + ExecuteLoadingRequest(new QmLoggingInEvent(), async () => + { + if (await loginFunction()) + QmEvent?.Invoke(this, new QmLoginEvent()); + }); + + private void ExecuteLoadingRequest(QmEvent qmLoadingEvent, Func requestAction) + { + QmEvent?.Invoke(this, qmLoadingEvent); + Task.Run(async () => + { + try + { + await requestAction(); + } + catch (Exception e) + { + Logger.Log(e.ToString()); + QmEvent?.Invoke(this, new QmErrorMessageEvent((e as ClientException)?.Message ?? QmStrings.UnknownError)); + } + }); + } + + private bool FinishLogin(QmResponse response, string email = null) + { + if (!response.IsSuccess) + { + HandleFailedLogin(response); + return false; + } + + qmUserSettings.AuthData = response.Data; + qmUserSettings.Email = email ?? qmUserSettings.Email; + qmUserSettingsService.SaveSettings(); + + apiService.SetToken(response.Data.Token); + return true; + } + + private void HandleFailedLogin(QmResponse response) + { + string message; + switch (response.StatusCode) + { + case HttpStatusCode.BadGateway: + message = QmStrings.ServerUnreachableError; + break; + case HttpStatusCode.Unauthorized: + message = QmStrings.InvalidUsernamePasswordError; + break; + default: + var responseBody = response.ReadContentAsStringAsync().Result; + message = string.Format(QmStrings.LoggingInUnknownErrorFormat, response.ReasonPhrase, responseBody); + break; + } + + QmEvent?.Invoke(this, new QmErrorMessageEvent(message ?? QmStrings.UnknownError)); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmSettingsService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmSettingsService.cs new file mode 100644 index 000000000..2a0e4f782 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmSettingsService.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ClientCore; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Rampastring.Tools; +using Rampastring.XNAUI; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; + +public class QmSettingsService +{ + private static readonly string SettingsFile = ClientConfiguration.Instance.QuickMatchIniPath; + + private const string BasicSectionKey = "Basic"; + private const string SoundsSectionKey = "Sounds"; + private const string HeaderLogosSectionKey = "HeaderLogos"; + + private const string MatchFoundSoundFileKey = "MatchFoundSoundFile"; + private const string AllowedLaddersKey = "AllowedLadders"; + + private QmSettings qmSettings; + + public QmSettings GetSettings() => qmSettings ??= LoadSettings(); + + private static QmSettings LoadSettings() + { + if (!File.Exists(SettingsFile)) + { + Logger.Log($"No QuickMatch settings INI not found: {SettingsFile}"); + return null; + } + + var settings = new QmSettings(); + var iniFile = new IniFile(SettingsFile); + LoadBasicSettings(iniFile, settings); + LoadSoundSettings(iniFile, settings); + LoadHeaderLogoSettings(iniFile, settings); + + return settings; + } + + private static void LoadBasicSettings(IniFile iniFile, QmSettings settings) + { + IniSection basicSection = iniFile.GetSection(BasicSectionKey); + if (basicSection == null) + return; + + settings.MatchFoundWaitSeconds = QmSettings.DefaultMatchFoundWaitSeconds; + settings.AllowedLadders = basicSection.GetStringValue(AllowedLaddersKey, string.Empty).Split(',').ToList(); + } + + private static void LoadSoundSettings(IniFile iniFile, QmSettings settings) + { + IniSection soundsSection = iniFile.GetSection(SoundsSectionKey); + + string matchFoundSoundFile = soundsSection?.GetStringValue(MatchFoundSoundFileKey, null); + if (matchFoundSoundFile == null) + return; + + matchFoundSoundFile = SafePath.CombineFilePath("Resources", matchFoundSoundFile); + if (File.Exists(matchFoundSoundFile)) + settings.MatchFoundSoundFile = matchFoundSoundFile; + } + + private static void LoadHeaderLogoSettings(IniFile iniFile, QmSettings settings) + { + IniSection headerLogosSection = iniFile.GetSection(HeaderLogosSectionKey); + if (headerLogosSection == null) + return; + + foreach (KeyValuePair keyValuePair in headerLogosSection.Keys.Where(keyValuePair => AssetLoader.AssetExists(keyValuePair.Value))) + settings.HeaderLogos.Add(keyValuePair.Key, AssetLoader.LoadTexture(keyValuePair.Value)); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmUserSettingsService.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmUserSettingsService.cs new file mode 100644 index 000000000..438f3d8c2 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Services/QmUserSettingsService.cs @@ -0,0 +1,14 @@ +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Services; + +public class QmUserSettingsService +{ + private QmUserSettings qmUserSettings; + + public QmUserSettings GetSettings() => qmUserSettings ??= QmUserSettings.Load(); + + public void SaveSettings() => qmUserSettings.Save(); + + public void ClearAuthData() => qmUserSettings.ClearAuthData(); +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmHttpClient.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmHttpClient.cs new file mode 100644 index 000000000..2f7f0b8ec --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmHttpClient.cs @@ -0,0 +1,63 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using Newtonsoft.Json; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +public class QmHttpClient : HttpClient +{ + private const string DefaultContentType = "application/json"; + + public async Task> GetAsync(QmRequest qmRequest) + { + HttpResponseMessage httpResponse = await GetAsync(qmRequest.Url); + return new QmResponse(qmRequest, httpResponse); + } + + /// + /// This method can be used to explicitly set the Response.Data value upon successful or failed API responses. + /// + public async Task> GetAsync(QmRequest qmRequest, T successDataValue, T failedDataValue) + { + try + { + QmResponse response = await GetAsync(qmRequest); + response.Data = response.IsSuccess ? successDataValue : failedDataValue; + + return response; + } + catch (Exception e) + { + Logger.Log(e.ToString()); + return new QmResponse(qmRequest, new HttpResponseMessage(HttpStatusCode.InternalServerError) { ReasonPhrase = e.Message }); + } + } + + public async Task> GetAsync(string url) + => await GetAsync(new QmRequest { Url = url }); + + public async Task> GetAsync(string url, T successDataValue, T failedDataValue) + => await GetAsync(new QmRequest { Url = url }, successDataValue, failedDataValue); + + public async Task> PostAsync(QmRequest qmRequest, object data) + { + try + { + HttpContent content = data as HttpContent ?? new StringContent(JsonConvert.SerializeObject(data), Encoding.Default, DefaultContentType); + return new QmResponse(qmRequest, await PostAsync(qmRequest.Url, content)); + } + catch (Exception e) + { + Logger.Log(e.ToString()); + return new QmResponse(qmRequest, new HttpResponseMessage(HttpStatusCode.InternalServerError) { ReasonPhrase = e.Message }); + } + } + + public async Task> PostAsync(string url, object data) => await PostAsync(new QmRequest { Url = url }, data); +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmMatchFoundTimer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmMatchFoundTimer.cs new file mode 100644 index 000000000..f4fa01302 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmMatchFoundTimer.cs @@ -0,0 +1,26 @@ +using System; +using System.Timers; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +public class QmMatchFoundTimer : Timer +{ + private const int MatchupFoundTimerInterval = 100; + + public QmSpawn Spawn { get; set; } + + public QmMatchFoundTimer() : base(MatchupFoundTimerInterval) + { + AutoReset = true; + } + + public int GetInterval() => MatchupFoundTimerInterval; + + public void SetSpawn(QmSpawn spawn) + { + Spawn = spawn; + } + + public void SetElapsedAction(Action elapsedAction) => Elapsed += (_, _) => elapsedAction(); +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmRequestTypes.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmRequestTypes.cs new file mode 100644 index 000000000..d9a927f8f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmRequestTypes.cs @@ -0,0 +1,8 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +public static class QmRequestTypes +{ + public const string Quit = "quit"; + public const string Update = "update"; + public const string MatchMeUp = "match me up"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmResponseTypes.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmResponseTypes.cs new file mode 100644 index 000000000..b9bf79208 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmResponseTypes.cs @@ -0,0 +1,11 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +public static class QmResponseTypes +{ + public const string Error = "error"; + public const string Fatal = "fatal"; + public const string Spawn = "spawn"; + public const string Update = "update"; + public const string Quit = "quit"; + public const string Wait = "please wait"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmStrings.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmStrings.cs new file mode 100644 index 000000000..acba93b56 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmStrings.cs @@ -0,0 +1,77 @@ +using Localization; + +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +public static class QmStrings +{ + // Error Messages + public static string TokenExpiredError => "QuickMatch token is expired".L10N("QM:Error:TokenExpired"); + + public static string NoLaddersFoundError => "No quick match ladders currently found.".L10N("QM:Error:NoLaddersFound"); + + public static string NoUserAccountsFoundError => "No user accounts found in quick match. Are you registered for this month?".L10N("QM:Error:NoUserAccountsFound"); + + public static string LoadingLadderStatsError => "Error loading ladder stats".L10N("QM:Error:LoadingLadderStats"); + + public static string LoadingLadderMapsError => "Error loading ladder maps".L10N("QM:Error:LoadingLadderMaps"); + + public static string ServerUnreachableError => "Server unreachable".L10N("QM:Error:ServerUnreachable"); + + public static string InvalidUsernamePasswordError => "Invalid username/password".L10N("QM:Error:InvalidUsernamePassword"); + + public static string NoSideSelectedError => "No side selected".L10N("QM:Error:NoSideSelected"); + + public static string NoLadderSelectedError => "No ladder selected".L10N("QM:Error:NoLadderSelected"); + + public static string UnknownError => "Unknown error occurred".L10N("QM:Error:Unknown"); + + public static string LoggingInUnknownError => "Error logging in".L10N("QM:Error:LoggingInUnknown"); + + public static string CancelingMatchRequestError => "Error canceling match request".L10N("QM:Error:CancelingMatchRequest"); + + public static string RequestingMatchUnknownError => "Error requesting match".L10N("QM:Error:RequestingMatchUnknown"); + + public static string LoadingLaddersAndAccountsUnknownError => "Error loading ladders and accounts...".L10N("QM:Error:LoadingLaddersAndAccountsUnknown"); + + // Error Messages Formatted + public static string LoadingLadderMapsErrorFormat => "Error loading ladder maps: {0}".L10N("QM:Error:LoadingLadderMapsFormat"); + + public static string LoadingLadderStatsErrorFormat => "Error loading ladder stats: {0}".L10N("QM:Error:LoadingLadderStatsFormat"); + + public static string LoadingUserAccountsErrorFormat => "Error loading user accounts: {0}".L10N("QM:Error:LoadingUserAccountsFormat"); + + public static string LoadingLaddersErrorFormat => "Error loading ladders: {0}".L10N("QM:Error:LoadingLaddersFormat"); + + public static string LoggingInUnknownErrorFormat => "Error logging in: {0}, {1}".L10N("QM:Error:LoggingInUnknownFormat"); + + public static string RequestingMatchErrorFormat => "Error requesting match: {0}".L10N("QM:Error:RequestingMatchFormat"); + + public static string UnableToCreateMatchRequestDataError => "Unable to create match request data".L10N("QM:Error:UnableToCreateMatchRequestDataError"); + + // UI Messages + public static string GenericErrorTitle => "Error".L10N("QM:UI:GenericErrorTitle"); + + public static string LogoutConfirmation => "Are you sure you want to log out?".L10N("QM:UI:LogoutConfirmation"); + + public static string ConfirmationCaption => "Confirmation".L10N("QM:UI:ConfirmationCaption"); + + public static string RequestingMatchStatus => "Requesting match".L10N("QM:UI:RequestingMatchStatus"); + + public static string CancelingMatchRequestStatus => "Canceling match request".L10N("QM:UI:CancelingMatchRequest"); + + public static string LoadingStats => "Loading stats...".L10N("QM:UI:LoadingStats"); + + public static string LoadingLaddersAndAccountsStatus => "Loading ladders and accounts".L10N("QM:UI:LoadingLaddersAndAccountsStatus"); + + public static string LoadingLadderMapsStatus => "Loading ladder maps".L10N("QM:UI:LoadingLadderMapsStatus"); + + public static string LoggingInStatus => "Logging in".L10N("QM:UI:LoggingInStatus"); + + public static string RandomSideName => "Random".L10N("QM:UI:RandomSideName"); + + public static string MatchupFoundConfirmMsg => "Matchup found! Are you ready?".L10N("QM:UI:MatchupFoundConfirm:Msg"); + + public static string MatchupFoundConfirmYes => "I'm Ready".L10N("QM:UI:MatchupFoundConfirm:Yes"); + + public static string MatchupFoundConfirmNo => "Cancel".L10N("QM:UI:MatchupFoundConfirm:No"); +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmUpdateRequestStatuses.cs b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmUpdateRequestStatuses.cs new file mode 100644 index 000000000..fa697e8e9 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/QuickMatch/Utilities/QmUpdateRequestStatuses.cs @@ -0,0 +1,9 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +public static class QmUpdateRequestStatuses +{ + public const string Ready = "Ready"; + public const string NotReady = "NotReady"; + public const string GameFinished = "GameFinished"; + public const string Reached = "Reached"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 7c91a1690..b99903236 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.Threading; using System.Linq; using System.Threading.Tasks; using ClientCore; @@ -330,5 +329,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/DXMainClient/MockData/QuickMatch/qm_find_match_fatal_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_fatal_response.json new file mode 100644 index 000000000..b00b676fe --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_fatal_response.json @@ -0,0 +1,5 @@ +{ + "type": "fatal", + "message": "fatal message", + "description": "fatal description" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_game_spawned_request.json b/DXMainClient/MockData/QuickMatch/qm_find_match_game_spawned_request.json new file mode 100644 index 000000000..8f38d4325 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_game_spawned_request.json @@ -0,0 +1,14 @@ +{ + "peers": [ + { + "address": "127.0.0.1", + "id": "1234567890", + "port": 51143, + "rtt": 59 + } + ], + "seed": 123, + "status": "GameSpawned", + "type": "update", + "version": "2.0" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_game_spawned_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_game_spawned_response.json new file mode 100644 index 000000000..ad0bb6dfe --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_game_spawned_response.json @@ -0,0 +1,3 @@ +{ + "message": "update qm match: GameSpawned" +} \ No newline at end of file diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_gamefinished_request.json b/DXMainClient/MockData/QuickMatch/qm_find_match_gamefinished_request.json new file mode 100644 index 000000000..f1fe941cb --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_gamefinished_request.json @@ -0,0 +1,6 @@ +{ + "seed": 123, + "status": "GameFinished", + "type": "update", + "version": "1.74" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_gamefinished_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_gamefinished_response.json new file mode 100644 index 000000000..e318f55ef --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_gamefinished_response.json @@ -0,0 +1,3 @@ +{ + "message": "update qm match: GameFinished" +} \ No newline at end of file diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_im_ready_request.json b/DXMainClient/MockData/QuickMatch/qm_find_match_im_ready_request.json new file mode 100644 index 000000000..cd8feb787 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_im_ready_request.json @@ -0,0 +1,6 @@ +{ + "seed": 123, + "status": "Ready", + "type": "update", + "version": "2.0" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_im_ready_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_im_ready_response.json new file mode 100644 index 000000000..ffd750548 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_im_ready_response.json @@ -0,0 +1,3 @@ +{ + "message": "update qm match: Ready" +} \ No newline at end of file diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_please_wait_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_please_wait_response.json new file mode 100644 index 000000000..36f913e5f --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_please_wait_response.json @@ -0,0 +1,5 @@ +{ + "type": "please wait", + "checkback": 10, + "no_sooner_than": 5 +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_quit_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_quit_response.json new file mode 100644 index 000000000..3116ae077 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_quit_response.json @@ -0,0 +1,3 @@ +{ + "type": "quit" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_reached_request.json b/DXMainClient/MockData/QuickMatch/qm_find_match_reached_request.json new file mode 100644 index 000000000..5f80a94e0 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_reached_request.json @@ -0,0 +1,6 @@ +{ + "seed": 123, + "status": "Reached", + "type": "update", + "version": "2.0" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_reached_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_reached_response.json new file mode 100644 index 000000000..b7bafb661 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_reached_response.json @@ -0,0 +1,3 @@ +{ + "message": "update qm match: Reached" +} \ No newline at end of file diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_request.json b/DXMainClient/MockData/QuickMatch/qm_find_match_request.json new file mode 100644 index 000000000..401c10d60 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_request.json @@ -0,0 +1,52 @@ +{ + "ai_dat": false, + "ddraw": "38f9496b915547ea816a1cd716b65c0d", + "exe_hash": "f265a5c6ce05488884563a5de6e323d7", + "ip_address": "127.0.0.1", + "ip_port": 51143, + "ipv6_address": "", + "ipv6_port": 0, + "lan_ip": "127.0.0.1", + "lan_port": 51143, + "map_bitfield": 2147483647, + "map_sides": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "platform": "win32", + "session": "", + "side": 0, + "type": "match me up", + "version": "2.0" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_find_match_spawn_response.json b/DXMainClient/MockData/QuickMatch/qm_find_match_spawn_response.json new file mode 100644 index 000000000..025948a3f --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_find_match_spawn_response.json @@ -0,0 +1,50 @@ +{ + "type": "spawn", + "gameID": 603498, + "spawn": { + "SpawnLocations": { + "Multi1": -1, + "Multi2": -1 + }, + "Settings": { + "UIMapName": "Copacabana", + "MapHash": "d3797ec3d3e29cfa2ac81f9e08c9e7ebab142952", + "Seed": -1976956982, + "GameID": -1976956982, + "WOLGameID": -1976956982, + "Host": "No", + "IsSpectator": "No", + "Name": "devo1929", + "Port": 51143, + "Side": 0, + "Color": 0, + "GameSpeed": "0", + "Credits": "10000", + "UnitCount": "0", + "Superweapons": "Yes", + "Tournament": "1", + "ShortGame": "Yes", + "Bases": "Yes", + "MCVRedeploy": "Yes", + "MultipleFactory": "Yes", + "Crates": "No", + "GameMode": "Battle", + "FrameSendRate": "4", + "DisableSWvsYuri": "Yes" + }, + "Other1": { + "Name": "neogrant", + "Side": 0, + "Color": 1, + "Ip": "98.111.198.94", + "Port": 1024, + "IPv6": "", + "PortV6": 0, + "LanIP": "", + "LanPort": 51143 + } + }, + "client": { + "show_map_preview": 1 + } +} diff --git a/DXMainClient/MockData/QuickMatch/qm_ladder_maps_ra2_response.json b/DXMainClient/MockData/QuickMatch/qm_ladder_maps_ra2_response.json new file mode 100644 index 000000000..9ee8f631e --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_ladder_maps_ra2_response.json @@ -0,0 +1,76 @@ +[ + { + "id": 2571, + "ladder_id": 0, + "map_id": 1412, + "description": "Across the Frost", + "bit_idx": 0, + "valid": 1, + "spawn_order": "0,0", + "created_at": "2022-10-31 23:07:40", + "updated_at": "2022-10-31 23:07:40", + "team1_spawn_order": " ", + "team2_spawn_order": "", + "allowed_sides": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 2, + 9, + -1 + ], + "admin_description": "Across the Frost", + "map_pool_id": 56, + "rejectable": 1, + "default_reject": 0, + "hash": "e13e94fc4b4d19e9be93682f5e31d8e416f762f5", + "map": { + "id": 1412, + "hash": "e13e94fc4b4d19e9be93682f5e31d8e416f762f5", + "name": "across_the_frost_e13e94fc4b4d19e9be93682f5e31d8e416f762f5", + "ladder_id": 1 + } + }, + { + "id": 2572, + "ladder_id": 0, + "map_id": 669, + "description": "Arabian Oasis", + "bit_idx": 1, + "valid": 1, + "spawn_order": "0,0", + "created_at": "2022-10-31 23:07:40", + "updated_at": "2022-10-31 23:07:40", + "team1_spawn_order": " ", + "team2_spawn_order": "", + "allowed_sides": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 2, + 9, + -1 + ], + "admin_description": "Arabian Oasis", + "map_pool_id": 56, + "rejectable": 1, + "default_reject": 0, + "hash": "37ee5d7c1e4127538979857c45b1747af36e8f64", + "map": { + "id": 669, + "hash": "37ee5d7c1e4127538979857c45b1747af36e8f64", + "name": "Arabian Oasis v2.2", + "ladder_id": 1 + } + } +] diff --git a/DXMainClient/MockData/QuickMatch/qm_ladder_maps_yr_response.json b/DXMainClient/MockData/QuickMatch/qm_ladder_maps_yr_response.json new file mode 100644 index 000000000..3398e6b8a --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_ladder_maps_yr_response.json @@ -0,0 +1,76 @@ +[ + { + "id": 2573, + "ladder_id": 0, + "map_id": 680, + "description": "Cavern's Of Siberia", + "bit_idx": 2, + "valid": 1, + "spawn_order": "0,0", + "created_at": "2022-10-31 23:07:40", + "updated_at": "2022-10-31 23:07:40", + "team1_spawn_order": " ", + "team2_spawn_order": "", + "allowed_sides": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 2, + 9, + -1 + ], + "admin_description": "Cavern's Of Siberia", + "map_pool_id": 56, + "rejectable": 1, + "default_reject": 0, + "hash": "2f11a7459c9951fbfd6b5bbd73cccd7cf9b0bf5c", + "map": { + "id": 680, + "hash": "2f11a7459c9951fbfd6b5bbd73cccd7cf9b0bf5c", + "name": "4_caverns_v410", + "ladder_id": 1 + } + }, + { + "id": 2575, + "ladder_id": 0, + "map_id": 1408, + "description": "Copacabana", + "bit_idx": 4, + "valid": 1, + "spawn_order": "0,0", + "created_at": "2022-10-31 23:07:40", + "updated_at": "2022-10-31 23:07:40", + "team1_spawn_order": " ", + "team2_spawn_order": "", + "allowed_sides": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 2, + 9, + -1 + ], + "admin_description": "Copacabana", + "map_pool_id": 56, + "rejectable": 1, + "default_reject": 0, + "hash": "d3797ec3d3e29cfa2ac81f9e08c9e7ebab142952", + "map": { + "id": 1408, + "hash": "d3797ec3d3e29cfa2ac81f9e08c9e7ebab142952", + "name": "copacabana_d3797ec3d3e29cfa2ac81f9e08c9e7ebab142952", + "ladder_id": 1 + } + } +] diff --git a/DXMainClient/MockData/QuickMatch/qm_ladder_stats_response.json b/DXMainClient/MockData/QuickMatch/qm_ladder_stats_response.json new file mode 100644 index 000000000..0ef7b5b44 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_ladder_stats_response.json @@ -0,0 +1,12 @@ +{ + "recentMatchedPlayers": 0, + "queuedPlayers": 2, + "past24hMatches": 0, + "recentMatches": 0, + "activeMatches": 0, + "time": { + "date": "2022-11-08 11:03:14.546747", + "timezone_type": 3, + "timezone": "UTC" + } +} diff --git a/DXMainClient/MockData/QuickMatch/qm_ladders_response.json b/DXMainClient/MockData/QuickMatch/qm_ladders_response.json new file mode 100644 index 000000000..06092657a --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_ladders_response.json @@ -0,0 +1,273 @@ +[ + { + "id": 1, + "name": "Ladder 1", + "abbreviation": "yr", + "game": "yr", + "created_at": "-0001-11-30 00:00:00", + "updated_at": "2022-11-01 00:16:36", + "clans_allowed": 0, + "game_object_schema_id": 1, + "map_pool_id": 56, + "private": 0, + "sides": [ + { + "id": 1, + "ladder_id": 1, + "local_id": 0, + "name": "America", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 2, + "ladder_id": 1, + "local_id": 1, + "name": "Korea", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 3, + "ladder_id": 1, + "local_id": 3, + "name": "Germany", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 4, + "ladder_id": 1, + "local_id": 4, + "name": "Great Britain", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 5, + "ladder_id": 1, + "local_id": 5, + "name": "Libya", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 6, + "ladder_id": 1, + "local_id": 6, + "name": "Iraq", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 7, + "ladder_id": 1, + "local_id": 7, + "name": "Cuba", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 8, + "ladder_id": 1, + "local_id": 8, + "name": "Russia", + "created_at": "2017-07-28 09:58:22", + "updated_at": "2017-07-28 09:58:22" + }, + { + "id": 13, + "ladder_id": 1, + "local_id": 2, + "name": "France", + "created_at": "2017-09-04 05:41:03", + "updated_at": "2017-09-04 05:41:03" + }, + { + "id": 14, + "ladder_id": 1, + "local_id": 9, + "name": "Yuri", + "created_at": "2017-09-04 05:41:03", + "updated_at": "2017-09-04 05:41:03" + }, + { + "id": 24, + "ladder_id": 1, + "local_id": -1, + "name": "Random", + "created_at": "-0001-11-30 00:00:00", + "updated_at": "-0001-11-30 00:00:00" + } + ], + "vetoes": 6, + "allowed_sides": [ + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "current": "11-2022", + "qm_ladder_rules": { + "id": 1, + "ladder_id": 1, + "player_count": 2, + "map_vetoes": 6, + "max_difference": 498, + "all_sides": "0,1,2,3,4,5,6,7,8,9", + "allowed_sides": "-1,0,1,2,3,4,5,6,7,8,9", + "created_at": "2017-07-28 04:50:56", + "updated_at": "2022-11-01 21:54:13", + "bail_time": 0, + "bail_fps": 0, + "tier2_rating": 0, + "rating_per_second": 0.75, + "max_points_difference": 400, + "points_per_second": 1, + "use_elo_points": 1, + "wol_k": 64, + "show_map_preview": 1, + "reduce_map_repeats": 3, + "ladder_rules_message": "1. No pushing or intentionally feeding players points.\n2. Using multiple accounts to bypass the 1 nick restriction is not allowed.\n3. Disconnecting games is not allowed.\n4. Using a VPN to hide your identity is not allowed.\n5. No cancelling matches consecutively to block players from QMing.\n6. No abusing bugs on your opponents to gain an upper hand.\n7. Games that end in stalemate may be washed if photo\/video is provided as proof of the stalemate.\n8. Games that need to be washed must be reported within 1 week. On the last 2 days of the month old games will not be washed.\n9. Opening menu and lowering the game speed during a game is not allowed.\n10. You are not allowed to impersonate other players\n11. You are not allowed to quit matches at start of match if you don't like the opponent\/faction or whatever, this goes against competitive fair play. This is a competitive environment you should respect the game, your opponents, and the ladder or else don't play.\n12. If you are lagging games below 50fps you are subject to a cooldown.\n13. If a game is lagging you may report the game to be washed if you quit before 1min into the game. Fps should be below 50fps for it to be washed.", + "ladder_discord": "https:\/\/discord.com\/invite\/aJRJFe5" + } + }, + { + "id": 2, + "name": "Ladder 2", + "abbreviation": "ra2", + "game": "yr", + "created_at": "2022-04-22 02:26:06", + "updated_at": "2022-11-01 00:16:07", + "clans_allowed": 0, + "game_object_schema_id": 1, + "map_pool_id": 57, + "private": 0, + "sides": [ + { + "id": 36, + "ladder_id": 2, + "local_id": 0, + "name": "America", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 37, + "ladder_id": 2, + "local_id": 1, + "name": "Korea", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 38, + "ladder_id": 2, + "local_id": 2, + "name": "France", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 39, + "ladder_id": 2, + "local_id": 3, + "name": "Germany", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 40, + "ladder_id": 2, + "local_id": 4, + "name": "Great Britain", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 41, + "ladder_id": 2, + "local_id": 5, + "name": "Libya", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 42, + "ladder_id": 2, + "local_id": 6, + "name": "Iraq", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 43, + "ladder_id": 2, + "local_id": 7, + "name": "Cuba", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 44, + "ladder_id": 2, + "local_id": 8, + "name": "Russia", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-04-22 02:26:07" + }, + { + "id": 67, + "ladder_id": 2, + "local_id": -1, + "name": "Random", + "created_at": "2022-06-14 02:13:59", + "updated_at": "2022-06-14 02:13:59" + } + ], + "vetoes": 8, + "allowed_sides": [ + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "current": "11-2022", + "qm_ladder_rules": { + "id": 4, + "ladder_id": 2, + "player_count": 2, + "map_vetoes": 8, + "max_difference": 400, + "all_sides": "0,1,2,3,4,5,6,7,8", + "allowed_sides": "-1,0,1,2,3,4,5,6,7,8", + "created_at": "2022-04-22 02:26:07", + "updated_at": "2022-11-05 23:03:52", + "bail_time": 0, + "bail_fps": 0, + "tier2_rating": 0, + "rating_per_second": 0.75, + "max_points_difference": 400, + "points_per_second": 2, + "use_elo_points": 1, + "wol_k": 64, + "show_map_preview": 1, + "reduce_map_repeats": 5, + "ladder_rules_message": "1. No pushing or intentionally feeding players points.\r\n2. Using multiple accounts to bypass the 1 nick restriction is not allowed.\r\n3. Disconnecting games is not allowed.\r\n4. Using a VPN to hide your identity is not allowed.\r\n5. No cancelling matches consecutively to block players from QMing.\r\n6. No abusing bugs on your opponents to gain an upper hand.\r\n7. Games that end in stalemate may be washed if photo\/video is provided as proof of the stalemate.\r\n8. Games that need to be washed must be reported within 1 week. On the last 2 days of the month old games will not be washed.\r\n9. Opening menu and lowering the game speed during a game is not allowed.\r\n10. You are not allowed to impersonate other players\r\n11. You are not allowed to quit matches at start of match if you don't like the opponent\/faction or whatever, this goes against competitive fair play. This is a competitive environment you should respect the game, your opponents, and the ladder or else don't play.\r\n12. If you are lagging games below 50fps you are subject to a cooldown.\r\n13. If a game is lagging you may report the game to be washed if you quit before 1min into the game. Fps should be below 50fps for it to be washed.", + "ladder_discord": "https:\/\/discord.com\/invite\/aJRJFe5" + } + } +] diff --git a/DXMainClient/MockData/QuickMatch/qm_login_response.json b/DXMainClient/MockData/QuickMatch/qm_login_response.json new file mode 100644 index 000000000..eced869e1 --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_login_response.json @@ -0,0 +1,5 @@ +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtb2NrLWNuY25ldCIsImlhdCI6MTY2NzkxNDMwNywiZXhwIjoxOTIwMzc1MTA3LCJhdWQiOiJtb2NrLWNuY25ldC1xbSIsInN1YiI6IjEyMzQ1In0.-i3E9cVw8RRqXUGbUEtsp8Drx3Wx6lZ64i9LoCXKDnw", + "email": "test@gmail.com", + "name": "testuserme" +} diff --git a/DXMainClient/MockData/QuickMatch/qm_user_accounts_response.json b/DXMainClient/MockData/QuickMatch/qm_user_accounts_response.json new file mode 100644 index 000000000..3e2d1872e --- /dev/null +++ b/DXMainClient/MockData/QuickMatch/qm_user_accounts_response.json @@ -0,0 +1,38 @@ +[ + { + "id": 1, + "username": "testuserme", + "ladder_id": 1, + "card_id": 0, + "ladder": { + "id": 1, + "name": "Ladder 1", + "abbreviation": "yr", + "game": "yr", + "created_at": "2022-11-01 00:00:00", + "updated_at": "2022-11-01 00:00:00", + "clans_allowed": 0, + "game_object_schema_id": 1, + "map_pool_id": 1, + "private": 0 + } + }, + { + "id": 2, + "username": "testuserme", + "ladder_id": 2, + "card_id": 0, + "ladder": { + "id": 2, + "name": "Ladder 2", + "abbreviation": "ra2", + "game": "ra2", + "created_at": "2022-11-01 00:00:00", + "updated_at": "2022-11-01 00:00:00", + "clans_allowed": 0, + "game_object_schema_id": 2, + "map_pool_id": 2, + "private": 0 + } + } +] diff --git a/DXMainClient/Services/ApiService.cs b/DXMainClient/Services/ApiService.cs new file mode 100644 index 000000000..a86a273d2 --- /dev/null +++ b/DXMainClient/Services/ApiService.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using DTAClient.Domain.Multiplayer.CnCNet; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Requests; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Responses; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Utilities; + +namespace DTAClient.Services; + +public class ApiService : IDisposable +{ + public readonly ApiSettings ApiSettings; + private QmHttpClient _httpClient; + private string token; + + public ApiService() + { + ApiSettings = ApiSettingsService.GetInstance().GetSettings(); + } + + public void SetToken(string token) + { + this.token = token; + HttpClient httpClient = GetHttpClient(); + httpClient.DefaultRequestHeaders.Clear(); + if (!string.IsNullOrEmpty(token)) + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {this.token}"); + } + + public virtual async Task>> LoadLadderMapsForAbbrAsync(string ladderAbbreviation) + => await GetAsync>(string.Format(ApiSettings.GetLadderMapsUrlFormat, ladderAbbreviation)); + + public virtual async Task> LoadLadderStatsForAbbrAsync(string ladderAbbreviation) + => await GetAsync(string.Format(ApiSettings.GetLadderStatsUrlFormat, ladderAbbreviation)); + + public virtual async Task>> LoadUserAccountsAsync() + => await GetAsync>(ApiSettings.GetUserAccountsUrl); + + public virtual async Task>> LoadLaddersAsync() + => await GetAsync>(ApiSettings.GetLaddersUrl); + + public virtual async Task> LoginAsync(string email, string password) + => await PostAsync(ApiSettings.LoginUrl, new QMLoginRequest { Email = email, Password = password }); + + public virtual async Task> RefreshAsync() + => await GetAsync(ApiSettings.RefreshUrl); + + public virtual async Task> IsServerAvailable() + => await GetAsync(new QmRequest { Url = ApiSettings.ServerStatusUrl }, true, false); + + public virtual async Task> QuickMatchRequestAsync(string ladder, string playerName, QmRequest qmRequest) + { + qmRequest.Url = string.Format(ApiSettings.QuickMatchUrlFormat, ladder, playerName); + return await PostAsync(qmRequest, qmRequest); + } + + private QmHttpClient GetHttpClient() => + _httpClient ??= new QmHttpClient { BaseAddress = new Uri(ApiSettings.BaseUrl), Timeout = TimeSpan.FromSeconds(10) }; + + private async Task> GetAsync(string url) + => await GetAsync(new QmRequest { Url = url }); + + private async Task> GetAsync(string url, T successDataValue, T failedDataValue) + => await GetAsync(new QmRequest { Url = url }, successDataValue, failedDataValue); + + private async Task> GetAsync(QmRequest qmRequest) + { + QmHttpClient httpClient = GetHttpClient(); + return await httpClient.GetAsync(qmRequest); + } + + private async Task> GetAsync(QmRequest qmRequest, T successDataValue, T failedDataValue) + { + QmHttpClient httpClient = GetHttpClient(); + return await httpClient.GetAsync(qmRequest, successDataValue, failedDataValue); + } + + private async Task> PostAsync(string url, object data) + => await PostAsync(new QmRequest { Url = url }, data); + + private async Task> PostAsync(QmRequest qmRequest, object data) + { + QmHttpClient httpClient = GetHttpClient(); + return await httpClient.PostAsync(qmRequest, data); + } + + public void Dispose() + { + _httpClient?.Dispose(); + } +} \ No newline at end of file diff --git a/DXMainClient/Services/ApiSettingsService.cs b/DXMainClient/Services/ApiSettingsService.cs new file mode 100644 index 000000000..a39cd5a04 --- /dev/null +++ b/DXMainClient/Services/ApiSettingsService.cs @@ -0,0 +1,57 @@ +using System.IO; +using ClientCore; +using DTAClient.Domain.Multiplayer.CnCNet; +using Rampastring.Tools; + +namespace DTAClient.Services; + +public class ApiSettingsService +{ + private static readonly string SettingsFile = ClientConfiguration.Instance.ApiIniPath; + private ApiSettings apiSettings; + + public static ApiSettingsService Instance { get; set; } + + private const string UrlsSectionKey = "URLs"; + private const string BaseUrlKey = "Base"; + private const string LoginUrlKey = "Login"; + private const string RefreshUrlKey = "Refresh"; + private const string ServerStatusUrlKey = "ServerStatus"; + private const string GetUserAccountsUrlKey = "GetUserAccounts"; + private const string GetLaddersUrlKey = "GetLadders"; + private const string GetLadderMapsUrlKey = "GetLadderMaps"; + + public static ApiSettingsService GetInstance() => Instance ??= new ApiSettingsService(); + + public ApiSettings GetSettings() => apiSettings ??= LoadSettings(); + + private static ApiSettings LoadSettings() + { + if (!File.Exists(SettingsFile)) + { + Logger.Log($"API settings INI not found: {SettingsFile}"); + return null; + } + + var settings = new ApiSettings(); + var iniFile = new IniFile(SettingsFile); + LoadUrls(iniFile, settings); + + return settings; + } + + private static void LoadUrls(IniFile iniFile, ApiSettings settings) + { + IniSection urlsSection = iniFile.GetSection(UrlsSectionKey); + if (urlsSection == null) + return; + + settings.BaseUrl = urlsSection.GetStringValue(BaseUrlKey, ApiSettings.DefaultBaseUrl); + settings.LoginUrl = urlsSection.GetStringValue(LoginUrlKey, ApiSettings.DefaultLoginUrl); + settings.RefreshUrl = urlsSection.GetStringValue(RefreshUrlKey, ApiSettings.DefaultRefreshUrl); + settings.ServerStatusUrl = urlsSection.GetStringValue(ServerStatusUrlKey, ApiSettings.DefaultServerStatusUrl); + settings.GetUserAccountsUrl = urlsSection.GetStringValue(GetUserAccountsUrlKey, ApiSettings.DefaultGetUserAccountsUrl); + settings.GetLaddersUrl = urlsSection.GetStringValue(GetLaddersUrlKey, ApiSettings.DefaultGetLaddersUrl); + settings.GetLadderMapsUrlFormat = urlsSection.GetStringValue(GetLadderMapsUrlKey, ApiSettings.DefaultGetLadderMapsUrl); + } +} \ No newline at end of file diff --git a/DXMainClient/Services/SpawnService.cs b/DXMainClient/Services/SpawnService.cs new file mode 100644 index 000000000..efde2aad5 --- /dev/null +++ b/DXMainClient/Services/SpawnService.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using ClientCore; +using DTAClient.Domain.Multiplayer; +using DTAClient.Domain.Multiplayer.CnCNet.QuickMatch.Models; +using Rampastring.Tools; + +namespace DTAClient.Services; + +public class SpawnService +{ + private readonly MapLoader mapLoader; + + public SpawnService( + MapLoader mapLoader + ) + { + this.mapLoader = mapLoader; + } + + public void WriteSpawnInfo(QmSpawn spawn) + { + IniFile spawnIni = GetSpawnIniFile(); + + AddSpawnSettingsSection(spawn, spawnIni); + AddSpawnOtherSections(spawn, spawnIni); + AddSpawnLocationsSection(spawn, spawnIni); + AddSpawnTunnelSection(spawn, spawnIni); + + spawnIni.WriteIniFile(); + + WriteSpawnMapIni(spawn.Settings.MapHash); + } + + public void WriteSpawnMapIni(string mapHash) + { + Map map = mapLoader.GetMapForSHA(mapHash); + IniFile mapIni = map.GetMapIni(); + mapIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI)); + } + + private static void AddSpawnSettingsSection(QmSpawn spawn, IniFile spawnIni) + { + var settings = new IniSection("Settings"); + settings.SetStringValue("Scenario", "spawnmap.ini"); + settings.SetStringValue("QuickMatch", "Yes"); + + foreach (PropertyInfo prop in spawn.Settings.GetType().GetProperties()) + settings.SetStringValue(prop.Name, prop.GetValue(spawn.Settings).ToString()); + + spawnIni.AddSection(settings); + } + + private static void AddSpawnOtherSections(QmSpawn spawn, IniFile spawnIni) + { + for (int i = 0; i < spawn.Others.Count; i++) + { + // Headers for OTHER# sections are 1-based index + var otherSection = new IniSection($"Other{i + 1}"); + QmSpawnOther other = spawn.Others[i]; + + foreach (PropertyInfo otherProp in other.GetType().GetProperties()) + otherSection.SetStringValue(otherProp.Name, otherProp.GetValue(other).ToString()); + + spawnIni.AddSection(otherSection); + } + } + + private static void AddSpawnLocationsSection(QmSpawn spawn, IniFile spawnIni) + { + var spawnLocationsSection = new IniSection("SpawnLocations"); + foreach (KeyValuePair spawnLocation in spawn.SpawnLocations) + spawnLocationsSection.SetStringValue(spawnLocation.Key, spawnLocation.Value.ToString()); + + spawnIni.AddSection(spawnLocationsSection); + } + + private static void AddSpawnTunnelSection(QmSpawn spawn, IniFile spawnIni) + { + var tunnel = new IniSection("Tunnel"); + tunnel.SetStringValue("Ip", "52.232.96.199"); + tunnel.SetIntValue("Port", 50001); + spawnIni.AddSection(tunnel); + } + + private static IniFile GetSpawnIniFile() + { + FileInfo spawnerMapSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); + spawnerMapSettingsFile.Delete(); + return new IniFile(spawnerMapSettingsFile.FullName); + } +} \ No newline at end of file diff --git a/Docs/INISystem.md b/Docs/INISystem.md index 1269a1974..a40786983 100644 --- a/Docs/INISystem.md +++ b/Docs/INISystem.md @@ -7,8 +7,8 @@ The `[ParserConstants]` section of the `GlobalThemeSettings.ini` file contains c ### Predefined System Constants -`RESOLUTION_WIDTH`: the width of the window when it is initialized -`RESOLUTION_HEIGHT`: the height of the window when it is initialized +`RESOLUTION_WIDTH`: the width of the window when it is initialized +`RESOLUTION_HEIGHT`: the height of the window when it is initialized ### User Defined Constants @@ -22,7 +22,7 @@ $X=MY_EXAMPLE_CONSTANT ``` _NOTE: Constants can only be used in [dynamic control properties](#dynamic-control-properties)_ -## Control properties: +## Control properties: Below lists basic and dynamic control properties. Ordering of properties is important. If there is a property that relies on the size of a control, the properties must set the size of that control first. @@ -31,121 +31,121 @@ Basic control properties cannot use constants #### XNAControl -`X` = `{integer}` the X location of the control -`Y` = `{integer}` the Y location of the control -`Location` = `{comma separated integers}` the X and Y location of the control. -`Width` = `{integer}` the Width of the control -`Height` = `{integer}` the Height of the control -`Size` = `{comma separated integers}` the Width and Height of the control. -`Text` = `{string}` the text to display for the control (ex: buttons, labels, etc...) -`Visible` = `{true/false or yes/no}` whether or not the control should be visible by default -`Enabled` = `{true/false or yes/no}` whether or not the control should be enabled by default -`DistanceFromRightBorder` = `{integer}` the distance of the right edge of this control from the right edge of its parent. This control MUST have a parent. -`DistanceFromBottomBorder` = `{integer}` the distance of the bottom edge of this control from the bottom edge of its parent. This control MUST have a parent. -`FillWidth` = `{integer}` this will set the width of this control to fill the parent/window MINUS this value, starting from the its X position -`FillHeight` = `{integer}` this will set the height of this control to fill the parent/window MINUS this value, starting from the its Y position -`DrawOrder` -`UpdateOrder` -`RemapColor` +`X` = `{integer}` the X location of the control +`Y` = `{integer}` the Y location of the control +`Location` = `{comma separated integers}` the X and Y location of the control. +`Width` = `{integer}` the Width of the control +`Height` = `{integer}` the Height of the control +`Size` = `{comma separated integers}` the Width and Height of the control. +`Text` = `{string}` the text to display for the control (ex: buttons, labels, etc...) +`Visible` = `{true/false or yes/no}` whether or not the control should be visible by default +`Enabled` = `{true/false or yes/no}` whether or not the control should be enabled by default +`DistanceFromRightBorder` = `{integer}` the distance of the right edge of this control from the right edge of its parent. This control MUST have a parent. +`DistanceFromBottomBorder` = `{integer}` the distance of the bottom edge of this control from the bottom edge of its parent. This control MUST have a parent. +`FillWidth` = `{integer}` this will set the width of this control to fill the parent/window MINUS this value, starting from the its X position +`FillHeight` = `{integer}` this will set the height of this control to fill the parent/window MINUS this value, starting from the its Y position +`DrawOrder` +`UpdateOrder` +`RemapColor` #### XNAPanel -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`BorderColor` -`DrawMode` -`AlphaRate` -`BackgroundTexture` -`SolidColorBackgroundTexture` -`DrawBorders` -`Padding` +`BorderColor` +`DrawMode` +`AlphaRate` +`BackgroundTexture` +`SolidColorBackgroundTexture` +`DrawBorders` +`Padding` #### XNAExtraPanel -_(inherits XNAPanel)_ +_(inherits [XNAPanel](#xnapanel))_ -`BackgroundTexture` +`BackgroundTexture` #### XNALabel -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`RemapColor` -`TextColor` -`FontIndex` -`AnchorPoint` -`TextAnchor` -`TextShadowDistance` +`RemapColor` +`TextColor` +`FontIndex` +`AnchorPoint` +`TextAnchor` +`TextShadowDistance` #### XNAButton -_(inherits XNAControl)_ - -`TextColorIdle` -`TextColorHover` -`HoverSoundEffect` -`ClickSoundEffect` -`AdaptiveText` -`AlphaRate` -`FontIndex` -`IdleTexture` -`HoverTexture` -`TextShadowDistance` +_(inherits [XNAControl](#xnacontrol))_ + +`TextColorIdle` +`TextColorHover` +`HoverSoundEffect` +`ClickSoundEffect` +`AdaptiveText` +`AlphaRate` +`FontIndex` +`IdleTexture` +`HoverTexture` +`TextShadowDistance` #### XNAClientButton -_(inherits XNAButton)_ +_(inherits [XNAButton](#xnabutton))_ -`MatchTextureSize` +`MatchTextureSize` #### XNALinkButton -_(inherits XNAClientButton)_ +_(inherits [XNAClientButton](#xnaclientbutton))_ -`URL` -`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines +`URL` +`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines #### XNACheckbox -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`FontIndex` -`IdleColor` -`HighlightColor` -`AlphaRate` -`AllowChecking` -`Checked` +`FontIndex` +`IdleColor` +`HighlightColor` +`AlphaRate` +`AllowChecking` +`Checked` #### XNAClientCheckbox -_(inherits XNACheckBox)_ +_(inherits [XNACheckBox](#xnacheckbox))_ `ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines #### XNADropDown -_(inherits XNAControl)_ - -`OpenUp` -`DropDownTexture` -`DropDownOpenTexture` -`ItemHeight` -`ClickSoundEffect` -`FontIndex` -`BorderColor` -`FocusColor` -`BackColor` -`~~DisabledItemColor~~` -`OptionN` +_(inherits [XNAControl](#xnacontrol))_ + +`OpenUp` +`DropDownTexture` +`DropDownOpenTexture` +`ItemHeight` +`ClickSoundEffect` +`FontIndex` +`BorderColor` +`FocusColor` +`BackColor` +`~~DisabledItemColor~~` +`OptionN` #### XNAClientDropDown -_(inherits XNADropDown)_ +_(inherits [XNADropDown](#xnadropdown))_ -`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines +`ToolTip` = {string} tooltip for checkbox. '@' can be used for newlines #### XNATabControl -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`RemapColor` -`TextColor` -`TextColorDisabled` -`RemoveTabIndexN` +`RemapColor` +`TextColor` +`TextColorDisabled` +`RemoveTabIndexN` #### XNATextBox -_(inherits XNAControl)_ +_(inherits [XNAControl](#xnacontrol))_ -`MaximumTextLength` +`MaximumTextLength` ### Basic Control Property Examples ``` @@ -176,49 +176,49 @@ Following controls are only available as children of `XNAOptionsPanel` and deriv ##### SettingCheckBox _(inherits XNAClientCheckBox)_ -`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. -`WriteSettingValue` = `{true/false or yes/no}` enable to write a specific string value to setting INI key instead of the checked state of the checkbox. Defaults to `false`. -`EnabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is checked. -`DisabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is not checked. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. -`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. -`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. +`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. +`WriteSettingValue` = `{true/false or yes/no}` enable to write a specific string value to setting INI key instead of the checked state of the checkbox. Defaults to `false`. +`EnabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is checked. +`DisabledSettingValue` = `{string}` value to write to setting INI key if `WriteSettingValue` is set and checkbox is not checked. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. +`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. ##### FileSettingCheckBox _(inherits XNAClientCheckBox)_ -`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. -`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. -`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. -`CheckAvailability` = `{true/false or yes/no}` if set, whether or not the checkbox can be (un)checked depends on if the files to copy are actually present. Defaults to `false`. -`ResetUnavailableValue` = `{true/false or yes/no}` if set together with `CheckAvailability`, checkbox set to a value that is unavailable will be reset back to `DefaultValue`. Defaults to `false`. -`EnabledFileN` = `{comma-separated strings}` files to copy if checkbox is checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). -`DisabledFileN` = `{comma-separated strings}` files to copy if checkbox is not checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). +`DefaultValue` = `{true/false or yes/no}` default state of the checkbox. Value of `Checked` will be used if it is set and this isn't. Otherwise defaults to `false`. +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_Checked`. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`ParentCheckBoxName` = `{string}` name of a `XNAClientCheckBox` control to use as a parent checkbox that is required to either be checked or unchecked, depending on value of `ParentCheckBoxRequiredValue` for this checkbox to be enabled. Only works if name can be resolved to an existing control belonging to same parent as current checkbox. +`ParentCheckBoxRequiredValue` = `{true/false or yes/no}` state required from the parent checkbox for this one to be enabled. Defaults to `true`. +`CheckAvailability` = `{true/false or yes/no}` if set, whether or not the checkbox can be (un)checked depends on if the files to copy are actually present. Defaults to `false`. +`ResetUnavailableValue` = `{true/false or yes/no}` if set together with `CheckAvailability`, checkbox set to a value that is unavailable will be reset back to `DefaultValue`. Defaults to `false`. +`EnabledFileN` = `{comma-separated strings}` files to copy if checkbox is checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). +`DisabledFileN` = `{comma-separated strings}` files to copy if checkbox is not checked. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). ##### SettingDropDown _(inherits XNAClientDropDown)_ -`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. -`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_SelectedIndex`. -`WriteSettingValue` = `{true/false or yes/no}` enable to write selected item value to the setting INI key instead of the checked state of the checkbox. Defaults to `false`. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. +`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, otherwise `CONTROLNAME_SelectedIndex`. +`WriteSettingValue` = `{true/false or yes/no}` enable to write selected item value to the setting INI key instead of the checked state of the checkbox. Defaults to `false`. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. ##### FileSettingDropDown _(inherits XNAClientDropDown)_ -`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. -`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). -`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. -`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_SelectedIndex`. -`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. -`ItemXFileN` = `{comma-separated strings}` files to copy when dropdown item X is selected. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). +`Items` = `{comma-separated strings}` comma-separated list of strings to include as items to display on the dropdown control. +`DefaultValue` = `{integer}` default item index of the dropdown. Defaults to 0 (first item). +`SettingSection` = `{string}` name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. +`SettingKey` = `{string}` name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_SelectedIndex`. +`RestartRequired` = `{true/false or yes/no}` whether or not this setting requires restarting the client to apply. Defaults to `false`. +`ItemXFileN` = `{comma-separated strings}` files to copy when dropdown item X is selected. N starts from 0 and is incremented by 1 until no value is found. Expects 2-3 comma-separated strings in following format: source path relative to game root folder, destination path relative to game root folder and a [file operation option](#appendix-file-operation-options). ##### Appendix: File Operation Options @@ -234,11 +234,11 @@ Dynamic Control Properties CAN use constants These can ONLY be used in parent controls that inherit the `INItializableWindow` class -`$X` = ``{integer}`` the X location of the control -`$Y` = ``{integer}`` the Y location of the control -`$Width` = ``{integer}`` the Width of the control -`$Height` = ``{integer}`` the Height of the control -`$TextAnchor` +`$X` = ``{integer}`` the X location of the control +`$Y` = ``{integer}`` the Y location of the control +`$Width` = ``{integer}`` the Width of the control +`$Height` = ``{integer}`` the Height of the control +`$TextAnchor` ### Dynamic Control Property Examples ``` diff --git a/build/AfterPublish.targets b/build/AfterPublish.targets index d8853bba6..d31fecd18 100644 --- a/build/AfterPublish.targets +++ b/build/AfterPublish.targets @@ -36,6 +36,7 @@ + @@ -60,6 +61,7 @@ + @@ -78,6 +80,7 @@ + @@ -123,4 +126,4 @@ - \ No newline at end of file +