Compare commits

...

9 commits

12 changed files with 323 additions and 187 deletions

View file

@ -1,7 +1,7 @@
# Gamecraft Mod Manager
A manager that handles everything needed to use mods for Gamecraft.
# Techblox Mod Manager
A manager that handles everything needed to use mods for Techblox.
**Note:** If you already have a mod installed the manager won't know about what files it has until it's updated through GCMM. If you uninstall the mod before updating it, it'll only remove the plugin dll and then treat the other mod files as part of Gamecraft.
**Note:** If you already have a mod installed the manager won't know about what files it has until it's updated through TBMM. If you uninstall the mod before updating it, it'll only remove the plugin dll and then treat the other mod files as part of Techblox.
## Features
* Download and run GCIPA if needed
@ -12,8 +12,14 @@ A manager that handles everything needed to use mods for Gamecraft.
## Mod requirements
* For a mod to be listed, it needs to have a regular release (so not a prerelease) with exactly 1 recognised attached asset.
* That asset can be either a dll if the mod doesn't have any other files, or a zip archive.
* If the zip contains a folder named Plugins, the manager will unzip it to Gamecraft's directory instead of the Plugins directory.
* If the zip contains a folder named Plugins, the manager will unzip it to Techblox's directory instead of the Plugins directory.
* The mod dll must use the same name as the repository, including casing. The manager will rename single dlls automatically but can't handle zipped mods with different names.
* If you have any other files as part of the release, the mod one (that needs to be downloaded to the game) must have the same name as the repository.
* The release tag must be the same as the assembly version of the mod with an optional `v` prefix. It can also have a `-preview` suffix if it **only** works with preview versions of the game
* Add the mod to the list at https://git.exmods.org/ExMods/html-site/src/branch/master/site/mods/modlist.tsv
### Examples
* For a mod named ExampleMod that also needs ExampleDll to work: zip the mod and the dll up in an archive named ExampleMod.zip and make sure the dll is also named ExampleMod.dll
* For a mod named ExampleMod that needs to add an asset to the game, put the dll in a Plugins directory and the asset where it should be in the game and zip that together (Plugins/ExampleMod.dll and TechbloxPreview_Data/...)
* For a mod named ExampleMod that has a separate app for it in the same repository or you want to attach something else in the release, *just do it:* as long as the zip is named ExampleMod.zip the mod manager will (only) download that zip
* If you only have a single dll, it can be named anything, though why would you not name it the same?!

View file

@ -6,7 +6,7 @@ namespace TBMM
public class Configuration
{
public string GamePath { get; set; }
public AutoPatchingState AutoPatch { get; set; } = AutoPatchingState.Unspecified;
public bool KeepPatched { get; set; }
public static Configuration Load()
{

View file

@ -30,7 +30,7 @@
{
System.Windows.Forms.ListViewGroup listViewGroup1 = new System.Windows.Forms.ListViewGroup("Installed", System.Windows.Forms.HorizontalAlignment.Center);
System.Windows.Forms.ListViewGroup listViewGroup2 = new System.Windows.Forms.ListViewGroup("Available", System.Windows.Forms.HorizontalAlignment.Center);
System.Windows.Forms.ListViewItem listViewItem1 = new System.Windows.Forms.ListViewItem(new string[] {"Mod", "modtainers", "1.0", "2020.06.15. 2:01:43"}, -1);
System.Windows.Forms.ListViewItem listViewItem1 = new System.Windows.Forms.ListViewItem(new string[] { "Mod", "modtainers", "1.0", "2020.06.15. 2:01:43" }, -1);
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.modlist = new System.Windows.Forms.ListView();
this.modName = new System.Windows.Forms.ColumnHeader();
@ -43,17 +43,16 @@
this.playbtn = new System.Windows.Forms.Button();
this.settingsbtn = new System.Windows.Forms.Button();
this.findlog = new System.Windows.Forms.Button();
this.unpatched = new System.Windows.Forms.CheckBox();
this.modinfobox = new System.Windows.Forms.RichTextBox();
this.refreshbtn = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// modlist
//
this.modlist.Anchor = ((System.Windows.Forms.AnchorStyles) ((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.modlist.BackColor = System.Drawing.Color.FromArgb(((int) (((byte) (216)))), ((int) (((byte) (240)))), ((int) (((byte) (216)))));
this.modlist.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.modlist.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(216)))), ((int)(((byte)(240)))), ((int)(((byte)(216)))));
this.modlist.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.modlist.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {this.modName, this.modAuthor, this.modVersion, this.modTimestamp});
this.modlist.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { this.modName, this.modAuthor, this.modVersion, this.modTimestamp });
this.modlist.ForeColor = System.Drawing.Color.Green;
this.modlist.FullRowSelect = true;
listViewGroup1.Header = "Installed";
@ -62,11 +61,11 @@
listViewGroup2.Header = "Available";
listViewGroup2.HeaderAlignment = System.Windows.Forms.HorizontalAlignment.Center;
listViewGroup2.Name = "available";
this.modlist.Groups.AddRange(new System.Windows.Forms.ListViewGroup[] {listViewGroup1, listViewGroup2});
this.modlist.Groups.AddRange(new System.Windows.Forms.ListViewGroup[] { listViewGroup1, listViewGroup2 });
this.modlist.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
this.modlist.HideSelection = false;
listViewItem1.Group = listViewGroup1;
this.modlist.Items.AddRange(new System.Windows.Forms.ListViewItem[] {listViewItem1});
this.modlist.Items.AddRange(new System.Windows.Forms.ListViewItem[] { listViewItem1 });
this.modlist.Location = new System.Drawing.Point(12, 47);
this.modlist.Name = "modlist";
this.modlist.Size = new System.Drawing.Size(491, 433);
@ -98,10 +97,10 @@
//
// status
//
this.status.Anchor = ((System.Windows.Forms.AnchorStyles) ((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.status.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.status.AutoSize = true;
this.status.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.status.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (238)));
this.status.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.status.ForeColor = System.Drawing.Color.Lime;
this.status.Location = new System.Drawing.Point(8, 487);
this.status.Name = "status";
@ -111,11 +110,11 @@
//
// installbtn
//
this.installbtn.Anchor = ((System.Windows.Forms.AnchorStyles) ((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.installbtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.installbtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.installbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int) (((byte) (0)))), ((int) (((byte) (40)))), ((int) (((byte) (0)))));
this.installbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.installbtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.installbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (238)));
this.installbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.installbtn.Location = new System.Drawing.Point(509, 12);
this.installbtn.Name = "installbtn";
this.installbtn.Size = new System.Drawing.Size(263, 49);
@ -126,11 +125,11 @@
//
// uninstallbtn
//
this.uninstallbtn.Anchor = ((System.Windows.Forms.AnchorStyles) ((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.uninstallbtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.uninstallbtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Black;
this.uninstallbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.Black;
this.uninstallbtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.uninstallbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (238)));
this.uninstallbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.uninstallbtn.ForeColor = System.Drawing.Color.Green;
this.uninstallbtn.Location = new System.Drawing.Point(509, 67);
this.uninstallbtn.Name = "uninstallbtn";
@ -144,9 +143,9 @@
//
this.playbtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom;
this.playbtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.playbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int) (((byte) (0)))), ((int) (((byte) (40)))), ((int) (((byte) (0)))));
this.playbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.playbtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.playbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 20.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (238)));
this.playbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 20.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.playbtn.Location = new System.Drawing.Point(250, 487);
this.playbtn.Name = "playbtn";
this.playbtn.Size = new System.Drawing.Size(300, 62);
@ -157,9 +156,9 @@
//
// settingsbtn
//
this.settingsbtn.Anchor = ((System.Windows.Forms.AnchorStyles) ((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.settingsbtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.settingsbtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.settingsbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int) (((byte) (0)))), ((int) (((byte) (40)))), ((int) (((byte) (0)))));
this.settingsbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.settingsbtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.settingsbtn.Location = new System.Drawing.Point(667, 487);
this.settingsbtn.Name = "settingsbtn";
@ -171,9 +170,9 @@
//
// findlog
//
this.findlog.Anchor = ((System.Windows.Forms.AnchorStyles) ((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.findlog.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.findlog.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.findlog.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int) (((byte) (0)))), ((int) (((byte) (40)))), ((int) (((byte) (0)))));
this.findlog.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.findlog.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.findlog.Location = new System.Drawing.Point(666, 522);
this.findlog.Name = "findlog";
@ -183,21 +182,9 @@
this.findlog.UseVisualStyleBackColor = true;
this.findlog.Click += new System.EventHandler(this.findlog_Click);
//
// unpatched
//
this.unpatched.Anchor = ((System.Windows.Forms.AnchorStyles) ((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.unpatched.AutoSize = true;
this.unpatched.Location = new System.Drawing.Point(12, 534);
this.unpatched.Name = "unpatched";
this.unpatched.Size = new System.Drawing.Size(218, 17);
this.unpatched.TabIndex = 8;
this.unpatched.Text = "Disable mods (check if game is crashing)";
this.unpatched.UseVisualStyleBackColor = true;
this.unpatched.CheckedChanged += new System.EventHandler(this.unpatched_CheckedChanged);
//
// modinfobox
//
this.modinfobox.Anchor = ((System.Windows.Forms.AnchorStyles) (((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Right)));
this.modinfobox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Right)));
this.modinfobox.BackColor = System.Drawing.Color.Black;
this.modinfobox.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.modinfobox.ForeColor = System.Drawing.Color.Lime;
@ -211,9 +198,9 @@
// refreshbtn
//
this.refreshbtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.refreshbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int) (((byte) (0)))), ((int) (((byte) (40)))), ((int) (((byte) (0)))));
this.refreshbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.refreshbtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.refreshbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (238)));
this.refreshbtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.refreshbtn.Location = new System.Drawing.Point(12, 12);
this.refreshbtn.Name = "refreshbtn";
this.refreshbtn.Size = new System.Drawing.Size(105, 29);
@ -230,7 +217,6 @@
this.ClientSize = new System.Drawing.Size(784, 561);
this.Controls.Add(this.refreshbtn);
this.Controls.Add(this.modinfobox);
this.Controls.Add(this.unpatched);
this.Controls.Add(this.findlog);
this.Controls.Add(this.settingsbtn);
this.Controls.Add(this.playbtn);
@ -239,9 +225,11 @@
this.Controls.Add(this.status);
this.Controls.Add(this.modlist);
this.ForeColor = System.Drawing.Color.Lime;
this.Icon = ((System.Drawing.Icon) (resources.GetObject("$this.Icon")));
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "MainForm";
this.Text = "Techblox Mod Manager";
this.Activated += new System.EventHandler(this.MainForm_Activated);
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing);
this.Load += new System.EventHandler(this.Form1_Load);
this.Shown += new System.EventHandler(this.MainForm_Shown);
this.ResumeLayout(false);
@ -261,7 +249,6 @@
private System.Windows.Forms.Button settingsbtn;
private System.Windows.Forms.ColumnHeader modAuthor;
private System.Windows.Forms.Button findlog;
private System.Windows.Forms.CheckBox unpatched;
private System.Windows.Forms.RichTextBox modinfobox;
private System.Windows.Forms.Button refreshbtn;
}

View file

@ -23,7 +23,7 @@ namespace TBMM
public Configuration Configuration { get; }
private readonly ResourceManager resources;
private readonly Dictionary<string, ModInfo> mods = new Dictionary<string, ModInfo>();
private readonly Dictionary<string, ModInfo> mods = new();
private readonly ModInfo gcipa = new ModInfo { Author = "modtainers", Name = "GCIPA" };
private readonly ModInfo tbmm = new ModInfo { Author = "NorbiPeti", Name = "TBMM" };
private DateTime lastGameUpdateTime;
@ -35,15 +35,13 @@ To get started, click on a mod and select Install mod. Most mods need TechbloxMo
Then launch Techblox by clicking on the Play button below. Mods are only loaded if you start the game from here.
This will first download and run the patcher (GCIPA) if needed. If all goes well, after some time a modded Techblox should launch.
After a Techblox update there's a good chance that mods will break. If this happens you may get errors when trying to start Techblox.
Until updated versions are released, use the ""Disable mods"" checkbox at the bottom to launch the game without mods.
If you launch the game through the launcher after an update and encounter an error, either repair the game or launch it through the mod manager.
After a Techblox update there's a good chance that mods will break. If this happens you may get errors when trying to start Techblox through the mod manager.
Until updated versions are released, launch the game without mods through its own launcher.
Disclaimer:
This mod manager and the mods in the list are made by the ExMods developers. We are not associated with Freejam or Techblox. Modify Techblox at your own risk.
If you encounter an issue while any mods are installed, report it to us. If you think it's an issue with the game, test again with the ""Disable mods"" option checked before reporting to Freejam.
You may also want to verify the game's files in the launcher.
If you encounter an issue while any mods are installed, report it to us. If you think it's an issue with the game, test again by launching the game through the official launcher before reporting to Freejam.
";
private async void Form1_Load(object sender, EventArgs e)
@ -75,19 +73,31 @@ You may also want to verify the game's files in the launcher.
status.Text = resources.GetString("Status_Game_not_found");
return;
}
DeleteEmptyPluginsDir(out bool pexists, out bool dexists);
if (!pexists && dexists)
unpatched.Checked = true; //It will call the event but that won't do anything
DeleteEmptyPluginsDir(out _, out _);
await RefreshEverything(evenMods);
}
private async void playbtn_Click(object sender, EventArgs e)
{
if (playbtn.ForeColor == Color.Green) return; //Disabled
if (mods.Any(mod => mod.Value.Installed && mod.Value.Broken is true))
if (MessageBox.Show("Some installed mods are known to be broken on the current version of the game. The game might crash.",
"Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) == DialogResult.Cancel)
return;
await UpdateAPI();
await PatchStartGame(); //It will call EndWork();
}
private void UpdatePlayButtonColor()
{
if (mods.Any(mod => mod.Value.Installed && mod.Value.Broken is true))
playbtn.ForeColor = Color.Red;
else if (mods.Any(mod => mod.Value.Installed && mod.Value.Outdated(lastGameUpdateTime)))
playbtn.ForeColor = Color.DarkOrange;
else
playbtn.ForeColor = Color.Lime;
}
private void settingsbtn_Click(object sender, EventArgs e)
{
if (settingsbtn.ForeColor == Color.Green) return; //Disabled
@ -128,7 +138,9 @@ You may also want to verify the game's files in the launcher.
{
if (mod.Updatable)
addText("New version available! " + mod.UpdateDetails, Color.Aqua);
if (mod.LastUpdated != default && mod.LastUpdated < lastGameUpdateTime)
if (mod.Broken is true)
addText("Outdated mod! It has been confirmed that the mod is broken on the current version of the game.", Color.Red);
else if (mod.Outdated(lastGameUpdateTime))
addText("Outdated mod! It may not work properly on the current version of the game.", Color.DarkOrange);
if (mod.Description != null)
modinfobox.AppendText(mod.Description.Replace("\n", Environment.NewLine));
@ -154,14 +166,6 @@ You may also want to verify the game's files in the launcher.
installbtn.Text = "Install mod";
break;
}
if (unpatched.Checked)
{ //Don't allow (un)installing mods if mods are disabled
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
modlist.Enabled = false;
}
else
modlist.Enabled = true;
}
private async void installbtn_Click(object sender, EventArgs e)
@ -174,7 +178,7 @@ You may also want to verify the game's files in the launcher.
if (item.Group.Name == "installed" && (mod.DownloadURL == null || mod.LatestVersion <= mod.Version)) continue;
await InstallMod(mod);
}
EndWork();
EndWork(CheckIfPatched());
}
private void uninstallbtn_Click(object sender, EventArgs e)
@ -185,7 +189,7 @@ You may also want to verify the game's files in the launcher.
if (item.Group.Name != "installed") continue;
UninstallMod(mods[item.Name]);
}
EndWork(); //Update button states
EndWork(CheckIfPatched()); //Update button states
}
private void findlog_Click(object sender, EventArgs e)
@ -198,35 +202,6 @@ You may also want to verify the game's files in the launcher.
}
}
private void unpatched_CheckedChanged(object sender, EventArgs e)
{ //Not using the patcher's revert option because sometimes it restores the wrong files - the game can be patched without mods
if (CheckNoExe())
return;
CheckIfPatched();
modlist_SelectedIndexChanged(modlist, null);
string plugins = GamePath("\\Plugins");
string disabled = GamePath("\\Plugins_Disabled");
DeleteEmptyPluginsDir(out bool pexists, out bool dexists);
if (unpatched.Checked)
{
if (pexists)
{
if (dexists)
Directory.Delete(disabled, true); //Resolving conflicts would be complicated so delete the other mods - this shouldn't happen normally
Directory.Move(plugins, disabled);
}
}
else
{
if (dexists)
{
if (pexists)
Directory.Delete(plugins, true);
Directory.Move(disabled, plugins);
}
}
}
private void DeleteEmptyPluginsDir(out bool pexists, out bool dexists)
{
string plugins = GamePath("\\Plugins");
@ -252,7 +227,8 @@ You may also want to verify the game's files in the launcher.
private async Task RefreshEverything(bool evenMods)
{
CheckIfPatched(); //Set from placeholder
if (CheckIfPatched() == GameState.Patched) //Set from placeholder & unpatch if game was patched
HandleGameExit(null, EventArgs.Empty);
lastGameUpdateTime = GetGameVersionAsDate();
var mods = GetInstalledMods();
if (evenMods)
@ -265,5 +241,21 @@ You may also want to verify the game's files in the launcher.
{
Focus();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.Cancel) return;
if (Configuration.KeepPatched || CheckIfPatched(out bool patched) != GameState.InGame || !patched) return;
if (MessageBox.Show("The game is still running. The mod manager needs to be running until the game closes to restore the game files." +
" If you proceed you won't be able to play online until you start the mod manager again.\n\n" +
"Are you sure you want TBMM to exit before the game does?", "Game still running",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.No)
e.Cancel = true;
}
private void MainForm_Activated(object sender, EventArgs e)
{
CheckIfPatched();
}
}
}

View file

@ -66,6 +66,7 @@ namespace TBMM
}
GetInstalledMods(); //Update list
}
UpdatePlayButtonColor();
}
public void ExtractMod(ZipArchive archive, string destinationDirectoryName, ModInfo mod)
@ -170,6 +171,7 @@ namespace TBMM
{
MessageBox.Show("Could not remove mod files! Make sure the game isn't running.\n" + e.Message);
}
UpdatePlayButtonColor();
}
public async Task UpdateAPI()

View file

@ -59,6 +59,7 @@ namespace TBMM
public async Task GetAvailableMods()
{
bool preview = GetExe()?.Contains("Preview") ?? false;
//byte anyModOutdated = 0;
using (var client = GetClient())
{
string str = await client.DownloadStringTaskAsync("https://exmods.org/mods/modlist.tsv");
@ -66,11 +67,22 @@ namespace TBMM
{
var sp = line.Split('\t');
if (sp.Length < 2) continue;
DateTime updated = default;
bool broken = false;
if (sp.Length > 2)
{
if (DateTime.TryParse(sp[2].Trim(), out var updatedAt))
updated = updatedAt;
else if (sp[2].Trim().ToLower() == "broken")
broken = true;
}
var mod = new ModInfo
{
Author = sp[0].Trim(),
Name = sp[1].Trim(),
LastUpdated = sp.Length > 2 ? DateTime.Parse(sp[2].Trim()) : default
LastUpdated = updated,
Broken = broken
};
if (await FetchModInfo(mod, preview, true)) //If it's actually a mod
AddUpdateModInList(mod);
@ -85,6 +97,7 @@ namespace TBMM
== DialogResult.Yes)
Process.Start(tbmm.DownloadURL);
}
UpdatePlayButtonColor();
}
public async Task<bool> FetchModInfo(ModInfo mod, bool preview, bool desc)
@ -175,6 +188,7 @@ namespace TBMM
omod.Description = mod.Description ?? omod.Description;
omod.DownloadURL = mod.DownloadURL ?? omod.DownloadURL;
omod.UpdateDetails = mod.UpdateDetails ?? omod.UpdateDetails;
omod.Broken = mod.Broken ?? omod.Broken;
items[1].Text = omod.Author ?? "";
items[2].Text = (omod.Version ?? omod.LatestVersion)?.ToString();
items[3].Text = omod.LatestVersion != null ? omod.LastUpdated.ToString() : "";
@ -191,8 +205,10 @@ namespace TBMM
}
if (mod.LatestVersion != null && mod.Version != null && mod.Version < mod.LatestVersion)
item.ForeColor = Color.Blue;
else if (mod.LastUpdated != default && mod.LastUpdated < lastGameUpdateTime)
item.ForeColor = Color.OrangeRed;
else if(mod.Broken is true)
item.ForeColor = Color.Red;
else if (mod.Outdated(lastGameUpdateTime))
item.ForeColor = Color.DarkOrange;
else
item.ForeColor = modlist.ForeColor;
}
@ -210,6 +226,7 @@ namespace TBMM
delete.Add(name);
}
delete.ForEach(name => { mods.Remove(name); modlist.Items[name].Remove(); });
UpdatePlayButtonColor();
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
@ -11,54 +12,82 @@ namespace TBMM
{
partial class MainForm
{
public GameState CheckIfPatched()
public GameState CheckIfPatched() => CheckIfPatched(out _);
public GameState CheckIfPatched(out bool patched)
{
Dictionary<GameState, (string Status, string Extra, string Play)> statusTexts = new()
{
{ GameState.NotFound, ("Game not found", "Specify the game's location in settings", "") },
{ GameState.InGame, ("Game is running", "", "In-game") },
{ GameState.NoPatcher, ("Patcher missing", "Clicking Play will install it", "") },
{ GameState.OldPatcher, ("Patcher outdated", "nClicking play will update it", "") },
{ GameState.Unpatched, ("Unpatched", "", "") },
{ GameState.Patched, ("Patched", "", "") }
};
void SetStatusText(GameState state, bool patched)
{
var (statusText, extra, play) = statusTexts[state];
if (extra.Length == 0) extra = patched ? "Cannot join online mode" : "Online mode available";
if (play.Length == 0) play = "Play modded";
status.Text = $"Status: {statusText}\n{extra}";
if (Configuration.KeepPatched)
status.Text += "\nUnpatch on exit disabled";
playbtn.Text = play;
}
if (GetExe() == null)
{
status.Text = "Status: Game not found";
patched = false;
SetStatusText(GameState.NotFound, false);
return GameState.NotFound;
}
string pnp = "Patch && Play";
if (!File.Exists(GamePath(@"\IPA.exe")))
bool gameIsRunning = CheckIfGameIsRunning();
if (gameIsRunning)
{
status.Text = "Status: Patcher missing\nClicking Play will install it";
playbtn.Text = pnp;
return GameState.NoPatcher;
UpdateButton(playbtn, false); //Don't allow (un)installing mods if game is running
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
modlist.Enabled = false;
}
if (gcipa.Updatable && !(gcipa.Version == new Version(1, 0, 0, 0) && gcipa.LatestVersion == new Version(4, 0, 0, 0)))
else
{
status.Text = "Status: Patcher outdated\nClicking play will update it";
playbtn.Text = pnp;
return GameState.OldPatcher;
if (!working) UpdateButton(playbtn, true);
modlist.Enabled = true;
}
string nopatch = "Status: Unpatched\nClicking Play patches it";
string gc = GetExe().Replace(".exe", "");
string backups = GamePath(@"\IPA\Backups\" + gc);
if (!Directory.Exists(backups))
GameState GetPatchedState()
{
status.Text = nopatch;
playbtn.Text = pnp;
return GameState.Unpatched;
if (!File.Exists(GamePath(@"\IPA.exe")))
return GameState.NoPatcher;
if (gcipa.Updatable && !(gcipa.Version == new Version(1, 0, 0, 0) &&
gcipa.LatestVersion == new Version(4, 0, 0, 0))
&& !gameIsRunning)
return GameState.OldPatcher;
string gc = GetExe(withExtension: false);
string backups = GamePath(@"\IPA\Backups\" + gc);
if (!Directory.Exists(backups))
return GameState.Unpatched;
string backup = Directory.EnumerateDirectories(backups)
.OrderByDescending(Directory.GetLastWriteTimeUtc).FirstOrDefault();
if (backup == null)
return GameState.Unpatched;
if (File.GetLastWriteTime(GamePath($@"\{gc}_Data\Managed\Assembly-CSharp.dll"))
> //If the file was updated at least 2 minutes after patching
Directory.GetLastWriteTime(backup).AddMinutes(2)
|| !File.Exists(GamePath($@"\{gc}_Data\Managed\IllusionInjector.dll")))
return GameState.Unpatched;
return GameState.Patched;
}
string backup = Directory.EnumerateDirectories(backups).OrderByDescending(name => Directory.GetLastWriteTimeUtc(name)).FirstOrDefault();
if (backup == null)
{
status.Text = nopatch;
playbtn.Text = pnp;
return GameState.Unpatched;
}
if (File.GetLastWriteTime(GamePath($@"\{gc}_Data\Managed\Assembly-CSharp.dll"))
> //If the file was updated at least 2 minutes after patching
Directory.GetLastWriteTime(backup).AddMinutes(2)
|| !File.Exists(GamePath($@"\{gc}_Data\Managed\IllusionInjector.dll")))
{
status.Text = nopatch;
playbtn.Text = pnp;
return GameState.Unpatched;
}
status.Text = "Status: " + (unpatched.Checked ? "Mods disabled" : "Patched");
playbtn.Text = "Play" + (unpatched.Checked ? " unmodded" : "");
return GameState.Patched;
var patchedState = GetPatchedState();
var finalState = gameIsRunning ? GameState.InGame : patchedState;
patched = patchedState == GameState.Patched;
SetStatusText(finalState, patchedState == GameState.Patched);
return finalState;
}
public async Task<bool?> PatchStartGame(string command = null)
@ -82,7 +111,7 @@ namespace TBMM
{
case GameState.NotFound:
MessageBox.Show("Techblox not found! Set the correct path in Settings.");
EndWork(false);
EndWork(status, false);
return retOpenedWindowShouldStay;
case GameState.NoPatcher:
case GameState.OldPatcher:
@ -94,7 +123,7 @@ namespace TBMM
) + "\n\nIt will be downloaded from https://git.exmods.org/modtainers/GCIPA/releases and ran to patch the game. You can validate the game to restore the original game files or simply disable mods at any time.",
"Patcher download needed", MessageBoxButtons.OKCancel) == DialogResult.Cancel)
{
EndWork();
EndWork(status);
return retOpenedWindowShouldStay;
}
this.status.Text = "Status: Patching...";
@ -124,31 +153,13 @@ namespace TBMM
{
case GameState.NoPatcher: //Make sure it actually worked
case GameState.OldPatcher:
EndWork(false);
EndWork(status, false);
return retOpenedWindowShouldStay;
case GameState.Unpatched:
{ //TODO: Wine
EnsureShown(false);
var psi = new ProcessStartInfo(GamePath(@"\IPA.exe"), GetExe() + " --nowait")
{
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = Configuration.GamePath,
CreateNoWindow = true
};
var process = Process.Start(psi);
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.EnableRaisingEvents = true;
modinfobox.Text = "";
DataReceivedEventHandler onoutput = (sender, e) =>
{
Invoke((Action)(() => modinfobox.Text += e.Data + Environment.NewLine));
};
process.OutputDataReceived += onoutput;
process.ErrorDataReceived += onoutput;
var (handler, task) = CheckStartGame(command);
var process = ExecutePatcher(true);
process.Exited += handler;
await task;
}
@ -165,13 +176,41 @@ namespace TBMM
return retOpenedWindowShouldStay;
}
private Process ExecutePatcher(bool patch)
{
var psi = new ProcessStartInfo(GamePath(@"\IPA.exe"), $"{GetExe()} --nowait {(patch ? "" : "--revert")}")
{
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = Configuration.GamePath,
CreateNoWindow = true
};
var process = Process.Start(psi);
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.EnableRaisingEvents = true;
modinfobox.Text = "";
DataReceivedEventHandler onoutput = (sender, e) =>
{
Invoke((Action)(() => modinfobox.Text += e.Data + Environment.NewLine));
};
process.OutputDataReceived += onoutput;
process.ErrorDataReceived += onoutput;
return process;
}
public enum GameState
{
NotFound,
NoPatcher,
OldPatcher,
Unpatched,
Patched
Patched,
/// <summary>
/// Doesn't matter if patched, don't do anything if the game is running
/// </summary>
InGame
}
}
}

View file

@ -16,13 +16,23 @@ namespace TBMM
{
if (enabled)
{
button.ForeColor = Color.Lime;
if (button.ForeColor == Color.Green)
button.ForeColor = Color.Lime;
else if(button.ForeColor == Color.DarkRed)
button.ForeColor = Color.Red;
else if (button.ForeColor == Color.FromArgb(127, 65, 0))
button.ForeColor = Color.DarkOrange;
button.FlatAppearance.MouseOverBackColor = Color.FromArgb(0, 40, 0);
button.FlatAppearance.MouseDownBackColor = Color.Green;
}
else
{
button.ForeColor = Color.Green;
if (button.ForeColor == Color.Lime)
button.ForeColor = Color.Green;
else if (button.ForeColor == Color.Red)
button.ForeColor = Color.DarkRed;
else if (button.ForeColor == Color.DarkOrange)
button.ForeColor = Color.FromArgb(127, 65, 0);
button.FlatAppearance.MouseOverBackColor = Color.Black;
button.FlatAppearance.MouseDownBackColor = Color.Black;
}
@ -72,20 +82,32 @@ namespace TBMM
status.Text = "Status: Patching failed";
return;
}
if (CheckIfPatched() == GameState.Patched || unpatched.Checked)
var patched = CheckIfPatched();
if (patched == GameState.Patched)
{
Process process = null;
if (command != null)
{
if (sender is Process) //Patched just now
CheckCompatibilityAndDisableMods();
await CheckModUpdatesAsync();
Process.Start(command);
process = Process.Start(command);
}
else if (Environment.OSVersion.Platform == PlatformID.Win32NT)
Process.Start(new ProcessStartInfo(GamePath("\\" + GetExe()))
{
process = Process.Start(new ProcessStartInfo(GamePath("\\" + GetExe()))
{
WorkingDirectory = GamePath("\\") //Mods are only loaded if the working directory is correct
});
EndWork(false);
}
if (process is null)
throw new NullReferenceException("Game process is null");
process.EnableRaisingEvents = true;
process.Exited += HandleGameExit;
_gameProcess = (process, true);
patched = CheckIfPatched(); // Set in-game status
}
EndWork(patched, false);
tcs.SetResult(null);
};
if (InvokeRequired)
@ -95,15 +117,60 @@ namespace TBMM
}, tcs.Task);
}
private void CheckCompatibilityAndDisableMods()
private void HandleGameExit(object sender, EventArgs e)
{
if (!unpatched.Checked && MessageBox.Show("If the game updated just now, some mods may be incompatible or they may work just fine." +
" Do you want to try running with mods?" +
"\n\nClick Yes to start the game with mods (after a small update or if you just installed TBMM)" +
"\nClick No to disable mods before starting the game (after a major update)" +
"\n\nYou can always enable/disable mods by launching TBMM.",
"Possible incompatibility warning", MessageBoxButtons.YesNo) == DialogResult.No)
unpatched.Checked = true;
Debug.WriteLine("Handling game exit");
_gameProcess = (null, false);
if (InvokeMethod(CheckIfPatched) != GameState.Patched || Configuration.KeepPatched)
return;
InvokeMethod(() => ExecutePatcher(false)).Exited += (_, _) =>
{
if (InvokeMethod(CheckIfPatched) == GameState.Patched)
{
MessageBox.Show("Failed to unpatch game, launching through the launcher will fail because of anticheat. " +
"Check the output in the panel on the right.\n\n" +
"Please try starting the game again by clicking Play.", "Patcher error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
};
}
private (Process Process, bool ExitHandlerAdded) _gameProcess = (null, false);
private bool CheckIfGameIsRunning()
{
Debug.WriteLine($"Check if game is running: {_gameProcess}");
switch (_gameProcess.Process)
{
case { HasExited: false }:
Debug.WriteLine("Game has not exited");
return true;
case { HasExited: true }:
Debug.WriteLine("Game has seemingly exited without handling, probably as part of the exit handler");
return false;
default:
Debug.WriteLine($"Process seems to be null: {_gameProcess}");
_gameProcess = (Process.GetProcessesByName(GetExe(withExtension: false)).FirstOrDefault(), false);
Debug.WriteLine($"Game process exited already, got new process object: {_gameProcess}");
if (_gameProcess.Process == null) return false;
if (_gameProcess.Process.HasExited)
{
Debug.WriteLine($"Game has exited already: {_gameProcess}");
_gameProcess = (null, false);
return false;
}
else
{
Debug.WriteLine($"Game is still running");
if (_gameProcess.ExitHandlerAdded) return true;
Debug.WriteLine("Game running and no exit handler yet, adding it");
_gameProcess.Process.Exited += HandleGameExit;
_gameProcess.Process.EnableRaisingEvents = true;
_gameProcess.ExitHandlerAdded = true;
return true;
}
}
}
private async Task CheckModUpdatesAsync()
@ -130,6 +197,14 @@ namespace TBMM
return client;
}
public T InvokeMethod<T>(Func<T> func)
{
if (InvokeRequired)
return (T)Invoke(func);
else
return func();
}
private bool working = false;
/// <summary>
/// Some simple "locking", only allow one operation at a time
@ -143,18 +218,17 @@ namespace TBMM
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
UpdateButton(settingsbtn, false);
unpatched.Enabled = false;
return true;
}
public void EndWork(bool desc = true)
public void EndWork(GameState state, bool desc = true)
{
working = false;
UpdateButton(playbtn, true);
if (state != GameState.InGame)
UpdateButton(playbtn, true);
UpdateButton(settingsbtn, true);
if (desc)
modlist_SelectedIndexChanged(modlist, null);
unpatched.Enabled = true;
}
/// <summary>
@ -168,12 +242,12 @@ namespace TBMM
return ((gamepath ?? Configuration.GamePath) + path).Replace('\\', Path.DirectorySeparatorChar);
}
public string GetExe(string path = null)
public string GetExe(string path = null, bool withExtension = true)
{
if (File.Exists(GamePath("\\Techblox.exe", path)))
return "Techblox.exe";
return "Techblox" + (withExtension ? ".exe" : "");
if (File.Exists(GamePath("\\TechbloxPreview.exe", path)))
return "TechbloxPreview.exe";
return "TechbloxPreview" + (withExtension ? ".exe" : "");
return null;
}
@ -196,7 +270,7 @@ namespace TBMM
public DateTime GetGameVersionAsDate()
{
if (Configuration.GamePath == null) return default;
using var fs = File.OpenRead(GamePath("\\TechbloxPreview_Data\\globalgamemanagers"));
using var fs = File.OpenRead(GamePath($"\\{GetExe(withExtension: false)}_Data\\globalgamemanagers"));
using var sr = new StreamReader(fs);
char[] data = new char[512];
while(!sr.EndOfStream)

View file

@ -31,5 +31,8 @@ namespace TBMM
public HashSet<string> ModFiles { get; set; }
public string UpdateDetails { get; set; }
public bool Updatable => Version != null && LatestVersion != null && Version < LatestVersion;
public bool? Broken { get; set; }
public bool Outdated(DateTime lastGameUpdateTime) => LastUpdated != default && LastUpdated < lastGameUpdateTime;
}
}

View file

@ -34,12 +34,13 @@
this.browsebtn = new System.Windows.Forms.Button();
this.savebtn = new System.Windows.Forms.Button();
this.cancelbtn = new System.Windows.Forms.Button();
this.keepPatched = new System.Windows.Forms.CheckBox();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (238)));
this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.label1.Location = new System.Drawing.Point(12, 15);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(116, 20);
@ -68,7 +69,7 @@
//
this.savebtn.DialogResult = System.Windows.Forms.DialogResult.OK;
this.savebtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.savebtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int) (((byte) (0)))), ((int) (((byte) (40)))), ((int) (((byte) (0)))));
this.savebtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.savebtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.savebtn.Location = new System.Drawing.Point(270, 113);
this.savebtn.Name = "savebtn";
@ -82,7 +83,7 @@
//
this.cancelbtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelbtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.cancelbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int) (((byte) (0)))), ((int) (((byte) (40)))), ((int) (((byte) (0)))));
this.cancelbtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.cancelbtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.cancelbtn.Location = new System.Drawing.Point(352, 113);
this.cancelbtn.Name = "cancelbtn";
@ -92,6 +93,16 @@
this.cancelbtn.UseVisualStyleBackColor = true;
this.cancelbtn.Click += new System.EventHandler(this.cancelbtn_Click);
//
// keepPatched
//
this.keepPatched.AutoSize = true;
this.keepPatched.Location = new System.Drawing.Point(12, 61);
this.keepPatched.Name = "keepPatched";
this.keepPatched.Size = new System.Drawing.Size(330, 17);
this.keepPatched.TabIndex = 5;
this.keepPatched.Text = "Keep game patched (prevents online gameplay until unchecked)";
this.keepPatched.UseVisualStyleBackColor = true;
//
// SettingsForm
//
this.AcceptButton = this.savebtn;
@ -100,6 +111,7 @@
this.BackColor = System.Drawing.Color.Black;
this.CancelButton = this.cancelbtn;
this.ClientSize = new System.Drawing.Size(439, 148);
this.Controls.Add(this.keepPatched);
this.Controls.Add(this.cancelbtn);
this.Controls.Add(this.savebtn);
this.Controls.Add(this.browsebtn);
@ -107,7 +119,7 @@
this.Controls.Add(this.label1);
this.ForeColor = System.Drawing.Color.Lime;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon) (resources.GetObject("$this.Icon")));
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SettingsForm";
@ -120,6 +132,8 @@
this.PerformLayout();
}
private System.Windows.Forms.CheckBox keepPatched;
#endregion
private System.Windows.Forms.Label label1;

View file

@ -16,6 +16,7 @@ namespace TBMM
{
mainForm = (MainForm) Owner;
gamelocation.Text = mainForm.Configuration.GamePath;
keepPatched.Checked = mainForm.Configuration.KeepPatched;
}
private void browsebtn_Click(object sender, EventArgs e)
@ -26,6 +27,7 @@ namespace TBMM
private void savebtn_Click(object sender, EventArgs e)
{
mainForm.Configuration.GamePath = gamelocation.Text;
mainForm.Configuration.KeepPatched = keepPatched.Checked;
mainForm.Configuration.Save();
Close();
}

View file

@ -6,12 +6,12 @@
<UseWindowsForms>true</UseWindowsForms>
<SignAssembly>false</SignAssembly>
<ApplicationIcon>favicon.ico</ApplicationIcon>
<Version>1.5.0</Version>
<Version>1.6.0</Version>
<Authors>NorbiPeti</Authors>
<Company>ExMods</Company>
<Description>A mod manager for Techblox. It automatically downloads and runs GCIPA and allows the user to install mods.</Description>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
<LangVersion>8</LangVersion>
<LangVersion>9</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">