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 LoadManifest() { var text = await Http.GetStringAsync(SiteUrl + "manifest.json?t=" + DateTime.UtcNow.Ticks); return Json().Deserialize(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; 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 data = null; try { if (File.Exists(file)) data = Json().DeserializeObject(File.ReadAllText(file)) as Dictionary; } catch { } if (data == null) data = new Dictionary(); 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 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(); 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 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 { } }); } } }