Compare commits

..

37 commits

Author SHA1 Message Date
63d57ccd12 Fix game exit handling popup, color Play button when a mod is broken/outdated 2022-04-09 01:36:55 +02:00
009f905379 Fix README.md 2022-04-09 00:00:42 +02:00
946fbf254d Fix running game check 2022-03-20 21:01:06 +01:00
62963b6fd2 Add debug code and update version 2022-03-20 20:45:25 +01:00
4eeec67ac4 Add option to keep the game patched and don't ask on exit if not patched 2022-03-20 18:23:23 +01:00
ae60c43cfe Add warning on close, display whether online mode can be used 2022-02-21 01:15:18 +01:00
333e8d6040 Remove unpatched checkbox and handle the game's running status
- Disabling mod installs while the game is running
- Automatically detecting whether the game is running and unpatching if needed
2022-02-20 22:53:19 +01:00
ae89291c55 Add support for known outdated mods, unpatching on game exit 2022-02-14 00:10:30 +01:00
4ddab53b6d Update README 2021-07-12 02:21:15 +02:00
1cb3c200a9 Remove autopatching setting 2021-07-12 01:22:14 +02:00
b6ad8fd3ea Move mod file list to the game's folder (#1)
And improve getting TBMM version
2021-07-11 17:04:32 +02:00
3ef12efc9a Check if mod repo exists 2021-06-16 02:00:04 +02:00
c0bbd97953 Rename project 2021-06-16 01:27:11 +02:00
43db83717f Read game version from the game, begin renaming to TBMM everywhere 2021-06-16 01:25:48 +02:00
adfe43f1a3 Remove autopatching altogether and detect launcher path 2021-06-15 23:47:13 +02:00
8676e232da Use symlinks to create a modded copy 2021-05-16 19:45:34 +02:00
a7d26ebdb8 Fix auto-patching and game detection
I also realized that this auto-patching isn't gonna work well, so I should probably resort to file swapping or something
2021-05-16 02:11:58 +02:00
e36dcb8477 Separate file for localisation, remove validate button, exe detect fix 2021-05-16 01:43:59 +02:00
a27f3449be Switch to custom configuration and remove unneeded configs 2021-05-16 01:08:27 +02:00
0551535455 Steam --> launcher, autolaunch support in progress, localization 2021-05-16 00:29:11 +02:00
c43446e2cb Add USING_STEAM checks 2021-05-15 03:00:35 +02:00
199e77c68f Update to work with .NET 5 tools, GC->TB 2021-05-15 02:21:05 +02:00
3cf307a3b8 Just one more fix and dialog (no patcher, command line) 2021-01-21 02:14:18 +01:00
0a36e1a0c6 Don't load each mod data when auto-starting and other fixes 2021-01-21 00:59:16 +01:00
c11758a427 Auto-patch and other fixes, add checks and a setting
Also, possibly fixed detecting the game in Steam's default library
2021-01-20 22:41:21 +01:00
b2d9b1073f Use detected Steam locations, update launch options 2021-01-20 18:31:34 +01:00
5aa0f788c3 Read Steam location and user from registry 2021-01-20 02:38:33 +01:00
3520015649 Ask to change Steam config & file selecting 2021-01-20 02:21:22 +01:00
ac6d07df59 Add support for running from the command line
To be used from Steam
2021-01-20 00:49:11 +01:00
890a650093 Add text about outdated mods, refactor that bit 2021-01-02 00:12:52 +01:00
4eec201547 Drunk programming isn't necessarily a good idea 2021-01-01 04:45:51 +01:00
854e02dd0d Show outdated mods in orange 2020-12-31 18:05:38 +01:00
c3c9ee0a16 Sorting, apply to all, fixes
Sorting mods when (un)installing
Added Apply to all checkbox to a custom message box that replaces the one that shows up when the mod files already exist without the mod manager knowing about it
Removing IPA.zip after unzipping
Fix deleting files when their folder doesn't exist
2020-10-04 01:08:19 +02:00
cb4473de58 Check GCMM updates while we're at it 2020-10-03 15:41:31 +02:00
1f413eebda Update version and reword a message 2020-10-03 15:26:05 +02:00
faa88157bb Fix deletion, delete from list, improve patch detection 2020-10-03 13:32:53 +02:00
f522432208 Fix DLL conflicts, support updating GCIPA
If the mod consists of a dll then it failed to update it because it already existed
Added support for updating GCIPA, using both the existing mod info fetching and the patcher downloading code
2020-08-23 23:38:10 +02:00
32 changed files with 3322 additions and 841 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}") = "GCMM", "GCMM\GCMM.csproj", "{4022166A-FEA4-4B26-B83F-58B4D2004976}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TBMM", "TBMM\TBMM.csproj", "{4022166A-FEA4-4B26-B83F-58B4D2004976}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -1,257 +0,0 @@
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,139 +0,0 @@
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
}
}
}

View file

@ -1,164 +0,0 @@
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

@ -1,23 +0,0 @@
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());
}
}
}

View file

@ -1,74 +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 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

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

View file

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

23
TBMM/Configuration.cs Normal file
View file

@ -0,0 +1,23 @@
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));
}
}
}

128
TBMM/CustomMessageBox.Designer.cs generated Normal file
View file

@ -0,0 +1,128 @@
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;
}
}

36
TBMM/CustomMessageBox.cs Normal file
View file

@ -0,0 +1,36 @@
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;
}
}
}

19
TBMM/DialogUtils.cs Normal file
View file

@ -0,0 +1,19 @@
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;
}
}

130
TBMM/Localization.Designer.cs generated Normal file
View file

@ -0,0 +1,130 @@
//------------------------------------------------------------------------------
// <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);
}
}
}
}

45
TBMM/Localization.resx Normal file
View file

@ -0,0 +1,45 @@
<?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,4 +1,4 @@
namespace GCMM
namespace TBMM
{
partial class MainForm
{
@ -30,41 +30,29 @@
{
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 = ((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.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.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";
@ -73,17 +61,15 @@
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;
@ -196,22 +182,9 @@
this.findlog.UseVisualStyleBackColor = true;
this.findlog.Click += new System.EventHandler(this.findlog_Click);
//
// unpatched
//
this.unpatched.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.unpatched.AutoSize = true;
this.unpatched.Location = new System.Drawing.Point(12, 534);
this.unpatched.Name = "unpatched";
this.unpatched.Size = new System.Drawing.Size(218, 17);
this.unpatched.TabIndex = 8;
this.unpatched.Text = "Disable mods (check if game is crashing)";
this.unpatched.UseVisualStyleBackColor = true;
this.unpatched.CheckedChanged += new System.EventHandler(this.unpatched_CheckedChanged);
//
// modinfobox
//
this.modinfobox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Right)));
this.modinfobox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Right)));
this.modinfobox.BackColor = System.Drawing.Color.Black;
this.modinfobox.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.modinfobox.ForeColor = System.Drawing.Color.Lime;
@ -236,30 +209,14 @@
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);
@ -270,11 +227,13 @@
this.ForeColor = System.Drawing.Color.Lime;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "MainForm";
this.Text = "Gamecraft Mod Manager";
this.Text = "Techblox Mod Manager";
this.Activated += new System.EventHandler(this.MainForm_Activated);
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing);
this.Load += new System.EventHandler(this.Form1_Load);
this.Shown += new System.EventHandler(this.MainForm_Shown);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
@ -290,10 +249,8 @@
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;
}
}

261
TBMM/MainForm.cs Normal file
View file

@ -0,0 +1,261 @@
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,15 +1,13 @@
using GCMM.Properties;
using Newtonsoft.Json;
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 GCMM
namespace TBMM
{
partial class MainForm
{
@ -18,7 +16,7 @@ namespace GCMM
if (mod.DownloadURL == null) return;
if (CheckNoExe())
return;
if (mod.Name != "GamecraftModdingAPI")
if (mod.Name != "TechbloxModdingAPI")
await UpdateAPI();
var tmp = Directory.CreateDirectory("temp");
var plugins = Directory.CreateDirectory(GamePath(@"\Plugins"));
@ -29,7 +27,12 @@ namespace GCMM
string disposition = client.ResponseHeaders["Content-Disposition"];
string filename = disposition.Substring(disposition.IndexOf("filename=") + 10).Replace("\"", "");
if (filename.EndsWith(".dll"))
File.Move(tmppath, plugins.FullName + Path.DirectorySeparatorChar + mod.Name + ".dll"); //Force mod name to make uninstalls & identifying easier
{
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);
}
else if (filename.EndsWith(".zip"))
{
bool pluginOnly = true;
@ -52,7 +55,7 @@ namespace GCMM
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 : Settings.Default.GamePath, mod);
ExtractMod(archive, pluginOnly ? plugins.FullName : Configuration.GamePath, mod);
}
File.Delete(tmppath);
}
@ -63,6 +66,7 @@ namespace GCMM
}
GetInstalledMods(); //Update list
}
UpdatePlayButtonColor();
}
public void ExtractMod(ZipArchive archive, string destinationDirectoryName, ModInfo mod)
@ -70,6 +74,7 @@ namespace GCMM
LoadFileList(mod);
DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName);
string destinationDirectoryFullPath = di.FullName;
bool? skipExisting = null;
foreach (ZipArchiveEntry file in archive.Entries)
{
@ -91,7 +96,16 @@ namespace GCMM
&& !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 (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)
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)
continue;
}
@ -104,18 +118,27 @@ namespace GCMM
public void SaveFileList(ModInfo mod)
{
if (mod.ModFiles != null)
File.WriteAllText(mod.Name + ".json", JsonConvert.SerializeObject(mod.ModFiles));
{
Directory.CreateDirectory(GamePath("\\ModInfo"));
File.WriteAllText(GamePath($"\\ModInfo\\{mod.Name}.json"), JsonConvert.SerializeObject(mod.ModFiles));
}
}
public void LoadFileList(ModInfo mod)
{
if (File.Exists(mod.Name + ".json"))
mod.ModFiles = JsonConvert.DeserializeObject<HashSet<string>>(File.ReadAllText(mod.Name + ".json"));
else
mod.ModFiles = new HashSet<string>();
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>();
}
public void UninstallMod(ModInfo mod)
{
try
{
LoadFileList(mod);
if (mod.ModFiles.Count == 0) //A single DLL
@ -124,26 +147,44 @@ namespace GCMM
{
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.EnumerateFiles().Any())
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();
}
}
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();
}
public async Task UpdateAPI()
{
if (!mods.ContainsKey("GamecraftModdingAPI"))
if (!mods.ContainsKey("TechbloxModdingAPI"))
return;
var gcmapi = mods["GamecraftModdingAPI"];
var gcmapi = mods["TechbloxModdingAPI"];
if (!gcmapi.Installed || gcmapi.Updatable)
{
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)
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)
await InstallMod(gcmapi);
}
}

View file

@ -1,18 +1,17 @@
using GCMM.Properties;
using Newtonsoft.Json.Linq;
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 GCMM
namespace TBMM
{
partial class MainForm
{
@ -32,7 +31,7 @@ namespace GCMM
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, LastUpdated = File.GetLastWriteTime(modPath) };
var mod = new ModInfo { Name = Path.GetFileNameWithoutExtension(modPath), Version = an.Version };
AddUpdateModInList(mod);
installed.Add(mod.Name);
}
@ -40,12 +39,27 @@ namespace GCMM
{ //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 void GetAvailableMods()
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");
@ -53,23 +67,55 @@ namespace GCMM
{
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()
Name = sp[1].Trim(),
LastUpdated = updated,
Broken = broken
};
if (await FetchModInfo(mod, preview)) //If it's actually a mod
if (await FetchModInfo(mod, preview, true)) //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)
public async Task<bool> FetchModInfo(ModInfo mod, bool preview, bool desc)
{
string repoURL = "/api/v1/repos/" + mod.Author + "/" + mod.Name + "/releases";
using (var client = GetClient())
{
var arr = JArray.Parse(await client.DownloadStringTaskAsync(repoURL));
string str;
try
{
str = await client.DownloadStringTaskAsync(repoURL);
}
catch (WebException)
{
return false;
}
var arr = JArray.Parse(str);
var release = arr.FirstOrDefault(rel =>
{
if ((bool) rel["prerelease"] || (bool) rel["draft"])
@ -102,20 +148,25 @@ namespace GCMM
});
mod.DownloadURL = asset?["browser_download_url"]?.ToString();
mod.LastUpdated = (DateTime)release["published_at"];
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
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"].ToString();
mod.UpdateDetails = release["name"] + "\n\n" + release["body"];
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()));
mod.Description = Encoding.UTF8.GetString(Convert.FromBase64String(obj["content"]?.ToString() ?? string.Empty));
}
catch(WebException)
catch (WebException)
{ //It returns a HTTP 500 if it doesn't exist...
}
}
return true;
}
}
@ -137,33 +188,45 @@ namespace GCMM
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.LastUpdated.ToString();
items[3].Text = omod.LatestVersion != null ? 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.LastUpdated.ToString() }, modlist.Groups[mod.Installed ? "installed" : "available"]);
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.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);
}
delete.ForEach(name => { mods.Remove(name); modlist.Items[name].Remove(); });
UpdatePlayButtonColor();
}
}
}

216
TBMM/MainPatcher.cs Normal file
View file

@ -0,0 +1,216 @@
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
}
}
}

294
TBMM/MainUtils.cs Normal file
View file

@ -0,0 +1,294 @@
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

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GCMM
namespace TBMM
{
public class ModInfo
{
@ -22,11 +22,17 @@ namespace GCMM
/// 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;
}
}

51
TBMM/Program.cs Normal file
View file

@ -0,0 +1,51 @@
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

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

View file

@ -1,4 +1,4 @@
namespace GCMM
namespace TBMM
{
partial class SettingsForm
{
@ -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.useProxy = new System.Windows.Forms.CheckBox();
this.keepPatched = 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(271, 74);
this.savebtn.Location = new System.Drawing.Point(270, 113);
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, 74);
this.cancelbtn.Location = new System.Drawing.Point(352, 113);
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);
//
// useProxy
// keepPatched
//
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;
this.keepPatched.AutoSize = true;
this.keepPatched.Location = new System.Drawing.Point(12, 61);
this.keepPatched.Name = "keepPatched";
this.keepPatched.Size = new System.Drawing.Size(330, 17);
this.keepPatched.TabIndex = 5;
this.keepPatched.Text = "Keep game patched (prevents online gameplay until unchecked)";
this.keepPatched.UseVisualStyleBackColor = true;
//
// SettingsForm
//
@ -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, 109);
this.Controls.Add(this.useProxy);
this.ClientSize = new System.Drawing.Size(439, 148);
this.Controls.Add(this.keepPatched);
this.Controls.Add(this.cancelbtn);
this.Controls.Add(this.savebtn);
this.Controls.Add(this.browsebtn);
@ -130,9 +130,10 @@
this.Load += new System.EventHandler(this.Form1_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
private System.Windows.Forms.CheckBox keepPatched;
#endregion
private System.Windows.Forms.Label label1;
@ -140,7 +141,6 @@
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,21 +1,12 @@
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;
using System.Windows.Forms;
namespace GCMM
namespace TBMM
{
public partial class SettingsForm : Form
{
private MainForm mainForm;
public SettingsForm()
{
InitializeComponent();
@ -23,9 +14,9 @@ namespace GCMM
private void Form1_Load(object sender, EventArgs e)
{
gamelocation.Text = Settings.Default.GamePath;
useProxy.Checked = Settings.Default.UseProxy;
mainForm = Owner as MainForm;
mainForm = (MainForm) Owner;
gamelocation.Text = mainForm.Configuration.GamePath;
keepPatched.Checked = mainForm.Configuration.KeepPatched;
}
private void browsebtn_Click(object sender, EventArgs e)
@ -35,9 +26,9 @@ namespace GCMM
private void savebtn_Click(object sender, EventArgs e)
{
Settings.Default.GamePath = gamelocation.Text;
Settings.Default.UseProxy = useProxy.Checked;
Settings.Default.Save();
mainForm.Configuration.GamePath = gamelocation.Text;
mainForm.Configuration.KeepPatched = keepPatched.Checked;
mainForm.Configuration.Save();
Close();
}

1870
TBMM/SettingsForm.resx Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,16 +2,16 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net461</TargetFramework>
<TargetFramework>net472</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<SignAssembly>false</SignAssembly>
<ApplicationIcon>favicon.ico</ApplicationIcon>
<AssemblyVersion>1.2.0.0</AssemblyVersion>
<FileVersion>1.2.0.0</FileVersion>
<Version>1.2.0</Version>
<Version>1.6.0</Version>
<Authors>NorbiPeti</Authors>
<Company>ExMods</Company>
<Description>A mod manager for Gamecraft. It automatically downloads and runs GCIPA and allows the user to install mods.</Description>
<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>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -26,6 +26,8 @@
<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>
@ -37,18 +39,18 @@
<Compile Update="SettingsForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<Compile Update="Localization.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DependentUpon>Localization.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<EmbeddedResource Update="Localization.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View file

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

View file

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB