Compare commits

..

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

32 changed files with 841 additions and 3322 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

@ -2,16 +2,16 @@
<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>
<AssemblyVersion>1.2.0.0</AssemblyVersion>
<FileVersion>1.2.0.0</FileVersion>
<Version>1.2.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,8 +26,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>
<ItemGroup>
@ -39,18 +37,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
{
@ -23,36 +23,48 @@
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
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,15 +73,17 @@
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);
this.modlist.Sorting = System.Windows.Forms.SortOrder.Ascending;
this.modlist.TabIndex = 0;
this.modlist.UseCompatibleStateImageBehavior = false;
this.modlist.View = System.Windows.Forms.View.Details;
@ -182,9 +196,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 +236,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 +270,11 @@
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 +290,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;
}
}

257
GCMM/MainForm.cs Normal file
View file

@ -0,0 +1,257 @@
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.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace GCMM
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private Dictionary<string, ModInfo> mods = new Dictionary<string, ModInfo>();
private string defaultInfo = @"
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 GamecraftModdingAPI as well.
Then, simply click Play. 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 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.
You don't have to use the mod manager to run the game each time, though it will tell you about mod updates when they come.
However, you need to run it and click ""Patch & Play"" each time there's a Gamecraft update.
Disclaimer:
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 with the ""Disable mods"" option checked before reporting to Freejam.
You may also want to verify the game's files by right clicking the game in Steam and choosing Properties, going to Local files and clicking Verify integrity of game files.
";
private void Form1_Load(object sender, EventArgs e)
{
if (Settings.Default.NeedsUpdate)
{
Settings.Default.Upgrade();
Settings.Default.NeedsUpdate = false;
Settings.Default.Save();
}
modlist.Items.Clear();
UpdateButton(installbtn, false);
modinfobox.Text = defaultInfo;
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
refreshbtn_Click(refreshbtn, null);
}
private async void playbtn_Click(object sender, EventArgs e)
{
if (playbtn.ForeColor == Color.Green) return; //Disabled
await UpdateAPI();
await PatchStartGame(); //It will call EndWork();
}
private void settingsbtn_Click(object sender, EventArgs e)
{
if (settingsbtn.ForeColor == Color.Green) return; //Disabled
var sf = new SettingsForm();
sf.ShowDialog(this);
}
private void modlist_SelectedIndexChanged(object sender, EventArgs e)
{
if (working) return;
modinfobox.Clear();
switch (modlist.SelectedItems.Count)
{
case 0:
modinfobox.Text = defaultInfo;
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
break;
case 1:
default:
installbtn.Text = "Install mod";
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
bool install = false, update = false;
foreach (ListViewItem item in modlist.SelectedItems)
{
var mod = mods[item.Name];
if (modlist.SelectedItems.Count == 1)
{
bool up = mod.Updatable;
modinfobox.Text = ((up ? "New version available! " + mod.UpdateDetails + "\n\n"
: "") + mod.Description).Replace("\n", Environment.NewLine);
if(up)
{
modinfobox.Select(0, "New version available!".Length);
modinfobox.SelectionColor = Color.Aqua;
modinfobox.DeselectAll();
modinfobox.SelectionColor = modinfobox.ForeColor;
}
}
else
modinfobox.Text = modlist.SelectedItems.Count + " mods selected";
if (mod.DownloadURL != null && !(mod.LatestVersion <= mod.Version))
{
UpdateButton(installbtn, true);
if (mod.Version != null)
update = true;
else
install = true;
}
if (mod.Version != null)
UpdateButton(uninstallbtn, true);
}
if (install && update)
installbtn.Text = "Install and update mod";
else if (update)
installbtn.Text = "Update mod";
else
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)
{
if (installbtn.ForeColor == Color.Green) return; //Disabled
if (!BeginWork()) return;
foreach (ListViewItem item in modlist.SelectedItems)
{
var mod = mods[item.Name];
if (item.Group.Name == "installed" && (mod.DownloadURL == null || mod.LatestVersion <= mod.Version)) continue;
await InstallMod(mod);
}
EndWork();
}
private void uninstallbtn_Click(object sender, EventArgs e)
{
if (uninstallbtn.ForeColor == Color.Green) return; //Disabled
foreach (ListViewItem item in modlist.SelectedItems)
{
if (item.Group.Name != "installed") continue;
UninstallMod(mods[item.Name]);
}
EndWork(); //Update button states
}
private void findlog_Click(object sender, EventArgs e)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
if (CheckNoExe(out string exe))
return;
Process.Start("explorer.exe", $@"/select,{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}Low\Freejam\{exe.Replace(".exe", "")}\Player.log");
}
}
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");
string disabled = GamePath("\\Plugins_Disabled");
pexists = Directory.Exists(plugins);
dexists = Directory.Exists(disabled);
if (pexists && !Directory.EnumerateFiles(plugins).Any())
{
Directory.Delete(plugins);
pexists = false;
}
if (dexists && !Directory.EnumerateFiles(disabled).Any())
{
Directory.Delete(disabled);
dexists = false;
}
}
private void refreshbtn_Click(object sender, EventArgs e)
{
CheckIfPatched();
var mods = GetInstalledMods();
GetAvailableMods();
CheckUninstalledMods(mods);
}
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 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;
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
Process.Start("steam://validate/1078000/");
else
Process.Start("xdg-open", "steam://validate/1078000/");
}
}
}

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"));
@ -27,12 +29,7 @@ namespace TBMM
string disposition = client.ResponseHeaders["Content-Disposition"];
string filename = disposition.Substring(disposition.IndexOf("filename=") + 10).Replace("\"", "");
if (filename.EndsWith(".dll"))
{
string name = plugins.FullName + Path.DirectorySeparatorChar + mod.Name + ".dll"; //Force mod name to make uninstalls & identifying easier
if (File.Exists(name))
File.Delete(name);
File.Move(tmppath, name);
}
File.Move(tmppath, plugins.FullName + Path.DirectorySeparatorChar + mod.Name + ".dll"); //Force mod name to make uninstalls & identifying easier
else if (filename.EndsWith(".zip"))
{
bool pluginOnly = true;
@ -55,7 +52,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 +63,6 @@ namespace TBMM
}
GetInstalledMods(); //Update list
}
UpdatePlayButtonColor();
}
public void ExtractMod(ZipArchive archive, string destinationDirectoryName, ModInfo mod)
@ -74,7 +70,6 @@ namespace TBMM
LoadFileList(mod);
DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName);
string destinationDirectoryFullPath = di.FullName;
bool? skipExisting = null;
foreach (ZipArchiveEntry file in archive.Entries)
{
@ -96,16 +91,7 @@ namespace TBMM
&& !mod.ModFiles.Contains(completeFileName) // OR it's known to be part of the mod already
&& file.FullName != "Plugins/" + mod.Name + ".dll") // OR it's the plugin's DLL (dll->zip release)
{
if (!skipExisting.HasValue)
{
var mbox = new CustomMessageBox("The mod zip contains a file that exists as part of the game. Do you want to skip this file?\n" + file.FullName + "\nOnly choose No if it's part of a previous installation of the mod.", "File is part of the game");
var result = mbox.ShowDialog(this);
if (mbox.ApplyToAll)
skipExisting = result == DialogResult.Yes;
if (result == DialogResult.Yes)
continue;
}
else if (skipExisting.Value)
if (MessageBox.Show("The mod zip contains a file that exists as part of the game. Do you want to skip this file?\n" + file.FullName + "\nOnly choose No if it's part of a previous installation of the mod.", "File is part of the game", MessageBoxButtons.YesNo) == DialogResult.Yes)
continue;
}
@ -118,73 +104,46 @@ 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)
{
try
LoadFileList(mod);
if (mod.ModFiles.Count == 0) //A single DLL
File.Delete(GamePath(@"\Plugins\" + mod.Name + ".dll"));
else //A ZIP
{
LoadFileList(mod);
if (mod.ModFiles.Count == 0) //A single DLL
File.Delete(GamePath(@"\Plugins\" + mod.Name + ".dll"));
else //A ZIP
foreach (string file in mod.ModFiles)
{
foreach (string file in mod.ModFiles)
{
if (!File.Exists(file)) continue; //If the folders don't exist then it errors
File.Delete(file);
var parent = Directory.GetParent(file);
if (!parent.EnumerateFileSystemInfos().Any())
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)
AddUpdateModInList(mod); //Update list
else
{
mods.Remove(mod.Name);
modlist.Items[mod.Name].Remove();
File.Delete(file);
var parent = Directory.GetParent(file);
if (!parent.EnumerateFiles().Any())
parent.Delete(); //May delete the Plugins dir if empty
}
}
catch (Exception e) when (e is UnauthorizedAccessException || e is IOException)
{
MessageBox.Show("Could not remove mod files! Make sure the game isn't running.\n" + e.Message);
}
UpdatePlayButtonColor();
File.Delete(mod.Name + ".json");
mod.Version = null; //Not installed
AddUpdateModInList(mod); //Update list
}
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",
MessageBoxButtons.OKCancel) == DialogResult.OK)
if (MessageBox.Show($"GamecraftModdingAPI will be {(gcmapi.Installed ? "updated" : "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,17 +1,18 @@
using Newtonsoft.Json.Linq;
using GCMM.Properties;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
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
{
@ -31,7 +32,7 @@ namespace TBMM
var an = AssemblyName.GetAssemblyName(modPath);
if (an.Name == "0Harmony") continue;
//Use filename to avoid differences between repository & assembly name casing
var mod = new ModInfo { Name = Path.GetFileNameWithoutExtension(modPath), Version = an.Version };
var mod = new ModInfo { Name = Path.GetFileNameWithoutExtension(modPath), Version = an.Version, LastUpdated = File.GetLastWriteTime(modPath) };
AddUpdateModInList(mod);
installed.Add(mod.Name);
}
@ -39,27 +40,12 @@ namespace TBMM
{ //Not a .NET assembly
}
}
try
{
string ipath = GamePath("\\IPA.exe"); //Heh
if (File.Exists(ipath))
{
var an = AssemblyName.GetAssemblyName(ipath);
gcipa.Version = an.Version;
}
}
catch (BadImageFormatException)
{ //Not a .NET assembly
}
tbmm.Version = Assembly.GetExecutingAssembly().GetName().Version;
return installed;
}
public async Task GetAvailableMods()
public async void 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,55 +53,23 @@ 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
if (await FetchModInfo(mod, preview)) //If it's actually a mod
AddUpdateModInList(mod);
}
}
if (tbmm.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)
== DialogResult.Yes)
Process.Start(tbmm.DownloadURL);
}
UpdatePlayButtonColor();
}
public async Task<bool> FetchModInfo(ModInfo mod, bool preview, bool desc)
public async Task<bool> FetchModInfo(ModInfo mod, bool preview)
{
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,24 +102,19 @@ 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"];
if (desc)
mod.UpdateDetails = release["name"] + "\n\n" + release["body"].ToString();
try
{
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));
}
catch (WebException)
{ //It returns a HTTP 500 if it doesn't exist...
}
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()));
}
catch(WebException)
{ //It returns a HTTP 500 if it doesn't exist...
}
return true;
}
@ -188,45 +137,33 @@ 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() : "";
items[3].Text = omod.LastUpdated.ToString();
item.Group = omod.Installed ? modlist.Groups["installed"] : modlist.Groups["available"];
modlist.Sort();
mod = omod;
}
else
{
mods.Add(mod.Name, mod);
item = new ListViewItem(new[] { mod.Name, mod.Author ?? "", (mod.Version ?? mod.LatestVersion)?.ToString() ?? "", mod.LatestVersion != null ? mod.LastUpdated.ToString() : "" }, modlist.Groups[mod.Installed ? "installed" : "available"]);
item = new ListViewItem(new[] { mod.Name, mod.Author ?? "", (mod.Version ?? mod.LatestVersion)?.ToString() ?? "", mod.LastUpdated.ToString() }, modlist.Groups[mod.Installed ? "installed" : "available"]);
item.Name = mod.Name;
modlist.Items.Add(item);
}
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))
item.ForeColor = Color.DarkOrange;
else
item.ForeColor = modlist.ForeColor;
}
public void CheckUninstalledMods(HashSet<string> installed)
{
List<string> delete = new List<string>();
foreach (string name in mods.Keys.Except(installed))
{
var mod = mods[name];
mod.Version = null;
if (mod.Author != null)
AddUpdateModInList(mod);
else
delete.Add(name);
AddUpdateModInList(mod);
}
delete.ForEach(name => { mods.Remove(name); modlist.Items[name].Remove(); });
UpdatePlayButtonColor();
}
}
}

139
GCMM/MainPatcher.cs Normal file
View file

@ -0,0 +1,139 @@
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;
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;
}
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))
{
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 PatchStartGame()
{
if (!BeginWork()) return;
foreach (ListViewItem item in modlist.SelectedItems)
item.Selected = false;
var status = CheckIfPatched();
switch (status)
{
case GameState.NotFound:
MessageBox.Show("Gamecraft not found! Set the correct path in Settings.");
EndWork(false);
return;
case GameState.NoPatcher:
{
if (MessageBox.Show("The patcher (GCIPA) is not found. It's necessary to load the mods. It will be downloaded from https://git.exmods.org/modtainers/GCIPA/releases and ran to patch the game. You can unpatch to run without mods at any time.", "Patcher download needed", MessageBoxButtons.OKCancel)
== DialogResult.Cancel)
{
EndWork();
return;
}
string releases = "/api/v1/repos/modtainers/GCIPA/releases";
string url;
this.status.Text = "Status: Patching...";
using (WebClient client = GetClient())
{
url = JArray.Parse(await client.DownloadStringTaskAsync(releases)).First["assets"].First["browser_download_url"].ToString();
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
}
}
status = CheckIfPatched();
break;
}
switch (status)
{
case GameState.NoPatcher: //Make sure it actually worked
EndWork(false);
return;
case GameState.Unpatched:
{ //TODO: Wine
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;
process.Exited += CheckStartGame;
}
break;
case GameState.Patched:
CheckStartGame(null, null);
break;
}
}
public enum GameState
{
NotFound,
NoPatcher,
Unpatched,
Patched
}
}
}

164
GCMM/MainUtils.cs Normal file
View file

@ -0,0 +1,164 @@
using GCMM.Properties;
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()
{ //TODO
string libs;
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
libs = @"C:\Program Files (x86)\Steam\steamapps\libraryfolders.vdf";
else
return null;
foreach (var line in File.ReadAllLines(libs).Concat(new[] {@"C:\Program Files (x86)\Steam\"}))
{
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 = @"C:\Program Files (x86)\Steam\steamapps\common\"; //TODO
ofd.CheckFileExists = true;
ofd.ShowDialog();
if (string.IsNullOrWhiteSpace(ofd.FileName))
return null;
return Directory.GetParent(ofd.FileName).FullName;
}
private void CheckStartGame(object sender, EventArgs e)
{
Action act = () =>
{
if (((sender as Process)?.ExitCode ?? 0) != 0)
{
status.Text = "Status: Patching failed";
return;
}
if (CheckIfPatched() == GameState.Patched || unpatched.Checked)
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
Process.Start("steam://run/1078000/");
else
Process.Start("xdg-open", "steam://run/1078000/");
};
if (InvokeRequired)
Invoke(act);
else
act();
EndWork(false);
}
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);
return true;
}
public void EndWork(bool desc = true)
{
working = false;
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 ?? 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;
}
}
}

View file

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TBMM
namespace GCMM
{
public class ModInfo
{
@ -22,17 +22,11 @@ namespace TBMM
/// Can be null.
/// </summary>
public string Author { get; set; }
/// <summary>
/// The time when the mod was last updated by its author.
/// </summary>
public DateTime LastUpdated { get; set; }
public bool Installed => Version != null;
public string DownloadURL { get; set; }
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;
}
}

23
GCMM/Program.cs Normal file
View file

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace GCMM
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
//Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}

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

@ -0,0 +1,74 @@
//------------------------------------------------------------------------------
// <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.5.0.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;
}
}
}
}

View file

@ -0,0 +1,18 @@
<?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>
</Settings>
</SettingsFile>

View file

@ -1,4 +1,4 @@
namespace TBMM
namespace GCMM
{
partial class SettingsForm
{
@ -23,8 +23,8 @@
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
@ -34,7 +34,7 @@
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.SuspendLayout();
//
// label1
@ -71,7 +71,7 @@
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.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.savebtn.Location = new System.Drawing.Point(270, 113);
this.savebtn.Location = new System.Drawing.Point(271, 74);
this.savebtn.Name = "savebtn";
this.savebtn.Size = new System.Drawing.Size(75, 23);
this.savebtn.TabIndex = 3;
@ -85,7 +85,7 @@
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.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.cancelbtn.Location = new System.Drawing.Point(352, 113);
this.cancelbtn.Location = new System.Drawing.Point(352, 74);
this.cancelbtn.Name = "cancelbtn";
this.cancelbtn.Size = new System.Drawing.Size(75, 23);
this.cancelbtn.TabIndex = 4;
@ -93,15 +93,15 @@
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;
//
// SettingsForm
//
@ -110,8 +110,8 @@
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Black;
this.CancelButton = this.cancelbtn;
this.ClientSize = new System.Drawing.Size(439, 148);
this.Controls.Add(this.keepPatched);
this.ClientSize = new System.Drawing.Size(439, 109);
this.Controls.Add(this.useProxy);
this.Controls.Add(this.cancelbtn);
this.Controls.Add(this.savebtn);
this.Controls.Add(this.browsebtn);
@ -130,9 +130,8 @@
this.Load += new System.EventHandler(this.Form1_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.CheckBox keepPatched;
}
#endregion
@ -141,6 +140,7 @@
private System.Windows.Forms.Button browsebtn;
private System.Windows.Forms.Button savebtn;
private System.Windows.Forms.Button cancelbtn;
private System.Windows.Forms.CheckBox useProxy;
}
}

View file

@ -1,12 +1,21 @@
using System;
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 SettingsForm : Form
{
private MainForm mainForm;
public SettingsForm()
{
InitializeComponent();
@ -14,9 +23,9 @@ namespace TBMM
private void Form1_Load(object sender, EventArgs e)
{
mainForm = (MainForm) Owner;
gamelocation.Text = mainForm.Configuration.GamePath;
keepPatched.Checked = mainForm.Configuration.KeepPatched;
gamelocation.Text = Settings.Default.GamePath;
useProxy.Checked = Settings.Default.UseProxy;
mainForm = Owner as MainForm;
}
private void browsebtn_Click(object sender, EventArgs e)
@ -26,9 +35,9 @@ namespace TBMM
private void savebtn_Click(object sender, EventArgs e)
{
mainForm.Configuration.GamePath = gamelocation.Text;
mainForm.Configuration.KeepPatched = keepPatched.Checked;
mainForm.Configuration.Save();
Settings.Default.GamePath = gamelocation.Text;
Settings.Default.UseProxy = useProxy.Checked;
Settings.Default.Save();
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,128 +0,0 @@
namespace TBMM
{
partial class CustomMessageBox
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(CustomMessageBox));
this.content = new System.Windows.Forms.Label();
this.yesbtn = new System.Windows.Forms.Button();
this.nobtn = new System.Windows.Forms.Button();
this.applyToAll = new System.Windows.Forms.CheckBox();
this.panel1 = new System.Windows.Forms.Panel();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
// content
//
this.content.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.content.Location = new System.Drawing.Point(12, 13);
this.content.Name = "content";
this.content.Size = new System.Drawing.Size(367, 55);
this.content.TabIndex = 0;
this.content.Text = "Text";
//
// yesbtn
//
this.yesbtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.yesbtn.Location = new System.Drawing.Point(216, 106);
this.yesbtn.Name = "yesbtn";
this.yesbtn.Size = new System.Drawing.Size(75, 23);
this.yesbtn.TabIndex = 1;
this.yesbtn.Text = "Yes";
this.yesbtn.UseVisualStyleBackColor = true;
//
// nobtn
//
this.nobtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.nobtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.nobtn.Location = new System.Drawing.Point(297, 106);
this.nobtn.Name = "nobtn";
this.nobtn.Size = new System.Drawing.Size(75, 23);
this.nobtn.TabIndex = 2;
this.nobtn.Text = "No";
this.nobtn.UseVisualStyleBackColor = true;
//
// applyToAll
//
this.applyToAll.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.applyToAll.AutoSize = true;
this.applyToAll.Location = new System.Drawing.Point(12, 83);
this.applyToAll.Name = "applyToAll";
this.applyToAll.Size = new System.Drawing.Size(77, 17);
this.applyToAll.TabIndex = 4;
this.applyToAll.Text = "Apply to all";
this.applyToAll.UseVisualStyleBackColor = true;
//
// panel1
//
this.panel1.BackColor = System.Drawing.SystemColors.ControlLightLight;
this.panel1.Controls.Add(this.content);
this.panel1.Location = new System.Drawing.Point(1, 0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(382, 80);
this.panel1.TabIndex = 5;
//
// CustomMessageBox
//
this.AcceptButton = this.yesbtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.SystemColors.ControlLight;
this.ClientSize = new System.Drawing.Size(384, 141);
this.ControlBox = false;
this.Controls.Add(this.panel1);
this.Controls.Add(this.applyToAll);
this.Controls.Add(this.nobtn);
this.Controls.Add(this.yesbtn);
this.ForeColor = System.Drawing.SystemColors.ControlText;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "CustomMessageBox";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.panel1.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label content;
private System.Windows.Forms.Button yesbtn;
private System.Windows.Forms.Button nobtn;
private System.Windows.Forms.CheckBox applyToAll;
private System.Windows.Forms.Panel panel1;
}
}

View file

@ -1,36 +0,0 @@
using System.Windows.Forms;
namespace TBMM
{
public partial class CustomMessageBox : Form
{
public bool ApplyToAll { get; private set; }
public CustomMessageBox()
{
InitializeComponent();
yesbtn.Click += (sender, e) =>
{
DialogResult = DialogResult.Yes;
ApplyToAll = applyToAll.Checked;
Close();
};
nobtn.Click += (sender, e) =>
{
DialogResult = DialogResult.No;
ApplyToAll = applyToAll.Checked;
Close();
};
/*cancelbtn.Click += (sender, e) =>
{
DialogResult = DialogResult.Cancel;
Close();
};*/
}
public CustomMessageBox(string message, string caption) : this()
{
content.Text = message;
Text = caption;
}
}
}

File diff suppressed because it is too large Load diff

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,261 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
{
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 ModInfo gcipa = new ModInfo { Author = "modtainers", Name = "GCIPA" };
private readonly ModInfo tbmm = new ModInfo { Author = "NorbiPeti", Name = "TBMM" };
private DateTime lastGameUpdateTime;
private const string defaultInfo = @"
Techblox 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.
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 by launching the game through the official launcher before reporting to Freejam.
";
private async void Form1_Load(object sender, EventArgs e)
{
if (mods.Values.All(mod => mod.LatestVersion == null)) //Not (fully) loaded yet
await LoadEverything(true);
}
public async Task LoadEverything(bool evenMods)
{
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();
}
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");
return;
}
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
var sf = new SettingsForm();
sf.ShowDialog(this);
}
private void modlist_SelectedIndexChanged(object sender, EventArgs e)
{
if (working) return;
modinfobox.Clear();
switch (modlist.SelectedItems.Count)
{
case 0:
modinfobox.Text = defaultInfo;
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
break;
case 1:
default:
installbtn.Text = "Install mod";
UpdateButton(installbtn, false);
UpdateButton(uninstallbtn, false);
bool install = false, update = false;
Action<string, Color> addText = (txt, color) =>
{
int start = modinfobox.Text.Length;
modinfobox.AppendText(txt + Environment.NewLine + Environment.NewLine);
modinfobox.Select(start, txt.Length);
modinfobox.SelectionColor = color;
modinfobox.DeselectAll();
modinfobox.SelectionColor = modinfobox.ForeColor;
};
foreach (ListViewItem item in modlist.SelectedItems)
{
var mod = mods[item.Name];
if (modlist.SelectedItems.Count == 1)
{
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.Description != null)
modinfobox.AppendText(mod.Description.Replace("\n", Environment.NewLine));
}
else
modinfobox.Text = modlist.SelectedItems.Count + " mods selected";
if (mod.DownloadURL != null && !(mod.LatestVersion <= mod.Version))
{
UpdateButton(installbtn, true);
if (mod.Version != null)
update = true;
else
install = true;
}
if (mod.Version != null)
UpdateButton(uninstallbtn, true);
}
if (install && update)
installbtn.Text = "Install and update mod";
else if (update)
installbtn.Text = "Update mod";
else
installbtn.Text = "Install mod";
break;
}
}
private async void installbtn_Click(object sender, EventArgs e)
{
if (installbtn.ForeColor == Color.Green) return; //Disabled
if (!BeginWork()) return;
foreach (ListViewItem item in modlist.SelectedItems)
{
var mod = mods[item.Name];
if (item.Group.Name == "installed" && (mod.DownloadURL == null || mod.LatestVersion <= mod.Version)) continue;
await InstallMod(mod);
}
EndWork(CheckIfPatched());
}
private void uninstallbtn_Click(object sender, EventArgs e)
{
if (uninstallbtn.ForeColor == Color.Green) return; //Disabled
foreach (ListViewItem item in modlist.SelectedItems)
{
if (item.Group.Name != "installed") continue;
UninstallMod(mods[item.Name]);
}
EndWork(CheckIfPatched()); //Update button states
}
private void findlog_Click(object sender, EventArgs e)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
if (CheckNoExe(out string exe))
return;
Process.Start("explorer.exe", $@"/select,{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}Low\Freejam\{exe.Replace(".exe", "")}\Player.log");
}
}
private void DeleteEmptyPluginsDir(out bool pexists, out bool dexists)
{
string plugins = GamePath("\\Plugins");
string disabled = GamePath("\\Plugins_Disabled");
pexists = Directory.Exists(plugins);
dexists = Directory.Exists(disabled);
if (pexists && !Directory.EnumerateFiles(plugins).Any())
{
Directory.Delete(plugins);
pexists = false;
}
if (dexists && !Directory.EnumerateFiles(disabled).Any())
{
Directory.Delete(disabled);
dexists = false;
}
}
private async void refreshbtn_Click(object sender, EventArgs e)
{
await RefreshEverything(true);
}
private async Task RefreshEverything(bool evenMods)
{
if (CheckIfPatched() == GameState.Patched) //Set from placeholder & unpatch if game was patched
HandleGameExit(null, EventArgs.Empty);
lastGameUpdateTime = GetGameVersionAsDate();
var mods = GetInstalledMods();
if (evenMods)
await GetAvailableMods();
CheckUninstalledMods(mods);
CheckIfPatched(); //Check after getting the available mods to show GCIPA updates
}
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,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,51 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TBMM
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
//Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (args.Length > 0)
{
DealWithCommandLineAsync(args);
Application.Run();
return;
}
Application.Run(new MainForm());
}
private static async void DealWithCommandLineAsync(string[] args)
{
var form = new MainForm();
await form.LoadEverything(false);
bool? shouldStay = null;
if (args[0] == "-start" && args.Length > 1)
shouldStay = await form.PatchStartGame(args[1]);
else
MessageBox.Show("Supported options:\n-start <command> - Starts the game using the given command");
if (shouldStay.HasValue)
{
form.FormClosed += (sender, e) => Application.Exit();
if (!shouldStay.Value)
{
await Task.Delay(2000);
form.Close();
}
}
else
Environment.Exit(0);
}
}
}

View file

@ -1,8 +0,0 @@
{
"profiles": {
"GCMM": {
"commandName": "Project",
"commandLineArgs": ""
}
}
}