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