Files
2026-09-28 15:05:19 -07:00

254 lines
9.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace SotfModLoader
{
public static class ModInstaller
{
public const string SiteUrl = "https://justinoros.github.io/sotf-modloader/";
public const string StateFile = "sotf-modloader.json";
private const int Concurrency = 6;
private static readonly HttpClient Http = CreateClient();
public static Version AppVersion
{
get { return Assembly.GetExecutingAssembly().GetName().Version; }
}
private static HttpClient CreateClient()
{
var client = new HttpClient();
client.Timeout = TimeSpan.FromMinutes(10);
client.DefaultRequestHeaders.UserAgent.ParseAdd("SotfModLoader/" + AppVersion.ToString(3));
return client;
}
private static JavaScriptSerializer Json()
{
return new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
}
public static async Task<Manifest> LoadManifest()
{
var text = await Http.GetStringAsync(SiteUrl + "manifest.json?t=" + DateTime.UtcNow.Ticks);
return Json().Deserialize<Manifest>(text);
}
public static bool IsGameRunning()
{
return Process.GetProcessesByName("SonsOfTheForest").Length > 0 ||
Process.GetProcessesByName("SonsOfTheForestDS").Length > 0;
}
public static string FullPath(string gameDir, string relative)
{
var root = Path.GetFullPath(gameDir).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
var full = Path.GetFullPath(Path.Combine(root, relative.Replace('/', Path.DirectorySeparatorChar)));
if (!full.StartsWith(root, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("The mod list contains an unsafe path: " + relative);
return full;
}
public static ModStatus Status(string gameDir, Package mod)
{
if (string.IsNullOrEmpty(mod.dll) || !File.Exists(FullPath(gameDir, mod.dll)))
return new ModStatus();
string version = null;
if (!string.IsNullOrEmpty(mod.manifestPath))
version = ReadValue(FullPath(gameDir, mod.manifestPath), "version");
return new ModStatus { Installed = true, Version = version, Current = version == mod.version };
}
public static ModStatus RedLoaderStatus(string gameDir, Package redloader)
{
var installed = File.Exists(Path.Combine(gameDir, "version.dll")) &&
Directory.Exists(Path.Combine(gameDir, "_Redloader"));
if (!installed)
return new ModStatus();
var version = ReadValue(Path.Combine(gameDir, StateFile), "redloader");
return new ModStatus { Installed = true, Version = version, Current = version == redloader.version };
}
private static string ReadValue(string file, string key)
{
try
{
if (!File.Exists(file))
return null;
var data = Json().DeserializeObject(File.ReadAllText(file)) as Dictionary<string, object>;
object value;
if (data != null && data.TryGetValue(key, out value) && value != null)
return value.ToString();
}
catch
{
}
return null;
}
public static void WriteRedLoaderVersion(string gameDir, string version)
{
var file = Path.Combine(gameDir, StateFile);
Dictionary<string, object> data = null;
try
{
if (File.Exists(file))
data = Json().DeserializeObject(File.ReadAllText(file)) as Dictionary<string, object>;
}
catch
{
}
if (data == null)
data = new Dictionary<string, object>();
data["redloader"] = version;
File.WriteAllText(file, Json().Serialize(data));
}
private static string FileUrl(Package pkg, string path)
{
return SiteUrl + pkg.@base + string.Join("/", path.Split('/').Select(Uri.EscapeDataString));
}
public static async Task Install(string gameDir, Package pkg, IProgress<string> progress)
{
var label = pkg.name + " " + pkg.version;
var total = pkg.files.Count;
var data = new byte[total][];
var done = 0;
using (var gate = new SemaphoreSlim(Concurrency))
{
var tasks = new List<Task>();
for (var i = 0; i < total; i++)
{
var index = i;
tasks.Add(Task.Run(async () =>
{
await gate.WaitAsync().ConfigureAwait(false);
try
{
data[index] = await Http.GetByteArrayAsync(FileUrl(pkg, pkg.files[index].path)).ConfigureAwait(false);
var count = Interlocked.Increment(ref done);
progress.Report(label + ": downloading " + count + " of " + total + " files");
}
finally
{
gate.Release();
}
}));
}
await Task.WhenAll(tasks);
}
if (pkg.roots != null)
{
foreach (var root in pkg.roots)
Remove(gameDir, root);
}
for (var i = 0; i < total; i++)
{
progress.Report(label + ": writing " + (i + 1) + " of " + total + " files");
var target = FullPath(gameDir, pkg.files[i].path);
Directory.CreateDirectory(Path.GetDirectoryName(target));
File.WriteAllBytes(target, data[i]);
}
}
public static async Task InstallZip(string gameDir, string url, string label, IProgress<string> progress)
{
byte[] data;
using (var response = await Http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
var total = response.Content.Headers.ContentLength ?? 0;
using (var stream = await response.Content.ReadAsStreamAsync())
using (var memory = new MemoryStream())
{
var buffer = new byte[81920];
long read = 0;
var lastPercent = -1;
int count;
while ((count = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
memory.Write(buffer, 0, count);
read += count;
if (total > 0)
{
var percent = (int)(read * 100 / total);
if (percent != lastPercent)
{
lastPercent = percent;
progress.Report(label + ": downloading " + percent + "%");
}
}
}
data = memory.ToArray();
}
}
using (var zip = new ZipArchive(new MemoryStream(data), ZipArchiveMode.Read))
{
var entries = zip.Entries
.Where(e => !string.IsNullOrEmpty(e.Name) && !e.FullName.EndsWith("/") && !e.FullName.EndsWith("\\"))
.ToList();
for (var i = 0; i < entries.Count; i++)
{
var entry = entries[i];
progress.Report(label + ": writing " + (i + 1) + " of " + entries.Count + " files");
var target = FullPath(gameDir, entry.FullName.Replace('\\', '/'));
Directory.CreateDirectory(Path.GetDirectoryName(target));
using (var input = entry.Open())
using (var output = File.Create(target))
input.CopyTo(output);
}
}
}
public static void Uninstall(string gameDir, Package pkg)
{
if (pkg.roots == null)
return;
foreach (var root in pkg.roots)
Remove(gameDir, root);
}
private static void Remove(string gameDir, string relative)
{
var full = FullPath(gameDir, relative);
if (Directory.Exists(full))
Directory.Delete(full, true);
else if (File.Exists(full))
File.Delete(full);
}
public static void CountDownload(Package pkg)
{
if (string.IsNullOrEmpty(pkg.downloadUrl))
return;
Task.Run(async () =>
{
try
{
using (await Http.GetAsync(pkg.downloadUrl, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
}
}
catch
{
}
});
}
}
}