Compare commits

..

No commits in common. "master" and "v1.4.0" have entirely different histories.

31 changed files with 987 additions and 983 deletions

View file

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30011.22
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TBMM", "TBMM\TBMM.csproj", "{4022166A-FEA4-4B26-B83F-58B4D2004976}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCMM", "GCMM\GCMM.csproj", "{4022166A-FEA4-4B26-B83F-58B4D2004976}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -1,4 +1,4 @@
namespace TBMM
namespace GCMM
{
partial class CustomMessageBox
{

View file

@ -1,6 +1,17 @@
using System.Windows.Forms;
using GCMM.Properties;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
namespace GCMM
{
public partial class CustomMessageBox : Form
{

View file

@ -2,16 +2,14 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net461</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<SignAssembly>false</SignAssembly>
<ApplicationIcon>favicon.ico</ApplicationIcon>
<Version>1.6.0</Version>
<Version>1.4.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>9</LangVersion>
<Description>A mod manager for Gamecraft. It automatically downloads and runs GCIPA and allows the user to install mods.</Description>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -26,7 +24,6 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.IO.Compression" Version="4.3.0" />
<PackageReference Include="System.Resources.Extensions" Version="5.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
@ -39,18 +36,18 @@
<Compile Update="SettingsForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="Localization.Designer.cs">
<DesignTime>True</DesignTime>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Localization.resx</DependentUpon>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Localization.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
</Project>

View file

@ -1,4 +1,4 @@
namespace TBMM
namespace GCMM
{
partial class MainForm
{
@ -30,29 +30,41 @@
{
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();
this.modAuthor = new System.Windows.Forms.ColumnHeader();
this.modVersion = new System.Windows.Forms.ColumnHeader();
this.modTimestamp = new System.Windows.Forms.ColumnHeader();
this.modName = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.modAuthor = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.modVersion = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.modTimestamp = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.status = new System.Windows.Forms.Label();
this.installbtn = new System.Windows.Forms.Button();
this.uninstallbtn = new System.Windows.Forms.Button();
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.validatebtn = 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.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";
@ -61,11 +73,14 @@
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);
@ -182,9 +197,22 @@
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;
@ -209,14 +237,30 @@
this.refreshbtn.UseVisualStyleBackColor = true;
this.refreshbtn.Click += new System.EventHandler(this.refreshbtn_Click);
//
// validatebtn
//
this.validatebtn.FlatAppearance.MouseDownBackColor = System.Drawing.Color.Green;
this.validatebtn.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(40)))), ((int)(((byte)(0)))));
this.validatebtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.validatebtn.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.validatebtn.Location = new System.Drawing.Point(398, 12);
this.validatebtn.Name = "validatebtn";
this.validatebtn.Size = new System.Drawing.Size(105, 29);
this.validatebtn.TabIndex = 11;
this.validatebtn.Text = "Validate game";
this.validatebtn.UseVisualStyleBackColor = true;
this.validatebtn.Click += new System.EventHandler(this.validatebtn_Click);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Black;
this.ClientSize = new System.Drawing.Size(784, 561);
this.Controls.Add(this.validatebtn);
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);
@ -227,13 +271,12 @@
this.ForeColor = System.Drawing.Color.Lime;
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.Text = "Gamecraft Mod Manager";
this.Load += new System.EventHandler(this.Form1_Load);
this.Shown += new System.EventHandler(this.MainForm_Shown);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
@ -249,8 +292,10 @@
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;
private System.Windows.Forms.Button validatebtn;
}
}

View file

@ -1,47 +1,51 @@
using System;
using GCMM.Properties;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Resources;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
namespace GCMM
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
resources = new ResourceManager("TBMM.Localization", Assembly.GetExecutingAssembly());
Configuration = Configuration.Load();
}
public Configuration Configuration { get; }
private readonly ResourceManager resources;
private readonly Dictionary<string, ModInfo> mods = new();
private readonly Dictionary<string, ModInfo> mods = new Dictionary<string, ModInfo>();
private readonly ModInfo gcipa = new ModInfo { Author = "modtainers", Name = "GCIPA" };
private readonly ModInfo tbmm = new ModInfo { Author = "NorbiPeti", Name = "TBMM" };
private readonly ModInfo gcmm = new ModInfo { Author = "NorbiPeti", Name = "GCMM" };
private DateTime lastGameUpdateTime;
private string steamPath;
private const string defaultInfo = @"
Techblox Mod Manager
Gamecraft Mod Manager
If you click on a mod it will show some info about it. The install instructions there are usually for manual installs.
To get started, click on a mod and select Install mod. Most mods need TechbloxModdingAPI as well so it'll be installed.
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.
To get started, click on a mod and select Install mod. Most mods need GamecraftModdingAPI as well so it'll be installed.
Then launch Gamecraft: if you enabled auto-patching then you can use Steam but if you didn't then you must use the Play button.
This will first download and run the patcher (GCIPA) if needed. If all goes well, after some time a modded Gamecraft 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 through the mod manager.
Until updated versions are released, launch the game without mods through its own launcher.
After a Gamecraft update there's a good chance that mods will break. If this happens you may get errors when trying to start Gamecraft.
Until updated versions are released, use the ""Disable mods"" checkbox at the bottom to launch the game without mods.
If you enabled auto-patching you will get a warning about this.
If you don't have auto-patching enabled then you will need to run the mod manager each time Gamecraft updates and click ""Patch & Play"".
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.
This mod manager and the mods in the list are made by the ExMods developers. We are not associated with Freejam or Gamecraft. Modify Gamecraft 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 by launching the game through the official launcher before reporting to Freejam.
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 by clicking on the Validate game button.
";
private async void Form1_Load(object sender, EventArgs e)
@ -52,52 +56,64 @@ If you encounter an issue while any mods are installed, report it to us. If you
public async Task LoadEverything(bool evenMods)
{
if (Settings.Default.NeedsUpdate)
{
Settings.Default.Upgrade();
Settings.Default.NeedsUpdate = false;
Settings.Default.Save();
}
modlist.Items.Clear();
mods.Clear(); //This method may get called twice when ran from the command line
UpdateButton(installbtn, false);
modinfobox.Text = defaultInfo;
if (string.IsNullOrWhiteSpace(Configuration.GamePath) || GetExe() == null)
{
Configuration.GamePath = GetGameFolder();
if (string.IsNullOrWhiteSpace(Configuration.GamePath))
{
DialogUtils.ShowWarning(resources.GetString("Game_not_found"), "");
Configuration.GamePath = SelectGameFolder();
}
var (steamPath, user) = GetSteamLocationAndUser();
if (steamPath != null)
this.steamPath = steamPath;
else
DialogUtils.ShowInfo(string.Format(resources.GetString("Found_game_at"), Configuration.GamePath), "");
Configuration.Save();
}
if(string.IsNullOrWhiteSpace(Configuration.GamePath))
{
status.Text = resources.GetString("Status_Game_not_found");
MessageBox.Show("Steam not found! If you have Steam installed, please report this to ExMods.\n\nThe Steam install is checked to autodetect where the game is installed and to optionally configure auto-patching.", "Steam not found");
status.Text = "Status: Steam not found";
return;
}
DeleteEmptyPluginsDir(out _, out _);
if (Settings.Default.SteamUserID == 0 && Settings.Default.AutoLaunch)
{
if (MessageBox.Show("Do you want GCMM to change the game's launch settings so it can ensure the game is patched?\n\n" +
"If you say yes, GCMM will do a quick check before the game is launched and patches if necessary. " +
"This way you (hopefully) won't see crashes after a Gamecraft update.\n\n" +
"Note that this also means that if you (re)move GCMM without disabling this in the settings then you won't be able to launch Gamecraft until you change the launch options in Steam.",
"GCMM auto-patching", MessageBoxButtons.YesNo) == DialogResult.Yes)
DetectConfigLocationAndAutoStart(steamPath, ref user);
else
Settings.Default.AutoLaunch = false;
Settings.Default.Save();
}
if (string.IsNullOrWhiteSpace(Settings.Default.GamePath) || GetExe() == null)
{
Settings.Default.GamePath = GetGameFolder();
if (string.IsNullOrWhiteSpace(Settings.Default.GamePath))
Settings.Default.GamePath = SelectGameFolder();
else
MessageBox.Show("Found game at " + Settings.Default.GamePath);
Settings.Default.Save();
}
if(string.IsNullOrWhiteSpace(Settings.Default.GamePath))
{
status.Text = "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
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
@ -138,10 +154,8 @@ If you encounter an issue while any mods are installed, report it to us. If you
{
if (mod.Updatable)
addText("New version available! " + mod.UpdateDetails, Color.Aqua);
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.LastUpdated < lastGameUpdateTime)
addText("Outdated mod! It may not work properly on the latest version of the game.", Color.DarkOrange);
if (mod.Description != null)
modinfobox.AppendText(mod.Description.Replace("\n", Environment.NewLine));
}
@ -166,6 +180,14 @@ If you encounter an issue while any mods are installed, report it to us. If you
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)
@ -178,7 +200,7 @@ If you encounter an issue while any mods are installed, report it to us. If you
if (item.Group.Name == "installed" && (mod.DownloadURL == null || mod.LatestVersion <= mod.Version)) continue;
await InstallMod(mod);
}
EndWork(CheckIfPatched());
EndWork();
}
private void uninstallbtn_Click(object sender, EventArgs e)
@ -189,7 +211,7 @@ If you encounter an issue while any mods are installed, report it to us. If you
if (item.Group.Name != "installed") continue;
UninstallMod(mods[item.Name]);
}
EndWork(CheckIfPatched()); //Update button states
EndWork(); //Update button states
}
private void findlog_Click(object sender, EventArgs e)
@ -202,6 +224,35 @@ If you encounter an issue while any mods are installed, report it to us. If you
}
}
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");
@ -227,9 +278,8 @@ If you encounter an issue while any mods are installed, report it to us. If you
private async Task RefreshEverything(bool evenMods)
{
if (CheckIfPatched() == GameState.Patched) //Set from placeholder & unpatch if game was patched
HandleGameExit(null, EventArgs.Empty);
lastGameUpdateTime = GetGameVersionAsDate();
CheckIfPatched(); //Set from placeholder
lastGameUpdateTime = await GetLastGameUpdateTime();
var mods = GetInstalledMods();
if (evenMods)
await GetAvailableMods();
@ -237,25 +287,27 @@ If you encounter an issue while any mods are installed, report it to us. If you
CheckIfPatched(); //Check after getting the available mods to show GCIPA updates
}
private void validatebtn_Click(object sender, EventArgs e)
{
if (CheckNoExe())
return;
if (MessageBox.Show("Validating the game's files is useful if the game doesn't start even without mods." +
" Make sure to click Refresh once Steam finished verifying the game." +
" The Steam window that shows the progress might open in the background." +
" Note that if auto-patching isn't enabled then you will need to patch the game again using the Play button in order to use mods." +
"\n\nContinue?", "Verify game files", MessageBoxButtons.OKCancel) == DialogResult.Cancel)
return;
string exe = GetExe();
File.Delete(GamePath($@"\{exe.Replace(".exe", "")}_Data\Managed\IllusionInjector.dll")); //Used to check if game is patched
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
Process.Start("steam://validate/1078000/");
else
Process.Start("xdg-open", "steam://validate/1078000/");
}
private void MainForm_Shown(object sender, EventArgs e)
{
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

@ -1,13 +1,15 @@
using Newtonsoft.Json;
using GCMM.Properties;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
namespace GCMM
{
partial class MainForm
{
@ -16,7 +18,7 @@ namespace TBMM
if (mod.DownloadURL == null) return;
if (CheckNoExe())
return;
if (mod.Name != "TechbloxModdingAPI")
if (mod.Name != "GamecraftModdingAPI")
await UpdateAPI();
var tmp = Directory.CreateDirectory("temp");
var plugins = Directory.CreateDirectory(GamePath(@"\Plugins"));
@ -55,7 +57,7 @@ namespace TBMM
if (!modFound)
if (MessageBox.Show("The mod was not found in the downloaded archive. It likely means it's using a different name for the dll file. The mod manager will not be able to track this mod if you continue. Do you want to continue?", "Mod not found in archive", MessageBoxButtons.YesNo) == DialogResult.No)
return;
ExtractMod(archive, pluginOnly ? plugins.FullName : Configuration.GamePath, mod);
ExtractMod(archive, pluginOnly ? plugins.FullName : Settings.Default.GamePath, mod);
}
File.Delete(tmppath);
}
@ -66,7 +68,6 @@ namespace TBMM
}
GetInstalledMods(); //Update list
}
UpdatePlayButtonColor();
}
public void ExtractMod(ZipArchive archive, string destinationDirectoryName, ModInfo mod)
@ -118,22 +119,15 @@ namespace TBMM
public void SaveFileList(ModInfo mod)
{
if (mod.ModFiles != null)
{
Directory.CreateDirectory(GamePath("\\ModInfo"));
File.WriteAllText(GamePath($"\\ModInfo\\{mod.Name}.json"), JsonConvert.SerializeObject(mod.ModFiles));
}
File.WriteAllText(mod.Name + ".json", JsonConvert.SerializeObject(mod.ModFiles));
}
public void LoadFileList(ModInfo mod)
{
string[] paths =
{
GamePath($"\\ModInfo\\{mod.Name}.json"),
mod.Name + ".json"
};
mod.ModFiles =
paths.Where(File.Exists).Select(File.ReadAllText).Select(JsonConvert.DeserializeObject<HashSet<string>>)
.FirstOrDefault() ?? new HashSet<string>();
if (File.Exists(mod.Name + ".json"))
mod.ModFiles = JsonConvert.DeserializeObject<HashSet<string>>(File.ReadAllText(mod.Name + ".json"));
else
mod.ModFiles = new HashSet<string>();
}
public void UninstallMod(ModInfo mod)
@ -154,9 +148,6 @@ namespace TBMM
parent.Delete(); //May delete the Plugins dir if empty
}
}
if (File.Exists(GamePath($"\\ModInfo\\{mod.Name}.json")))
File.Delete(GamePath($"\\ModInfo\\{mod.Name}.json"));
File.Delete(mod.Name + ".json");
mod.Version = null; //Not installed
if (mod.Author != null)
@ -171,19 +162,18 @@ namespace TBMM
{
MessageBox.Show("Could not remove mod files! Make sure the game isn't running.\n" + e.Message);
}
UpdatePlayButtonColor();
}
public async Task UpdateAPI()
{
if (!mods.ContainsKey("TechbloxModdingAPI"))
if (!mods.ContainsKey("GamecraftModdingAPI"))
return;
var gcmapi = mods["TechbloxModdingAPI"];
var gcmapi = mods["GamecraftModdingAPI"];
if (!gcmapi.Installed || gcmapi.Updatable)
{
if (MessageBox.Show(gcmapi.Installed ?
"TechbloxModdingAPI will be updated as there's a new version available. It's needed for most mods to function."
: "TechbloxModdingAPI will be installed as most mods need it to work. You can uninstall it if you're sure you don't need it.", "API needed",
"GamecraftModdingAPI will be updated as there's a new version available. It's needed for most mods to function."
: "GamecraftModdingAPI will be installed as most mods need it to work. You can uninstall it if you're sure you don't need it.", "API needed",
MessageBoxButtons.OKCancel) == DialogResult.OK)
await InstallMod(gcmapi);
}

View file

@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq;
using GCMM.Properties;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -7,11 +8,12 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
namespace GCMM
{
partial class MainForm
{
@ -51,15 +53,24 @@ namespace TBMM
catch (BadImageFormatException)
{ //Not a .NET assembly
}
tbmm.Version = Assembly.GetExecutingAssembly().GetName().Version;
try
{
string mmpath = "GCMM.exe";
if (File.Exists(mmpath))
{
var an = AssemblyName.GetAssemblyName(mmpath);
gcmm.Version = an.Version;
}
}
catch (BadImageFormatException)
{ //Not a .NET assembly
}
return installed;
}
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");
@ -67,37 +78,24 @@ 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 = updated,
Broken = broken
Name = sp[1].Trim()
};
if (await FetchModInfo(mod, preview, true)) //If it's actually a mod
AddUpdateModInList(mod);
}
}
if (tbmm.LatestVersion == null) //Only check once
if (gcmm.LatestVersion == null) //Only check once
{
await FetchModInfo(gcipa, preview, false);
await FetchModInfo(tbmm, preview, false);
if (tbmm.Updatable)
if (MessageBox.Show("There is a TBMM update available! Do you want to download it now? If yes, extract it over this installation.\n\n" + tbmm.UpdateDetails, "Mod Manager update", MessageBoxButtons.YesNo)
await FetchModInfo(gcmm, preview, false);
if (gcmm.Updatable)
if (MessageBox.Show("There is a GCMM update available! Do you want to download it now? If yes, extract it over this installation.\n\n" + gcmm.UpdateDetails, "Mod Manager update", MessageBoxButtons.YesNo)
== DialogResult.Yes)
Process.Start(tbmm.DownloadURL);
Process.Start(gcmm.DownloadURL);
}
UpdatePlayButtonColor();
}
public async Task<bool> FetchModInfo(ModInfo mod, bool preview, bool desc)
@ -105,17 +103,7 @@ namespace TBMM
string repoURL = "/api/v1/repos/" + mod.Author + "/" + mod.Name + "/releases";
using (var client = GetClient())
{
string str;
try
{
str = await client.DownloadStringTaskAsync(repoURL);
}
catch (WebException)
{
return false;
}
var arr = JArray.Parse(str);
var arr = JArray.Parse(await client.DownloadStringTaskAsync(repoURL));
var release = arr.FirstOrDefault(rel =>
{
if ((bool) rel["prerelease"] || (bool) rel["draft"])
@ -148,20 +136,18 @@ namespace TBMM
});
mod.DownloadURL = asset?["browser_download_url"]?.ToString();
var lastUpdated = (DateTime)release["published_at"];
if (mod.LastUpdated < lastUpdated)
mod.LastUpdated = lastUpdated; //If there's a newer release than the last known working date
mod.LastUpdated = (DateTime)release["published_at"];
var ver = verstr.Split('.').Select(str => int.Parse(str)).ToArray();
int getver(byte i) => ver.Length > i ? ver[i] : 0; //By default it sets values not present to -1, but we need them to be 0
mod.LatestVersion = new Version(getver(0), getver(1), getver(2), getver(3));
mod.UpdateDetails = release["name"] + "\n\n" + release["body"];
mod.UpdateDetails = release["name"] + "\n\n" + release["body"].ToString();
if (desc)
{
try
{
var obj = JObject.Parse(await client.DownloadStringTaskAsync("/api/v1/repos/" + mod.Author + "/" + mod.Name + "/contents/README.md"));
mod.Description = Encoding.UTF8.GetString(Convert.FromBase64String(obj["content"]?.ToString() ?? string.Empty));
mod.Description = Encoding.UTF8.GetString(Convert.FromBase64String(obj["content"].ToString()));
}
catch (WebException)
{ //It returns a HTTP 500 if it doesn't exist...
@ -188,7 +174,6 @@ 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() : "";
@ -205,9 +190,7 @@ namespace TBMM
}
if (mod.LatestVersion != null && mod.Version != null && mod.Version < mod.LatestVersion)
item.ForeColor = Color.Blue;
else if(mod.Broken is true)
item.ForeColor = Color.Red;
else if (mod.Outdated(lastGameUpdateTime))
else if (mod.LastUpdated != default && mod.LastUpdated < lastGameUpdateTime)
item.ForeColor = Color.DarkOrange;
else
item.ForeColor = modlist.ForeColor;
@ -226,7 +209,6 @@ namespace TBMM
delete.Add(name);
}
delete.ForEach(name => { mods.Remove(name); modlist.Items[name].Remove(); });
UpdatePlayButtonColor();
}
}
}

182
GCMM/MainPatcher.cs Normal file
View file

@ -0,0 +1,182 @@
using GCMM.Properties;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Reflection;
namespace GCMM
{
partial class MainForm
{
public GameState CheckIfPatched()
{
if (GetExe() == null)
{
status.Text = "Status: Game not found";
return GameState.NotFound;
}
string pnp = "Patch && Play";
if (!File.Exists(GamePath(@"\IPA.exe")))
{
status.Text = "Status: Patcher missing\nClicking Play will install it";
playbtn.Text = pnp;
return GameState.NoPatcher;
}
if (gcipa.Updatable && !(gcipa.Version == new Version(1, 0, 0, 0) && gcipa.LatestVersion == new Version(4, 0, 0, 0)))
{
status.Text = "Status: Patcher outdated\nClicking play will update it";
playbtn.Text = pnp;
return GameState.OldPatcher;
}
string nopatch = "Status: Unpatched\nClicking Play patches it";
string gc = GetExe().Replace(".exe", "");
string backups = GamePath(@"\IPA\Backups\" + gc);
if (!Directory.Exists(backups))
{
status.Text = nopatch;
playbtn.Text = pnp;
return GameState.Unpatched;
}
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;
}
public async Task<bool?> PatchStartGame(string command = null)
{
if (!BeginWork()) return false;
foreach (ListViewItem item in modlist.SelectedItems)
item.Selected = false;
bool? retOpenedWindowShouldStay = null;
void EnsureShown(bool stay)
{
if (!Visible)
{
Show();
retOpenedWindowShouldStay = stay;
TopMost = true; //It opens in the background otherwise - should be fine since it only shows for a couple seconds
}
}
var status = CheckIfPatched();
//bool justDownloadedPatcherSoDontWarnAboutIncompatibility = false;
switch (status)
{
case GameState.NotFound:
MessageBox.Show("Gamecraft not found! Set the correct path in Settings.");
EndWork(false);
return retOpenedWindowShouldStay;
case GameState.NoPatcher:
case GameState.OldPatcher:
{
EnsureShown(false);
if (MessageBox.Show((status == GameState.NoPatcher
? "The patcher (GCIPA) is not found. It's necessary to load the mods."
: "There is a patcher update available!"
) + "\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();
return retOpenedWindowShouldStay;
}
this.status.Text = "Status: Patching...";
int C = 0;
while (gcipa.DownloadURL == null && C < 20)
await Task.Delay(500); //The EnsureShown() call should download info about GCIPA
if (gcipa.DownloadURL == null)
{
MessageBox.Show("Could not get information about GCIPA in time. Please run GCMM manually.");
return retOpenedWindowShouldStay;
}
using (WebClient client = GetClient())
{
string url = gcipa.DownloadURL;
await client.DownloadFileTaskAsync(url, "IPA.zip");
using (var fs = new FileStream("IPA.zip", FileMode.Open))
using (var za = new ZipArchive(fs))
za.ExtractToDirectory(Settings.Default.GamePath, true); //Overwrite files that were left from a previous install of the patcher
File.Delete("IPA.zip");
}
}
GetInstalledMods(); //Update patcher state, should be fine for this rare event
status = CheckIfPatched();
break;
}
switch (status)
{
case GameState.NoPatcher: //Make sure it actually worked
case GameState.OldPatcher:
EndWork(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 = Settings.Default.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);
process.Exited += handler;
await task;
}
break;
case GameState.Patched:
{
//CheckStartGame(command)(null, null);
var (handler, task) = CheckStartGame(command);
handler(null, null);
await task;
}
break;
}
return retOpenedWindowShouldStay;
}
public enum GameState
{
NotFound,
NoPatcher,
OldPatcher,
Unpatched,
Patched
}
}
}

331
GCMM/MainUtils.cs Normal file
View file

@ -0,0 +1,331 @@
using GCMM.Properties;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace GCMM
{
partial class MainForm
{
public void UpdateButton(Button button, bool enabled)
{
if (enabled)
{
button.ForeColor = Color.Lime;
button.FlatAppearance.MouseOverBackColor = Color.FromArgb(0, 40, 0);
button.FlatAppearance.MouseDownBackColor = Color.Green;
}
else
{
button.ForeColor = Color.Green;
button.FlatAppearance.MouseOverBackColor = Color.Black;
button.FlatAppearance.MouseDownBackColor = Color.Black;
}
}
public string GetGameFolder()
{
string libs;
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
libs = steamPath + @"\steamapps\libraryfolders.vdf";
else
return null;
foreach (var line in File.ReadAllLines(libs).Concat(new[] { "\t\"19\"\t\t\"" + steamPath + "\"" }))
{
var regex = new Regex("\\t\"\\d+\"\\t\\t\"(.+)\"");
var match = regex.Match(line);
if (!match.Success)
continue;
string library = match.Groups[1].Value.Replace("\\\\", "\\");
library += @"\steamapps\common\";
if (GetExe(library + "Gamecraft") != null)
return library + "Gamecraft";
if (GetExe(library + "RobocraftX") != null)
return library + "RobocraftX";
}
return null;
}
public string SelectGameFolder()
{
var ofd = new OpenFileDialog();
ofd.Filter = "Gamecraft executable|Gamecraft.exe|Gamecraft Preview executable|GamecraftPreview.exe";
ofd.Title = "Game location";
ofd.InitialDirectory = steamPath + @"\steamapps\common\";
ofd.CheckFileExists = true;
ofd.ShowDialog();
if (string.IsNullOrWhiteSpace(ofd.FileName))
return null;
return Directory.GetParent(ofd.FileName).FullName;
}
public (string, int) AskForSteamLogin()
{
while (MessageBox.Show("Couid not find your Steam configuration to set launch options.\n\n" +
"Please make sure you are logged into Steam and click Retry or click Cancel to skip setting this up.",
"Steam config not found", MessageBoxButtons.RetryCancel) != DialogResult.Cancel)
{
var ret = GetSteamLocationAndUser();
if (ret != (null, 0))
return ret;
}
return (null, 0);
}
/// <summary>
/// Does not return the current value if setting it is not possible because Steam is running.
/// </summary>
/// <param name="autoLaunch">The value to set or null to keep as is</param>
/// <returns>The current value, unless setting it while Steam is running</returns>
public bool UpdateOrGetSteamConfigToAutoStart(bool? autoLaunch)
{
string commandToUse = Application.ExecutablePath + " -start %command%";
if (autoLaunch.HasValue && Process.GetProcessesByName("steam").Length > 0)
{ //Setting it while Steam is running
if (MessageBox.Show("Cannot set launch options while Steam is running." +
(autoLaunch.Value
? " Do you want to do it manually? If so, it will be copied on your clipboard." +
" Right click the game, select Properties and paste it in the launch options field."
: " Do you want to do it manually?" +
" If so, right click the game, select Properties and remove the text from the launch options field."),
"Launch options in Steam", MessageBoxButtons.YesNo) == DialogResult.Yes && autoLaunch.Value)
Clipboard.SetText(commandToUse);
return false;
}
var regex = new Regex(@"(\t{6}""LaunchOptions""\t+"")(.*)("")");
string path = steamPath + @"\userdata\" + Settings.Default.SteamUserID + @"\config\localconfig.vdf";
var lines = File.ReadAllLines(path);
bool shouldMatch = false;
bool ret = false;
for (int i = 0; i < lines.Length; i++)
{
if (lines[i] == "\t\t\t\t\t\"1078000\"")
shouldMatch = true; //Found the game
else if(shouldMatch)
{
var match = regex.Match(lines[i]);
if (!match.Success)
continue;
ret = match.Groups[2].Value.Contains("GCMM.exe");
string enabledCommand = match.Groups[1].Value + commandToUse.Replace("\\", "\\\\") + match.Groups[3].Value;
if (autoLaunch.HasValue)
{
if (autoLaunch.Value)
lines[i] = enabledCommand;
else
lines[i] = match.Groups[1].Value + match.Groups[3].Value;
File.WriteAllLines(path, lines);
}
else if (ret && lines[i] != enabledCommand) //GCMM got moved or something and it's only queried, not set
{
lines[i] = enabledCommand;
File.WriteAllLines(path, lines);
}
break;
}
}
return ret;
}
public (string, int) GetSteamLocationAndUser()
{
if (Environment.OSVersion.Platform != PlatformID.Win32NT) return (null, 0);
using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam\ActiveProcess"))
{
string path = (string)key?.GetValue("SteamClientDll");
path = path != null ? Directory.GetParent(path).FullName : null;
return (path, (int)(key?.GetValue("ActiveUser") ?? 0));
}
}
public void DetectConfigLocationAndAutoStart(string steamPath, ref int user)
{
string path = steamPath + @"\userdata";
if (user == 0)
{
var dirs = Directory.GetDirectories(path);
var goodPaths = (from dir in dirs
where File.Exists(dir + @"\config\localconfig.vdf")
select dir).ToArray();
if (goodPaths.Length != 1)
{
(_, user) = AskForSteamLogin();
path += user;
}
else
{
path = goodPaths[0];
user = int.Parse(Path.GetFileName(path));
}
}
else
path += "\\" + user;
path += @"\config\localconfig.vdf";
if (path != null && user != 0 && File.Exists(path))
{
Settings.Default.SteamUserID = user;
UpdateOrGetSteamConfigToAutoStart(true);
}
else
Settings.Default.AutoLaunch = false;
}
private (EventHandler, Task) CheckStartGame(string command)
{
var tcs = new TaskCompletionSource<object>();
return ((sender, e) =>
{
Action act = async () =>
{
if (((sender as Process)?.ExitCode ?? 0) != 0)
{
status.Text = "Status: Patching failed";
return;
}
if (CheckIfPatched() == GameState.Patched || unpatched.Checked)
if (command != null)
{
if (sender is Process) //Patched just now
CheckCompatibilityAndDisableMods();
await CheckModUpdatesAsync();
Process.Start(command);
}
else if (Environment.OSVersion.Platform == PlatformID.Win32NT)
Process.Start("steam://run/1078000/");
else
Process.Start("xdg-open", "steam://run/1078000/");
EndWork(false);
tcs.SetResult(null);
};
if (InvokeRequired)
Invoke(act);
else
act();
}, tcs.Task);
}
private void CheckCompatibilityAndDisableMods()
{
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 GCMM)" +
"\nClick No to disable mods before starting the game (after a major update)" +
"\n\nYou can always enable/disable mods by launching GCMM.",
"Possible incompatibility warning", MessageBoxButtons.YesNo) == DialogResult.No)
unpatched.Checked = true;
}
private async Task CheckModUpdatesAsync()
{
var updatable = mods.Values.Where(mod => mod.Updatable).ToArray();
if (updatable.Length == 0)
return;
if (MessageBox.Show("Mod update(s) available!\n\n"
+ updatable.Select(mod => mod.Name + " " + mod.LatestVersion).Aggregate((a, b) => a + "\n")
+ "\n\nDo you want to update them now? You can also update later by opening GCMM.",
"Update(s) available", MessageBoxButtons.YesNo) == DialogResult.No)
return;
foreach (var mod in updatable)
await InstallMod(mod);
MessageBox.Show("Mods updated");
}
public WebClient GetClient()
{
var client = new WebClient();
if (!Settings.Default.UseProxy)
client.Proxy = null;
client.Headers.Clear();
client.Headers[HttpRequestHeader.Accept] = "application/json";
client.BaseAddress = "https://git.exmods.org";
return client;
}
private bool working = false;
/// <summary>
/// Some simple "locking", only allow one operation at a time
/// </summary>
/// <returns>Whether the work can begin</returns>
public bool BeginWork()
{
if (working) return false;
working = true;
UpdateButton(playbtn, false);
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
UpdateButton(settingsbtn, false);
unpatched.Enabled = false;
return true;
}
public void EndWork(bool desc = true)
{
working = false;
UpdateButton(playbtn, true);
UpdateButton(settingsbtn, true);
if (desc)
modlist_SelectedIndexChanged(modlist, null);
unpatched.Enabled = true;
}
/// <summary>
/// Path must start with \
/// </summary>
/// <param name="path"></param>
/// <param name="gamepath"></param>
/// <returns></returns>
public string GamePath(string path, string gamepath = null)
{
return ((gamepath ?? Settings.Default.GamePath) + path).Replace('\\', Path.DirectorySeparatorChar);
}
public string GetExe(string path = null)
{
if (File.Exists(GamePath("\\Gamecraft.exe", path)))
return "Gamecraft.exe";
if (File.Exists(GamePath("\\GamecraftPreview.exe", path)))
return "GamecraftPreview.exe";
return null;
}
private bool CheckNoExe()
{
return CheckNoExe(out _);
}
private bool CheckNoExe(out string path)
{
path = GetExe();
if (path == null)
{
MessageBox.Show("Gamecraft not found! Set the correct path in Settings.");
return true;
}
return false;
}
public async Task<DateTime> GetLastGameUpdateTime()
{
/*using (var client = GetClient())
{
string html = await client.DownloadStringTaskAsync("https://api.steamcmd.net/v1/info/1078000");
var regex = new Regex("<i>timeupdated:</i>[^<]*<b>([^<]*)</b>");
var match = regex.Match(html);
if (!match.Success)
return default;
return new DateTime(1970, 1, 1).AddSeconds(long.Parse(match.Groups[1].Value));
}*/
//return new DateTime(2020, 12, 28);
return default;
}
}
}

View file

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TBMM
namespace GCMM
{
public class ModInfo
{
@ -31,8 +31,5 @@ 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

@ -4,7 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
namespace GCMM
{
static class Program
{

98
GCMM/Properties/Settings.Designer.cs generated Normal file
View file

@ -0,0 +1,98 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace GCMM.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string GamePath {
get {
return ((string)(this["GamePath"]));
}
set {
this["GamePath"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string WinePath {
get {
return ((string)(this["WinePath"]));
}
set {
this["WinePath"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool UseProxy {
get {
return ((bool)(this["UseProxy"]));
}
set {
this["UseProxy"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool NeedsUpdate {
get {
return ((bool)(this["NeedsUpdate"]));
}
set {
this["NeedsUpdate"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
public int SteamUserID {
get {
return ((int)(this["SteamUserID"]));
}
set {
this["SteamUserID"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool AutoLaunch {
get {
return ((bool)(this["AutoLaunch"]));
}
set {
this["AutoLaunch"] = value;
}
}
}
}

View file

@ -0,0 +1,24 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="GCMM.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="GamePath" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="WinePath" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="UseProxy" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="NeedsUpdate" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="SteamUserID" Type="System.Int32" Scope="User">
<Value Profile="(Default)">0</Value>
</Setting>
<Setting Name="AutoLaunch" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
</Settings>
</SettingsFile>

View file

@ -2,7 +2,7 @@
"profiles": {
"GCMM": {
"commandName": "Project",
"commandLineArgs": ""
"commandLineArgs": "-start steam:\"\""
}
}
}

View file

@ -1,4 +1,4 @@
namespace TBMM
namespace GCMM
{
partial class SettingsForm
{
@ -34,7 +34,8 @@
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.useProxy = new System.Windows.Forms.CheckBox();
this.autopatching = new System.Windows.Forms.CheckBox();
this.SuspendLayout();
//
// label1
@ -93,15 +94,25 @@
this.cancelbtn.UseVisualStyleBackColor = true;
this.cancelbtn.Click += new System.EventHandler(this.cancelbtn_Click);
//
// keepPatched
// useProxy
//
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;
this.useProxy.AutoSize = true;
this.useProxy.Location = new System.Drawing.Point(12, 49);
this.useProxy.Name = "useProxy";
this.useProxy.Size = new System.Drawing.Size(147, 17);
this.useProxy.TabIndex = 5;
this.useProxy.Text = "Use system proxy settings";
this.useProxy.UseVisualStyleBackColor = true;
//
// autopatching
//
this.autopatching.AutoSize = true;
this.autopatching.Location = new System.Drawing.Point(12, 72);
this.autopatching.Name = "autopatching";
this.autopatching.Size = new System.Drawing.Size(194, 17);
this.autopatching.TabIndex = 6;
this.autopatching.Text = "Enable auto-patching on game start";
this.autopatching.UseVisualStyleBackColor = true;
//
// SettingsForm
//
@ -111,7 +122,8 @@
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.autopatching);
this.Controls.Add(this.useProxy);
this.Controls.Add(this.cancelbtn);
this.Controls.Add(this.savebtn);
this.Controls.Add(this.browsebtn);
@ -130,9 +142,8 @@
this.Load += new System.EventHandler(this.Form1_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.CheckBox keepPatched;
}
#endregion
@ -141,6 +152,8 @@
private System.Windows.Forms.Button browsebtn;
private System.Windows.Forms.Button savebtn;
private System.Windows.Forms.Button cancelbtn;
private System.Windows.Forms.CheckBox useProxy;
private System.Windows.Forms.CheckBox autopatching;
}
}

64
GCMM/SettingsForm.cs Normal file
View file

@ -0,0 +1,64 @@
using GCMM.Properties;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace GCMM
{
public partial class SettingsForm : Form
{
private MainForm mainForm;
private bool autopatchingEnabled;
public SettingsForm()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
gamelocation.Text = Settings.Default.GamePath;
useProxy.Checked = Settings.Default.UseProxy;
mainForm = Owner as MainForm;
autopatchingEnabled = Settings.Default.SteamUserID != 0 && mainForm.UpdateOrGetSteamConfigToAutoStart(null);
autopatching.Checked = autopatchingEnabled;
}
private void browsebtn_Click(object sender, EventArgs e)
{
gamelocation.Text = mainForm.SelectGameFolder() ?? gamelocation.Text;
}
private void savebtn_Click(object sender, EventArgs e)
{
Settings.Default.GamePath = gamelocation.Text;
Settings.Default.UseProxy = useProxy.Checked;
if (autopatching.Checked != autopatchingEnabled)
{
if (autopatching.Checked && Settings.Default.SteamUserID == 0)
{
var (steamPath, user) = mainForm.GetSteamLocationAndUser();
if (user != 0)
mainForm.DetectConfigLocationAndAutoStart(steamPath, ref user);
Settings.Default.SteamUserID = user; //If it's 0 then it's no change
}
else
mainForm.UpdateOrGetSteamConfigToAutoStart(autopatching.Checked);
}
Settings.Default.Save();
Close();
}
private void cancelbtn_Click(object sender, EventArgs e)
{
Close();
}
}
}

View file

@ -6,7 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TBMM
namespace GCMM
{
public static class ZipArchiveExtensions
{ //https://stackoverflow.com/a/14795752/2703239

View file

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -1,7 +1,7 @@
# Techblox Mod Manager
A manager that handles everything needed to use mods for Techblox.
# Gamecraft Mod Manager
A manager that handles everything needed to use mods for 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.
**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.
## Features
* Download and run GCIPA if needed
@ -12,14 +12,8 @@ A manager that handles everything needed to use mods for Techblox.
## 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 Techblox's directory instead of the Plugins directory.
* If the zip contains a folder named Plugins, the manager will unzip it to Gamecraft'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

@ -1,9 +0,0 @@
namespace TBMM
{
public enum AutoPatchingState
{
Unspecified,
Disabled,
Enabled
}
}

View file

@ -1,23 +0,0 @@
using System.IO;
using Newtonsoft.Json;
namespace TBMM
{
public class Configuration
{
public string GamePath { get; set; }
public bool KeepPatched { get; set; }
public static Configuration Load()
{
if (File.Exists("configuration.json"))
return JsonConvert.DeserializeObject<Configuration>(File.ReadAllText("configuration.json"));
return new Configuration();
}
public void Save()
{
File.WriteAllText("configuration.json", JsonConvert.SerializeObject(this));
}
}
}

View file

@ -1,19 +0,0 @@
using System.Windows.Forms;
namespace TBMM
{
public static class DialogUtils
{
public static void ShowInfo(string message, string title) =>
MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Information);
public static void ShowError(string message, string title) =>
MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
public static void ShowWarning(string message, string title) =>
MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Warning);
public static bool ShowYesNoQuestion(string message, string title) =>
MessageBox.Show(message, title, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes;
}
}

View file

@ -1,130 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace TBMM {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Localization {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Localization() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TBMM.Localization", typeof(Localization).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Launcher settings updated..
/// </summary>
internal static string Change_launch_settings_done {
get {
return ResourceManager.GetString("Change_launch_settings_done", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to update launcher settings! You can still run TBMM manually or retry in settings..
/// </summary>
internal static string Change_launch_settings_error {
get {
return ResourceManager.GetString("Change_launch_settings_error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Do you want TBMM to change the game&apos;s launch settings so it can ensure the game is patched?
///
///If you say yes, TBMM will do a quick check before the game is launched and patches if necessary. This way you (hopefully) won&apos;t see crashes after a Techblox update.
///
///Note that this also means that if you (re)move TBMM without disabling this in the settings then you won&apos;t be able to launch Techblox until you reinstall TBMM or fix the launcher configuration..
/// </summary>
internal static string Change_launch_settings_question {
get {
return ResourceManager.GetString("Change_launch_settings_question", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TBMM auto-patching.
/// </summary>
internal static string Change_launch_settings_title {
get {
return ResourceManager.GetString("Change_launch_settings_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Found game at {0}.
/// </summary>
internal static string Found_game_at {
get {
return ResourceManager.GetString("Found_game_at", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game not found. Is it installed correctly? Anyway, please locate the game..
/// </summary>
internal static string Game_not_found {
get {
return ResourceManager.GetString("Game_not_found", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Status: Game not found.
/// </summary>
internal static string Status_Game_not_found {
get {
return ResourceManager.GetString("Status_Game_not_found", resourceCulture);
}
}
}
}

View file

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Found_game_at" xml:space="preserve">
<value>Found game at {0}</value>
</data>
<data name="Game_not_found" xml:space="preserve">
<value>Game not found. Is it installed correctly? Anyway, please locate the game.</value>
</data>
<data name="Status_Game_not_found" xml:space="preserve">
<value>Status: Game not found</value>
</data>
<data name="Change_launch_settings_question" xml:space="preserve">
<value>Do you want TBMM to change the game's launch settings so it can ensure the game is patched?
If you say yes, TBMM will do a quick check before the game is launched and patches if necessary. This way you (hopefully) won't see crashes after a Techblox update.
Note that this also means that if you (re)move TBMM without disabling this in the settings then you won't be able to launch Techblox until you reinstall TBMM or fix the launcher configuration.</value>
</data>
<data name="Change_launch_settings_title" xml:space="preserve">
<value>TBMM auto-patching</value>
</data>
<data name="Change_launch_settings_done" xml:space="preserve">
<value>Launcher settings updated.</value>
</data>
<data name="Change_launch_settings_error" xml:space="preserve">
<value>Failed to update launcher settings! You can still run TBMM manually or retry in settings.</value>
</data>
</root>

View file

@ -1,216 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
{
partial class MainForm
{
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)
{
patched = false;
SetStatusText(GameState.NotFound, false);
return GameState.NotFound;
}
bool gameIsRunning = CheckIfGameIsRunning();
if (gameIsRunning)
{
UpdateButton(playbtn, false); //Don't allow (un)installing mods if game is running
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
modlist.Enabled = false;
}
else
{
if (!working) UpdateButton(playbtn, true);
modlist.Enabled = true;
}
GameState GetPatchedState()
{
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;
}
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)
{
if (!BeginWork()) return false;
foreach (ListViewItem item in modlist.SelectedItems)
item.Selected = false;
bool? retOpenedWindowShouldStay = null;
void EnsureShown(bool stay)
{
if (!Visible)
{
Show();
retOpenedWindowShouldStay = stay;
TopMost = true; //It opens in the background otherwise - should be fine since it only shows for a couple seconds
}
}
var status = CheckIfPatched();
//bool justDownloadedPatcherSoDontWarnAboutIncompatibility = false;
switch (status)
{
case GameState.NotFound:
MessageBox.Show("Techblox not found! Set the correct path in Settings.");
EndWork(status, false);
return retOpenedWindowShouldStay;
case GameState.NoPatcher:
case GameState.OldPatcher:
{
EnsureShown(false);
if (MessageBox.Show((status == GameState.NoPatcher
? "The patcher (GCIPA) is not found. It's necessary to load the mods."
: "There is a patcher update available!"
) + "\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(status);
return retOpenedWindowShouldStay;
}
this.status.Text = "Status: Patching...";
int C = 0;
while (gcipa.DownloadURL == null && C < 20)
await Task.Delay(500); //The EnsureShown() call should download info about GCIPA
if (gcipa.DownloadURL == null)
{
MessageBox.Show("Could not get information about GCIPA in time. Please run TBMM manually.");
return retOpenedWindowShouldStay;
}
using (WebClient client = GetClient())
{
string url = gcipa.DownloadURL;
await client.DownloadFileTaskAsync(url, "IPA.zip");
using (var fs = new FileStream("IPA.zip", FileMode.Open))
using (var za = new ZipArchive(fs))
za.ExtractToDirectory(Configuration.GamePath, true); //Overwrite files that were left from a previous install of the patcher
File.Delete("IPA.zip");
}
}
GetInstalledMods(); //Update patcher state, should be fine for this rare event
status = CheckIfPatched();
break;
}
switch (status)
{
case GameState.NoPatcher: //Make sure it actually worked
case GameState.OldPatcher:
EndWork(status, false);
return retOpenedWindowShouldStay;
case GameState.Unpatched:
{ //TODO: Wine
EnsureShown(false);
var (handler, task) = CheckStartGame(command);
var process = ExecutePatcher(true);
process.Exited += handler;
await task;
}
break;
case GameState.Patched:
{
//CheckStartGame(command)(null, null);
var (handler, task) = CheckStartGame(command);
handler(null, null);
await task;
}
break;
}
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,
/// <summary>
/// Doesn't matter if patched, don't do anything if the game is running
/// </summary>
InGame
}
}
}

View file

@ -1,294 +0,0 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
namespace TBMM
{
partial class MainForm
{
public void UpdateButton(Button button, bool enabled)
{
if (enabled)
{
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
{
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;
}
}
public string GetGameFolder()
{
using var key =
Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Techblox Launcher") ??
Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Techblox Launcher");
string launcherPath = key?.GetValue("DisplayIcon") is string launcherExecutable
? Directory.GetParent(launcherExecutable)?.FullName
: null;
launcherPath ??= Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Techblox Launcher");
string launcherConfig = Path.Combine(launcherPath, "launcher_settings.ini");
if (!File.Exists(launcherConfig)) return null;
string path = File.ReadLines(launcherConfig)
.FirstOrDefault(line => line.StartsWith("133062..GAME_PATH="))
?.Substring("133062..GAME_PATH=".Length).Replace("/TBMM/", "/");
if (path != null) path = (path + "StandaloneWindows64").Replace('/', Path.DirectorySeparatorChar);
if (path != null && GetExe(path) != null) return path;
return null;
}
public string SelectGameFolder()
{
var ofd = new OpenFileDialog();
ofd.Filter = "Techblox executable|Techblox.exe|Techblox Preview executable|TechbloxPreview.exe";
ofd.Title = "Game location";
ofd.CheckFileExists = true;
ofd.ShowDialog();
return string.IsNullOrWhiteSpace(ofd.FileName) ? null : Directory.GetParent(ofd.FileName)?.FullName;
}
private (EventHandler, Task) CheckStartGame(string command)
{
var tcs = new TaskCompletionSource<object>();
return ((sender, e) =>
{
Action act = async () =>
{
if (((sender as Process)?.ExitCode ?? 0) != 0)
{
status.Text = "Status: Patching failed";
return;
}
var patched = CheckIfPatched();
if (patched == GameState.Patched)
{
Process process = null;
if (command != null)
{
await CheckModUpdatesAsync();
process = Process.Start(command);
}
else if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
process = Process.Start(new ProcessStartInfo(GamePath("\\" + GetExe()))
{
WorkingDirectory = GamePath("\\") //Mods are only loaded if the working directory is correct
});
}
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)
Invoke(act);
else
act();
}, tcs.Task);
}
private void HandleGameExit(object sender, EventArgs e)
{
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()
{
var updatable = mods.Values.Where(mod => mod.Updatable).ToArray();
if (updatable.Length == 0)
return;
if (MessageBox.Show("Mod update(s) available!\n\n"
+ updatable.Select(mod => mod.Name + " " + mod.LatestVersion).Aggregate((a, b) => a + "\n")
+ "\n\nDo you want to update them now? You can also update later by opening TBMM.",
"Update(s) available", MessageBoxButtons.YesNo) == DialogResult.No)
return;
foreach (var mod in updatable)
await InstallMod(mod);
MessageBox.Show("Mods updated");
}
public WebClient GetClient()
{
var client = new WebClient();
client.Headers.Clear();
client.Headers[HttpRequestHeader.Accept] = "application/json";
client.BaseAddress = "https://git.exmods.org";
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
/// </summary>
/// <returns>Whether the work can begin</returns>
public bool BeginWork()
{
if (working) return false;
working = true;
UpdateButton(playbtn, false);
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
UpdateButton(settingsbtn, false);
return true;
}
public void EndWork(GameState state, bool desc = true)
{
working = false;
if (state != GameState.InGame)
UpdateButton(playbtn, true);
UpdateButton(settingsbtn, true);
if (desc)
modlist_SelectedIndexChanged(modlist, null);
}
/// <summary>
/// Path must start with \
/// </summary>
/// <param name="path"></param>
/// <param name="gamepath"></param>
/// <returns></returns>
public string GamePath(string path, string gamepath = null)
{
return ((gamepath ?? Configuration.GamePath) + path).Replace('\\', Path.DirectorySeparatorChar);
}
public string GetExe(string path = null, bool withExtension = true)
{
if (File.Exists(GamePath("\\Techblox.exe", path)))
return "Techblox" + (withExtension ? ".exe" : "");
if (File.Exists(GamePath("\\TechbloxPreview.exe", path)))
return "TechbloxPreview" + (withExtension ? ".exe" : "");
return null;
}
private bool CheckNoExe()
{
return CheckNoExe(out _);
}
private bool CheckNoExe(out string path)
{
path = GetExe();
if (path == null)
{
MessageBox.Show("Techblox not found! Set the correct path in Settings.");
return true;
}
return false;
}
public DateTime GetGameVersionAsDate()
{
if (Configuration.GamePath == null) return default;
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)
{
Array.Copy(data, 256, data, 0, 256);
int read = sr.ReadBlock(data, 256, 256);
for (int i = 0; i < data.Length - 11; i++)
{
if (data[i] == '2')
{
string date = new string(data, i, 11);
if (date.StartsWith("202") && DateTime.TryParse(date, out var ret))
return ret;
}
}
}
return default;
}
}
}

View file

@ -1,40 +0,0 @@
using System;
using System.Windows.Forms;
namespace TBMM
{
public partial class SettingsForm : Form
{
private MainForm mainForm;
public SettingsForm()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
mainForm = (MainForm) Owner;
gamelocation.Text = mainForm.Configuration.GamePath;
keepPatched.Checked = mainForm.Configuration.KeepPatched;
}
private void browsebtn_Click(object sender, EventArgs e)
{
gamelocation.Text = mainForm.SelectGameFolder() ?? gamelocation.Text;
}
private void savebtn_Click(object sender, EventArgs e)
{
mainForm.Configuration.GamePath = gamelocation.Text;
mainForm.Configuration.KeepPatched = keepPatched.Checked;
mainForm.Configuration.Save();
Close();
}
private void cancelbtn_Click(object sender, EventArgs e)
{
Close();
}
}
}