This commit is contained in:
+87
-18
@@ -51,6 +51,7 @@ namespace SotfModLoader
|
||||
private string _kind;
|
||||
private bool _busy;
|
||||
private ModStatus _redloaderStatus;
|
||||
private string _selectedRedLoader;
|
||||
private readonly Dictionary<string, ModStatus> _statuses = new Dictionary<string, ModStatus>();
|
||||
|
||||
public MainForm()
|
||||
@@ -124,8 +125,8 @@ namespace SotfModLoader
|
||||
Padding = new Padding(S(18), S(12), S(14), S(12))
|
||||
};
|
||||
_folderPanel.Paint += PaintFolderPanel;
|
||||
_folderName = new Label { AutoSize = true, Font = _rowTitleFont, ForeColor = Fog, BackColor = Bark };
|
||||
_folderHelp = new Label { AutoSize = true, Font = _metaFont, ForeColor = Lichen, BackColor = Bark };
|
||||
_folderName = new Label { AutoSize = false, Font = _rowTitleFont, ForeColor = Fog, BackColor = Bark };
|
||||
_folderHelp = new Label { AutoSize = false, Font = _metaFont, ForeColor = Lichen, BackColor = Bark };
|
||||
_folderButton = MakeButton("Choose folder", Style.Secondary, (s, e) => ChooseFolder());
|
||||
_folderPanel.Controls.Add(_folderName);
|
||||
_folderPanel.Controls.Add(_folderHelp);
|
||||
@@ -305,8 +306,8 @@ namespace SotfModLoader
|
||||
var width = _folderPanel.ClientSize.Width;
|
||||
_folderButton.Location = new Point(width - _folderButton.Width - S(14), S(14));
|
||||
var textWidth = Math.Max(S(160), _folderButton.Left - S(40));
|
||||
_folderName.MaximumSize = new Size(textWidth, 0);
|
||||
_folderHelp.MaximumSize = new Size(textWidth, 0);
|
||||
RowPanel.Fit(_folderName, textWidth);
|
||||
RowPanel.Fit(_folderHelp, textWidth);
|
||||
_folderName.Location = new Point(S(20), S(12));
|
||||
_folderHelp.Location = new Point(S(20), _folderName.Bottom + S(4));
|
||||
_folderPanel.Height = Math.Max(_folderHelp.Bottom, _folderButton.Bottom) + S(14);
|
||||
@@ -394,25 +395,90 @@ namespace SotfModLoader
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
private List<RedLoaderVersion> RedLoaderVersions()
|
||||
{
|
||||
var versions = _manifest.redloaderVersions;
|
||||
if (versions != null && versions.Count > 0)
|
||||
return versions;
|
||||
var rl = _manifest.redloader;
|
||||
return new List<RedLoaderVersion> { new RedLoaderVersion { version = rl.version, releaseUrl = rl.releaseUrl } };
|
||||
}
|
||||
|
||||
private int SelectedRedLoaderIndex(List<RedLoaderVersion> versions)
|
||||
{
|
||||
if (_selectedRedLoader == null)
|
||||
return 0;
|
||||
var index = versions.FindIndex(v => v.version == _selectedRedLoader);
|
||||
return index < 0 ? 0 : index;
|
||||
}
|
||||
|
||||
private Control RedLoaderRow()
|
||||
{
|
||||
var rl = _manifest.redloader;
|
||||
var versions = RedLoaderVersions();
|
||||
var selectedIndex = SelectedRedLoaderIndex(versions);
|
||||
var selected = versions[selectedIndex];
|
||||
var latest = versions[0];
|
||||
var status = _gameDir != null ? _redloaderStatus : null;
|
||||
var buttons = new List<Control>();
|
||||
|
||||
var controls = new List<Control>();
|
||||
if (status != null)
|
||||
{
|
||||
if (versions.Count > 1)
|
||||
{
|
||||
var combo = new ComboBox
|
||||
{
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
FlatStyle = FlatStyle.Flat,
|
||||
BackColor = Bark,
|
||||
ForeColor = Fog,
|
||||
Font = _buttonFont,
|
||||
Width = S(150),
|
||||
Margin = new Padding(S(6), S(2), 0, 0),
|
||||
Enabled = !_busy
|
||||
};
|
||||
for (var i = 0; i < versions.Count; i++)
|
||||
combo.Items.Add(i == 0 ? "Latest (" + versions[i].version + ")" : versions[i].version);
|
||||
combo.SelectedIndex = selectedIndex;
|
||||
combo.SelectedIndexChanged += (s, e) =>
|
||||
{
|
||||
_selectedRedLoader = combo.SelectedIndex <= 0 ? null : versions[combo.SelectedIndex].version;
|
||||
BeginInvoke((Action)Render);
|
||||
};
|
||||
controls.Add(combo);
|
||||
}
|
||||
|
||||
Button button = null;
|
||||
if (!status.Installed)
|
||||
button = MakeButton("Install RedLoader", Style.Primary, (s, e) => InstallRedLoader(selected));
|
||||
else if (status.Version == selected.version)
|
||||
button = MakeButton("Reinstall", Style.Secondary, (s, e) => InstallRedLoader(selected));
|
||||
else if (status.Version == null)
|
||||
button = MakeButton("Install " + selected.version, Style.Secondary, (s, e) => InstallRedLoader(selected));
|
||||
else if (selectedIndex == 0)
|
||||
button = MakeButton("Update", Style.Primary, (s, e) => InstallRedLoader(selected));
|
||||
else
|
||||
button = MakeButton("Install " + selected.version, Style.Primary, (s, e) => InstallRedLoader(selected));
|
||||
button.Enabled = !_busy;
|
||||
controls.Add(button);
|
||||
}
|
||||
|
||||
var parts = new List<string> { "Latest " + latest.version };
|
||||
if (status != null)
|
||||
{
|
||||
if (!status.Installed)
|
||||
buttons.Add(MakeButton("Install RedLoader", Style.Primary, (s, e) => InstallRedLoader()));
|
||||
else if (!status.Current)
|
||||
buttons.Add(MakeButton(status.Version != null ? "Update" : "Reinstall " + rl.version,
|
||||
status.Version != null ? Style.Primary : Style.Secondary, (s, e) => InstallRedLoader()));
|
||||
parts.Add("Not installed");
|
||||
else if (status.Version == null)
|
||||
parts.Add("Installed, version unknown");
|
||||
else if (status.Version == latest.version)
|
||||
parts.Add("Installed, up to date");
|
||||
else
|
||||
parts.Add("Installed " + status.Version);
|
||||
}
|
||||
foreach (var b in buttons)
|
||||
b.Enabled = !_busy;
|
||||
var metaColor = status != null && status.Installed && !status.Current ? Flare : Lichen;
|
||||
var metaColor = status != null && status.Installed && status.Version != latest.version ? Flare : Lichen;
|
||||
return new RowPanel("RedLoader", rl.repo,
|
||||
"Every mod here runs on RedLoader. Install it before adding mods.",
|
||||
VersionText(rl, status), metaColor, buttons,
|
||||
"Every mod here runs on RedLoader. Install it before adding mods. Pick an older version only if a mod needs one.",
|
||||
string.Join(" ", parts), metaColor, controls,
|
||||
_rowTitleFont, _bodyFont, _metaFont, Fog, Lichen, BarkLine, _scale);
|
||||
}
|
||||
|
||||
@@ -543,14 +609,17 @@ namespace SotfModLoader
|
||||
}
|
||||
}
|
||||
|
||||
private async void InstallRedLoader()
|
||||
private async void InstallRedLoader(RedLoaderVersion version)
|
||||
{
|
||||
var rl = _manifest.redloader;
|
||||
await Run(async progress =>
|
||||
{
|
||||
await ModInstaller.Install(_gameDir, rl, progress);
|
||||
ModInstaller.WriteRedLoaderVersion(_gameDir, rl.version);
|
||||
return "RedLoader " + rl.version + " installed. Launch the game once and wait for the main menu so it can finish setting up.";
|
||||
if (!string.IsNullOrEmpty(version.downloadUrl))
|
||||
await ModInstaller.InstallZip(_gameDir, version.downloadUrl, "RedLoader " + version.version, progress);
|
||||
else
|
||||
await ModInstaller.Install(_gameDir, rl, progress);
|
||||
ModInstaller.WriteRedLoaderVersion(_gameDir, version.version);
|
||||
return "RedLoader " + version.version + " installed. Launch the game once and wait for the main menu so it can finish setting up.";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -164,6 +165,56 @@ namespace SotfModLoader
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -26,6 +26,14 @@ namespace SotfModLoader
|
||||
public List<PackageFile> files { get; set; }
|
||||
}
|
||||
|
||||
public class RedLoaderVersion
|
||||
{
|
||||
public string version { get; set; }
|
||||
public string downloadUrl { get; set; }
|
||||
public string releaseUrl { get; set; }
|
||||
public long size { get; set; }
|
||||
}
|
||||
|
||||
public class AppInfo
|
||||
{
|
||||
public string version { get; set; }
|
||||
@@ -37,6 +45,7 @@ namespace SotfModLoader
|
||||
public string generated { get; set; }
|
||||
public string owner { get; set; }
|
||||
public Package redloader { get; set; }
|
||||
public List<RedLoaderVersion> redloaderVersions { get; set; }
|
||||
public List<Package> mods { get; set; }
|
||||
public AppInfo app { get; set; }
|
||||
}
|
||||
|
||||
+11
-4
@@ -89,8 +89,8 @@ namespace SotfModLoader
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
_title.LinkClicked += (s, e) => MainForm.OpenUrl(url);
|
||||
|
||||
_desc = new Label { Text = description ?? string.Empty, Font = bodyFont, AutoSize = true, ForeColor = textColor, BackColor = Color.Transparent };
|
||||
_meta = new Label { Text = meta ?? string.Empty, Font = metaFont, AutoSize = true, ForeColor = metaColor, BackColor = Color.Transparent };
|
||||
_desc = new Label { Text = description ?? string.Empty, Font = bodyFont, AutoSize = false, ForeColor = textColor, BackColor = Color.Transparent };
|
||||
_meta = new Label { Text = meta ?? string.Empty, Font = metaFont, AutoSize = false, ForeColor = metaColor, BackColor = Color.Transparent };
|
||||
|
||||
_actions = new FlowLayoutPanel
|
||||
{
|
||||
@@ -116,6 +116,13 @@ namespace SotfModLoader
|
||||
return (int)Math.Round(v * _scale);
|
||||
}
|
||||
|
||||
public static void Fit(Label label, int width)
|
||||
{
|
||||
var size = TextRenderer.MeasureText(label.Text.Length > 0 ? label.Text : " ", label.Font,
|
||||
new Size(width, int.MaxValue), TextFormatFlags.WordBreak | TextFormatFlags.NoPadding);
|
||||
label.Size = new Size(width, size.Height + 2);
|
||||
}
|
||||
|
||||
public void Arrange(int width)
|
||||
{
|
||||
Width = width;
|
||||
@@ -124,11 +131,11 @@ namespace SotfModLoader
|
||||
var textWidth = Math.Max(S(160), width - actionsWidth - S(24));
|
||||
|
||||
_title.Location = new Point(0, S(14));
|
||||
_desc.MaximumSize = new Size(textWidth, 0);
|
||||
Fit(_desc, textWidth);
|
||||
_desc.Location = new Point(0, _title.Bottom + S(2));
|
||||
_desc.Visible = _desc.Text.Length > 0;
|
||||
var y = _desc.Visible ? _desc.Bottom : _title.Bottom;
|
||||
_meta.MaximumSize = new Size(textWidth, 0);
|
||||
Fit(_meta, textWidth);
|
||||
_meta.Location = new Point(0, y + S(6));
|
||||
|
||||
var bottom = Math.Max(_meta.Bottom, _actions.Controls.Count > 0 ? _actions.Bottom : 0);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<AssemblyName>SotfModLoader</AssemblyName>
|
||||
<RootNamespace>SotfModLoader</RootNamespace>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>1.1.0</Version>
|
||||
<Product>Sons of the Forest Mod Loader</Product>
|
||||
<Company>JustinOros</Company>
|
||||
<Authors>JustinOros</Authors>
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
|
||||
@@ -218,6 +218,31 @@ def build_redloader():
|
||||
}
|
||||
|
||||
|
||||
def build_redloader_versions(limit=10):
|
||||
try:
|
||||
releases = api(f"/repos/{REDLOADER_REPO}/releases?per_page=30")
|
||||
except (urllib.error.URLError, OSError) as e:
|
||||
print(f"warning: could not list RedLoader releases: {e}")
|
||||
return []
|
||||
versions = []
|
||||
for release in releases:
|
||||
if release.get("draft") or release.get("prerelease"):
|
||||
continue
|
||||
asset = zip_asset(release)
|
||||
if not asset:
|
||||
continue
|
||||
versions.append({
|
||||
"version": release["tag_name"],
|
||||
"downloadUrl": asset["browser_download_url"],
|
||||
"releaseUrl": release["html_url"],
|
||||
"size": asset.get("size", 0),
|
||||
})
|
||||
if len(versions) >= limit:
|
||||
break
|
||||
print(f"RedLoader versions: {', '.join(v['version'] for v in versions)}")
|
||||
return versions
|
||||
|
||||
|
||||
def build_app():
|
||||
try:
|
||||
releases = api(f"/repos/{APP_REPO}/releases?per_page=30")
|
||||
@@ -285,6 +310,7 @@ def main():
|
||||
"app": build_app(),
|
||||
"bundle": build_bundle(redloader, mods),
|
||||
"redloader": redloader,
|
||||
"redloaderVersions": build_redloader_versions(),
|
||||
"mods": mods,
|
||||
}
|
||||
(OUT / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user