mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
launcher: Rec-Room-styled UI; PowerShell-free patcher; Launch button
UI redesign: - Warm coral + teal palette over deep purple-navy (closer to Rec Room's marketing aesthetic). New wordmark gradient (coral -> pink -> teal). - Chunky 18px rounded cards on the first-run wizard with per-mode accent tinting (coral for host, teal for join) and clear CTAs. - Form fields wrapped in elevated panels with sectional titles + pill status chips in the header (HOST MODE / JOIN MODE / SETTINGS). - Drop-shadow effects on primary buttons + cards for depth. Patcher rewrite: - ClientPatcher.cs no longer shells out to PowerShell; the launcher unzips MelonLoader, copies the DorkNet plugin DLL, and renders the user-data config template — all in C# in-process. - Patcher zip now driven by a manifest.json at root (schema v1). Per-version branches publish: manifest.json + MelonLoader.zip + DorkNet.ClientMod.dll + config template. - RELEASES.md updated with the new MelonLoader manifest contract and the placeholder substitution rules. Rec Room launch path: - New Backend/RecRoomLauncher.cs runs Recroom_Release.exe directly from the picked install folder's parent dir (no Steam). - 'Launch Rec Room' button appears after a successful patch in both host + join modes.
This commit is contained in:
+163
-40
@@ -4,47 +4,118 @@
|
||||
StartupUri="MainWindow.xaml">
|
||||
|
||||
<Application.Resources>
|
||||
<!-- ── Dark theme palette ─────────────────────────────────────── -->
|
||||
<SolidColorBrush x:Key="Bg" Color="#0E0F13" />
|
||||
<SolidColorBrush x:Key="BgElev" Color="#181A21" />
|
||||
<SolidColorBrush x:Key="BgElev2" Color="#21232D" />
|
||||
<SolidColorBrush x:Key="Border" Color="#2A2D3A" />
|
||||
<SolidColorBrush x:Key="Ink" Color="#E8EBF3" />
|
||||
<SolidColorBrush x:Key="InkSoft" Color="#9AA0B4" />
|
||||
<SolidColorBrush x:Key="InkFaint" Color="#5E6478" />
|
||||
<SolidColorBrush x:Key="Brand" Color="#7C5CFF" />
|
||||
<SolidColorBrush x:Key="BrandHover" Color="#9A7EFF" />
|
||||
<SolidColorBrush x:Key="Danger" Color="#FF6B6B" />
|
||||
<SolidColorBrush x:Key="Ok" Color="#44D68A" />
|
||||
<!-- ── Palette ─────────────────────────────────────────────────
|
||||
Rec Room-adjacent — warm coral + teal on a deep purple-navy.
|
||||
Avoids the cold-grey "developer tool" look in favour of the
|
||||
friendly game-launcher aesthetic. -->
|
||||
<SolidColorBrush x:Key="Bg" Color="#16102A" />
|
||||
<SolidColorBrush x:Key="BgDeeper" Color="#0E0A1F" />
|
||||
<SolidColorBrush x:Key="BgElev" Color="#221833" />
|
||||
<SolidColorBrush x:Key="BgElev2" Color="#2E2245" />
|
||||
<SolidColorBrush x:Key="Border" Color="#3A2C56" />
|
||||
<SolidColorBrush x:Key="BorderSoft" Color="#2A2042" />
|
||||
<SolidColorBrush x:Key="Ink" Color="#F7F3FF" />
|
||||
<SolidColorBrush x:Key="InkSoft" Color="#B8AED4" />
|
||||
<SolidColorBrush x:Key="InkFaint" Color="#6E608E" />
|
||||
|
||||
<!-- ── TextBox / PasswordBox base ──────────────────────────────── -->
|
||||
<!-- Brand: warm coral, with teal as the secondary accent. -->
|
||||
<SolidColorBrush x:Key="Brand" Color="#FF7B5C" />
|
||||
<SolidColorBrush x:Key="BrandHover" Color="#FF9277" />
|
||||
<SolidColorBrush x:Key="BrandDeep" Color="#E55F40" />
|
||||
<SolidColorBrush x:Key="Teal" Color="#44D6C4" />
|
||||
<SolidColorBrush x:Key="TealDeep" Color="#2DB8A6" />
|
||||
<SolidColorBrush x:Key="Sunshine" Color="#FFD555" />
|
||||
<SolidColorBrush x:Key="Ok" Color="#5BE0A6" />
|
||||
<SolidColorBrush x:Key="Danger" Color="#FF6B8A" />
|
||||
|
||||
<!-- Wordmark gradient: coral → pink → teal across the "DorkNet" letters. -->
|
||||
<LinearGradientBrush x:Key="WordmarkBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#FF7B5C" Offset="0" />
|
||||
<GradientStop Color="#FF5C95" Offset="0.55" />
|
||||
<GradientStop Color="#44D6C4" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<!-- Subtle card-back gradient — radial illusion via linear top-left
|
||||
tint into the base elev colour. -->
|
||||
<LinearGradientBrush x:Key="CardBg" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#2A1F40" Offset="0" />
|
||||
<GradientStop Color="#221833" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="CardBgHost" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#3A1F35" Offset="0" />
|
||||
<GradientStop Color="#221833" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="CardBgJoin" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#1F3540" Offset="0" />
|
||||
<GradientStop Color="#221833" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<!-- Soft drop-shadow effect used on cards / primary buttons. -->
|
||||
<DropShadowEffect x:Key="CardShadow" BlurRadius="24" ShadowDepth="0"
|
||||
Color="#000000" Opacity="0.45" />
|
||||
<DropShadowEffect x:Key="BtnShadow" BlurRadius="16" ShadowDepth="2"
|
||||
Color="#FF5C40" Opacity="0.35" />
|
||||
|
||||
<!-- ── TextBox ─────────────────────────────────────────────────── -->
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||
<Setter Property="CaretBrush" Value="{StaticResource Ink}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="8,6" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="BorderThickness" Value="1.5" />
|
||||
<Setter Property="Padding" Value="12,10" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="10">
|
||||
<ScrollViewer x:Name="PART_ContentHost"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
VerticalScrollBarVisibility="Auto" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ── ComboBox ────────────────────────────────────────────────── -->
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="Padding" Value="8,6" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="BorderThickness" Value="1.5" />
|
||||
<Setter Property="Padding" Value="12,10" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Button styles ───────────────────────────────────────────── -->
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Buttons ─────────────────────────────────────────────────── -->
|
||||
<Style x:Key="Btn" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev2}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="14,8" />
|
||||
<Setter Property="BorderThickness" Value="1.5" />
|
||||
<Setter Property="Padding" Value="16,10" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
@@ -52,7 +123,7 @@
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6">
|
||||
CornerRadius="10">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="{TemplateBinding Padding}" />
|
||||
@@ -63,64 +134,116 @@
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSoft}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
|
||||
<Setter Property="Background" Value="{StaticResource BgElev}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PrimaryBtn" TargetType="Button" BasedOn="{StaticResource Btn}">
|
||||
<Setter Property="Background" Value="{StaticResource Brand}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BrandDeep}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="Padding" Value="22,12" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Effect" Value="{StaticResource BtnShadow}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource BrandHover}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BrandHover}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev2}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
|
||||
<Setter Property="Effect" Value="{x:Null}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- ── Card button for first-run wizard ────────────────────────── -->
|
||||
<Style x:Key="CardBtn" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev}" />
|
||||
<Style x:Key="TealBtn" TargetType="Button" BasedOn="{StaticResource Btn}">
|
||||
<Setter Property="Background" Value="{StaticResource Teal}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TealDeep}" />
|
||||
<Setter Property="Foreground" Value="#0E0A1F" />
|
||||
<Setter Property="Padding" Value="22,12" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#5DE8D6" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GhostBtn" TargetType="Button" BasedOn="{StaticResource Btn}">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Card buttons for first-run wizard ───────────────────────── -->
|
||||
<Style x:Key="ChoiceCardBtn" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource CardBg}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="20" />
|
||||
<Setter Property="BorderThickness" Value="1.5" />
|
||||
<Setter Property="Padding" Value="28" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Effect" Value="{StaticResource CardShadow}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
<Border x:Name="cardBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="10">
|
||||
CornerRadius="18">
|
||||
<ContentPresenter Margin="{TemplateBinding Padding}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="cardBorder" Property="BorderBrush"
|
||||
Value="{StaticResource Brand}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- ── Section header text ─────────────────────────────────────── -->
|
||||
<!-- ── Section panel — used to group form fields ───────────────── -->
|
||||
<Style x:Key="Panel" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSoft}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Padding" Value="20" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Field label text ────────────────────────────────────────── -->
|
||||
<Style x:Key="FieldLabel" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource InkSoft}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Margin" Value="0,0,0,8" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Margin" Value="0,0,0,4" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="Margin" Value="0,0,0,12" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
|
||||
</Style>
|
||||
|
||||
<!-- Pill-style "status chip" used in section headers. -->
|
||||
<Style x:Key="StatusChip" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource BgElev2}" />
|
||||
<Setter Property="CornerRadius" Value="999" />
|
||||
<Setter Property="Padding" Value="10,4" />
|
||||
</Style>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -1,67 +1,192 @@
|
||||
using System.IO;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DorkNet.Launcher.Backend;
|
||||
|
||||
/// <summary>Invokes the per-version branch's <c>install-plugin.ps1</c>
|
||||
/// (or <c>install-legacy-client.ps1</c> as a fallback) on the user's
|
||||
/// Rec Room install. The patcher zip is unpacked by
|
||||
/// <see cref="ReleaseDownloader.EnsurePatcherAsync"/> first; we just
|
||||
/// run the script from inside that unpacked tree.</summary>
|
||||
/// <summary>Applies a DorkNet client patch to the user's Rec Room
|
||||
/// install entirely in-process — no PowerShell scripts. The patcher
|
||||
/// zip fetched from a per-version branch's release MUST contain a
|
||||
/// <c>manifest.json</c> at root describing what to do; the launcher
|
||||
/// reads it and performs each step in C#.
|
||||
///
|
||||
/// <para>DorkNet ships against <b>MelonLoader</b>. The manifest's
|
||||
/// default paths target a MelonLoader install layout; per-version
|
||||
/// branches can override any path explicitly.</para>
|
||||
///
|
||||
/// <para>Manifest schema (v1):
|
||||
/// <code>
|
||||
/// {
|
||||
/// "$schema_version": 1,
|
||||
/// "loader_archive": "MelonLoader.zip", // optional — unzipped into <recroom-root>\
|
||||
/// "plugin_dll": "DorkNet.ClientMod.dll", // required — copied to plugin_dest
|
||||
/// "plugin_dest": "MelonLoader/Mods", // optional — defaults to "MelonLoader/Mods"
|
||||
/// "config_template": "dorknet-clientmod.json.template", // optional — rendered with {HOST}/{PHOTON_APPID}/{PHOTON_VOICE_APPID}
|
||||
/// "config_dest": "MelonLoader/UserData/dorknet-clientmod.json", // optional — defaults shown
|
||||
/// "old_plugin_paths": ["BepInEx/plugins/DorkNet.ClientPatch.dll"] // optional — deleted before install
|
||||
/// }
|
||||
/// </code>
|
||||
/// Config template placeholders: <c>{HOST}</c>, <c>{PHOTON_APPID}</c>,
|
||||
/// <c>{PHOTON_VOICE_APPID}</c>. Anything else passes through unchanged.</para></summary>
|
||||
public sealed class ClientPatcher
|
||||
{
|
||||
public async Task<PatchResult> ApplyAsync(
|
||||
private const string DefaultPluginDest = "MelonLoader/Mods";
|
||||
private const string DefaultConfigDest = "MelonLoader/UserData/dorknet-clientmod.json";
|
||||
|
||||
public Task<PatchResult> ApplyAsync(
|
||||
string patcherDir,
|
||||
string recRoomPath,
|
||||
string recRoomDataPath,
|
||||
string photonAppId,
|
||||
string photonVoiceAppId,
|
||||
string apexHost,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var script = Path.Combine(patcherDir, "install-plugin.ps1");
|
||||
if (!File.Exists(script))
|
||||
return Task.Run(() => ApplyCore(patcherDir, recRoomDataPath,
|
||||
photonAppId, photonVoiceAppId, apexHost), ct);
|
||||
}
|
||||
|
||||
private PatchResult ApplyCore(
|
||||
string patcherDir,
|
||||
string recRoomDataPath,
|
||||
string photonAppId,
|
||||
string photonVoiceAppId,
|
||||
string apexHost)
|
||||
{
|
||||
var log = new System.Text.StringBuilder();
|
||||
try
|
||||
{
|
||||
// Some branches may ship only the legacy installer.
|
||||
script = Path.Combine(patcherDir, "install-legacy-client.ps1");
|
||||
if (!File.Exists(script))
|
||||
{
|
||||
if (string.IsNullOrEmpty(recRoomDataPath))
|
||||
return PatchResult.Failure("Rec Room install path is empty.");
|
||||
var recRoomRoot = Path.GetDirectoryName(recRoomDataPath.TrimEnd(Path.DirectorySeparatorChar))
|
||||
?? throw new InvalidOperationException("Couldn't resolve parent dir of " + recRoomDataPath);
|
||||
if (!Directory.Exists(recRoomRoot))
|
||||
return PatchResult.Failure($"Rec Room root does not exist: {recRoomRoot}");
|
||||
|
||||
var manifestPath = Path.Combine(patcherDir, "manifest.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
return PatchResult.Failure(
|
||||
"Patcher zip is missing install-plugin.ps1 and install-legacy-client.ps1. " +
|
||||
"Open an issue with the version key + the contents of " +
|
||||
$"{patcherDir}.");
|
||||
$"Patcher zip is missing manifest.json — the per-version " +
|
||||
$"branch's release is on an older PowerShell-based format. " +
|
||||
$"Looked in: {patcherDir}");
|
||||
var manifest = JsonSerializer.Deserialize<PatcherManifest>(
|
||||
File.ReadAllText(manifestPath),
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
if (manifest is null)
|
||||
return PatchResult.Failure("manifest.json parse failed.");
|
||||
|
||||
log.AppendLine($"[patch] manifest schema v{manifest.SchemaVersion}");
|
||||
log.AppendLine($"[patch] target: {recRoomRoot}");
|
||||
|
||||
// 1. Unpack MelonLoader (or whatever loader the branch chose).
|
||||
if (!string.IsNullOrEmpty(manifest.LoaderArchive))
|
||||
{
|
||||
var zipPath = Path.Combine(patcherDir, manifest.LoaderArchive);
|
||||
if (!File.Exists(zipPath))
|
||||
return PatchResult.Failure($"Loader archive listed in manifest but missing on disk: {zipPath}");
|
||||
log.AppendLine($"[patch] extracting loader from {Path.GetFileName(zipPath)} → {recRoomRoot}");
|
||||
ExtractZipOverwrite(zipPath, recRoomRoot);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.AppendLine("[patch] no loader archive in manifest — assuming user already has MelonLoader installed");
|
||||
}
|
||||
|
||||
// 2. Remove stale plugin paths (e.g. a previous BepInEx-era DLL).
|
||||
foreach (var old in manifest.OldPluginPaths ?? Array.Empty<string>())
|
||||
{
|
||||
var stale = Path.Combine(recRoomRoot, old.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (File.Exists(stale))
|
||||
{
|
||||
File.Delete(stale);
|
||||
log.AppendLine($"[patch] removed stale plugin: {old}");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Copy the DorkNet plugin DLL.
|
||||
if (string.IsNullOrEmpty(manifest.PluginDll))
|
||||
return PatchResult.Failure("manifest.plugin_dll is required.");
|
||||
var dllSrc = Path.Combine(patcherDir, manifest.PluginDll);
|
||||
if (!File.Exists(dllSrc))
|
||||
return PatchResult.Failure($"plugin DLL listed in manifest but missing on disk: {dllSrc}");
|
||||
var pluginDestDir = Path.Combine(recRoomRoot,
|
||||
(manifest.PluginDest ?? DefaultPluginDest).Replace('/', Path.DirectorySeparatorChar));
|
||||
Directory.CreateDirectory(pluginDestDir);
|
||||
var dllDest = Path.Combine(pluginDestDir, Path.GetFileName(manifest.PluginDll));
|
||||
File.Copy(dllSrc, dllDest, overwrite: true);
|
||||
log.AppendLine($"[patch] installed plugin: {Path.GetFileName(manifest.PluginDll)} → {pluginDestDir}");
|
||||
|
||||
// 4. Render the config template.
|
||||
if (!string.IsNullOrEmpty(manifest.ConfigTemplate))
|
||||
{
|
||||
var templatePath = Path.Combine(patcherDir, manifest.ConfigTemplate);
|
||||
if (!File.Exists(templatePath))
|
||||
return PatchResult.Failure($"config template listed in manifest but missing: {templatePath}");
|
||||
|
||||
var configDestRel = (manifest.ConfigDest ?? DefaultConfigDest)
|
||||
.Replace('/', Path.DirectorySeparatorChar);
|
||||
var configDest = Path.Combine(recRoomRoot, configDestRel);
|
||||
var configDestDir = Path.GetDirectoryName(configDest);
|
||||
if (!string.IsNullOrEmpty(configDestDir)) Directory.CreateDirectory(configDestDir);
|
||||
|
||||
var rendered = File.ReadAllText(templatePath)
|
||||
.Replace("{HOST}", apexHost ?? "")
|
||||
.Replace("{PHOTON_APPID}", photonAppId ?? "")
|
||||
.Replace("{PHOTON_VOICE_APPID}",
|
||||
string.IsNullOrEmpty(photonVoiceAppId) ? photonAppId ?? "" : photonVoiceAppId);
|
||||
File.WriteAllText(configDest, rendered);
|
||||
log.AppendLine($"[patch] wrote config: {Path.GetFileName(configDest)}");
|
||||
}
|
||||
|
||||
log.AppendLine("[patch] done.");
|
||||
return PatchResult.Success(log.ToString());
|
||||
}
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
catch (Exception ex)
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
WorkingDirectory = patcherDir,
|
||||
};
|
||||
psi.ArgumentList.Add("-NoProfile");
|
||||
psi.ArgumentList.Add("-ExecutionPolicy"); psi.ArgumentList.Add("Bypass");
|
||||
psi.ArgumentList.Add("-File"); psi.ArgumentList.Add(script);
|
||||
psi.ArgumentList.Add("-RecRoomPath"); psi.ArgumentList.Add(recRoomPath);
|
||||
psi.ArgumentList.Add("-PhotonAppId"); psi.ArgumentList.Add(photonAppId);
|
||||
psi.ArgumentList.Add("-PhotonVoiceAppId");
|
||||
psi.ArgumentList.Add(string.IsNullOrEmpty(photonVoiceAppId) ? photonAppId : photonVoiceAppId);
|
||||
if (!string.IsNullOrEmpty(apexHost))
|
||||
{
|
||||
psi.ArgumentList.Add("-ServerHost"); psi.ArgumentList.Add(apexHost);
|
||||
log.AppendLine();
|
||||
log.AppendLine("[patch] FAILED: " + ex.Message);
|
||||
return PatchResult.Failure(log.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
using var p = Process.Start(psi)
|
||||
?? throw new InvalidOperationException("Failed to spawn powershell.exe");
|
||||
var stdout = await p.StandardOutput.ReadToEndAsync(ct);
|
||||
var stderr = await p.StandardError.ReadToEndAsync(ct);
|
||||
await p.WaitForExitAsync(ct);
|
||||
/// <summary>Extracts a zip on top of an existing directory, replacing
|
||||
/// any files that collide. <see cref="ZipFile.ExtractToDirectory"/>
|
||||
/// throws on existing files; we walk entries ourselves. Also guards
|
||||
/// against zip-slip path-traversal entries (file names that try to
|
||||
/// escape destRoot via <c>../</c>).</summary>
|
||||
private static void ExtractZipOverwrite(string zipPath, string destRoot)
|
||||
{
|
||||
var destFull = Path.GetFullPath(destRoot);
|
||||
using var zip = ZipFile.OpenRead(zipPath);
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
var safeName = entry.FullName.Replace('/', Path.DirectorySeparatorChar);
|
||||
var fullDest = Path.GetFullPath(Path.Combine(destRoot, safeName));
|
||||
if (!fullDest.StartsWith(destFull + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|
||||
&& fullDest != destFull)
|
||||
throw new InvalidOperationException(
|
||||
$"Refusing to extract '{entry.FullName}' — path escapes destination root.");
|
||||
|
||||
return p.ExitCode == 0
|
||||
? PatchResult.Success(stdout)
|
||||
: PatchResult.Failure(string.IsNullOrEmpty(stderr) ? stdout : stderr);
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
Directory.CreateDirectory(fullDest);
|
||||
continue;
|
||||
}
|
||||
var dir = Path.GetDirectoryName(fullDest);
|
||||
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||
entry.ExtractToFile(fullDest, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PatcherManifest
|
||||
{
|
||||
[JsonPropertyName("$schema_version")] public int SchemaVersion { get; set; }
|
||||
[JsonPropertyName("loader_archive")] public string? LoaderArchive { get; set; }
|
||||
[JsonPropertyName("plugin_dll")] public string? PluginDll { get; set; }
|
||||
[JsonPropertyName("plugin_dest")] public string? PluginDest { get; set; }
|
||||
[JsonPropertyName("config_template")] public string? ConfigTemplate { get; set; }
|
||||
[JsonPropertyName("config_dest")] public string? ConfigDest { get; set; }
|
||||
[JsonPropertyName("old_plugin_paths")] public string[]? OldPluginPaths { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace DorkNet.Launcher.Backend;
|
||||
|
||||
/// <summary>Launches the user's already-patched Rec Room install
|
||||
/// directly via <c>Recroom_Release.exe</c>. Never goes through Steam —
|
||||
/// DorkNet users may not own the game on Steam.</summary>
|
||||
public static class RecRoomLauncher
|
||||
{
|
||||
/// <summary>The picked install path is the <c>Recroom_Release_Data</c>
|
||||
/// folder; the executable lives at <c><parent>\Recroom_Release.exe</c>.
|
||||
/// Returns true if Process.Start succeeded.</summary>
|
||||
public static bool TryLaunch(string recRoomDataPath, out string error)
|
||||
{
|
||||
error = "";
|
||||
if (string.IsNullOrEmpty(recRoomDataPath))
|
||||
{
|
||||
error = "No Rec Room install picked.";
|
||||
return false;
|
||||
}
|
||||
var parent = Path.GetDirectoryName(recRoomDataPath.TrimEnd(Path.DirectorySeparatorChar));
|
||||
if (string.IsNullOrEmpty(parent))
|
||||
{
|
||||
error = $"Couldn't resolve the parent directory of '{recRoomDataPath}'.";
|
||||
return false;
|
||||
}
|
||||
var exe = Path.Combine(parent, "Recroom_Release.exe");
|
||||
if (!File.Exists(exe))
|
||||
{
|
||||
error = $"Recroom_Release.exe not found at '{exe}'. " +
|
||||
"Pick the *_Data folder that sits next to it.";
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = exe,
|
||||
WorkingDirectory = parent,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+389
-268
@@ -2,284 +2,405 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="DorkNet"
|
||||
Width="900" Height="640"
|
||||
MinWidth="640" MinHeight="480"
|
||||
Width="960" Height="700"
|
||||
MinWidth="720" MinHeight="540"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource Bg}"
|
||||
TextElement.Foreground="{StaticResource Ink}"
|
||||
FontFamily="Segoe UI">
|
||||
FontFamily="Segoe UI Variable, Segoe UI"
|
||||
TextOptions.TextFormattingMode="Ideal"
|
||||
TextOptions.TextRenderingMode="ClearType">
|
||||
|
||||
<Grid Margin="32,24">
|
||||
<!-- View slots — only one is visible at a time. CodeBehind toggles
|
||||
Visibility based on AppState.Mode + the settings drawer flag. -->
|
||||
<!-- Soft radial-ish glow in the top-left corner of the window for
|
||||
atmosphere. WPF doesn't have a true radial gradient on Background
|
||||
without nested Border tricks, so we stack a linear-gradient
|
||||
overlay on top of the solid Bg. -->
|
||||
<Grid>
|
||||
<Grid.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#2A1E48" Offset="0" />
|
||||
<GradientStop Color="#16102A" Offset="0.6" />
|
||||
<GradientStop Color="#16102A" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Grid.Background>
|
||||
|
||||
<!-- ── First-run wizard ───────────────────────────────────────── -->
|
||||
<StackPanel x:Name="FirstRunView" Visibility="Visible">
|
||||
<StackPanel HorizontalAlignment="Center" Margin="0,16,0,24">
|
||||
<TextBlock Text="DorkNet"
|
||||
FontSize="36" FontWeight="Bold"
|
||||
Foreground="{StaticResource Brand}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="Self-hostable 2020 Rec Room backend"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
HorizontalAlignment="Center" Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
<Grid Margin="36,28">
|
||||
<!-- View slots — only one Visible at a time; code-behind toggles. -->
|
||||
|
||||
<Grid Margin="0,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Column="0" Style="{StaticResource CardBtn}"
|
||||
Click="OnPickHost">
|
||||
<StackPanel>
|
||||
<TextBlock Text="🛠" FontSize="28" Margin="0,0,0,8" />
|
||||
<TextBlock Text="Host a server" FontSize="16"
|
||||
FontWeight="SemiBold" Margin="0,0,0,8" />
|
||||
<TextBlock Foreground="{StaticResource InkSoft}"
|
||||
FontSize="12" TextWrapping="Wrap"
|
||||
Text="Run a server for you and your friends. Picks a Rec Room version, downloads the matching server, opens a tunnel, shares a join code." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="2" Style="{StaticResource CardBtn}"
|
||||
Click="OnPickJoin">
|
||||
<StackPanel>
|
||||
<TextBlock Text="🎮" FontSize="28" Margin="0,0,0,8" />
|
||||
<TextBlock Text="Join a friend" FontSize="16"
|
||||
FontWeight="SemiBold" Margin="0,0,0,8" />
|
||||
<TextBlock Foreground="{StaticResource InkSoft}"
|
||||
FontSize="12" TextWrapping="Wrap"
|
||||
Text="Paste a join code, point at your Rec Room install. The launcher fetches the matching patcher." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="You can switch modes later under Settings."
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{StaticResource InkFaint}"
|
||||
FontSize="11" Margin="0,24,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Host view ──────────────────────────────────────────────── -->
|
||||
<Grid x:Name="HostView" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0" Margin="0,0,0,16">
|
||||
<TextBlock Text="Host" FontSize="20" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<Button HorizontalAlignment="Right" Style="{StaticResource Btn}"
|
||||
Content="Settings" Click="OnOpenSettings" />
|
||||
</Grid>
|
||||
<Border Grid.Row="0" VerticalAlignment="Bottom"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1" />
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="REC ROOM INSTALL" Style="{StaticResource FieldLabel}" />
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
|
||||
Content="Pick..." Click="OnPickRecRoom" Margin="8,0,0,0" />
|
||||
<Border Background="{StaticResource BgElev}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="8,6">
|
||||
<TextBlock x:Name="HostRecRoomPath"
|
||||
Text="(not picked)"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="SERVER NAME (SHOWN TO JOINERS)" Style="{StaticResource FieldLabel}" />
|
||||
<TextBox x:Name="ServerNameInput"
|
||||
LostFocus="OnServerNameChanged" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="REC ROOM VERSION TO HOST FOR" Style="{StaticResource FieldLabel}" />
|
||||
<ComboBox x:Name="VersionSelect" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="PHOTON APPIDS (FREE AT DASHBOARD.PHOTONENGINE.COM)" Style="{StaticResource FieldLabel}" />
|
||||
<TextBox x:Name="PhotonAppIdInput"
|
||||
Margin="0,0,0,6"
|
||||
LostFocus="OnPhotonChanged" />
|
||||
<TextBox x:Name="PhotonVoiceAppIdInput"
|
||||
Margin="0,0,0,6"
|
||||
LostFocus="OnPhotonChanged" />
|
||||
<ComboBox x:Name="PhotonRegionSelect"
|
||||
SelectionChanged="OnPhotonChanged">
|
||||
<ComboBoxItem Content="US" Tag="us" />
|
||||
<ComboBoxItem Content="EU" Tag="eu" />
|
||||
<ComboBoxItem Content="Asia" Tag="asia" />
|
||||
<ComboBoxItem Content="Japan" Tag="jp" />
|
||||
<ComboBoxItem Content="South America" Tag="sa" />
|
||||
<ComboBoxItem Content="Korea" Tag="kr" />
|
||||
<ComboBoxItem Content="India" Tag="in" />
|
||||
<ComboBoxItem Content="Australia" Tag="au" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,8">
|
||||
<Button x:Name="HostStartBtn" Style="{StaticResource PrimaryBtn}"
|
||||
Content="Start hosting" Click="OnHostStart" />
|
||||
<Button x:Name="HostStopBtn" Style="{StaticResource Btn}"
|
||||
Content="Stop" Click="OnHostStop"
|
||||
Margin="8,0,0,0" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Background="{StaticResource BgElev}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="12" MinHeight="40" Margin="0,8,0,16">
|
||||
<TextBlock x:Name="HostStatusText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextWrapping="Wrap" Text="Idle." />
|
||||
</Border>
|
||||
|
||||
<StackPanel x:Name="JoinCodePanel" Visibility="Collapsed">
|
||||
<TextBlock Text="JOIN CODE (SHARE THIS)" Style="{StaticResource FieldLabel}" />
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
|
||||
Content="Copy" Click="OnCopyJoinCode" Margin="8,0,0,0" />
|
||||
<Border Background="{StaticResource BgElev2}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="8,6">
|
||||
<TextBox x:Name="JoinCodeText" IsReadOnly="True"
|
||||
FontFamily="Consolas" FontSize="11"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
|
||||
Text="Joiners paste this into their DorkNet launcher's Join screen."
|
||||
Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Join view ──────────────────────────────────────────────── -->
|
||||
<Grid x:Name="JoinView" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0" Margin="0,0,0,16">
|
||||
<TextBlock Text="Join" FontSize="20" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<Button HorizontalAlignment="Right" Style="{StaticResource Btn}"
|
||||
Content="Settings" Click="OnOpenSettings" />
|
||||
</Grid>
|
||||
<Border Grid.Row="0" VerticalAlignment="Bottom"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1" />
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="PASTE THE JOIN CODE YOUR HOST SHARED" Style="{StaticResource FieldLabel}" />
|
||||
<TextBox x:Name="JoinCodeInput"
|
||||
AcceptsReturn="True" TextWrapping="Wrap"
|
||||
Height="60" VerticalContentAlignment="Top"
|
||||
TextChanged="OnJoinCodeInputChanged" />
|
||||
<Button Style="{StaticResource Btn}"
|
||||
Content="Preview" Click="OnDecodeJoinCode"
|
||||
HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<Border x:Name="JoinPreviewBorder" Visibility="Collapsed"
|
||||
Background="{StaticResource BgElev}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="12" Margin="0,0,0,16">
|
||||
<TextBlock x:Name="JoinPreviewText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="REC ROOM INSTALL" Style="{StaticResource FieldLabel}" />
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
|
||||
Content="Pick..." Click="OnPickRecRoom" Margin="8,0,0,0" />
|
||||
<Border Background="{StaticResource BgElev}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="8,6">
|
||||
<TextBlock x:Name="JoinRecRoomPath"
|
||||
Text="(not picked)"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button x:Name="JoinApplyBtn" Style="{StaticResource PrimaryBtn}"
|
||||
Content="Patch and launch" Click="OnJoinApply"
|
||||
HorizontalAlignment="Left" IsEnabled="False"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<Border Background="{StaticResource BgElev}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="12" MinHeight="40">
|
||||
<TextBlock x:Name="JoinStatusText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextWrapping="Wrap" Text="Idle." />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Settings view ──────────────────────────────────────────── -->
|
||||
<Grid x:Name="SettingsView" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0" Margin="0,0,0,16">
|
||||
<TextBlock Text="Settings" FontSize="20" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<Button HorizontalAlignment="Right" Style="{StaticResource Btn}"
|
||||
Content="← Back" Click="OnBackFromSettings" />
|
||||
</Grid>
|
||||
<Border Grid.Row="0" VerticalAlignment="Bottom"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1" />
|
||||
|
||||
<StackPanel Grid.Row="1">
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="MODE" Style="{StaticResource FieldLabel}" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Style="{StaticResource Btn}"
|
||||
Content="Switch to Host" Click="OnPickHost" />
|
||||
<Button Style="{StaticResource Btn}"
|
||||
Content="Switch to Join" Click="OnPickJoin"
|
||||
Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="CACHE LOCATIONS" Style="{StaticResource FieldLabel}" />
|
||||
<TextBlock x:Name="CachePathsText"
|
||||
<!-- ── First-run wizard ───────────────────────────────────── -->
|
||||
<StackPanel x:Name="FirstRunView" Visibility="Visible"
|
||||
VerticalAlignment="Center">
|
||||
<StackPanel HorizontalAlignment="Center" Margin="0,0,0,40">
|
||||
<TextBlock Text="DorkNet"
|
||||
FontSize="56" FontWeight="Bold"
|
||||
Foreground="{StaticResource WordmarkBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
FontFamily="Segoe UI Variable, Segoe UI" />
|
||||
<TextBlock Text="Play together on your own Rec Room server"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
FontSize="11" TextWrapping="Wrap" />
|
||||
FontSize="16"
|
||||
HorizontalAlignment="Center" Margin="0,6,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="ABOUT" Style="{StaticResource FieldLabel}" />
|
||||
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="11">
|
||||
DorkNet Launcher v0.1 —
|
||||
<Hyperlink NavigateUri="https://github.com/DorkSquadRR/DorkNet"
|
||||
Foreground="{StaticResource BrandHover}"
|
||||
RequestNavigate="OnNavigate">GitHub</Hyperlink>
|
||||
— AGPL-3.0
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<Grid Margin="40,0" MaxWidth="780" HorizontalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Column="0" Style="{StaticResource ChoiceCardBtn}"
|
||||
Background="{StaticResource CardBgHost}"
|
||||
Click="OnPickHost">
|
||||
<StackPanel>
|
||||
<Border CornerRadius="14" Width="56" Height="56"
|
||||
Background="#3D2B57" Margin="0,0,0,18"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock Text="🛠" FontSize="32"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<TextBlock Text="Host a server" FontSize="22"
|
||||
FontWeight="Bold" Margin="0,0,0,10" />
|
||||
<TextBlock Foreground="{StaticResource InkSoft}"
|
||||
FontSize="13" TextWrapping="Wrap" LineHeight="20"
|
||||
Text="Run a server for you and your friends. Pick a Rec Room version, the launcher downloads the matching server, opens a tunnel, and hands you a shareable join code." />
|
||||
<Border Margin="0,18,0,0" CornerRadius="8"
|
||||
Background="{StaticResource Brand}" Padding="14,8"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock Text="Get started →" Foreground="White"
|
||||
FontWeight="SemiBold" FontSize="13" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="2" Style="{StaticResource ChoiceCardBtn}"
|
||||
Background="{StaticResource CardBgJoin}"
|
||||
Click="OnPickJoin">
|
||||
<StackPanel>
|
||||
<Border CornerRadius="14" Width="56" Height="56"
|
||||
Background="#1C3C4A" Margin="0,0,0,18"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock Text="🎮" FontSize="32"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<TextBlock Text="Join a friend" FontSize="22"
|
||||
FontWeight="Bold" Margin="0,0,0,10" />
|
||||
<TextBlock Foreground="{StaticResource InkSoft}"
|
||||
FontSize="13" TextWrapping="Wrap" LineHeight="20"
|
||||
Text="Paste a join code from your host, point at your Rec Room install. The launcher fetches the matching patcher and gets you connected." />
|
||||
<Border Margin="0,18,0,0" CornerRadius="8"
|
||||
Background="{StaticResource Teal}" Padding="14,8"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock Text="Get started →" Foreground="#0E0A1F"
|
||||
FontWeight="SemiBold" FontSize="13" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="You can switch modes anytime under Settings."
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{StaticResource InkFaint}"
|
||||
FontSize="12" Margin="0,32,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Host view ──────────────────────────────────────────── -->
|
||||
<Grid x:Name="HostView" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header bar with wordmark + view label + settings button -->
|
||||
<Grid Grid.Row="0" Margin="0,0,0,28">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="DorkNet" FontSize="22" FontWeight="Bold"
|
||||
Foreground="{StaticResource WordmarkBrush}" />
|
||||
<Border Style="{StaticResource StatusChip}" Margin="14,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="HOST MODE" FontSize="10"
|
||||
FontWeight="Bold" Foreground="{StaticResource Brand}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button HorizontalAlignment="Right" Style="{StaticResource GhostBtn}"
|
||||
Content="Settings" Click="OnOpenSettings" />
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
Padding="0,0,8,0">
|
||||
<StackPanel>
|
||||
<!-- Setup panel -->
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Setup" />
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="REC ROOM INSTALL" />
|
||||
<DockPanel Margin="0,0,0,16">
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
|
||||
Content="Pick…" Click="OnPickRecRoom" Margin="8,0,0,0" />
|
||||
<Border Background="{StaticResource BgDeeper}"
|
||||
BorderBrush="{StaticResource BorderSoft}" BorderThickness="1"
|
||||
CornerRadius="10" Padding="12,10">
|
||||
<TextBlock x:Name="HostRecRoomPath"
|
||||
Text="(not picked)"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextTrimming="CharacterEllipsis" FontSize="13" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="SERVER NAME (SHOWN TO JOINERS)" />
|
||||
<TextBox x:Name="ServerNameInput" Margin="0,0,0,16"
|
||||
LostFocus="OnServerNameChanged" />
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="REC ROOM VERSION TO HOST FOR" />
|
||||
<ComboBox x:Name="VersionSelect" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Photon panel -->
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Photon AppIds" />
|
||||
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="12"
|
||||
Margin="0,-8,0,12" TextWrapping="Wrap">
|
||||
Photon Cloud powers in-room realtime sync. Free to register at
|
||||
<Hyperlink NavigateUri="https://dashboard.photonengine.com"
|
||||
Foreground="{StaticResource Brand}"
|
||||
RequestNavigate="OnNavigate">dashboard.photonengine.com</Hyperlink>;
|
||||
DorkNet never bundles one.
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="REALTIME APPID" />
|
||||
<TextBox x:Name="PhotonAppIdInput" Margin="0,0,0,12"
|
||||
LostFocus="OnPhotonChanged" />
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="VOICE APPID (OPTIONAL)" />
|
||||
<TextBox x:Name="PhotonVoiceAppIdInput" Margin="0,0,0,12"
|
||||
LostFocus="OnPhotonChanged" />
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="CLOUD REGION" />
|
||||
<ComboBox x:Name="PhotonRegionSelect"
|
||||
SelectionChanged="OnPhotonChanged">
|
||||
<ComboBoxItem Content="US" Tag="us" />
|
||||
<ComboBoxItem Content="EU" Tag="eu" />
|
||||
<ComboBoxItem Content="Asia" Tag="asia" />
|
||||
<ComboBoxItem Content="Japan" Tag="jp" />
|
||||
<ComboBoxItem Content="South America" Tag="sa" />
|
||||
<ComboBoxItem Content="Korea" Tag="kr" />
|
||||
<ComboBoxItem Content="India" Tag="in" />
|
||||
<ComboBoxItem Content="Australia" Tag="au" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Action area -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4,0,16">
|
||||
<Button x:Name="HostStartBtn" Style="{StaticResource PrimaryBtn}"
|
||||
Content="Start hosting" Click="OnHostStart" />
|
||||
<Button x:Name="HostStopBtn" Style="{StaticResource Btn}"
|
||||
Content="Stop" Click="OnHostStop"
|
||||
Margin="10,0,0,0" Visibility="Collapsed" />
|
||||
<Button x:Name="HostLaunchBtn" Style="{StaticResource TealBtn}"
|
||||
Content="Launch Rec Room" Click="OnLaunchRecRoom"
|
||||
Margin="10,0,0,0" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Status -->
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Status" />
|
||||
<TextBlock x:Name="HostStatusText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextWrapping="Wrap" Text="Idle." FontSize="14" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Join code -->
|
||||
<Border x:Name="JoinCodePanel" Visibility="Collapsed"
|
||||
Style="{StaticResource Panel}"
|
||||
Background="{StaticResource CardBgHost}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}"
|
||||
Text="🎉 Share this with friends" />
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource PrimaryBtn}"
|
||||
Content="Copy" Click="OnCopyJoinCode" Margin="10,0,0,0" />
|
||||
<Border Background="{StaticResource BgDeeper}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="10" Padding="12,10">
|
||||
<TextBox x:Name="JoinCodeText" IsReadOnly="True"
|
||||
FontFamily="Cascadia Mono, Consolas" FontSize="12"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
|
||||
Text="Paste this in their DorkNet launcher's Join screen."
|
||||
Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Join view ──────────────────────────────────────────── -->
|
||||
<Grid x:Name="JoinView" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="0,0,0,28">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="DorkNet" FontSize="22" FontWeight="Bold"
|
||||
Foreground="{StaticResource WordmarkBrush}" />
|
||||
<Border Style="{StaticResource StatusChip}" Margin="14,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="JOIN MODE" FontSize="10"
|
||||
FontWeight="Bold" Foreground="{StaticResource Teal}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button HorizontalAlignment="Right" Style="{StaticResource GhostBtn}"
|
||||
Content="Settings" Click="OnOpenSettings" />
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
Padding="0,0,8,0">
|
||||
<StackPanel>
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Join code" />
|
||||
|
||||
<TextBlock Style="{StaticResource FieldLabel}"
|
||||
Text="PASTE THE JOIN CODE YOUR HOST SHARED" />
|
||||
<TextBox x:Name="JoinCodeInput"
|
||||
AcceptsReturn="True" TextWrapping="Wrap"
|
||||
Height="80" VerticalContentAlignment="Top"
|
||||
FontFamily="Cascadia Mono, Consolas" FontSize="12"
|
||||
TextChanged="OnJoinCodeInputChanged" />
|
||||
<Button Style="{StaticResource Btn}"
|
||||
Content="Preview" Click="OnDecodeJoinCode"
|
||||
HorizontalAlignment="Left" Margin="0,10,0,0" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="JoinPreviewBorder" Visibility="Collapsed"
|
||||
Style="{StaticResource Panel}"
|
||||
Background="{StaticResource CardBgJoin}"
|
||||
Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Server preview" />
|
||||
<TextBlock x:Name="JoinPreviewText"
|
||||
Foreground="{StaticResource Ink}"
|
||||
TextWrapping="Wrap" FontSize="14" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Your Rec Room install" />
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="LOCATION" />
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
|
||||
Content="Pick…" Click="OnPickRecRoom" Margin="8,0,0,0" />
|
||||
<Border Background="{StaticResource BgDeeper}"
|
||||
BorderBrush="{StaticResource BorderSoft}" BorderThickness="1"
|
||||
CornerRadius="10" Padding="12,10">
|
||||
<TextBlock x:Name="JoinRecRoomPath"
|
||||
Text="(not picked)"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextTrimming="CharacterEllipsis" FontSize="13" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4,0,16">
|
||||
<Button x:Name="JoinApplyBtn" Style="{StaticResource PrimaryBtn}"
|
||||
Content="Patch" Click="OnJoinApply"
|
||||
IsEnabled="False" />
|
||||
<Button x:Name="JoinLaunchBtn" Style="{StaticResource TealBtn}"
|
||||
Content="Launch Rec Room" Click="OnLaunchRecRoom"
|
||||
Margin="10,0,0,0" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Style="{StaticResource Panel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Status" />
|
||||
<TextBlock x:Name="JoinStatusText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextWrapping="Wrap" Text="Idle." FontSize="14" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Settings view ──────────────────────────────────────── -->
|
||||
<Grid x:Name="SettingsView" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="0,0,0,28">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="DorkNet" FontSize="22" FontWeight="Bold"
|
||||
Foreground="{StaticResource WordmarkBrush}" />
|
||||
<Border Style="{StaticResource StatusChip}" Margin="14,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="SETTINGS" FontSize="10"
|
||||
FontWeight="Bold" Foreground="{StaticResource Sunshine}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button HorizontalAlignment="Right" Style="{StaticResource GhostBtn}"
|
||||
Content="← Back" Click="OnBackFromSettings" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="1">
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Switch mode" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Style="{StaticResource Btn}"
|
||||
Content="Host" Click="OnPickHost" />
|
||||
<Button Style="{StaticResource Btn}"
|
||||
Content="Join" Click="OnPickJoin"
|
||||
Margin="10,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Cache locations" />
|
||||
<TextBlock x:Name="CachePathsText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
FontSize="12" TextWrapping="Wrap"
|
||||
FontFamily="Cascadia Mono, Consolas" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource Panel}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="About" />
|
||||
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="13">
|
||||
DorkNet Launcher v0.1 ·
|
||||
<Hyperlink NavigateUri="https://github.com/DorkSquadRR/DorkNet"
|
||||
Foreground="{StaticResource Brand}"
|
||||
RequestNavigate="OnNavigate">github.com/DorkSquadRR/DorkNet</Hyperlink>
|
||||
· AGPL-3.0
|
||||
</TextBlock>
|
||||
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
|
||||
Margin="0,8,0,0">
|
||||
Not affiliated with Rec Room Inc. or Against Gravity.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -238,7 +238,8 @@ public partial class MainWindow : Window
|
||||
});
|
||||
JoinCodeText.Text = code;
|
||||
JoinCodePanel.Visibility = Visibility.Visible;
|
||||
HostStatusText.Text = $"Hosting at {publicUrl}";
|
||||
HostStatusText.Text = $"Hosting at {publicUrl}. Share the join code below or launch Rec Room to test.";
|
||||
HostLaunchBtn.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -253,6 +254,7 @@ public partial class MainWindow : Window
|
||||
await ShutdownServerAsync();
|
||||
HostStartBtn.IsEnabled = true;
|
||||
HostStopBtn.Visibility = Visibility.Collapsed;
|
||||
HostLaunchBtn.Visibility = Visibility.Collapsed;
|
||||
JoinCodePanel.Visibility = Visibility.Collapsed;
|
||||
HostStatusText.Text = "Stopped.";
|
||||
}
|
||||
@@ -325,12 +327,22 @@ public partial class MainWindow : Window
|
||||
payload.PhotonAppId, payload.PhotonVoiceAppId, payload.Host);
|
||||
if (!patch.Ok) { ShowError("Patcher failed: " + Truncate(patch.Log, 500)); return; }
|
||||
|
||||
JoinStatusText.Text = "Patched. Launch Rec Room from Steam to connect.";
|
||||
JoinStatusText.Text = $"Patched and ready. Connecting to {payload.Host}. " +
|
||||
"Click Launch Rec Room when you're ready to play.";
|
||||
JoinLaunchBtn.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch (Exception ex) { ShowError(ex.Message); }
|
||||
finally { RefreshJoinReadiness(); }
|
||||
}
|
||||
|
||||
private void OnLaunchRecRoom(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_state.RecRoomPath))
|
||||
{ ShowError("Pick your Rec Room install first."); return; }
|
||||
if (!RecRoomLauncher.TryLaunch(_state.RecRoomPath, out var error))
|
||||
ShowError(error);
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
private static string FormatProgress(string label, DownloadProgress p)
|
||||
{
|
||||
|
||||
+78
-29
@@ -6,8 +6,8 @@ branch, then fetches release artifacts from each branch's GitHub
|
||||
Releases.
|
||||
|
||||
Per-version branches must publish releases that follow this contract,
|
||||
otherwise the launcher reports "no release found" to users on host /
|
||||
join.
|
||||
otherwise the launcher reports "no release found" / "manifest missing"
|
||||
to users on host / join.
|
||||
|
||||
## Tag naming
|
||||
|
||||
@@ -26,12 +26,10 @@ Examples (matching the current `release_tag_prefix` values):
|
||||
| `march-2020-03-10` | `v1-march` | `v1-march-2026.06.15` |
|
||||
|
||||
The launcher picks the most-recent release (by `published_at`) whose
|
||||
tag starts with the prefix, then looks for the asset names below.
|
||||
tag starts with the prefix, then looks for the two asset names below.
|
||||
|
||||
## Required assets
|
||||
|
||||
Each release must attach these two files:
|
||||
|
||||
### 1. `dorknet-server-{version_key}-win-x64.zip`
|
||||
|
||||
A self-contained .NET 9 publish of `DorkNet.Server` for `win-x64`.
|
||||
@@ -42,42 +40,93 @@ Build with:
|
||||
```pwsh
|
||||
cd DorkNet.Server
|
||||
dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true
|
||||
# Then zip the publish dir's contents (NOT the dir itself):
|
||||
cd bin/Release/net9.0/win-x64/publish
|
||||
Compress-Archive -Path * -DestinationPath ../../../../../dorknet-server-{version_key}-win-x64.zip
|
||||
```
|
||||
|
||||
The zip's root must contain `DorkNet.Server.exe` (or any `.exe`; the
|
||||
launcher scans for it). Folder layout below that is preserved as-is.
|
||||
launcher scans for it).
|
||||
|
||||
### 2. `dorknet-clientpatch-{version_key}.zip`
|
||||
|
||||
The contents of the per-version branch's `tools/` directory limited
|
||||
to the patcher scripts the launcher invokes:
|
||||
The MelonLoader-based client patcher, packed into a single zip with a
|
||||
**manifest.json** at the zip root telling the launcher what to do.
|
||||
The launcher performs every step in C# — no PowerShell scripts run.
|
||||
|
||||
- `install-plugin.ps1` (preferred — BepInEx route)
|
||||
- `install-legacy-client.ps1` (fallback — direct byte-patch route)
|
||||
- Any helper scripts these reference
|
||||
- The compiled `DorkNet.ClientPatch.dll` if `install-plugin.ps1`
|
||||
expects a prebuilt DLL
|
||||
|
||||
The launcher invokes the patcher script with these arguments:
|
||||
**Required layout inside the zip:**
|
||||
|
||||
```
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File install-plugin.ps1 `
|
||||
-RecRoomPath <user's path> `
|
||||
-PhotonAppId <user's> `
|
||||
-PhotonVoiceAppId <user's> `
|
||||
-ServerHost <apex host or join-code host>
|
||||
manifest.json (described below)
|
||||
MelonLoader.zip (optional — the MelonLoader install tree)
|
||||
DorkNet.ClientMod.dll (required — the DorkNet MelonLoader plugin)
|
||||
dorknet-clientmod.json.template (optional — UserData config template)
|
||||
```
|
||||
|
||||
So the script must accept those four parameters. If your branch's
|
||||
patcher needs additional config, default to safe values.
|
||||
**`manifest.json` schema (v1):**
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema_version": 1,
|
||||
"loader_archive": "MelonLoader.zip",
|
||||
"plugin_dll": "DorkNet.ClientMod.dll",
|
||||
"plugin_dest": "MelonLoader/Mods",
|
||||
"config_template": "dorknet-clientmod.json.template",
|
||||
"config_dest": "MelonLoader/UserData/dorknet-clientmod.json",
|
||||
"old_plugin_paths": [
|
||||
"BepInEx/plugins/DorkNet.ClientPatch.dll"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All fields except `$schema_version` and `plugin_dll` are optional.
|
||||
Defaults:
|
||||
|
||||
| Field | Default |
|
||||
|---|---|
|
||||
| `plugin_dest` | `MelonLoader/Mods` |
|
||||
| `config_dest` | `MelonLoader/UserData/dorknet-clientmod.json` |
|
||||
| `loader_archive` | (none — assumes user has MelonLoader installed) |
|
||||
| `config_template` | (none — no config written) |
|
||||
|
||||
**`config_template` placeholders** rendered before write:
|
||||
|
||||
| Placeholder | Source |
|
||||
|---|---|
|
||||
| `{HOST}` | Host's tunnel hostname (or join code's `host` field) |
|
||||
| `{PHOTON_APPID}` | Host's Photon Realtime AppId |
|
||||
| `{PHOTON_VOICE_APPID}` | Host's Photon Voice AppId (falls back to Realtime if empty) |
|
||||
|
||||
Example template:
|
||||
|
||||
```json
|
||||
{
|
||||
"ServerHost": "{HOST}",
|
||||
"PhotonAppId": "{PHOTON_APPID}",
|
||||
"PhotonVoiceAppId": "{PHOTON_VOICE_APPID}"
|
||||
}
|
||||
```
|
||||
|
||||
## What the launcher does on Apply
|
||||
|
||||
Given the manifest above, the launcher does (in this order):
|
||||
|
||||
1. Extracts `loader_archive` over the user's Rec Room install root
|
||||
(the parent of `Recroom_Release_Data`). MelonLoader's `version.dll`
|
||||
and `MelonLoader/` tree land in the right places.
|
||||
2. Deletes any `old_plugin_paths` (cleans up an old BepInEx-era DLL,
|
||||
a previous plugin filename, etc.).
|
||||
3. Copies `plugin_dll` to `<recroom-root>/<plugin_dest>/`.
|
||||
4. Renders `config_template` (placeholder substitution) and writes
|
||||
to `<recroom-root>/<config_dest>`.
|
||||
|
||||
No PowerShell, no separate Steamless invocation, no byte-level
|
||||
metadata patching in v1 — keep the patcher zip declarative. Future
|
||||
schema versions can add fields for those without breaking older
|
||||
launchers (they ignore unknown fields).
|
||||
|
||||
## Verifying a release before publishing
|
||||
|
||||
```pwsh
|
||||
# Locally simulate the launcher's fetch.
|
||||
$tag = "v1-december-2026.06.15"
|
||||
gh release view $tag --repo DorkSquadRR/DorkNet
|
||||
gh release download $tag --repo DorkSquadRR/DorkNet `
|
||||
@@ -85,12 +134,12 @@ gh release download $tag --repo DorkSquadRR/DorkNet `
|
||||
--pattern "dorknet-clientpatch-december_2020_12_18.zip"
|
||||
```
|
||||
|
||||
Then unzip + boot the server locally to confirm it starts, and
|
||||
unzip + run the patcher against a clean Rec Room install to confirm
|
||||
patching works.
|
||||
Then unzip + inspect: the patcher zip should have `manifest.json` at
|
||||
root, the named DLL should exist, the `config_template` (if listed)
|
||||
should exist, etc.
|
||||
|
||||
## Future: GitHub Actions workflow
|
||||
|
||||
Each per-version branch should land a `.github/workflows/release.yml`
|
||||
that builds + zips + publishes the two artifacts on tag push. Not yet
|
||||
implemented; releases are manual for now.
|
||||
that builds + packages the artifacts on tag push. Not yet implemented;
|
||||
releases are manual for now.
|
||||
|
||||
Reference in New Issue
Block a user