diff --git a/DXClient.sln b/DXClient.sln
index d1c82ea33..92fcbab3b 100644
--- a/DXClient.sln
+++ b/DXClient.sln
@@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecondStageUpdater", "Secon
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientUpdater", "ClientUpdater\ClientUpdater.csproj", "{551D080B-5624-4793-AC31-69D77C62F6B1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrationTool", "MigrationTool\MigrationTool.csproj", "{5A4EB355-10FB-4705-98E7-FB4A972CB94C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
UniversalGLDebug|Any CPU = UniversalGLDebug|Any CPU
@@ -447,6 +449,70 @@ Global
{551D080B-5624-4793-AC31-69D77C62F6B1}.WindowsXNARelease|x64.Build.0 = WindowsXNARelease|x64
{551D080B-5624-4793-AC31-69D77C62F6B1}.WindowsXNARelease|x86.ActiveCfg = WindowsXNARelease|x86
{551D080B-5624-4793-AC31-69D77C62F6B1}.WindowsXNARelease|x86.Build.0 = WindowsXNARelease|x86
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|Any CPU.ActiveCfg = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|Any CPU.Build.0 = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|ARM64.ActiveCfg = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|ARM64.Build.0 = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|x64.ActiveCfg = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|x64.Build.0 = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|x86.ActiveCfg = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLDebug|x86.Build.0 = UniversalGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|Any CPU.ActiveCfg = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|Any CPU.Build.0 = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|ARM64.ActiveCfg = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|ARM64.Build.0 = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|x64.ActiveCfg = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|x64.Build.0 = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|x86.ActiveCfg = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.UniversalGLRelease|x86.Build.0 = UniversalGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|Any CPU.ActiveCfg = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|Any CPU.Build.0 = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|ARM64.ActiveCfg = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|ARM64.Build.0 = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|x64.ActiveCfg = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|x64.Build.0 = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|x86.ActiveCfg = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXDebug|x86.Build.0 = WindowsDXDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|Any CPU.ActiveCfg = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|Any CPU.Build.0 = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|ARM64.ActiveCfg = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|ARM64.Build.0 = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|x64.ActiveCfg = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|x64.Build.0 = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|x86.ActiveCfg = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsDXRelease|x86.Build.0 = WindowsDXRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|Any CPU.ActiveCfg = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|Any CPU.Build.0 = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|ARM64.ActiveCfg = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|ARM64.Build.0 = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|x64.ActiveCfg = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|x64.Build.0 = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|x86.ActiveCfg = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLDebug|x86.Build.0 = WindowsGLDebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|Any CPU.ActiveCfg = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|Any CPU.Build.0 = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|ARM64.ActiveCfg = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|ARM64.Build.0 = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|x64.ActiveCfg = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|x64.Build.0 = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|x86.ActiveCfg = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsGLRelease|x86.Build.0 = WindowsGLRelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|Any CPU.ActiveCfg = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|Any CPU.Build.0 = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|ARM64.ActiveCfg = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|ARM64.Build.0 = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|x64.ActiveCfg = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|x64.Build.0 = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|x86.ActiveCfg = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNADebug|x86.Build.0 = WindowsXNADebug|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|Any CPU.ActiveCfg = WindowsXNARelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|Any CPU.Build.0 = WindowsXNARelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|ARM64.ActiveCfg = WindowsXNARelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|ARM64.Build.0 = WindowsXNARelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|x64.ActiveCfg = WindowsXNARelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|x64.Build.0 = WindowsXNARelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|x86.ActiveCfg = WindowsXNARelease|Any CPU
+ {5A4EB355-10FB-4705-98E7-FB4A972CB94C}.WindowsXNARelease|x86.Build.0 = WindowsXNARelease|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Directory.Build.props b/Directory.Build.props
index 9779c1a85..21a360140 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -30,7 +30,10 @@
WindowsXNA
-
+
net48;net8.0-windows
net8.0
@@ -51,6 +54,13 @@
AnyCPU
+
+ net8.0-windows
+ AnyCPU
+ enable
+ Exe
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
index f43472c3e..49c4db864 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -23,6 +23,19 @@
Projects="$(MSBuildThisFileDirectory)SecondStageUpdater\SecondStageUpdater.csproj"
Properties="TargetFramework=$(TargetFramework.Split('-')[0]);Platform=AnyCPU;RuntimeIdentifier=" />
+
+
+
+
+
+
+
+
@@ -138,6 +151,22 @@
+
+
+ $(PublishDir)\..\MigrationTool\
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 648c8d8f8..010ee8e9f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -34,11 +34,14 @@
-
+
-
-
+
+
diff --git a/Docs/Migration-INI.md b/Docs/Migration-INI.md
index 79a038373..0fad12545 100644
--- a/Docs/Migration-INI.md
+++ b/Docs/Migration-INI.md
@@ -724,7 +724,7 @@ DrawBorders=true
Size=1230,750
```
-## Edit `GlobalThemeSettings.ini`
+## Edit `DTACnCNetClient.ini`
This file now also contains the `ParserConstants` section, which lists user-defined constants used for positioning controls within panels and windows. **Without this section, the client will crash with new `GameLobbyBase.ini` layout**.
@@ -806,6 +806,8 @@ Location=470,137
Location=0,200
[btnForceUpdate]
+Location=407,213
+Size=133,23
```
2. **OPTIONAL** Add sections:
diff --git a/Docs/Migration.md b/Docs/Migration.md
index 8c539a884..3b668df2a 100644
--- a/Docs/Migration.md
+++ b/Docs/Migration.md
@@ -1,6 +1,14 @@
Migrating from older versions
-----------------------------
+> [!NOTE]
+> CnCNet has a program called `Migration Tool` that implements all the instructions described down below notices to automate the migration process.
+> To use this tool, download the latest nightly build and run `Resources\BinariesNET8\MigrationTool\MigrationTool.exe` with older client path as the first argument.
+>
+> Example: `MigrationTool.exe C:\TiberianSunClient`
+>
+> It is recommended that you back up the client configuration before running the migration tool.
+
This document lists all the breaking changes and how to address them. Each section corresponds to the migration steps that are required to upgrade to the selected version. If you're skipping multiple versions in the upgrade process - you have to apply all corresponding migration steps.
> [!NOTE]
diff --git a/MigrationTool/MigrationTool.csproj b/MigrationTool/MigrationTool.csproj
new file mode 100644
index 000000000..c31d4053c
--- /dev/null
+++ b/MigrationTool/MigrationTool.csproj
@@ -0,0 +1,29 @@
+
+
+ CnCNet.MigrationTool
+ CnCNet Client Migration Tool
+ CnCNet.MigrationTool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MigrationTool/Patch.cs b/MigrationTool/Patch.cs
new file mode 100644
index 000000000..9e86161ce
--- /dev/null
+++ b/MigrationTool/Patch.cs
@@ -0,0 +1,153 @@
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+
+using Rampastring.Tools;
+using ClientCore.Enums;
+namespace MigrationTool;
+
+internal abstract class Patch
+{
+ public Version ClientVersion { get; protected set; }
+ public ClientType Game { get; protected set; }
+ public DirectoryInfo ClientDir { get; protected set; }
+ public DirectoryInfo ResouresDir { get; protected set; }
+
+ public Patch(string clientPath)
+ {
+ ClientDir = SafePath.GetDirectory(clientPath);
+ ResouresDir = SafePath.GetDirectory(SafePath.CombineFilePath(clientPath, "Resources"));
+
+ // Predict client type by guessing game engine files
+ Game = ClientType.TS;
+
+ if (SafePath.GetFile(SafePath.CombineFilePath(ClientDir.FullName, "Ares.dll")).Exists)
+ {
+ Game = ClientType.Ares;
+ }
+ else if (SafePath.GetFile(SafePath.CombineFilePath(ClientDir.FullName, "gamemd-spawn.dll")).Exists)
+ {
+ Game = ClientType.YR;
+ }
+ }
+
+ public virtual void Apply()
+ {
+ Logger.Log($"Applying patch for client version {ClientVersion.ToString().Replace('_', '.')}...");
+ }
+
+ public Patch AddKeyWithLog(IniFile src, string section, string key, string value)
+ {
+ if (src.KeyExists(section, key))
+ {
+ Logger.Log($"Update {src.FileName}: Skip adding [{section}]->{key}, reason: already exist");
+ }
+ else
+ {
+ Logger.Log($"Update {src.FileName}: Add [{section}]->{key}={value}");
+ if (!src.SectionExists(section)) src.AddSection(section);
+ src.GetSection(section).AddKey(key, value);
+ }
+
+ return this;
+ }
+
+ public Patch RemoveKeyWithLog(IniFile src, string section, string key)
+ {
+ if (!src.KeyExists(section, key))
+ {
+ Logger.Log($"Update {src.FileName}: Skip removing [{section}]->{key}, reason: doesn't exist");
+ }
+ else
+ {
+ Logger.Log($"Update {src.FileName}: Remove [{section}]->{key}={src.GetSection(section).Keys.First(kvp => kvp.Key == key).Value}");
+ src.GetSection(section).RemoveKey(key);
+ }
+
+ return this;
+ }
+
+ public void CalculatePositions(IniFile ini, string parent, string child)
+ {
+ int parentX, parentY, childX, childY;
+ parentX = parentY = childX = childY = 0;
+
+ var parentKeys = ini.GetSectionKeys(parent);
+ var childKeys = ini.GetSectionKeys(child);
+
+ var positionKeys = new List() { "$X", "$Y", "X", "Y", "Location" };
+
+ Logger.Log($"Update {ini.FileName}: Fix position for {child} control in {parent}");
+
+ foreach (var control in new List>() { parentKeys, childKeys })
+ {
+ int tmpX, tmpY;
+ tmpX = tmpY = 0;
+
+ foreach (var key in control.Where(key => positionKeys.Contains(key)))
+ {
+ switch (key)
+ {
+ case ("$X"):
+ case ("X"):
+ tmpX = ini.GetIntValue(control == parentKeys ? parent : child, key, tmpX);
+ continue;
+ case ("$Y"):
+ case ("Y"):
+ tmpY = ini.GetIntValue(control == parentKeys ? parent : child, key, tmpY);
+ continue;
+ case ("Location"):
+ var value = ini.GetStringValue(control == parentKeys ? parent : child, key, string.Empty).Split(',');
+ tmpX = Conversions.IntFromString(value[0], tmpX);
+ tmpY = Conversions.IntFromString(value[1], tmpY);
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (control == parentKeys)
+ {
+ parentX = tmpX;
+ parentY = tmpY;
+ }
+ else
+ {
+ childX = tmpX;
+ childY = tmpY;
+ }
+ }
+
+ positionKeys.ForEach(key => ini.RemoveKey(child, key));
+
+ childX = childX - parentX;
+ childY = childY - parentY;
+
+ ini.GetSection(child).AddKey("$X", $"{childX}");
+ ini.GetSection(child).AddKey("$Y", $"{childY}");
+ }
+
+ public Patch TransferKeys(IniFile srcIni, string srcSection, IniFile desIni, string? desSection = null)
+ {
+ desSection ??= srcSection;
+
+ srcIni.GetSectionKeys(srcSection)
+ .ForEach(key => AddKeyWithLog(desIni, desSection, key, srcIni.GetStringValue(srcSection, key, string.Empty)));
+
+ return this;
+ }
+
+ public bool TryApply()
+ {
+ try
+ {
+ Apply();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
+
diff --git a/MigrationTool/Patch_Latest.cs b/MigrationTool/Patch_Latest.cs
new file mode 100644
index 000000000..d01b6dbe0
--- /dev/null
+++ b/MigrationTool/Patch_Latest.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+
+using Rampastring.Tools;
+
+namespace MigrationTool;
+
+internal class Patch_Latest : Patch
+{
+ public Patch_Latest(string clientPath) : base(clientPath)
+ {
+ ClientVersion = Version.Latest;
+ }
+
+ public override void Apply()
+ {
+ base.Apply();
+
+ // Write latest patch there
+ }
+}
+
diff --git a/MigrationTool/Patch_v2_11_0.cs b/MigrationTool/Patch_v2_11_0.cs
new file mode 100644
index 000000000..074fa4599
--- /dev/null
+++ b/MigrationTool/Patch_v2_11_0.cs
@@ -0,0 +1,425 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+using Rampastring.Tools;
+
+namespace MigrationTool;
+
+internal class Patch_v2_11_0 : Patch
+{
+ public Patch_v2_11_0(string clientPath) : base(clientPath)
+ {
+ ClientVersion = Version.v2_11_0;
+ }
+
+ public override void Apply()
+ {
+ base.Apply();
+
+ // Remove Rampastring.Tools from Resources directory (not recursive)
+ Logger.Log("Remove Resources\\Rampastring.Tools.* (* -- dll, pdb, xml)");
+ SafePath.DeleteFileIfExists(ResouresDir.FullName, "Rampastring.Tools.dll");
+ SafePath.DeleteFileIfExists(ResouresDir.FullName, "Rampastring.Tools.pdb");
+ SafePath.DeleteFileIfExists(ResouresDir.FullName, "Rampastring.Tools.xml");
+
+ // Add GenericWindow.ini->[GenericWindow]->DrawBorders=false
+ {
+ var genericWindowIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "GenericWindow.ini"));
+ AddKeyWithLog(genericWindowIni, "GenericWindow", "DrawBorders", "false");
+ if (genericWindowIni.SectionExists("ExtraControls"))
+ genericWindowIni.GetSection("ExtraControls").SectionName = "$ExtraControls";
+ genericWindowIni.WriteIniFile();
+ }
+
+ // Rename OptionsWindow.ini->[*]->{CustomSettingFileCheckBox -- > FileSettingCheckBox & CustomSettingFileDropDown --> FileSettingDropDown}
+ {
+ Logger.Log("Renaming CustomSettingFileCheckBox to FileSettingCheckBox and CustomSettingFileDropDown to FileSettingDropDown");
+ IniFile optionsWindowIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "OptionsWindow.ini"));
+ foreach (var section in optionsWindowIni.GetSections())
+ {
+ foreach (var key in optionsWindowIni.GetSectionKeys(section))
+ {
+ var value = optionsWindowIni.GetStringValue(section, key, string.Empty);
+
+ if (value.Contains(":CustomSettingFileCheckBox"))
+ {
+ optionsWindowIni.SetStringValue(section, key, value.Replace(":CustomSettingFileCheckBox", ":FileSettingCheckBox"));
+ continue;
+ }
+
+ if (value.Contains(":CustomSettingFileDropDown"))
+ {
+ optionsWindowIni.SetStringValue(section, key, value.Replace(":CustomSettingFileDropDown", ":FileSettingDropDown"));
+ continue;
+ }
+ }
+ }
+
+ // Add new sections into OptionsWindow.ini
+ {
+ var addKey = (string section, string key, string value) => AddKeyWithLog(optionsWindowIni, section, key, value);
+ addKey("lblPlayerName", "Location", "12,195");
+ addKey("tbPlayerName", "Location", "113,193");
+ addKey("lblNotice", "Location", "12,220");
+ addKey("btnConfigureHotkeys", "Location", "12,290");
+ addKey("chkDisablePrivateMessagePopup", "Location", "12,138");
+ addKey("chkDisablePrivateMessagePopup", "Text", "Disable private message pop-ups");
+ addKey("chkAllowGameInvitesFromFriendsOnly", "Location", "276,68");
+ addKey("chkAllowGameInvitesFromFriendsOnly", "Text", "Only receive game invitations@from friends");
+ addKey("lblAllPrivateMessagesFrom", "Location", "276,138");
+ addKey("ddAllowPrivateMessagesFrom", "Location", "470,137");
+ addKey("gameListPanel", "Location", "0,200");
+ addKey("btnForceUpdate", "Location", "407,213");
+ addKey("btnForceUpdate", "Size", "133,23");
+ }
+ optionsWindowIni.WriteIniFile();
+ }
+
+ // Add DTACnCNetClient.ini
+ {
+ IniFile dtaCnCNetClientIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "DTACnCNetClient.ini"));
+ var addKey = (string key, string value) => AddKeyWithLog(dtaCnCNetClientIni, "ParserConstants", key, value);
+ addKey("DEFAULT_LBL_HEIGHT", "12");
+ addKey("DEFAULT_CONTROL_HEIGHT", "21");
+ addKey("DEFAULT_BUTTON_HEIGHT", "23");
+ addKey("BUTTON_WIDTH_133", "133");
+ addKey("OPEN_BUTTON_WIDTH", "18");
+ addKey("OPEN_BUTTON_HEIGHT", "22");
+ addKey("EMPTY_SPACE_TOP", "12");
+ addKey("EMPTY_SPACE_BOTTOM", "12");
+ addKey("EMPTY_SPACE_SIDES", "12");
+ addKey("BUTTON_SPACING", "12");
+ addKey("LABEL_SPACING", "6");
+ addKey("CHECKBOX_SPACING", "24");
+ addKey("LOBBY_EMPTY_SPACE_SIDES", "12");
+ addKey("LOBBY_PANEL_SPACING", "10");
+ addKey("GAME_OPTION_COLUMN_SPACING", "160");
+ addKey("GAME_OPTION_ROW_SPACING", "6");
+ addKey("GAME_OPTION_DD_WIDTH", "132");
+ addKey("GAME_OPTION_DD_HEIGHT", "22");
+ dtaCnCNetClientIni.WriteIniFile();
+ }
+
+ // Add PlayerExtraOptionsPanel.ini
+ {
+ IniFile playerExtraOptionsPanelIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "PlayerExtraOptionsPanel.ini"));
+ var addKey = (string section, string key, string value) => AddKeyWithLog(playerExtraOptionsPanelIni, section, key, value);
+ addKey("btnClose", "Location", "220,0");
+ addKey("btnClose", "Size", "18,18");
+ addKey("lblHeader", "Location", "12,6");
+ addKey("chkBoxForceRandomSides", "Location", "12,28");
+ addKey("chkBoxForceRandomColors", "Location", "12,50");
+ addKey("chkBoxForceRandomTeams", "Location", "12,72");
+ addKey("chkBoxForceRandomStarts", "Location", "12,94");
+ addKey("chkBoxUseTeamStartMappings", "Location", "12,130");
+ addKey("btnHelp", "Location", "160,130");
+ addKey("lblPreset", "Location", "12,156");
+ addKey("ddTeamStartMappingPreset", "Location", "65,154");
+ addKey("ddTeamStartMappingPreset", "Size", "157,21");
+ addKey("teamStartMappingsPanel", "Location", "12,189");
+ playerExtraOptionsPanelIni.WriteIniFile();
+ }
+
+ string GameLobbyBase = nameof(GameLobbyBase);
+
+ // Rework skirmish/lan/cncnet lobbies ini's
+ if (File.Exists(SafePath.CombineFilePath(ResouresDir.FullName, $"{GameLobbyBase}.ini")))
+ {
+ Logger.Log($"Update lobbies has been skipped, {GameLobbyBase}.ini already exists");
+ }
+ else
+ {
+ string MultiplayerGameLobby = nameof(MultiplayerGameLobby);
+ string SkirmishLobby = nameof(SkirmishLobby);
+ string ExtraControls = nameof(ExtraControls);
+ List gameOptionsIniControlKeys = new() { "CheckBoxes", "DropDowns", "Labels" };
+
+ // Old configs
+ IniFile skirmishLobbyIni_old = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, $"{SkirmishLobby}.ini"));
+ IniFile multiplayerGameLobbyIni_old = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, $"{MultiplayerGameLobby}.ini"));
+ IniFile gameOptionsIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "GameOptions.ini"));
+
+ // New configs
+ IniFile gameLobbyBaseIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, $"{GameLobbyBase}.ini"));
+ IniFile skirmishLobbyIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, $"{SkirmishLobby}_New.ini"));
+ IniFile multiplayerGameLobbyIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, $"{MultiplayerGameLobby}_New.ini"));
+ IniFile lanGameLobbyIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "LANGameLobby.ini"));
+ IniFile cncnetGameLobbyIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "CnCNetGameLobby.ini"));
+
+ // Add random color to the GameOptions.ini
+ AddKeyWithLog(gameOptionsIni, "General", "RandomColor", "168,168,168");
+
+ // Delete old inheritance
+ if (skirmishLobbyIni_old.SectionExists("INISystem"))
+ skirmishLobbyIni_old.RemoveSection("INISystem");
+ if (multiplayerGameLobbyIni_old.SectionExists("INISystem"))
+ multiplayerGameLobbyIni_old.RemoveSection("INISystem");
+
+ // Add inheritance
+ //AddKeyWithLog(gameLobbyBaseIni, "INISystem", "BasedOn", "GenericWindow.ini");
+ AddKeyWithLog(skirmishLobbyIni, "INISystem", "BasedOn", $"{GameLobbyBase}.ini");
+ AddKeyWithLog(multiplayerGameLobbyIni, "INISystem", "BasedOn", $"{GameLobbyBase}.ini");
+ AddKeyWithLog(lanGameLobbyIni, "INISystem", "BasedOn", $"{MultiplayerGameLobby}.ini");
+ AddKeyWithLog(cncnetGameLobbyIni, "INISystem", "BasedOn", $"{MultiplayerGameLobby}.ini");
+
+ // Transfer old SkirmishLobby.ini->[ExtraControls] to new SkirmishLobby.ini->[$ExtraControls]
+ if (skirmishLobbyIni_old.SectionExists($"{ExtraControls}"))
+ {
+ TransferKeys(skirmishLobbyIni_old, $"{ExtraControls}", skirmishLobbyIni, $"${ExtraControls}");
+ skirmishLobbyIni_old.RemoveSection($"{ExtraControls}");
+
+ foreach (var key in skirmishLobbyIni.GetSectionKeys($"${ExtraControls}"))
+ {
+ var section = skirmishLobbyIni.GetStringValue($"${ExtraControls}", key, string.Empty).Split(':')[0];
+ TransferKeys(skirmishLobbyIni_old, section, skirmishLobbyIni);
+ skirmishLobbyIni_old.RemoveSection(section);
+ }
+ }
+
+ // Configure GameLobbyBase.ini
+ {
+ // Add [SkirmishLobby]
+ {
+ var addKey = (string key) => AddKeyWithLog(gameLobbyBaseIni, $"{SkirmishLobby}", key, gameOptionsIni.GetStringValue($"{SkirmishLobby}", key, string.Empty));
+ addKey("PlayerOptionLocationX");
+ addKey("PlayerOptionLocationY");
+ addKey("PlayerOptionVerticalMargin");
+ addKey("PlayerOptionHorizontalMargin");
+ addKey("PlayerOptionCaptionLocationY");
+ addKey("PlayerNameWidth");
+ addKey("SideWidth");
+ addKey("ColorWidth");
+ addKey("StartWidth");
+ addKey("TeamWidth");
+ }
+ TransferKeys(skirmishLobbyIni_old, $"{SkirmishLobby}", gameLobbyBaseIni);
+ AddKeyWithLog(gameLobbyBaseIni, $"{SkirmishLobby}", "$CC-SK-GOP", "GameOptionsPanel:XNAPanel");
+ skirmishLobbyIni_old.RemoveSection($"{SkirmishLobby}");
+
+ TransferKeys(skirmishLobbyIni_old, "GameOptionsPanel", gameLobbyBaseIni);
+ skirmishLobbyIni_old.RemoveSection("GameOptionsPanel");
+
+ // Transfer checkboxes, dropdowns, labels from GameOptions.ini to GameLobbyBase.ini->[GameOptionsPanel]
+ int outerIndex = 0;
+ foreach (var itemName in gameOptionsIniControlKeys)
+ {
+ string itemType = itemName switch
+ {
+ "CheckBoxes" => "GameLobbyCheckBox",
+ "DropDowns" => "GameLobbyDropDown",
+ "Labels" => "XNALabel",
+ _ => throw new Exception($"Unknown type of elements {itemName}")
+ };
+
+ string[] items = gameOptionsIni.GetStringValue($"{SkirmishLobby}", itemName, string.Empty).Split([","], StringSplitOptions.RemoveEmptyEntries);
+ for (int i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ AddKeyWithLog(gameLobbyBaseIni, "GameOptionsPanel", $"$CC_{i + outerIndex}", $"{item}:{itemType}");
+ TransferKeys(gameOptionsIni, item, gameLobbyBaseIni);
+ CalculatePositions(gameLobbyBaseIni, "GameOptionsPanel", item);
+ }
+
+ outerIndex += items.Length;
+ }
+
+ // Add other elements in GameLobbyBase.ini->[SkirmishLobby]
+ {
+ var addControl = (string controlKey, string section, string controlType) =>
+ {
+ AddKeyWithLog(gameLobbyBaseIni, $"{SkirmishLobby}", controlKey, $"{section}:{controlType}");
+ try
+ {
+ TransferKeys(skirmishLobbyIni_old, section, gameLobbyBaseIni);
+ }
+ catch
+ {
+ gameLobbyBaseIni.AddSection(section);
+ }
+ skirmishLobbyIni_old.RemoveSection(section);
+ };
+
+ addControl("$CC-SK00", "btnLaunchGame", "GameLaunchButton");
+ addControl("$CC-SK01", "MapPreviewBox", "MapPreviewBox");
+ addControl("$CC-SK02", "PlayerOptionsPanel", "XNAPanel");
+ addControl("$CC-SK03", "ddGameMode", "XNAClientDropDown");
+ addControl("$CC-SK04", "tbMapSearch", "XNASuggestionTextBox");
+ addControl("$CC-SK05", "btnPickRandomMap", "XNAClientButton");
+ addControl("$CC-SK06", "lblGameModeSelect", "XNALabel");
+ addControl("$CC-SK07", "lbMapList", "XNAMultiColumnListBox");
+ addControl("$CC-SK08", "lblMapSize", "XNALabel");
+ addControl("$CC-SK09", "lblGameMode", "XNALabel");
+ addControl("$CC-SK10", "lblMapAuthor", "XNALabel");
+ addControl("$CC-SK11", "lblMapName", "XNALabel");
+ addControl("$CC-SK12", "btnLeaveGame", "XNAClientButton");
+
+ AddKeyWithLog(gameLobbyBaseIni, $"{SkirmishLobby}", "$CC-SK13", "btnSaveLoadGameOptions:XNAClientButton");
+ AddKeyWithLog(gameLobbyBaseIni, "btnSaveLoadGameOptions", "IdleTexture", "comboBoxArrow.png");
+ AddKeyWithLog(gameLobbyBaseIni, "btnSaveLoadGameOptions", "HoverTexture", "comboBoxArrow.png");
+ AddKeyWithLog(gameLobbyBaseIni, "btnSaveLoadGameOptions", "$Width", "18");
+ AddKeyWithLog(gameLobbyBaseIni, "btnSaveLoadGameOptions", "$Height", "21");
+ AddKeyWithLog(gameLobbyBaseIni, "btnSaveLoadGameOptions", "$X", "getRight(GameOptionsPanel) - getWidth($Self) - 1");
+ AddKeyWithLog(gameLobbyBaseIni, "btnSaveLoadGameOptions", "$Y", "getY(GameOptionsPanel) + 1");
+
+ skirmishLobbyIni_old.GetSections().ForEach(x => TransferKeys(skirmishLobbyIni_old, x, gameLobbyBaseIni));
+ }
+ }
+
+ // Transfer old MultiplayerGameLobby.ini->[ExtraControls] to new MultiplayerGameLobby.ini->[$ExtraControls]
+ if (multiplayerGameLobbyIni_old.SectionExists($"{ExtraControls}"))
+ {
+ TransferKeys(multiplayerGameLobbyIni_old, $"{ExtraControls}", multiplayerGameLobbyIni, $"${ExtraControls}");
+ multiplayerGameLobbyIni_old.RemoveSection($"{ExtraControls}");
+ foreach (var key in multiplayerGameLobbyIni.GetSectionKeys($"${ExtraControls}"))
+ {
+ var value = multiplayerGameLobbyIni.GetStringValue($"${ExtraControls}", key, string.Empty).Split(':')[0];
+ TransferKeys(multiplayerGameLobbyIni_old, value, multiplayerGameLobbyIni);
+ multiplayerGameLobbyIni_old.RemoveSection(value);
+ }
+ }
+
+ // Configure MultiplayerGameLobby.ini
+ {
+ AddKeyWithLog(multiplayerGameLobbyIni, $"{MultiplayerGameLobby}", "$BaseSection", $"{SkirmishLobby}");
+
+ // Add keys into [MultiplayerGameLobby] if values are changed with comparison to [SkirmishLobby]
+ foreach (var key in gameLobbyBaseIni.GetSectionKeys($"{SkirmishLobby}").Where(elem => !elem.StartsWith("$")))
+ {
+ var valueSkirmish = gameLobbyBaseIni.GetStringValue($"{SkirmishLobby}", key, string.Empty);
+ var valueMultiplayer = gameOptionsIni.GetStringValue($"{MultiplayerGameLobby}", key, string.Empty);
+
+ if (valueMultiplayer != valueSkirmish)
+ AddKeyWithLog(multiplayerGameLobbyIni, $"{MultiplayerGameLobby}", key, valueMultiplayer);
+ }
+
+ // Find controls to exclude and include
+ List skirmishControls = new();
+ List multiplayerControls = new();
+ gameOptionsIniControlKeys.ForEach(x => skirmishControls.AddRange(gameOptionsIni.GetStringValue($"{SkirmishLobby}", x, string.Empty).Split(',')));
+ gameOptionsIniControlKeys.ForEach(x => multiplayerControls.AddRange(gameOptionsIni.GetStringValue($"{MultiplayerGameLobby}", x, string.Empty).Split(',')));
+ var excludeControls = skirmishControls.Except(multiplayerControls).ToList();
+ var addControls = multiplayerControls.Except(skirmishControls).ToList();
+
+ // Disable skirmish lobby only controls from GameOptions.ini
+ excludeControls.ForEach(x =>
+ AddKeyWithLog(multiplayerGameLobbyIni, x, "Visible", "false")
+ .AddKeyWithLog(multiplayerGameLobbyIni, x, "Enabled", "false"));
+
+ // Add multiplayer lobby only controls from GameOptions.ini
+ addControls.ForEach(control =>
+ AddKeyWithLog(
+ multiplayerGameLobbyIni,
+ "GameOptionsPanel",
+ $"$CC-M{addControls.IndexOf(control)}",
+ control + ':' + control.Substring(0, 3) switch
+ {
+ "chk" => "GameLobbyCheckBox",
+ "cmb" => "GameLobbyDropDown",
+ "lbl" => "XNALabel",
+ _ => throw new Exception($"GameOptions.ini contains unknown type of contol with name {control}")
+ })
+ .TransferKeys(gameOptionsIni, control, multiplayerGameLobbyIni)
+ .CalculatePositions(multiplayerGameLobbyIni, "GameOptionsPanel", control));
+
+ // Add other elements in MultiplayerGameLobby.ini->[MultiplayerGameLobby]
+ {
+ var addControl = (string controlKey, string section, string controlType) =>
+ {
+ AddKeyWithLog(multiplayerGameLobbyIni, $"{MultiplayerGameLobby}", controlKey, $"{section}:{controlType}");
+ try
+ {
+ TransferKeys(multiplayerGameLobbyIni_old, section, multiplayerGameLobbyIni);
+ }
+ catch
+ {
+ multiplayerGameLobbyIni.AddSection(section);
+ }
+ multiplayerGameLobbyIni_old.RemoveSection(section);
+ };
+
+ addControl("$CC-MP01", "btnLockGame", "XNAClientButton");
+ addControl("$CC-MP02", "lbChatMessages_Host", "ChatListBox");
+ addControl("$CC-MP03", "lbChatMessages_Player", "ChatListBox");
+ addControl("$CC-MP04", "tbChatInput_Host", "XNAChatTextBox");
+ addControl("$CC-MP05", "tbChatInput_Player", "XNAChatTextBox");
+ addControl("$CC-MP06", "chkAutoReady", "XNAClientCheckBox");
+
+ AddKeyWithLog(multiplayerGameLobbyIni, $"{MultiplayerGameLobby}", "$CC-MP07", "lbChatMessages:ChatListBox");
+ AddKeyWithLog(multiplayerGameLobbyIni, $"{MultiplayerGameLobby}", "$CC-MP08", "tbChatInput:XNAChatTextBox");
+ TransferKeys(multiplayerGameLobbyIni, "lbChatMessages_Player", multiplayerGameLobbyIni, "lbChatMessages");
+ TransferKeys(multiplayerGameLobbyIni, "tbChatInput_Player", multiplayerGameLobbyIni, "tbChatInput");
+
+ multiplayerGameLobbyIni_old.GetSections().ForEach(x => TransferKeys(multiplayerGameLobbyIni_old, x, multiplayerGameLobbyIni));
+ }
+ }
+
+ // Configure CnCNetGameLobby.ini
+ AddKeyWithLog(cncnetGameLobbyIni, $"{MultiplayerGameLobby}", "$CC-MP99", "btnChangeTunnel:XNAClientButton");
+ AddKeyWithLog(cncnetGameLobbyIni, "btnChangeTunnel", "$Width", "133");
+ AddKeyWithLog(cncnetGameLobbyIni, "btnChangeTunnel", "$X", "getX(btnLeaveGame) - getWidth($Self) - BUTTON_SPACING");
+ AddKeyWithLog(cncnetGameLobbyIni, "btnChangeTunnel", "$Y", "getY(btnLaunchGame)");
+ AddKeyWithLog(cncnetGameLobbyIni, "btnChangeTunnel", "Text", "Change Tunnel");
+
+ // Remove empty keys
+ foreach (var ini in new List() { gameLobbyBaseIni, multiplayerGameLobbyIni, skirmishLobbyIni, lanGameLobbyIni, cncnetGameLobbyIni })
+ {
+ foreach (var section in ini.GetSections())
+ {
+ ini.GetSectionKeys(section)
+ .Where(key => string.IsNullOrWhiteSpace(ini.GetStringValue(section, key, string.Empty)))
+ .ToList()
+ .ForEach(key => ini.RemoveKey(section, key));
+ }
+ }
+
+ // Replace old configs with new one, delete placeholders, delete redundant sections
+ var sb = new StringBuilder();
+ gameOptionsIniControlKeys
+ .ForEach(x => sb.Append(gameOptionsIni.GetStringValue($"{SkirmishLobby}", x, string.Empty)).Append(','));
+ gameOptionsIniControlKeys
+ .ForEach(x => sb.Append(gameOptionsIni.GetStringValue($"{MultiplayerGameLobby}", x, string.Empty)).Append(','));
+ sb.ToString()
+ .Split(',')
+ .Distinct()
+ .Select(x => x = x.Trim())
+ .Where(x => !string.IsNullOrEmpty(x))
+ .ToList()
+ .ForEach(x => gameOptionsIni.RemoveSection(x));
+
+ gameOptionsIni.RemoveSection($"{SkirmishLobby}");
+ gameOptionsIni.RemoveSection($"{MultiplayerGameLobby}");
+ SafePath.DeleteFileIfExists(SafePath.CombineFilePath(ResouresDir.FullName, $"{SkirmishLobby}.ini"));
+ SafePath.DeleteFileIfExists(SafePath.CombineFilePath(ResouresDir.FullName, $"{MultiplayerGameLobby}.ini"));
+ skirmishLobbyIni.WriteIniFile(SafePath.CombineFilePath(ResouresDir.FullName, skirmishLobbyIni_old.FileName));
+ multiplayerGameLobbyIni.WriteIniFile(SafePath.CombineFilePath(ResouresDir.FullName, multiplayerGameLobbyIni_old.FileName));
+
+ gameOptionsIni.WriteIniFile();
+ gameLobbyBaseIni.WriteIniFile();
+ lanGameLobbyIni.WriteIniFile();
+ cncnetGameLobbyIni.WriteIniFile();
+ }
+
+ // Add new texture files
+ var assembly = Assembly.GetExecutingAssembly();
+ foreach (var resourceName in assembly.GetManifestResourceNames())
+ using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName))
+ {
+ var filename = resourceName.Replace($"{nameof(MigrationTool)}.Pictures.", string.Empty);
+ var filepath = SafePath.CombineFilePath(ResouresDir.FullName, filename);
+
+ if (!File.Exists(filepath))
+ {
+ using (FileStream fileStream = new FileStream(filepath, FileMode.CreateNew))
+ {
+ Logger.Log($"Copy {filename} to the {ResouresDir.FullName}");
+ resourceStream.CopyTo(fileStream);
+ }
+ }
+ }
+
+ }
+}
diff --git a/MigrationTool/Patch_v2_11_1.cs b/MigrationTool/Patch_v2_11_1.cs
new file mode 100644
index 000000000..4ea20f100
--- /dev/null
+++ b/MigrationTool/Patch_v2_11_1.cs
@@ -0,0 +1,44 @@
+using Rampastring.Tools;
+
+namespace MigrationTool;
+
+internal class Patch_v2_11_1 : Patch
+{
+ public Patch_v2_11_1(string clientPath) : base(clientPath)
+ {
+ ClientVersion = Version.v2_11_1;
+ }
+
+ public override void Apply()
+ {
+ base.Apply();
+
+ // Add ClientDefinitions.ini->[Settings]->RecommendedResolutions, MaximumRenderWidth, MaximumRenderHeight
+ IniFile clientDefsIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "ClientDefinitions.ini"));
+ AddKeyWithLog(clientDefsIni, "Settings", "MaximumRenderWidth", "1280");
+ AddKeyWithLog(clientDefsIni, "Settings", "MaximumRenderHeight", "720");
+ var width = clientDefsIni.GetStringValue("Settings", "MaximumRenderWidth", "1280");
+ var height = clientDefsIni.GetStringValue("Settings", "MaximumRenderHeight", "720");
+ AddKeyWithLog(clientDefsIni, "Settings", "RecommendedResolutions", $"{width}x{height}");
+ clientDefsIni.WriteIniFile();
+
+ // Rename GameLobbyBase.ini->[SkirmishLobby]->BtnSaveLoadGameOptions to btnSaveLoadGameOptions
+ Logger.Log("Update name from BtnSaveLoadGameOptions to btnSaveLoadGameOptions in GameLobbyBase.ini->[SkirmishLobby]");
+ IniFile glb = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "GameLobbyBase.ini"));
+ var presets = glb.GetSection("BtnSaveLoadGameOptions");
+
+ if (presets == null)
+ return;
+
+ presets.SectionName = "btnSaveLoadGameOptions";
+
+ foreach (var pair in glb.GetSection("SkirmishLobby").Keys)
+ {
+ if (pair.Value.Contains("BtnSaveLoadGameOptions"))
+ glb.SetStringValue("SkirmishLobby", pair.Key, pair.Value.Replace("BtnSaveLoadGameOptions", "btnSaveLoadGameOptions"));
+ }
+
+ glb.WriteIniFile();
+
+ }
+}
diff --git a/MigrationTool/Patch_v2_11_2.cs b/MigrationTool/Patch_v2_11_2.cs
new file mode 100644
index 000000000..5a2332b60
--- /dev/null
+++ b/MigrationTool/Patch_v2_11_2.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+using Rampastring.Tools;
+
+namespace MigrationTool;
+
+internal class Patch_v2_11_2 : Patch
+{
+ public Patch_v2_11_2(string clientPath) : base(clientPath)
+ {
+ ClientVersion = Version.v2_11_2;
+ }
+
+ public override void Apply()
+ {
+ base.Apply();
+
+ // Remove ClientUpdater.xml and SecondStageUpdater.xml
+ IniFile clientDefsIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "ClientDefinitions.ini"));
+ var listExtraXMLs = new List(2) { "ClientUpdater.xml", "SecondStageUpdater.xml" };
+ Logger.Log("Remove ClientUpdater.xml and SecondStageUpdater.xml");
+
+ foreach (var extraXml in listExtraXMLs)
+ {
+ Directory.GetFiles(ResouresDir.FullName, extraXml, SearchOption.AllDirectories)
+ .ToList()
+ .ForEach(elem => SafePath.DeleteFileIfExists(elem));
+ }
+
+ // Add ClientDefinitions.ini->[Settings]->ShowDevelopmentBuildWarnings
+ AddKeyWithLog(clientDefsIni, "Settings", "ShowDevelopmentBuildWarnings", "true");
+ clientDefsIni.WriteIniFile();
+
+ }
+}
diff --git a/MigrationTool/Patch_v2_12_1.cs b/MigrationTool/Patch_v2_12_1.cs
new file mode 100644
index 000000000..35dd7e76b
--- /dev/null
+++ b/MigrationTool/Patch_v2_12_1.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Rampastring.Tools;
+
+namespace MigrationTool;
+
+internal class Patch_v2_12_1 : Patch
+{
+ public Patch_v2_12_1(string clientPath) : base(clientPath)
+ {
+ ClientVersion = Version.v2_12_1;
+ }
+ public override void Apply()
+ {
+ base.Apply();
+
+ // And add ClientDefinitions.ini->[Settings]->ClientGameType
+ IniFile clientDefsIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "ClientDefinitions.ini"));
+ AddKeyWithLog(clientDefsIni, "Settings", "ClientGameType", Game.ToString());
+ clientDefsIni.WriteIniFile();
+
+ }
+}
diff --git a/MigrationTool/Patch_v2_12_6.cs b/MigrationTool/Patch_v2_12_6.cs
new file mode 100644
index 000000000..cb3eea99e
--- /dev/null
+++ b/MigrationTool/Patch_v2_12_6.cs
@@ -0,0 +1,31 @@
+using System.Linq;
+
+using Rampastring.Tools;
+
+namespace MigrationTool;
+
+internal class Patch_v2_12_6 : Patch
+{
+ public Patch_v2_12_6(string clientPath) : base(clientPath)
+ {
+ ClientVersion = Version.v2_12_6;
+ }
+
+ public override void Apply()
+ {
+ base.Apply();
+
+ // Add GameLobbyBase.ini->[ddPlayerColorX]->ItemsDrawMode
+ IniFile gmLobbyBaseIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "GameLobbyBase.ini"));
+ string ddPlayerColor = nameof(ddPlayerColor);
+ Enumerable.Range(0, 8).ToList().ForEach(i => AddKeyWithLog(gmLobbyBaseIni, ddPlayerColor + i, "ItemsDrawMode", "Text"));
+ gmLobbyBaseIni.WriteIniFile();
+
+ // Remove GenericWindow.ini->{ GameCreationWindow; GameCreationWindow_Advanced }->Size
+ IniFile genericWindowIni = new IniFile(SafePath.CombineFilePath(ResouresDir.FullName, "GenericWindow.ini"));
+ RemoveKeyWithLog(genericWindowIni, "GameCreationWindow", "Size");
+ RemoveKeyWithLog(genericWindowIni, "GameCreationWindow_Advanced", "Size");
+ genericWindowIni.WriteIniFile();
+ }
+}
+
diff --git a/MigrationTool/Pictures/favActive.png b/MigrationTool/Pictures/favActive.png
new file mode 100644
index 000000000..545be40b4
Binary files /dev/null and b/MigrationTool/Pictures/favActive.png differ
diff --git a/MigrationTool/Pictures/favActive_c.png b/MigrationTool/Pictures/favActive_c.png
new file mode 100644
index 000000000..23080d576
Binary files /dev/null and b/MigrationTool/Pictures/favActive_c.png differ
diff --git a/MigrationTool/Pictures/favInactive.png b/MigrationTool/Pictures/favInactive.png
new file mode 100644
index 000000000..f841e9e50
Binary files /dev/null and b/MigrationTool/Pictures/favInactive.png differ
diff --git a/MigrationTool/Pictures/noMapPreview.png b/MigrationTool/Pictures/noMapPreview.png
new file mode 100644
index 000000000..3b0579569
Binary files /dev/null and b/MigrationTool/Pictures/noMapPreview.png differ
diff --git a/MigrationTool/Pictures/sortAlphaActive.png b/MigrationTool/Pictures/sortAlphaActive.png
new file mode 100644
index 000000000..9243fd4e0
Binary files /dev/null and b/MigrationTool/Pictures/sortAlphaActive.png differ
diff --git a/MigrationTool/Pictures/sortAlphaAsc.png b/MigrationTool/Pictures/sortAlphaAsc.png
new file mode 100644
index 000000000..9243fd4e0
Binary files /dev/null and b/MigrationTool/Pictures/sortAlphaAsc.png differ
diff --git a/MigrationTool/Pictures/sortAlphaDesc.png b/MigrationTool/Pictures/sortAlphaDesc.png
new file mode 100644
index 000000000..ac1389e46
Binary files /dev/null and b/MigrationTool/Pictures/sortAlphaDesc.png differ
diff --git a/MigrationTool/Pictures/sortAlphaInactive.png b/MigrationTool/Pictures/sortAlphaInactive.png
new file mode 100644
index 000000000..49b928d97
Binary files /dev/null and b/MigrationTool/Pictures/sortAlphaInactive.png differ
diff --git a/MigrationTool/Pictures/sortAlphaNone.png b/MigrationTool/Pictures/sortAlphaNone.png
new file mode 100644
index 000000000..49b928d97
Binary files /dev/null and b/MigrationTool/Pictures/sortAlphaNone.png differ
diff --git a/MigrationTool/Pictures/statusAI.png b/MigrationTool/Pictures/statusAI.png
new file mode 100644
index 000000000..2b8a9399d
Binary files /dev/null and b/MigrationTool/Pictures/statusAI.png differ
diff --git a/MigrationTool/Pictures/statusClear.png b/MigrationTool/Pictures/statusClear.png
new file mode 100644
index 000000000..2ef08a0ba
Binary files /dev/null and b/MigrationTool/Pictures/statusClear.png differ
diff --git a/MigrationTool/Pictures/statusEmpty.png b/MigrationTool/Pictures/statusEmpty.png
new file mode 100644
index 000000000..707f0689c
Binary files /dev/null and b/MigrationTool/Pictures/statusEmpty.png differ
diff --git a/MigrationTool/Pictures/statusError.png b/MigrationTool/Pictures/statusError.png
new file mode 100644
index 000000000..eb4695908
Binary files /dev/null and b/MigrationTool/Pictures/statusError.png differ
diff --git a/MigrationTool/Pictures/statusInProgress.png b/MigrationTool/Pictures/statusInProgress.png
new file mode 100644
index 000000000..bc0b8be2d
Binary files /dev/null and b/MigrationTool/Pictures/statusInProgress.png differ
diff --git a/MigrationTool/Pictures/statusOk.png b/MigrationTool/Pictures/statusOk.png
new file mode 100644
index 000000000..d1c20d01f
Binary files /dev/null and b/MigrationTool/Pictures/statusOk.png differ
diff --git a/MigrationTool/Pictures/statusUnavailable.png b/MigrationTool/Pictures/statusUnavailable.png
new file mode 100644
index 000000000..aea07d0bb
Binary files /dev/null and b/MigrationTool/Pictures/statusUnavailable.png differ
diff --git a/MigrationTool/Pictures/statusWarning.png b/MigrationTool/Pictures/statusWarning.png
new file mode 100644
index 000000000..bf046b98e
Binary files /dev/null and b/MigrationTool/Pictures/statusWarning.png differ
diff --git a/MigrationTool/Program.cs b/MigrationTool/Program.cs
new file mode 100644
index 000000000..1c61b35c7
--- /dev/null
+++ b/MigrationTool/Program.cs
@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+using Rampastring.Tools;
+using ClientCore.Enums;
+using ClientCore.Extensions;
+
+namespace MigrationTool;
+
+internal sealed class Program
+{
+ private const string errMsg = "Unknown arguments detected. Use -h argument to print help information.";
+ private const string helpMsg =
+ """
+ CnCNet Client Migration Tool.
+
+ Execute this file with path to the unmigrated client directory as first argument.
+ """;
+
+ private static void Main(string[] args)
+ {
+ // Initialize logger
+ DirectoryInfo baseDirectory = SafePath.GetDirectory(Directory.GetCurrentDirectory());
+ FileInfo logFile = SafePath.GetFile(SafePath.CombineFilePath(baseDirectory.FullName, "MigrationTool.log"));
+ Logger.Initialize(logFile.DirectoryName, logFile.Name);
+ Logger.WriteLogFile = true;
+ Logger.WriteToConsole = false;
+ Logger.Log("CnCNet Client Migration Tool");
+ Logger.Log("Version: " + GitVersionInformation.AssemblySemVer);
+ Logger.WriteToConsole = true;
+
+ // Check arguments
+ switch (args.Length)
+ {
+ case 1:
+ string arg = args[0].Trim();
+
+ if (arg is "-h"
+ or "--help"
+ or "-?"
+ or "/?"
+ or "/h")
+ {
+ Console.WriteLine(helpMsg);
+ return;
+ }
+
+ if (!SafePath.GetDirectory(arg).Exists)
+ {
+ Console.WriteLine(errMsg);
+ return;
+ }
+
+ if (!SafePath.GetFile(SafePath.CombineFilePath(arg, "Resources", "ClientDefinitions.ini")).Exists)
+ {
+ Logger.Log("Unable to find Resources/ClientDefinitions.ini. Migration aborted.");
+ return;
+ }
+
+ var assembly = Assembly.GetExecutingAssembly();
+ Patch? patch = null;
+ try
+ {
+ // https://stackoverflow.com/questions/16038819/how-to-find-all-direct-subclasses-of-a-class-with-net-reflection
+ var patches = assembly.GetTypes().Where(type => type.BaseType == typeof(Patch)).ToList();
+ var patchNames = Enum.GetValues(typeof(Version));
+ foreach (var patchName in patchNames)
+ {
+ Type type = patches.Where(t => t.FullName == "MigrationTool.Patch_" + patchName.ToString()).First();
+ patch = (Patch)Activator.CreateInstance(type, arg);
+ patch?.Apply();
+ Console.WriteLine("");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Log("");
+ Logger.Log($"Unable to apply migration patch for client version {patch?.ClientVersion.ToString().Replace('_', '.')} due to an internal error. Message: {ex.ToString()}");
+ Logger.Log("Migration to the latest client version has been failed");
+ }
+
+ Console.WriteLine("Patching has been done.");
+
+ break;
+ case 0:
+ default:
+ Console.WriteLine(errMsg);
+ break;
+ }
+ }
+}
diff --git a/MigrationTool/Version.cs b/MigrationTool/Version.cs
new file mode 100644
index 000000000..2b1b7cdff
--- /dev/null
+++ b/MigrationTool/Version.cs
@@ -0,0 +1,11 @@
+namespace MigrationTool;
+
+public enum Version
+{
+ v2_11_0,
+ v2_11_1,
+ v2_11_2,
+ v2_12_1,
+ v2_12_6,
+ Latest,
+}
diff --git a/Scripts/build.ps1 b/Scripts/build.ps1
index 12a0ca0b2..affe5f478 100644
--- a/Scripts/build.ps1
+++ b/Scripts/build.ps1
@@ -32,7 +32,9 @@
build.ps1 Ares -IsDebug
Build for ares game on debug mode.
#>
-param(
+
+param
+(
[Parameter()]
[switch]
$IsDebug,
@@ -48,7 +50,8 @@ param(
)
$Script:ConfigurationSuffix = 'Release'
-if ($IsDebug) {
+if ($IsDebug)
+{
$Script:ConfigurationSuffix = 'Debug'
}
@@ -67,17 +70,21 @@ $Script:FrameworkBinariesFolderMap = @{
'net8.0-windows' = 'BinariesNET8'
}
-if (!$NoClean -AND (Test-Path $Script:CompiledRoot)) {
+if (!$NoClean -AND (Test-Path $Script:CompiledRoot))
+{
Remove-Item -Recurse -Force -LiteralPath $Script:CompiledRoot
}
-if ($null -EQ $IsWindows -AND 'Desktop' -EQ $PSEdition) {
+if ($null -EQ $IsWindows -AND 'Desktop' -EQ $PSEdition)
+{
$Script:IsWindows = $true
}
-function Script:Invoke-BuildProject {
+function Script:Invoke-BuildProject
+{
[CmdletBinding(DefaultParameterSetName = 'ByGame')]
- param (
+ param
+ (
[Parameter(Mandatory, ParameterSetName = 'Detail', Position = 0)]
[string]
$Engine,
@@ -86,8 +93,10 @@ function Script:Invoke-BuildProject {
$Framework
)
- process {
- if ($Engine) {
+ process
+ {
+ if ($Engine)
+ {
$Output = Join-Path $CompiledRoot 'Resources' ($FrameworkBinariesFolderMap[$Framework]) ($EngineSubFolderMap[$Engine])
$Private:ArgumentList = [System.Collections.Generic.List[string]]::new(11)
@@ -98,28 +107,35 @@ function Script:Invoke-BuildProject {
$Private:ArgumentList.Add("--framework:$Framework")
$Private:ArgumentList.Add("--output:$Output")
$Private:ArgumentList.Add('-property:SatelliteResourceLanguages=en')
- if ($Log) {
+ if ($Log)
+ {
$Private:ArgumentList.Add('-verbosity:diagnostic')
}
- if ($NoMove) {
+ if ($NoMove)
+ {
$Private:ArgumentList.Add('-property:NoMove=true')
}
# $Private:ArgumentList.Add("-property:AssemblyVersion=$AssemblySemVer")
# $Private:ArgumentList.Add("-property:FileVersion=$AssemblySemFileVer")
# $Private:ArgumentList.Add("-property:InformationalVersion=$InformationalVersion")
- if ($Engine -eq 'WindowsXNA') {
+ if ($Engine -eq 'WindowsXNA')
+ {
$Private:ArgumentList.Add('--arch=x86')
}
- & 'dotnet' $Private:ArgumentList
- if ($LASTEXITCODE) {
+ echo ''
+ & 'dotnet' $Private:ArgumentList
+ if ($LASTEXITCODE)
+ {
throw "Build failed for ${Engine}$Script:ConfigurationSuffix $Framework"
}
}
- else {
+ else
+ {
Invoke-BuildProject -Engine 'UniversalGL' -Framework 'net8.0'
- if ($IsWindows) {
+ if ($IsWindows)
+ {
@('WindowsDX', 'WindowsGL', 'WindowsXNA') | ForEach-Object {
$Private:Engine = $PSItem
@@ -134,4 +150,33 @@ function Script:Invoke-BuildProject {
}
}
+# Build client binaries
Script:Invoke-BuildProject
+
+# Build migration tool binaries
+$Private:MTFramework='net8.0-windows'
+$Private:MTCompiledPath = Join-Path $RepoRoot 'Compiled'
+$Private:MTProjectPath = Join-Path $RepoRoot 'MigrationTool' 'MigrationTool.csproj'
+$Private:MTOutput = Join-Path $MTCompiledPath 'Resources' $FrameworkBinariesFolderMap['net8.0-windows']
+$Private:MTArgumentList = [System.Collections.Generic.List[string]]::new(11)
+$Private:MTArgumentList.Add('publish')
+$Private:MTArgumentList.Add("$MTProjectPath")
+$Private:MTArgumentList.Add('--graph')
+$Private:MTArgumentList.Add("--framework:$MTFramework")
+$Private:MTArgumentList.Add("--output:$MTOutput\MigrationTool")
+$Private:MTArgumentList.Add('-property:SatelliteResourceLanguages=en')
+if ($Log)
+{
+ $Private:ArgumentList.Add('-verbosity:diagnostic')
+}
+if ($NoMove)
+{
+ $Private:ArgumentList.Add('-property:NoMove=true')
+}
+
+echo ''
+& 'dotnet' $MTArgumentList
+if ($LASTEXITCODE)
+{
+ throw "Build failed for Migration Tool"
+}