diff --git a/launcher/Backend/LocalOverrides.cs b/launcher/Backend/LocalOverrides.cs
new file mode 100644
index 0000000..dd4aa05
--- /dev/null
+++ b/launcher/Backend/LocalOverrides.cs
@@ -0,0 +1,80 @@
+using System.IO;
+
+namespace DorkNet.Launcher.Backend;
+
+/// Local dev-time overrides that bypass the GitHub-backed
+/// manifest + releases fetches. Set via environment variables before
+/// launching dorknet.exe:
+///
+///
+/// - DORKNET_LOCAL_MANIFEST = full path to a
+/// versions.json file. When set + exists,
+/// reads it from disk and
+/// skips the GitHub round-trip.
+/// - DORKNET_LOCAL_RELEASES = path to a directory whose
+/// children are {version_key}/server/ and
+/// {version_key}/patcher/. When the launcher would normally
+/// download a server / patcher zip, it points at these in-place
+/// instead — no copying, no markers, just read the dir.
+///
+///
+/// Both overrides are no-ops when the env var is unset or the
+/// path doesn't exist on disk, so they're safe to leave set
+/// permanently in a dev profile.
+public static class LocalOverrides
+{
+ public const string ManifestEnvVar = "DORKNET_LOCAL_MANIFEST";
+ public const string ReleasesEnvVar = "DORKNET_LOCAL_RELEASES";
+
+ /// Returns the configured manifest path if the env var is
+ /// set AND the file exists; null otherwise. Caller decides whether
+ /// to fall back to the network.
+ public static string? GetLocalManifestPath()
+ {
+ var path = Environment.GetEnvironmentVariable(ManifestEnvVar);
+ if (string.IsNullOrEmpty(path)) return null;
+ return File.Exists(path) ? path : null;
+ }
+
+ /// Returns the configured releases-root if the env var is
+ /// set AND the directory exists; null otherwise.
+ public static string? GetLocalReleasesRoot()
+ {
+ var path = Environment.GetEnvironmentVariable(ReleasesEnvVar);
+ if (string.IsNullOrEmpty(path)) return null;
+ return Directory.Exists(path) ? path : null;
+ }
+
+ /// Returns the local server-dir for
+ /// if the releases-root override is set and the subfolder exists.
+ /// Convention: {root}/{version_key}/server/.
+ public static string? GetLocalServerDir(string versionKey)
+ {
+ var root = GetLocalReleasesRoot();
+ if (root is null) return null;
+ var dir = Path.Combine(root, versionKey, "server");
+ return Directory.Exists(dir) ? dir : null;
+ }
+
+ /// Same as but for the
+ /// client patcher. Convention: {root}/{version_key}/patcher/.
+ public static string? GetLocalPatcherDir(string versionKey)
+ {
+ var root = GetLocalReleasesRoot();
+ if (root is null) return null;
+ var dir = Path.Combine(root, versionKey, "patcher");
+ return Directory.Exists(dir) ? dir : null;
+ }
+
+ /// Single-line description of what overrides are active,
+ /// for surfacing in the Settings view. Empty string if none.
+ public static string DescribeActive()
+ {
+ var parts = new List();
+ var m = GetLocalManifestPath();
+ if (m is not null) parts.Add($"manifest -> {m}");
+ var r = GetLocalReleasesRoot();
+ if (r is not null) parts.Add($"releases -> {r}");
+ return string.Join("\n", parts);
+ }
+}
diff --git a/launcher/Backend/ReleaseDownloader.cs b/launcher/Backend/ReleaseDownloader.cs
index a84d6fc..57679a1 100644
--- a/launcher/Backend/ReleaseDownloader.cs
+++ b/launcher/Backend/ReleaseDownloader.cs
@@ -44,6 +44,11 @@ public sealed class ReleaseDownloader
IProgress? progress,
CancellationToken ct = default)
{
+ // Dev-time override — use a local server build in-place rather
+ // than downloading from GitHub. Skips cache entirely.
+ var local = LocalOverrides.GetLocalServerDir(version.VersionKey);
+ if (local is not null) return local;
+
var dir = Path.Combine(AppPaths.ServersRoot, version.VersionKey);
var marker = Path.Combine(dir, ".dorknet-version");
// If the marker file exists, the server is already unpacked
@@ -73,6 +78,11 @@ public sealed class ReleaseDownloader
IProgress? progress,
CancellationToken ct = default)
{
+ // Dev-time override — same as EnsureServerAsync but for the
+ // client-patcher dir.
+ var local = LocalOverrides.GetLocalPatcherDir(version.VersionKey);
+ if (local is not null) return local;
+
var dir = Path.Combine(AppPaths.PatchersRoot, version.VersionKey);
var marker = Path.Combine(dir, ".dorknet-version");
if (File.Exists(marker)) return dir;
diff --git a/launcher/Backend/VersionsManifest.cs b/launcher/Backend/VersionsManifest.cs
index 27aa9ee..66899d0 100644
--- a/launcher/Backend/VersionsManifest.cs
+++ b/launcher/Backend/VersionsManifest.cs
@@ -30,6 +30,23 @@ public sealed class VersionsManifest
public async Task FetchAsync(CancellationToken ct = default)
{
+ // Dev-time override: read manifest from a local file instead of
+ // GitHub. Used for testing branch changes before they're public.
+ var localPath = LocalOverrides.GetLocalManifestPath();
+ if (localPath is not null)
+ {
+ try
+ {
+ var json = await File.ReadAllTextAsync(localPath, ct);
+ return JsonSerializer.Deserialize(json);
+ }
+ catch
+ {
+ // Bad local file — fall through to the network so the
+ // user gets a real error instead of silent emptiness.
+ }
+ }
+
try
{
var m = await Http.GetFromJsonAsync(ManifestUrl, ct);
diff --git a/launcher/MainWindow.xaml b/launcher/MainWindow.xaml
index 9c2500d..97b7a3d 100644
--- a/launcher/MainWindow.xaml
+++ b/launcher/MainWindow.xaml
@@ -661,6 +661,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/launcher/MainWindow.xaml.cs b/launcher/MainWindow.xaml.cs
index 0aafcca..9b245c2 100644
--- a/launcher/MainWindow.xaml.cs
+++ b/launcher/MainWindow.xaml.cs
@@ -166,6 +166,17 @@ public partial class MainWindow : Window
$"Patchers: {AppPaths.PatchersRoot}\n" +
$"Logs: {AppPaths.LogsRoot}";
+ var overrides = LocalOverrides.DescribeActive();
+ if (!string.IsNullOrEmpty(overrides))
+ {
+ LocalOverridesPanel.Visibility = Visibility.Visible;
+ LocalOverridesText.Text = overrides;
+ }
+ else
+ {
+ LocalOverridesPanel.Visibility = Visibility.Collapsed;
+ }
+
RefreshJoinReadiness();
ShowView(PickRoutingTarget());
}