Compare commits
10 Commits
ed70ff52e9
...
987fedd7e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 987fedd7e4 | |||
| 8d0c05bd8a | |||
| 95966d2d14 | |||
| 839d93f242 | |||
| db4534e427 | |||
| 4ee4a2bb43 | |||
| 4594cb9c00 | |||
| f280e107f3 | |||
| e0b9278da4 | |||
| f311429bc2 |
@@ -1,6 +1,7 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using ModpackLauncher.Services;
|
||||||
|
|
||||||
namespace ModpackLauncher;
|
namespace ModpackLauncher;
|
||||||
|
|
||||||
@@ -13,6 +14,11 @@ public partial class App : Application
|
|||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
|
// Clean up any .old launcher left behind by a self-update on a prior run.
|
||||||
|
// Best-effort: if the previous process hasn't fully released the handle yet
|
||||||
|
// we just retry on the next startup.
|
||||||
|
SelfUpdateService.CleanupAfterUpdate();
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow();
|
desktop.MainWindow = new MainWindow();
|
||||||
|
|||||||
@@ -259,7 +259,7 @@
|
|||||||
Foreground="#E8DFC8" FontSize="13" TextWrapping="Wrap"
|
Foreground="#E8DFC8" FontSize="13" TextWrapping="Wrap"
|
||||||
Text="A newer launcher is available." />
|
Text="A newer launcher is available." />
|
||||||
<Button Grid.Column="2" Name="UpdateBannerDownloadButton"
|
<Button Grid.Column="2" Name="UpdateBannerDownloadButton"
|
||||||
Classes="secondary" Content="Download"
|
Classes="secondary" Content="Install update"
|
||||||
Click="OnUpdateBannerDownloadClick" />
|
Click="OnUpdateBannerDownloadClick" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -128,22 +128,57 @@ public partial class MainWindow : Window
|
|||||||
if (current >= advertised) return;
|
if (current >= advertised) return;
|
||||||
|
|
||||||
UpdateBannerText.Text = $"A newer launcher (v{advertised}) is available -- you're on v{current.Major}.{current.Minor}.{current.Build}.";
|
UpdateBannerText.Text = $"A newer launcher (v{advertised}) is available -- you're on v{current.Major}.{current.Minor}.{current.Build}.";
|
||||||
UpdateBannerDownloadButton.Tag = manifest.LauncherUrl ?? "https://sijbers.uk/pack/BrassAndSigil-Launcher.exe";
|
UpdateBannerDownloadButton.Tag = manifest.LauncherUrl ?? "https://bns.sijbers.uk/launcher/BrassAndSigil-Launcher.exe";
|
||||||
UpdateBanner.IsVisible = true;
|
UpdateBanner.IsVisible = true;
|
||||||
AppendLog($"[update] Newer launcher available: v{advertised} (running v{current})");
|
AppendLog($"[update] Newer launcher available: v{advertised} (running v{current})");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUpdateBannerDownloadClick(object? sender, RoutedEventArgs e)
|
private async void OnUpdateBannerDownloadClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var url = UpdateBannerDownloadButton.Tag as string
|
var url = UpdateBannerDownloadButton.Tag as string;
|
||||||
?? "https://sijbers.uk/pack/BrassAndSigil-Launcher.exe";
|
if (string.IsNullOrEmpty(url))
|
||||||
|
{
|
||||||
|
AppendLog("[update] No download URL configured in manifest.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First click: try in-place self-update. On failure (network blip, write-protected
|
||||||
|
// dir, antivirus blocking), fall back to opening the browser so the user can still
|
||||||
|
// grab the new exe manually.
|
||||||
|
UpdateBannerDownloadButton.IsEnabled = false;
|
||||||
|
var originalLabel = UpdateBannerDownloadButton.Content as string ?? "Install update";
|
||||||
|
UpdateBannerDownloadButton.Content = "Updating...";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var svc = new ModpackLauncher.Services.SelfUpdateService();
|
||||||
|
var progress = new Progress<(long bytes, long? total)>(p =>
|
||||||
|
{
|
||||||
|
var mb = p.bytes / 1024.0 / 1024.0;
|
||||||
|
UpdateBannerDownloadButton.Content = p.total.HasValue
|
||||||
|
? $"{(double)p.bytes * 100 / p.total.Value:F0}%"
|
||||||
|
: $"{mb:F1} MB";
|
||||||
|
});
|
||||||
|
await svc.DownloadAndInstallAsync(url, progress, AppendLog);
|
||||||
|
// If the method returns, it means it didn't exit -- that shouldn't normally
|
||||||
|
// happen, but reset the UI just in case.
|
||||||
|
UpdateBannerDownloadButton.Content = originalLabel;
|
||||||
|
UpdateBannerDownloadButton.IsEnabled = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"[update] Auto-update failed: {ex.Message}");
|
||||||
|
AppendLog("[update] Opening browser as fallback...");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
|
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex2)
|
||||||
{
|
{
|
||||||
AppendLog($"[update] Couldn't open browser: {ex.Message}");
|
AppendLog($"[update] Browser fallback also failed: {ex2.Message}");
|
||||||
|
}
|
||||||
|
UpdateBannerDownloadButton.Content = originalLabel;
|
||||||
|
UpdateBannerDownloadButton.IsEnabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ public sealed class ManifestFile
|
|||||||
|
|
||||||
[JsonPropertyName("size")]
|
[JsonPropertyName("size")]
|
||||||
public long? Size { get; set; }
|
public long? Size { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "both" (default), "client", or "server". The launcher skips files
|
||||||
|
/// marked "server"; the server-tool skips files marked "client".
|
||||||
|
/// Null/missing/unknown = treated as "both" so manifests pre-dating
|
||||||
|
/// this field stay fully compatible.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("side")]
|
||||||
|
public string? Side { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PackVersionRecord
|
public sealed class PackVersionRecord
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<RootNamespace>ModpackLauncher</RootNamespace>
|
<RootNamespace>ModpackLauncher</RootNamespace>
|
||||||
<AssemblyName>ModpackLauncher</AssemblyName>
|
<AssemblyName>ModpackLauncher</AssemblyName>
|
||||||
<Version>0.4.6</Version>
|
<Version>0.4.8</Version>
|
||||||
<ApplicationIcon Condition="Exists('Assets\icon.ico')">Assets\icon.ico</ApplicationIcon>
|
<ApplicationIcon Condition="Exists('Assets\icon.ico')">Assets\icon.ico</ApplicationIcon>
|
||||||
|
|
||||||
<!-- Single-file self-contained publish defaults (Windows-only now due to WebView2) -->
|
<!-- Single-file self-contained publish defaults (Windows-only now due to WebView2) -->
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ public sealed class ManifestSyncService
|
|||||||
var missing = new System.Collections.Generic.List<ManifestFile>();
|
var missing = new System.Collections.Generic.List<ManifestFile>();
|
||||||
foreach (var file in manifest.Files)
|
foreach (var file in manifest.Files)
|
||||||
{
|
{
|
||||||
|
if (IsServerOnly(file)) continue; // never expected on disk client-side
|
||||||
var dest = Path.Combine(installDir, file.Path);
|
var dest = Path.Combine(installDir, file.Path);
|
||||||
if (!File.Exists(dest)) missing.Add(file);
|
if (!File.Exists(dest)) missing.Add(file);
|
||||||
}
|
}
|
||||||
@@ -113,6 +114,19 @@ public sealed class ManifestSyncService
|
|||||||
$"Pack: {manifest.Name ?? "modpack"} v{manifest.Version ?? "?"} (local: {local?.Version ?? "none"})"
|
$"Pack: {manifest.Name ?? "modpack"} v{manifest.Version ?? "?"} (local: {local?.Version ?? "none"})"
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Drop server-only files before computing wanted/download sets. Doing
|
||||||
|
// it once here means the rest of the sync (prune + download) doesn't
|
||||||
|
// need to know about sides. Unknown/missing/'both' = include on client.
|
||||||
|
var serverOnlyCount = manifest.Files.Count(f => IsServerOnly(f));
|
||||||
|
if (serverOnlyCount > 0)
|
||||||
|
{
|
||||||
|
manifest.Files = manifest.Files.Where(f => !IsServerOnly(f)).ToList();
|
||||||
|
progress.Report(new ProgressReport(
|
||||||
|
ProgressKind.Log,
|
||||||
|
$"Skipping {serverOnlyCount} server-only file(s) declared in manifest."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
var wantedPaths = new HashSet<string>(
|
var wantedPaths = new HashSet<string>(
|
||||||
manifest.Files.Select(f => NormalizePath(f.Path)),
|
manifest.Files.Select(f => NormalizePath(f.Path)),
|
||||||
StringComparer.OrdinalIgnoreCase
|
StringComparer.OrdinalIgnoreCase
|
||||||
@@ -249,4 +263,12 @@ public sealed class ManifestSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizePath(string p) => p.Replace('\\', '/').TrimStart('/');
|
private static string NormalizePath(string p) => p.Replace('\\', '/').TrimStart('/');
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if this manifest file is tagged as server-only and therefore
|
||||||
|
/// should not be installed on the client. Unknown/null/'both' = false
|
||||||
|
/// (always include) so manifests pre-dating the field still install.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsServerOnly(ManifestFile f)
|
||||||
|
=> string.Equals(f.Side, "server", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ModpackLauncher.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-place launcher self-update. Pattern matches Chrome/VS Code/Discord:
|
||||||
|
/// 1. Download new exe to "<current>.new"
|
||||||
|
/// 2. Rename running exe to "<current>.old" (Windows allows MoveFile on a running exe)
|
||||||
|
/// 3. Rename ".new" to the canonical name
|
||||||
|
/// 4. Process.Start the new exe with our args
|
||||||
|
/// 5. Exit -- the OS releases the .old file once the old process is gone
|
||||||
|
/// 6. Next launcher start deletes any leftover .old via CleanupAfterUpdate()
|
||||||
|
///
|
||||||
|
/// Fails loudly (throws) on any step. Caller falls back to browser-open download.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SelfUpdateService
|
||||||
|
{
|
||||||
|
private static readonly HttpClient _http = new()
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromMinutes(5),
|
||||||
|
DefaultRequestVersion = new Version(2, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string CurrentExePath =>
|
||||||
|
Environment.ProcessPath
|
||||||
|
?? throw new InvalidOperationException("Environment.ProcessPath was null (self-update needs a real exe path).");
|
||||||
|
|
||||||
|
public static string OldExePath => CurrentExePath + ".old";
|
||||||
|
public static string NewExePath => CurrentExePath + ".new";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete any leftover .old file from a prior self-update. Safe to call on every startup;
|
||||||
|
/// quietly retries next start if the file is still locked (rare timing edge case).
|
||||||
|
/// </summary>
|
||||||
|
public static void CleanupAfterUpdate(Action<string>? log = null)
|
||||||
|
{
|
||||||
|
var old = OldExePath;
|
||||||
|
if (!File.Exists(old)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(old);
|
||||||
|
log?.Invoke($"[update] Removed previous launcher backup: {Path.GetFileName(old)}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// The old process may still be releasing the file handle. Not fatal --
|
||||||
|
// we'll retry next start. Worst case the .old file sits there harmlessly.
|
||||||
|
log?.Invoke($"[update] Couldn't remove {Path.GetFileName(old)} yet ({ex.GetType().Name}); will retry next start.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download the new launcher, swap it in, restart with the same command line, and exit.
|
||||||
|
/// Returns only on failure (i.e. throws) -- on success the current process exits.
|
||||||
|
/// </summary>
|
||||||
|
public async Task DownloadAndInstallAsync(
|
||||||
|
string downloadUrl,
|
||||||
|
IProgress<(long bytesReceived, long? totalBytes)>? progress = null,
|
||||||
|
Action<string>? log = null,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var curPath = CurrentExePath;
|
||||||
|
var newPath = NewExePath;
|
||||||
|
var oldPath = OldExePath;
|
||||||
|
|
||||||
|
log?.Invoke($"[update] Downloading {downloadUrl}");
|
||||||
|
|
||||||
|
// 1. Download to .new, replacing any stale partial from a failed prior attempt
|
||||||
|
if (File.Exists(newPath)) File.Delete(newPath);
|
||||||
|
|
||||||
|
using (var resp = await _http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, ct))
|
||||||
|
{
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
var total = resp.Content.Headers.ContentLength;
|
||||||
|
await using var src = await resp.Content.ReadAsStreamAsync(ct);
|
||||||
|
await using var dst = new FileStream(newPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||||
|
|
||||||
|
var buffer = new byte[81920];
|
||||||
|
long received = 0;
|
||||||
|
int read;
|
||||||
|
while ((read = await src.ReadAsync(buffer, ct)) > 0)
|
||||||
|
{
|
||||||
|
await dst.WriteAsync(buffer.AsMemory(0, read), ct);
|
||||||
|
received += read;
|
||||||
|
progress?.Report((received, total));
|
||||||
|
}
|
||||||
|
log?.Invoke($"[update] Downloaded {received:N0} bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity: refuse to swap in something laughably small (a captive portal
|
||||||
|
// intercepted us, or the server returned an error body as 200 OK).
|
||||||
|
var fi = new FileInfo(newPath);
|
||||||
|
if (fi.Length < 1_000_000)
|
||||||
|
{
|
||||||
|
File.Delete(newPath);
|
||||||
|
throw new InvalidDataException($"Downloaded launcher is only {fi.Length:N0} bytes -- almost certainly not the real binary.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Move running exe to .old (this works while we're executing — NTFS resolves
|
||||||
|
// running processes by handle, not by path).
|
||||||
|
if (File.Exists(oldPath)) File.Delete(oldPath);
|
||||||
|
File.Move(curPath, oldPath);
|
||||||
|
log?.Invoke($"[update] Backed up current launcher to {Path.GetFileName(oldPath)}");
|
||||||
|
|
||||||
|
// 3. Move new exe into the canonical slot
|
||||||
|
File.Move(newPath, curPath);
|
||||||
|
log?.Invoke($"[update] New launcher staged at {Path.GetFileName(curPath)}");
|
||||||
|
|
||||||
|
// 4. Build the restart command. Pass through any args the current process was
|
||||||
|
// launched with (e.g. URI handler args from a future deep-link integration),
|
||||||
|
// skipping argv[0] which is the exe itself.
|
||||||
|
var args = Environment.GetCommandLineArgs();
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = curPath,
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(curPath) ?? Environment.CurrentDirectory,
|
||||||
|
};
|
||||||
|
for (int i = 1; i < args.Length; i++) psi.ArgumentList.Add(args[i]);
|
||||||
|
|
||||||
|
log?.Invoke("[update] Restarting with the new launcher...");
|
||||||
|
Process.Start(psi);
|
||||||
|
|
||||||
|
// Tiny grace period so the spawn has actually entered the kernel before we
|
||||||
|
// pull our pid out from under it. 100 ms is more than enough.
|
||||||
|
await Task.Delay(100, CancellationToken.None);
|
||||||
|
|
||||||
|
// Hard exit. We don't go through Avalonia shutdown -- Application.Lifetime.Exit()
|
||||||
|
// can hang on window close handlers, and we DO want to release our exe handle
|
||||||
|
// promptly so the new process's CleanupAfterUpdate() can delete our .old file.
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
-35
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing",
|
"$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing",
|
||||||
"name": "Brass and Sigil",
|
"name": "Brass and Sigil",
|
||||||
"version": "0.9.3",
|
"version": "0.12.0",
|
||||||
"minecraft": "1.21.1",
|
"minecraft": "1.21.1",
|
||||||
"loader": {
|
"loader": {
|
||||||
"type": "neoforge",
|
"type": "neoforge",
|
||||||
"version": "21.1.228"
|
"version": "21.1.228"
|
||||||
},
|
},
|
||||||
"lockedAt": "2026-05-04T14:22:58.7131203+01:00",
|
"lockedAt": "2026-05-04T13:22:58.7131203+00:00",
|
||||||
"mods": [
|
"mods": [
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
"path": "mods/create-1.21.1-6.0.10.jar",
|
"path": "mods/create-1.21.1-6.0.10.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/LNytGWDc/versions/UjX6dr61/create-1.21.1-6.0.10.jar",
|
"url": "https://cdn.modrinth.com/data/LNytGWDc/versions/UjX6dr61/create-1.21.1-6.0.10.jar",
|
||||||
"sha1": "0e97e49837bed766e6f28a4c95b04885d6acc353",
|
"sha1": "0e97e49837bed766e6f28a4c95b04885d6acc353",
|
||||||
"size": 19123767
|
"size": 19123767,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -27,7 +28,8 @@
|
|||||||
"path": "mods/create-aeronautics-bundled-1.21.1-1.2.1.jar",
|
"path": "mods/create-aeronautics-bundled-1.21.1-1.2.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/oWaK0Q19/versions/YhZLrAFC/create-aeronautics-bundled-1.21.1-1.2.1.jar",
|
"url": "https://cdn.modrinth.com/data/oWaK0Q19/versions/YhZLrAFC/create-aeronautics-bundled-1.21.1-1.2.1.jar",
|
||||||
"sha1": "fdf1ae69e8b6437e0196b3a35dd2325aa904aba9",
|
"sha1": "fdf1ae69e8b6437e0196b3a35dd2325aa904aba9",
|
||||||
"size": 33030286
|
"size": 33030286,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -37,7 +39,8 @@
|
|||||||
"path": "mods/sable-neoforge-1.21.1-1.2.2.jar",
|
"path": "mods/sable-neoforge-1.21.1-1.2.2.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/T9PomCSv/versions/3FMsUjO4/sable-neoforge-1.21.1-1.2.2.jar",
|
"url": "https://cdn.modrinth.com/data/T9PomCSv/versions/3FMsUjO4/sable-neoforge-1.21.1-1.2.2.jar",
|
||||||
"sha1": "c5ecd3fcf60a31d84112c708abe29e341b2d1b73",
|
"sha1": "c5ecd3fcf60a31d84112c708abe29e341b2d1b73",
|
||||||
"size": 12719293
|
"size": 12719293,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -47,7 +50,8 @@
|
|||||||
"path": "mods/createbigcannons-5.11.3+mc.1.21.1.jar",
|
"path": "mods/createbigcannons-5.11.3+mc.1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/GWp4jCJj/versions/bsGaXKEd/createbigcannons-5.11.3%2Bmc.1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/GWp4jCJj/versions/bsGaXKEd/createbigcannons-5.11.3%2Bmc.1.21.1.jar",
|
||||||
"sha1": "8b61fa850e260bdeb5d360576123f98c260afa50",
|
"sha1": "8b61fa850e260bdeb5d360576123f98c260afa50",
|
||||||
"size": 3715787
|
"size": 3715787,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -57,7 +61,8 @@
|
|||||||
"path": "mods/tfmg-1.2.0.jar",
|
"path": "mods/tfmg-1.2.0.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/USgVjXsk/versions/uDi14nbt/tfmg-1.2.0.jar",
|
"url": "https://cdn.modrinth.com/data/USgVjXsk/versions/uDi14nbt/tfmg-1.2.0.jar",
|
||||||
"sha1": "b520f3687f60a69eb265ff5b9a16759b9e124103",
|
"sha1": "b520f3687f60a69eb265ff5b9a16759b9e124103",
|
||||||
"size": 4924243
|
"size": 4924243,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -67,7 +72,8 @@
|
|||||||
"path": "mods/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar",
|
"path": "mods/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/uCdwusMi/versions/KkaaQtTD/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar",
|
"url": "https://cdn.modrinth.com/data/uCdwusMi/versions/KkaaQtTD/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar",
|
||||||
"sha1": "1ff0a8920e52add541471f7b32d0d389997145ba",
|
"sha1": "1ff0a8920e52add541471f7b32d0d389997145ba",
|
||||||
"size": 30019727
|
"size": 30019727,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -77,7 +83,8 @@
|
|||||||
"path": "mods/sodium-neoforge-0.6.13+mc1.21.1.jar",
|
"path": "mods/sodium-neoforge-0.6.13+mc1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/AANobbMI/versions/Pb3OXVqC/sodium-neoforge-0.6.13%2Bmc1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/AANobbMI/versions/Pb3OXVqC/sodium-neoforge-0.6.13%2Bmc1.21.1.jar",
|
||||||
"sha1": "38af70fa4dc4b2aaac636e92fdba3bedd5a025e1",
|
"sha1": "38af70fa4dc4b2aaac636e92fdba3bedd5a025e1",
|
||||||
"size": 1162994
|
"size": 1162994,
|
||||||
|
"side": "client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -87,7 +94,8 @@
|
|||||||
"path": "mods/iris-neoforge-1.8.12+mc1.21.1.jar",
|
"path": "mods/iris-neoforge-1.8.12+mc1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/YL57xq9U/versions/t3ruzodq/iris-neoforge-1.8.12%2Bmc1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/YL57xq9U/versions/t3ruzodq/iris-neoforge-1.8.12%2Bmc1.21.1.jar",
|
||||||
"sha1": "a3e6355915c7d3b2bc392724795113e51d289378",
|
"sha1": "a3e6355915c7d3b2bc392724795113e51d289378",
|
||||||
"size": 2438548
|
"size": 2438548,
|
||||||
|
"side": "client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -97,7 +105,8 @@
|
|||||||
"path": "mods/modernfix-neoforge-5.27.4+mc1.21.1.jar",
|
"path": "mods/modernfix-neoforge-5.27.4+mc1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/nmDcB62a/versions/6U8JVjdw/modernfix-neoforge-5.27.4%2Bmc1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/nmDcB62a/versions/6U8JVjdw/modernfix-neoforge-5.27.4%2Bmc1.21.1.jar",
|
||||||
"sha1": "2f39363f0d6d5a5ccc2a9e0f50ad3385611c3cb7",
|
"sha1": "2f39363f0d6d5a5ccc2a9e0f50ad3385611c3cb7",
|
||||||
"size": 562051
|
"size": 562051,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -107,7 +116,8 @@
|
|||||||
"path": "mods/ferritecore-7.0.3-neoforge.jar",
|
"path": "mods/ferritecore-7.0.3-neoforge.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/uXXizFIs/versions/x7kQWVju/ferritecore-7.0.3-neoforge.jar",
|
"url": "https://cdn.modrinth.com/data/uXXizFIs/versions/x7kQWVju/ferritecore-7.0.3-neoforge.jar",
|
||||||
"sha1": "9563692efb708b6b568df27a01ec52f6311928ef",
|
"sha1": "9563692efb708b6b568df27a01ec52f6311928ef",
|
||||||
"size": 121559
|
"size": 121559,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -117,7 +127,8 @@
|
|||||||
"path": "mods/architectury-13.0.8-neoforge.jar",
|
"path": "mods/architectury-13.0.8-neoforge.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/lhGA9TYQ/versions/ZxYGwlk0/architectury-13.0.8-neoforge.jar",
|
"url": "https://cdn.modrinth.com/data/lhGA9TYQ/versions/ZxYGwlk0/architectury-13.0.8-neoforge.jar",
|
||||||
"sha1": "6ca11d3cc136bf69bb8f4d56982481eb85b5100b",
|
"sha1": "6ca11d3cc136bf69bb8f4d56982481eb85b5100b",
|
||||||
"size": 584004
|
"size": 584004,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -127,7 +138,8 @@
|
|||||||
"path": "mods/rhino-2101.2.7-build.81.jar",
|
"path": "mods/rhino-2101.2.7-build.81.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/sk9knFPE/versions/ZdLtebKH/rhino-2101.2.7-build.81.jar",
|
"url": "https://cdn.modrinth.com/data/sk9knFPE/versions/ZdLtebKH/rhino-2101.2.7-build.81.jar",
|
||||||
"sha1": "480235a9f7749f68ce6fec3b9c3cac3428b92a4a",
|
"sha1": "480235a9f7749f68ce6fec3b9c3cac3428b92a4a",
|
||||||
"size": 882033
|
"size": 882033,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -137,7 +149,8 @@
|
|||||||
"path": "mods/ritchiesprojectilelib-2.1.2+mc.1.21.1-neoforge.jar",
|
"path": "mods/ritchiesprojectilelib-2.1.2+mc.1.21.1-neoforge.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/B3pb093D/versions/hZ6B2Z0x/ritchiesprojectilelib-2.1.2%2Bmc.1.21.1-neoforge.jar",
|
"url": "https://cdn.modrinth.com/data/B3pb093D/versions/hZ6B2Z0x/ritchiesprojectilelib-2.1.2%2Bmc.1.21.1-neoforge.jar",
|
||||||
"sha1": "ec2e4996f8bee8714173e603e379fef8a6901765",
|
"sha1": "ec2e4996f8bee8714173e603e379fef8a6901765",
|
||||||
"size": 76369
|
"size": 76369,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -147,7 +160,8 @@
|
|||||||
"path": "mods/kubejs-neoforge-2101.7.2-build.363.jar",
|
"path": "mods/kubejs-neoforge-2101.7.2-build.363.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/umyGl7zF/versions/Fe9CjPws/kubejs-neoforge-2101.7.2-build.363.jar",
|
"url": "https://cdn.modrinth.com/data/umyGl7zF/versions/Fe9CjPws/kubejs-neoforge-2101.7.2-build.363.jar",
|
||||||
"sha1": "d4e88254e8c26687d4c6aeb4dfa9c2ad70f260a2",
|
"sha1": "d4e88254e8c26687d4c6aeb4dfa9c2ad70f260a2",
|
||||||
"size": 2270442
|
"size": 2270442,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -157,7 +171,8 @@
|
|||||||
"path": "mods/jei-1.21.1-neoforge-19.27.0.340.jar",
|
"path": "mods/jei-1.21.1-neoforge-19.27.0.340.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/u6dRKJwZ/versions/YAcQ6elZ/jei-1.21.1-neoforge-19.27.0.340.jar",
|
"url": "https://cdn.modrinth.com/data/u6dRKJwZ/versions/YAcQ6elZ/jei-1.21.1-neoforge-19.27.0.340.jar",
|
||||||
"sha1": "27d0d85e7e32e926fc3664ab6815df5cdabb7941",
|
"sha1": "27d0d85e7e32e926fc3664ab6815df5cdabb7941",
|
||||||
"size": 1529391
|
"size": 1529391,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -167,7 +182,8 @@
|
|||||||
"path": "mods/Jade-1.21.1-NeoForge-15.10.5.jar",
|
"path": "mods/Jade-1.21.1-NeoForge-15.10.5.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/nvQzSEkH/versions/yd8FKCmx/Jade-1.21.1-NeoForge-15.10.5.jar",
|
"url": "https://cdn.modrinth.com/data/nvQzSEkH/versions/yd8FKCmx/Jade-1.21.1-NeoForge-15.10.5.jar",
|
||||||
"sha1": "d5bf134b3dbde9f5258666823900e21341dc0a50",
|
"sha1": "d5bf134b3dbde9f5258666823900e21341dc0a50",
|
||||||
"size": 725742
|
"size": 725742,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -177,7 +193,8 @@
|
|||||||
"path": "mods/Chunky-NeoForge-1.4.23.jar",
|
"path": "mods/Chunky-NeoForge-1.4.23.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/fALzjamp/versions/LuFhm4eU/Chunky-NeoForge-1.4.23.jar",
|
"url": "https://cdn.modrinth.com/data/fALzjamp/versions/LuFhm4eU/Chunky-NeoForge-1.4.23.jar",
|
||||||
"sha1": "ab0c74743a653020fe2dfc4986b43e893947f3e9",
|
"sha1": "ab0c74743a653020fe2dfc4986b43e893947f3e9",
|
||||||
"size": 340572
|
"size": 340572,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "curseforge",
|
"source": "curseforge",
|
||||||
@@ -187,7 +204,8 @@
|
|||||||
"path": "mods/ftb-library-neoforge-2101.1.31.jar",
|
"path": "mods/ftb-library-neoforge-2101.1.31.jar",
|
||||||
"url": "https://mediafilez.forgecdn.net/files/7746/959/ftb-library-neoforge-2101.1.31.jar",
|
"url": "https://mediafilez.forgecdn.net/files/7746/959/ftb-library-neoforge-2101.1.31.jar",
|
||||||
"sha1": "686d4e784c28c14f7760cc22b2de6a8573b56b74",
|
"sha1": "686d4e784c28c14f7760cc22b2de6a8573b56b74",
|
||||||
"size": 1411181
|
"size": 1411181,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "curseforge",
|
"source": "curseforge",
|
||||||
@@ -197,7 +215,8 @@
|
|||||||
"path": "mods/ftb-teams-neoforge-2101.1.9.jar",
|
"path": "mods/ftb-teams-neoforge-2101.1.9.jar",
|
||||||
"url": "https://mediafilez.forgecdn.net/files/7369/21/ftb-teams-neoforge-2101.1.9.jar",
|
"url": "https://mediafilez.forgecdn.net/files/7369/21/ftb-teams-neoforge-2101.1.9.jar",
|
||||||
"sha1": "328e04bf1a445870aacea8fe7637670f84272a8f",
|
"sha1": "328e04bf1a445870aacea8fe7637670f84272a8f",
|
||||||
"size": 291847
|
"size": 291847,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "curseforge",
|
"source": "curseforge",
|
||||||
@@ -207,7 +226,8 @@
|
|||||||
"path": "mods/ftb-chunks-neoforge-2101.1.14.jar",
|
"path": "mods/ftb-chunks-neoforge-2101.1.14.jar",
|
||||||
"url": "https://mediafilez.forgecdn.net/files/7608/681/ftb-chunks-neoforge-2101.1.14.jar",
|
"url": "https://mediafilez.forgecdn.net/files/7608/681/ftb-chunks-neoforge-2101.1.14.jar",
|
||||||
"sha1": "908b63b11d0e00ae6c9557d3fe6440bdbcf21bb7",
|
"sha1": "908b63b11d0e00ae6c9557d3fe6440bdbcf21bb7",
|
||||||
"size": 642340
|
"size": 642340,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -217,7 +237,8 @@
|
|||||||
"path": "mods/ars_nouveau-1.21.1-5.11.3.jar",
|
"path": "mods/ars_nouveau-1.21.1-5.11.3.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/TKB6INcv/versions/BmGGrC9A/ars_nouveau-1.21.1-5.11.3.jar",
|
"url": "https://cdn.modrinth.com/data/TKB6INcv/versions/BmGGrC9A/ars_nouveau-1.21.1-5.11.3.jar",
|
||||||
"sha1": "0af12dd7fda63a4261ceb302c9bb57fc235641c6",
|
"sha1": "0af12dd7fda63a4261ceb302c9bb57fc235641c6",
|
||||||
"size": 20689115
|
"size": 20689115,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -227,7 +248,8 @@
|
|||||||
"path": "mods/Terralith_1.21.x_v2.5.8.jar",
|
"path": "mods/Terralith_1.21.x_v2.5.8.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/8oi3bsk5/versions/MuJMtPGQ/Terralith_1.21.x_v2.5.8.jar",
|
"url": "https://cdn.modrinth.com/data/8oi3bsk5/versions/MuJMtPGQ/Terralith_1.21.x_v2.5.8.jar",
|
||||||
"sha1": "bee0cfb1a8cd4bf3d96bccea224fb45d74de9085",
|
"sha1": "bee0cfb1a8cd4bf3d96bccea224fb45d74de9085",
|
||||||
"size": 3115385
|
"size": 3115385,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -237,7 +259,8 @@
|
|||||||
"path": "mods/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar",
|
"path": "mods/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/kidLKymU/versions/8U0dIfSM/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar",
|
"url": "https://cdn.modrinth.com/data/kidLKymU/versions/8U0dIfSM/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar",
|
||||||
"sha1": "5d06a5850af7c577612d4592706a8e156bbe1cbf",
|
"sha1": "5d06a5850af7c577612d4592706a8e156bbe1cbf",
|
||||||
"size": 461244
|
"size": 461244,
|
||||||
|
"side": "server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -247,7 +270,8 @@
|
|||||||
"path": "mods/lithostitched-1.7.2-neoforge-21.1.jar",
|
"path": "mods/lithostitched-1.7.2-neoforge-21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/XaDC71GB/versions/IONexlgI/lithostitched-1.7.2-neoforge-21.1.jar",
|
"url": "https://cdn.modrinth.com/data/XaDC71GB/versions/IONexlgI/lithostitched-1.7.2-neoforge-21.1.jar",
|
||||||
"sha1": "ce35206214647131ebdf14212d1986349aeba79a",
|
"sha1": "ce35206214647131ebdf14212d1986349aeba79a",
|
||||||
"size": 810015
|
"size": 810015,
|
||||||
|
"side": "server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -257,7 +281,8 @@
|
|||||||
"path": "mods/c2me-neoforge-mc1.21.1-0.3.0+alpha.0.91.jar",
|
"path": "mods/c2me-neoforge-mc1.21.1-0.3.0+alpha.0.91.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/COlSi5iR/versions/9iPiN34N/c2me-neoforge-mc1.21.1-0.3.0%2Balpha.0.91.jar",
|
"url": "https://cdn.modrinth.com/data/COlSi5iR/versions/9iPiN34N/c2me-neoforge-mc1.21.1-0.3.0%2Balpha.0.91.jar",
|
||||||
"sha1": "c858c8becfb5205eb12aaf0420eb82c307c2e6a7",
|
"sha1": "c858c8becfb5205eb12aaf0420eb82c307c2e6a7",
|
||||||
"size": 4508649
|
"size": 4508649,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -267,7 +292,8 @@
|
|||||||
"path": "mods/noisium-neoforge-2.3.0+mc1.21-1.21.1.jar",
|
"path": "mods/noisium-neoforge-2.3.0+mc1.21-1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/KuNKN7d2/versions/nJBE6tif/noisium-neoforge-2.3.0%2Bmc1.21-1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/KuNKN7d2/versions/nJBE6tif/noisium-neoforge-2.3.0%2Bmc1.21-1.21.1.jar",
|
||||||
"sha1": "1bea6b61378ba80f038256c4345d9ff3b67928c4",
|
"sha1": "1bea6b61378ba80f038256c4345d9ff3b67928c4",
|
||||||
"size": 60296
|
"size": 60296,
|
||||||
|
"side": "server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -277,7 +303,8 @@
|
|||||||
"path": "mods/async-locator-refined-neoforge-1.21.1-1.5.3.jar",
|
"path": "mods/async-locator-refined-neoforge-1.21.1-1.5.3.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/LUIHK4LD/versions/3BdGHbV2/async-locator-refined-neoforge-1.21.1-1.5.3.jar",
|
"url": "https://cdn.modrinth.com/data/LUIHK4LD/versions/3BdGHbV2/async-locator-refined-neoforge-1.21.1-1.5.3.jar",
|
||||||
"sha1": "2993e3efc6d211ad8d4db179851dea6fdfff4e07",
|
"sha1": "2993e3efc6d211ad8d4db179851dea6fdfff4e07",
|
||||||
"size": 273320
|
"size": 273320,
|
||||||
|
"side": "server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -287,7 +314,8 @@
|
|||||||
"path": "mods/servercore-neoforge-1.5.10+1.21.1.jar",
|
"path": "mods/servercore-neoforge-1.5.10+1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/4WWQxlQP/versions/77MAnmOn/servercore-neoforge-1.5.10%2B1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/4WWQxlQP/versions/77MAnmOn/servercore-neoforge-1.5.10%2B1.21.1.jar",
|
||||||
"sha1": "4524cd40cfa5019d8b5fbcb628b1616031838a0c",
|
"sha1": "4524cd40cfa5019d8b5fbcb628b1616031838a0c",
|
||||||
"size": 1429522
|
"size": 1429522,
|
||||||
|
"side": "server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -297,7 +325,8 @@
|
|||||||
"path": "mods/lithium-neoforge-0.15.3+mc1.21.1.jar",
|
"path": "mods/lithium-neoforge-0.15.3+mc1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/RXHf27Wv/lithium-neoforge-0.15.3%2Bmc1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/RXHf27Wv/lithium-neoforge-0.15.3%2Bmc1.21.1.jar",
|
||||||
"sha1": "9fd5fa9076044180ae7f51672de74669196ec72e",
|
"sha1": "9fd5fa9076044180ae7f51672de74669196ec72e",
|
||||||
"size": 774148
|
"size": 774148,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -307,7 +336,8 @@
|
|||||||
"path": "mods/geckolib-neoforge-1.21.1-4.8.4.jar",
|
"path": "mods/geckolib-neoforge-1.21.1-4.8.4.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/8BmcQJ2H/versions/gFmrC8Ru/geckolib-neoforge-1.21.1-4.8.4.jar",
|
"url": "https://cdn.modrinth.com/data/8BmcQJ2H/versions/gFmrC8Ru/geckolib-neoforge-1.21.1-4.8.4.jar",
|
||||||
"sha1": "eb854c8ec53ef922a5f3877a1aa4c1ce1352e0ce",
|
"sha1": "eb854c8ec53ef922a5f3877a1aa4c1ce1352e0ce",
|
||||||
"size": 622582
|
"size": 622582,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -317,7 +347,8 @@
|
|||||||
"path": "mods/curios-neoforge-9.5.1+1.21.1.jar",
|
"path": "mods/curios-neoforge-9.5.1+1.21.1.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/vvuO3ImH/versions/yohfFbgD/curios-neoforge-9.5.1%2B1.21.1.jar",
|
"url": "https://cdn.modrinth.com/data/vvuO3ImH/versions/yohfFbgD/curios-neoforge-9.5.1%2B1.21.1.jar",
|
||||||
"sha1": "418fcd42e3a7844c9bdc71c9b6401fdb3894e0c4",
|
"sha1": "418fcd42e3a7844c9bdc71c9b6401fdb3894e0c4",
|
||||||
"size": 410690
|
"size": 410690,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -327,7 +358,8 @@
|
|||||||
"path": "mods/YungsApi-1.21.1-NeoForge-5.1.6.jar",
|
"path": "mods/YungsApi-1.21.1-NeoForge-5.1.6.jar",
|
||||||
"url": "https://cdn.modrinth.com/data/Ua7DFN59/versions/ZB22DE9q/YungsApi-1.21.1-NeoForge-5.1.6.jar",
|
"url": "https://cdn.modrinth.com/data/Ua7DFN59/versions/ZB22DE9q/YungsApi-1.21.1-NeoForge-5.1.6.jar",
|
||||||
"sha1": "e1c394779fb9e038e4f7a1b4558d0432607d263b",
|
"sha1": "e1c394779fb9e038e4f7a1b4558d0432607d263b",
|
||||||
"size": 388678
|
"size": 388678,
|
||||||
|
"side": "both"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -337,7 +369,30 @@
|
|||||||
"path": "shaderpacks/ComplementaryReimagined_r5.7.1.zip",
|
"path": "shaderpacks/ComplementaryReimagined_r5.7.1.zip",
|
||||||
"url": "https://cdn.modrinth.com/data/HVnmMxH1/versions/836bPNGo/ComplementaryReimagined_r5.7.1.zip",
|
"url": "https://cdn.modrinth.com/data/HVnmMxH1/versions/836bPNGo/ComplementaryReimagined_r5.7.1.zip",
|
||||||
"sha1": "b560f646a124d5204b1fb7321fec373b9c346fa5",
|
"sha1": "b560f646a124d5204b1fb7321fec373b9c346fa5",
|
||||||
"size": 522970
|
"size": 522970,
|
||||||
|
"side": "client"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "modrinth",
|
||||||
|
"slug": "simple-voice-chat",
|
||||||
|
"versionId": "6rT2RWh6",
|
||||||
|
"version": "neoforge-1.21.1-2.6.17",
|
||||||
|
"path": "mods/voicechat-neoforge-1.21.1-2.6.17.jar",
|
||||||
|
"url": "https://cdn.modrinth.com/data/9eGKb6K1/versions/6rT2RWh6/voicechat-neoforge-1.21.1-2.6.17.jar",
|
||||||
|
"sha1": "55fc0d529318620bd2ce7a635657014c5c98e189",
|
||||||
|
"size": 4902096,
|
||||||
|
"side": "both"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "modrinth",
|
||||||
|
"slug": "almostunified",
|
||||||
|
"versionId": "e8iYxxI3",
|
||||||
|
"version": "1.21.1-1.4.2+neoforge",
|
||||||
|
"path": "mods/almostunified-neoforge-1.21.1-1.4.2.jar",
|
||||||
|
"url": "https://cdn.modrinth.com/data/sdaSaQEz/versions/e8iYxxI3/almostunified-neoforge-1.21.1-1.4.2.jar",
|
||||||
|
"sha1": "f9a58fa95780f4b045d30559c1fdaedaa7f0fba3",
|
||||||
|
"size": 295093,
|
||||||
|
"side": "both"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultServer": {
|
"defaultServer": {
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#requires -Version 7
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Populate the `side` field on each mod entry in pack.lock.json by querying
|
||||||
|
Modrinth's project metadata. Run once after schema migration; thereafter,
|
||||||
|
Update-Pack.ps1 should set side automatically when adding new mods.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Modrinth returns `client_side` and `server_side` per project, with values
|
||||||
|
`required` / `optional` / `unsupported` / `unknown`. We map:
|
||||||
|
server_side = unsupported -> side = "client" (skip on server)
|
||||||
|
client_side = unsupported -> side = "server" (skip on launcher)
|
||||||
|
both required/optional/unknown -> side = "both" (install everywhere)
|
||||||
|
|
||||||
|
Conservative: only marks a mod side-restricted when the OTHER side is
|
||||||
|
explicitly `unsupported`. Anything ambiguous stays "both".
|
||||||
|
|
||||||
|
CurseForge mods can't be queried (no API key here) -- left at "both" by
|
||||||
|
default, manual edit if needed.
|
||||||
|
|
||||||
|
.PARAMETER LockPath
|
||||||
|
pack.lock.json to update in place. Defaults to ../pack/pack.lock.json.
|
||||||
|
|
||||||
|
.PARAMETER DryRun
|
||||||
|
Print proposed changes without writing the file.
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$LockPath = $(Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) '..\pack\pack.lock.json'),
|
||||||
|
[switch]$DryRun
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
|
$LockPath = (Resolve-Path $LockPath).Path
|
||||||
|
$json = Get-Content $LockPath -Raw | ConvertFrom-Json -Depth 20
|
||||||
|
|
||||||
|
if (-not $json.mods) { throw "pack.lock.json has no 'mods' array." }
|
||||||
|
|
||||||
|
function Get-ModrinthSide {
|
||||||
|
param([string]$Slug)
|
||||||
|
$proj = Invoke-RestMethod -Uri "https://api.modrinth.com/v2/project/$Slug" `
|
||||||
|
-Headers @{ 'User-Agent' = 'BrassAndSigil-Launcher/0.1 (matt@sijbers.uk)' }
|
||||||
|
$cs = $proj.client_side; $ss = $proj.server_side
|
||||||
|
if ($ss -eq 'unsupported' -and $cs -ne 'unsupported') { return 'client' }
|
||||||
|
elseif ($cs -eq 'unsupported' -and $ss -ne 'unsupported') { return 'server' }
|
||||||
|
else { return 'both' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Bootstrapping side field on $($json.mods.Count) mods..."
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$changes = 0
|
||||||
|
foreach ($mod in $json.mods) {
|
||||||
|
$existing = if ($mod.PSObject.Properties.Name -contains 'side') { $mod.side } else { $null }
|
||||||
|
if ($mod.source -ne 'modrinth') {
|
||||||
|
# CurseForge or other: default to 'both' if missing, leave alone otherwise.
|
||||||
|
if (-not $existing) {
|
||||||
|
$mod | Add-Member -NotePropertyName 'side' -NotePropertyValue 'both' -Force
|
||||||
|
Write-Host (" [{0}] {1,-26} {2}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, 'both (default; non-modrinth)')
|
||||||
|
$changes++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$side = Get-ModrinthSide -Slug $mod.slug
|
||||||
|
} catch {
|
||||||
|
Write-Warning (" $($mod.slug) lookup failed: $($_.Exception.Message) -- leaving as 'both'")
|
||||||
|
$side = 'both'
|
||||||
|
}
|
||||||
|
if ($existing -ne $side) {
|
||||||
|
$mod | Add-Member -NotePropertyName 'side' -NotePropertyValue $side -Force
|
||||||
|
Write-Host (" [{0}] {1,-26} {2}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $side)
|
||||||
|
$changes++
|
||||||
|
} else {
|
||||||
|
Write-Host (" [{0}] {1,-26} {2} (unchanged)" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $side) -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 100 # be polite to Modrinth's API
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "$changes mod(s) updated."
|
||||||
|
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "DryRun: not writing $LockPath." -ForegroundColor DarkGray
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Round-trip via JSON: Modrinth-returned ConvertFrom-Json objects don't always
|
||||||
|
# round-trip with consistent property order, so we serialise with sorted keys
|
||||||
|
# at the mod level to keep the lockfile diff minimal.
|
||||||
|
$json | ConvertTo-Json -Depth 20 | Set-Content -Path $LockPath -Encoding utf8
|
||||||
|
Write-Host "Wrote $LockPath" -ForegroundColor Green
|
||||||
+28
-4
@@ -77,14 +77,21 @@ $files = @()
|
|||||||
$totalBytes = 0L
|
$totalBytes = 0L
|
||||||
|
|
||||||
foreach ($mod in $lock.mods) {
|
foreach ($mod in $lock.mods) {
|
||||||
$files += [pscustomobject]@{
|
# `side` is emitted only when explicitly set in the lockfile -- a missing or
|
||||||
|
# null lockfile side means "both", and we keep the manifest clean by omitting
|
||||||
|
# the field rather than serialising "both" everywhere.
|
||||||
|
$entry = [ordered]@{
|
||||||
path = $mod.path
|
path = $mod.path
|
||||||
url = $mod.url
|
url = $mod.url
|
||||||
sha1 = $mod.sha1
|
sha1 = $mod.sha1
|
||||||
size = $mod.size
|
size = $mod.size
|
||||||
}
|
}
|
||||||
|
$hasSide = ($mod.PSObject.Properties.Name -contains 'side') -and $mod.side -and $mod.side -ne 'both'
|
||||||
|
if ($hasSide) { $entry.side = $mod.side }
|
||||||
|
$files += [pscustomobject]$entry
|
||||||
$totalBytes += $mod.size
|
$totalBytes += $mod.size
|
||||||
Write-Host (" [{0}] {1,-26} {2,-22} {3,8:N0} KB" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $mod.version, ($mod.size/1KB))
|
$sideTag = if ($hasSide) { " [$($mod.side)]" } else { '' }
|
||||||
|
Write-Host (" [{0}] {1,-26} {2,-22} {3,8:N0} KB{4}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $mod.version, ($mod.size/1KB), $sideTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Local overrides (configs, custom files not on Modrinth/CurseForge)
|
# Local overrides (configs, custom files not on Modrinth/CurseForge)
|
||||||
@@ -156,8 +163,25 @@ if ($LauncherExePath) {
|
|||||||
}
|
}
|
||||||
$launcherFile = Get-Item $LauncherExePath
|
$launcherFile = Get-Item $LauncherExePath
|
||||||
$launcherVersion = $launcherFile.VersionInfo.FileVersion
|
$launcherVersion = $launcherFile.VersionInfo.FileVersion
|
||||||
|
$versionSource = 'PE FileVersion'
|
||||||
|
|
||||||
|
# Linux PowerShell can't read PE FileVersion from a Windows .exe. When that
|
||||||
|
# happens, fall back to launcher/ModpackLauncher.csproj's <Version> (the
|
||||||
|
# source of truth that the Windows compile bakes into FileVersion anyway).
|
||||||
|
# Append ".0" so callers get the same 4-part form as the PE metadata.
|
||||||
if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
|
if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
|
||||||
throw "Launcher exe has no FileVersion -- set <Version> in ModpackLauncher.csproj and republish."
|
$csprojPath = Join-Path $here '..\launcher\ModpackLauncher.csproj'
|
||||||
|
if (Test-Path $csprojPath) {
|
||||||
|
[xml]$csproj = Get-Content $csprojPath
|
||||||
|
$csprojVersion = ($csproj.Project.PropertyGroup.Version | Where-Object { $_ }) | Select-Object -First 1
|
||||||
|
if ($csprojVersion) {
|
||||||
|
$launcherVersion = if ($csprojVersion -match '^\d+\.\d+\.\d+$') { "$csprojVersion.0" } else { $csprojVersion }
|
||||||
|
$versionSource = "csproj <Version> (fallback)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
|
||||||
|
throw "Launcher exe has no FileVersion and csproj <Version> couldn't be read either -- set <Version> in ModpackLauncher.csproj and republish."
|
||||||
}
|
}
|
||||||
# FileVersion is the four-component form (e.g. "0.1.0.0" for csproj <Version>0.1.0</Version>).
|
# FileVersion is the four-component form (e.g. "0.1.0.0" for csproj <Version>0.1.0</Version>).
|
||||||
# Embed as-is -- the launcher's Version.Parse handles it directly and avoids ambiguous
|
# Embed as-is -- the launcher's Version.Parse handles it directly and avoids ambiguous
|
||||||
@@ -167,7 +191,7 @@ if ($LauncherExePath) {
|
|||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host ("Launcher metadata embedded:")
|
Write-Host ("Launcher metadata embedded:")
|
||||||
Write-Host (" Version: {0}" -f $launcherVersion)
|
Write-Host (" Version: {0} ({1})" -f $launcherVersion, $versionSource)
|
||||||
Write-Host (" Url: {0}" -f $LauncherPublicUrl)
|
Write-Host (" Url: {0}" -f $LauncherPublicUrl)
|
||||||
Write-Host (" Source: {0}" -f $launcherFile.FullName)
|
Write-Host (" Source: {0}" -f $launcherFile.FullName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ if (-not (Test-Path $cfgPath)) {
|
|||||||
. $cfgPath
|
. $cfgPath
|
||||||
|
|
||||||
# Sanity-check required vars actually got set (template ships with CHANGE_ME).
|
# Sanity-check required vars actually got set (template ships with CHANGE_ME).
|
||||||
foreach ($v in 'DeployShare','ServerSshHost','ServerSshKey','ServerBinaryRemote') {
|
# $ServerSshKey is only required for SSH-mode deploys; skip it when ServerSshHost = 'local'.
|
||||||
|
$requiredVars = @('DeployShare','ServerSshHost','ServerBinaryRemote')
|
||||||
|
if ($ServerSshHost -ne 'local') { $requiredVars += 'ServerSshKey' }
|
||||||
|
foreach ($v in $requiredVars) {
|
||||||
$val = Get-Variable -Name $v -ValueOnly -ErrorAction Stop
|
$val = Get-Variable -Name $v -ValueOnly -ErrorAction Stop
|
||||||
if ($val -match 'CHANGE_ME' -or [string]::IsNullOrWhiteSpace($val)) {
|
if ($val -match 'CHANGE_ME' -or [string]::IsNullOrWhiteSpace($val)) {
|
||||||
throw "deploy.config.ps1 has placeholder/empty `$$v. Fill it in."
|
throw "deploy.config.ps1 has placeholder/empty `$$v. Fill it in."
|
||||||
@@ -120,14 +123,26 @@ if (($shouldRunPack -or $shouldRunLauncher) -and -not $DryRun -and -not $Force)
|
|||||||
# actually broken -- the atomic swap is safe with the daemon running). But
|
# actually broken -- the atomic swap is safe with the daemon running). But
|
||||||
# knowing the state up front lets us tailor the final "next steps" message
|
# knowing the state up front lets us tailor the final "next steps" message
|
||||||
# so a deploy success doesn't silently leave you on the old code.
|
# so a deploy success doesn't silently leave you on the old code.
|
||||||
|
# Set $ServerSshHost = 'local' in deploy.config.ps1 to skip SSH entirely
|
||||||
|
# and copy/inspect the server binary on the local filesystem. Use when the
|
||||||
|
# deploy machine IS the server (e.g. running Deploy-Brass.ps1 on glados).
|
||||||
|
$isLocalDeploy = ($ServerSshHost -eq 'local')
|
||||||
|
|
||||||
$daemonWasRunning = $false
|
$daemonWasRunning = $false
|
||||||
if ($shouldRunServer) {
|
if ($shouldRunServer) {
|
||||||
|
if ($isLocalDeploy) {
|
||||||
|
Write-Host "Pre-flight: checking daemon state locally..." -ForegroundColor DarkGray
|
||||||
|
$pgrepOut = & pgrep -f '/brass-sigil-server($| )' 2>$null
|
||||||
|
} else {
|
||||||
Write-Host "Pre-flight: checking daemon state on $ServerSshHost..." -ForegroundColor DarkGray
|
Write-Host "Pre-flight: checking daemon state on $ServerSshHost..." -ForegroundColor DarkGray
|
||||||
# Single-quoted PS string so PowerShell doesn't try to interpret the
|
# Single-quoted PS string so PowerShell doesn't try to interpret the
|
||||||
# bash-side metacharacters. The remote shell sees the literal pgrep
|
# bash-side metacharacters. Match the binary path followed by either
|
||||||
# command; the trailing $ anchors so we don't match run.sh wrappers.
|
# end-of-line OR a space, so we catch `brass-sigil-server run` (the
|
||||||
$remoteCmd = 'pgrep -f /brass-sigil-server$ 2>/dev/null'
|
# actual invocation) without matching the `/brass-sigil-server/...`
|
||||||
|
# directory path that appears in the run.sh wrapper's argv.
|
||||||
|
$remoteCmd = 'pgrep -f /brass-sigil-server($| ) 2>/dev/null'
|
||||||
$pgrepOut = & ssh -i $ServerSshKey -o ConnectTimeout=5 -o BatchMode=yes $ServerSshHost $remoteCmd 2>$null
|
$pgrepOut = & ssh -i $ServerSshKey -o ConnectTimeout=5 -o BatchMode=yes $ServerSshHost $remoteCmd 2>$null
|
||||||
|
}
|
||||||
$daemonWasRunning = -not [string]::IsNullOrWhiteSpace($pgrepOut)
|
$daemonWasRunning = -not [string]::IsNullOrWhiteSpace($pgrepOut)
|
||||||
if ($daemonWasRunning) {
|
if ($daemonWasRunning) {
|
||||||
Write-Host " Daemon is RUNNING. Atomic swap is safe -- but you'll need to" -ForegroundColor Yellow
|
Write-Host " Daemon is RUNNING. Atomic swap is safe -- but you'll need to" -ForegroundColor Yellow
|
||||||
@@ -199,12 +214,27 @@ $overridesLocal = Join-Path $repoRoot 'pack\overrides'
|
|||||||
$shareFiles = Join-Path $DeployShare 'files'
|
$shareFiles = Join-Path $DeployShare 'files'
|
||||||
if ($shouldRunPack -and (Test-Path $overridesLocal)) {
|
if ($shouldRunPack -and (Test-Path $overridesLocal)) {
|
||||||
Step "Mirror pack/overrides/ -> $shareFiles" {
|
Step "Mirror pack/overrides/ -> $shareFiles" {
|
||||||
|
# Prefer rsync where available (Linux/macOS); fall back to Windows robocopy.
|
||||||
|
# Both perform a mirror: orphan files in the destination are removed so
|
||||||
|
# the share matches the local overrides exactly.
|
||||||
|
if (Get-Command rsync -ErrorAction SilentlyContinue) {
|
||||||
|
$src = ($overridesLocal -replace '\\','/').TrimEnd('/') + '/'
|
||||||
|
$dst = ($shareFiles -replace '\\','/').TrimEnd('/') + '/'
|
||||||
|
if (-not (Test-Path $shareFiles)) { New-Item -ItemType Directory -Path $shareFiles -Force | Out-Null }
|
||||||
|
& rsync -a --delete $src $dst
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "rsync failed with exit $LASTEXITCODE" }
|
||||||
|
}
|
||||||
|
elseif (Get-Command robocopy -ErrorAction SilentlyContinue) {
|
||||||
# /MIR makes destination match source (deletes orphan files in $shareFiles).
|
# /MIR makes destination match source (deletes orphan files in $shareFiles).
|
||||||
# /XJ skips junctions, /R:1 /W:1 keeps retry behaviour sane on flaky shares.
|
# /XJ skips junctions, /R:1 /W:1 keeps retry behaviour sane on flaky shares.
|
||||||
robocopy $overridesLocal $shareFiles /MIR /XJ /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Host
|
robocopy $overridesLocal $shareFiles /MIR /XJ /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Host
|
||||||
# Robocopy returns 0-7 for success-with-info; 8+ is real failure.
|
# Robocopy returns 0-7 for success-with-info; 8+ is real failure.
|
||||||
if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit $LASTEXITCODE" }
|
if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit $LASTEXITCODE" }
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
throw "Neither rsync nor robocopy is available -- can't mirror pack overrides."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── Stage 5: publish launcher exe (only when Stage includes Launcher) ─────
|
# ─── Stage 5: publish launcher exe (only when Stage includes Launcher) ─────
|
||||||
@@ -224,8 +254,19 @@ if ($shouldRunPack) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── Stage 6: scp + atomic swap server binary ──────────────────────────────
|
# ─── Stage 6: copy + atomic swap server binary ─────────────────────────────
|
||||||
if ($shouldRunServer) {
|
if ($shouldRunServer) {
|
||||||
|
if ($isLocalDeploy) {
|
||||||
|
Step "Copy server binary -> $ServerBinaryRemote (local atomic swap)" {
|
||||||
|
$localNew = "$ServerBinaryRemote.new"
|
||||||
|
Copy-Item $serverExe $localNew -Force
|
||||||
|
& chmod +x $localNew
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "chmod failed with exit $LASTEXITCODE" }
|
||||||
|
Move-Item $localNew $ServerBinaryRemote -Force
|
||||||
|
$md5 = (& md5sum $ServerBinaryRemote) -split '\s+' | Select-Object -First 1
|
||||||
|
Write-Host " md5: $md5" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Step "scp server binary -> $ServerSshHost`:$ServerBinaryRemote (atomic swap)" {
|
Step "scp server binary -> $ServerSshHost`:$ServerBinaryRemote (atomic swap)" {
|
||||||
$remoteNew = "$ServerBinaryRemote.new"
|
$remoteNew = "$ServerBinaryRemote.new"
|
||||||
& scp -i $ServerSshKey -o ConnectTimeout=15 $serverExe "$ServerSshHost`:$remoteNew"
|
& scp -i $ServerSshKey -o ConnectTimeout=15 $serverExe "$ServerSshHost`:$remoteNew"
|
||||||
@@ -234,6 +275,7 @@ if ($shouldRunServer) {
|
|||||||
& ssh -i $ServerSshKey -o ConnectTimeout=10 $ServerSshHost $cmd
|
& ssh -i $ServerSshKey -o ConnectTimeout=10 $ServerSshHost $cmd
|
||||||
if ($LASTEXITCODE -ne 0) { throw "ssh swap failed with exit $LASTEXITCODE" }
|
if ($LASTEXITCODE -ne 0) { throw "ssh swap failed with exit $LASTEXITCODE" }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
if ($daemonWasRunning) {
|
if ($daemonWasRunning) {
|
||||||
Write-Host "Server binary swapped on disk, but the daemon was running before this" -ForegroundColor Yellow
|
Write-Host "Server binary swapped on disk, but the daemon was running before this" -ForegroundColor Yellow
|
||||||
|
|||||||
@@ -19,13 +19,20 @@ $ManifestPublicUrl = 'https://CHANGE_ME/pack/manifest.json'
|
|||||||
|
|
||||||
# ─── Server (brass-sigil-server daemon host) ───────────────────────────────
|
# ─── Server (brass-sigil-server daemon host) ───────────────────────────────
|
||||||
# user@host for the Linux box running the daemon.
|
# user@host for the Linux box running the daemon.
|
||||||
|
#
|
||||||
|
# Set to the literal string 'local' if Deploy-Brass.ps1 is being run on the
|
||||||
|
# server itself (e.g. on glados over the Telegram bridge). In local mode the
|
||||||
|
# script skips scp/ssh and copies the binary directly to $ServerBinaryRemote
|
||||||
|
# with an in-place atomic rename; $ServerSshKey is ignored.
|
||||||
$ServerSshHost = 'user@CHANGE_ME'
|
$ServerSshHost = 'user@CHANGE_ME'
|
||||||
|
|
||||||
# Path to the local SSH private key authorised on the server.
|
# Path to the local SSH private key authorised on the server.
|
||||||
|
# Ignored when $ServerSshHost = 'local'.
|
||||||
$ServerSshKey = "$env:USERPROFILE\.ssh\id_ed25519"
|
$ServerSshKey = "$env:USERPROFILE\.ssh\id_ed25519"
|
||||||
|
|
||||||
# Absolute path on the Linux box where the brass-sigil-server binary lives.
|
# Absolute path where the brass-sigil-server binary lives -- on the remote
|
||||||
# `Deploy-Brass.ps1` uploads to "<this>.new" then `mv` over for atomic swap.
|
# host in SSH mode, on the local filesystem in 'local' mode.
|
||||||
|
# `Deploy-Brass.ps1` writes to "<this>.new" then renames over for atomic swap.
|
||||||
$ServerBinaryRemote = '/home/user/brass-sigil-server/brass-sigil-server'
|
$ServerBinaryRemote = '/home/user/brass-sigil-server/brass-sigil-server'
|
||||||
|
|
||||||
# ─── Build outputs (don't normally need to change) ─────────────────────────
|
# ─── Build outputs (don't normally need to change) ─────────────────────────
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ public sealed class ManifestFile
|
|||||||
[JsonPropertyName("url")] public string Url { get; set; } = "";
|
[JsonPropertyName("url")] public string Url { get; set; } = "";
|
||||||
[JsonPropertyName("sha1")] public string? Sha1 { get; set; }
|
[JsonPropertyName("sha1")] public string? Sha1 { get; set; }
|
||||||
[JsonPropertyName("size")] public long? Size { get; set; }
|
[JsonPropertyName("size")] public long? Size { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "both" (default), "client", or "server". Server skips "client" files;
|
||||||
|
/// launcher skips "server" files. Null/missing = "both".
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("side")] public string? Side { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PackLock
|
public sealed class PackLock
|
||||||
@@ -59,4 +65,5 @@ public sealed class LockedMod
|
|||||||
[JsonPropertyName("url")] public string Url { get; set; } = "";
|
[JsonPropertyName("url")] public string Url { get; set; } = "";
|
||||||
[JsonPropertyName("sha1")] public string Sha1 { get; set; } = "";
|
[JsonPropertyName("sha1")] public string Sha1 { get; set; } = "";
|
||||||
[JsonPropertyName("size")] public long Size { get; set; }
|
[JsonPropertyName("size")] public long Size { get; set; }
|
||||||
|
[JsonPropertyName("side")] public string? Side { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,20 @@ public sealed class ManifestSync
|
|||||||
progress?.Report($"Pack: {manifest.Name} v{manifest.Version}");
|
progress?.Report($"Pack: {manifest.Name} v{manifest.Version}");
|
||||||
Directory.CreateDirectory(serverDir);
|
Directory.CreateDirectory(serverDir);
|
||||||
|
|
||||||
// Resolve which mods are server-side.
|
// Build-time tagging is authoritative: anything marked "client" is dropped
|
||||||
var skipSlugs = await ResolveServerSideSkipListAsync(manifest, ct);
|
// outright, anything marked "server" or "both" is kept. We only fall back
|
||||||
|
// to the runtime Modrinth lookup for files with NO explicit side -- this
|
||||||
|
// keeps un-tagged (or CurseForge-only) mods safe and matches pre-side
|
||||||
|
// behaviour for them.
|
||||||
|
var needsLookup = manifest.Files.Where(f => string.IsNullOrEmpty(f.Side)).ToList();
|
||||||
|
var skipSlugs = needsLookup.Count > 0
|
||||||
|
? await ResolveServerSideSkipListAsync(
|
||||||
|
new Manifest { Files = needsLookup }, ct)
|
||||||
|
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// Build the filtered list of files to keep on the server.
|
// Build the filtered list of files to keep on the server.
|
||||||
var keepFiles = manifest.Files
|
var keepFiles = manifest.Files
|
||||||
.Where(f => !ShouldSkipFile(f.Path, skipSlugs))
|
.Where(f => !IsClientOnly(f) && (!string.IsNullOrEmpty(f.Side) || !ShouldSkipFile(f.Path, skipSlugs)))
|
||||||
.ToList();
|
.ToList();
|
||||||
var skippedCount = manifest.Files.Count - keepFiles.Count;
|
var skippedCount = manifest.Files.Count - keepFiles.Count;
|
||||||
|
|
||||||
@@ -108,6 +116,9 @@ public sealed class ManifestSync
|
|||||||
|| name.Equals(slug, StringComparison.OrdinalIgnoreCase));
|
|| name.Equals(slug, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsClientOnly(ManifestFile f)
|
||||||
|
=> string.Equals(f.Side, "client", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>Walk the manifest's mod URLs; for Modrinth ones, look up server_side; build a skip set.</summary>
|
/// <summary>Walk the manifest's mod URLs; for Modrinth ones, look up server_side; build a skip set.</summary>
|
||||||
private async Task<HashSet<string>> ResolveServerSideSkipListAsync(Manifest manifest, CancellationToken ct)
|
private async Task<HashSet<string>> ResolveServerSideSkipListAsync(Manifest manifest, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user