Launch RecRoom.exe directly on 2020.12 EAC builds (skip the
Recroom_Release.exe EAC wrapper); fall back to whichever single
binary exists. Add an opt-in "trust self-signed TLS certs" toggle
that flows through ClientPatcher into the ClientMod config, off by
default since Localtunnel/Tunnelto serve valid certs.
- Default hosting mode is now Localtunnel — public *.loca.lt URL,
no account, auto-fetched on first run. Replaces the prototyped
Tailscale mode.
- Tailscale code purged from launcher + CLI; docs swapped to
Localtunnel throughout (easy-setup, joining-a-server, server-cli,
troubleshooting, faq, READMEs).
- Launcher sidebar gains an Open admin panel button — opens
https://admin.<apex> in the browser once the server is live.
- SQLite db moves to %APPDATA%\DorkNet\dorknet.db (server reads
Database__SqlitePath env var, launcher passes it). Existing dbs
under <serverDir>\data\ are migrated on first run.
- CLI Program.cs: --mode default flips from tailscale to localtunnel;
csproj links in LocaltunnelTunnel + LocaltunnelInstaller.
- New Backend files: LauncherNameServer, LocaltunnelInstaller,
LocaltunnelTunnel, SingleOriginServiceMap.
- .gitignore: exclude installer/out/.
MelonLoader 0.6.x scans <install root>\Mods\ for plugin DLLs and
ignores anything under MelonLoader\Mods\. Our default plugin_dest
was MelonLoader/Mods, so DorkNet.ClientMod.dll landed somewhere
MelonLoader never looked at — Player.log showed '0 Mods loaded.'
and Rec Room booted unpatched, hitting the real rec.net version
check which returns UpdateRequired.
Changed ClientPatcher's default + the manifest doc to 'Mods'.
Per-version branches also need 'plugin_dest' = 'Mods' in their
build-patcher-zip.ps1 manifest (separate commits).
Per-version patcher zips now ship a Goldberg steam_api64.dll +
steam_appid.txt alongside the existing MelonLoader stuff. The
manifest's new steam_stub block describes where to install them:
- api_dll / api_dest: stub DLL location and target path under
Rec Room root (typically Recroom_Release_Data/Plugins/...)
- api_backup_suffix: the original Valve DLL gets renamed to
<dest> + suffix on first install; re-runs detect the existing
backup and leave it alone so the real DLL is preserved
- appid_file / appid_value + appid_dest: writes a steam_appid.txt
next to the exe so the stub knows what game ID to claim
Block is optional — manifests without steam_stub patch nothing
extra, preserving back-compat with pre-stub patcher zips.
Steam-purchased Rec Room ships with SteamStub Variant 3.1 wrapping
Recroom_Release.exe so it refuses to launch without Steam loaded.
DorkNet spawns the exe directly, so for Steam installs we have to
unwrap it first or the user gets a silent crash on Launch.
- SteamDrmStripper.StripIfNeededAsync: PE-walks the exe's section
table looking for the .bind section SteamStub adds, and only
invokes Steamless when that section is present. Re-runs detect
the already-unwrapped exe and short-circuit. Backs up the
original next to the exe as Recroom_Release.exe.steamwrapped.
- SteamlessInstaller: resolves a usable Steamless.CLI.exe via
side-by-side -> %LOCALAPPDATA%\DorkNet\Steamless\ cache ->
download v3.1.0.5 from atom0s/Steamless GitHub releases. Same
pattern as CloudflaredInstaller.
- Inserted as a step between "Download client patcher" and "Patch
your Rec Room install" in both host (5 -> 6 steps) and join
(2 -> 3 steps). Step detail reports Stripped / AlreadyUnpacked /
NotApplicable so the user sees what happened.
Oculus / Quest builds and already-unwrapped installs skip the
Steamless download entirely.
The version dropdown sat silently empty when the GitHub manifest
fetch 404'd (private repo, network down, etc.) and there was no
DORKNET_LOCAL_MANIFEST to fall back to. Two fixes:
- csproj copies the repo-root versions.json next to dorknet.exe on
build, and LocalOverrides.GetLocalManifestPath checks that path
as a fallback after the env var. Dev checkouts against a private
repo now Just Work — no env vars needed.
- When the manifest can't be loaded, RefreshManifestAsync now
inserts a disabled placeholder "(no versions - check internet or
set DORKNET_LOCAL_MANIFEST)" in the dropdown so the user sees the
problem at a glance instead of a silent blank.
- LocalOverrides.DescribeManifestSource shows in Settings whether
the active manifest came from the env var, the side-by-side
fallback, or the live GitHub fetch.
Previously DescribeActive() only returned overrides when the path
resolved, so DORKNET_LOCAL_RELEASES set to a typo'd or unmounted
path would silently fall through to the GitHub fetch and the
Settings panel would stay hidden. Now the panel shows the env
var value with a [SET BUT NOT FOUND on disk] tag when the path
isn't there, so the user can debug 'I set it but it didn't work'
without reading the source.
Set DORKNET_LOCAL_MANIFEST and/or DORKNET_LOCAL_RELEASES to point
the launcher at on-disk artifacts instead of the public repo:
- DORKNET_LOCAL_MANIFEST: full path to a versions.json file.
VersionsManifest.FetchAsync reads it from disk and skips the
GitHub round-trip.
- DORKNET_LOCAL_RELEASES: path to a directory laid out as
{root}/{version_key}/server/ + .../patcher/. EnsureServerAsync /
EnsurePatcherAsync return those paths in-place — no download,
no copy, no marker files.
Both are silent no-ops when unset / missing on disk, so it's safe
to leave them set permanently in a dev profile. When at least one
is active, the Settings view surfaces a "Dev overrides active"
panel listing the paths so it's obvious the launcher isn't using
the public manifest.
Lets contributors iterate on per-version branch changes against
the launcher UI without needing to tag a public Release.
First launch went straight to a Host/Join card picker with no
framing. Replaced with a two-phase onboarding:
WelcomeView — branded intro with the wordmark, three feature
bullets (2 min setup, free, works with regular Rec Room), and one
big "Get started" button. Dismissed via AppState.WelcomeSeen.
SetupView — multi-step wizard with a STEP X OF N indicator:
1. Host vs Join card picker (self-advances on click)
2. Rec Room install (shows auto-detected path with a green "we
found it" banner; Pick... overrides)
3. (Host only) Server name + WHO CAN JOIN radios
4. (Host only) Photon AppIds — "Open setup walkthrough" launches
the existing PhotonWizard, or paste fields below for advanced
users; live status line confirms when AppIds are set
5. Done card with Finish button
Join mode hops 2 -> done since it doesn't need server config or
Photon. AppState.SetupComplete gates entry; future launches skip
the wizard and route directly to HostView / JoinView. Existing
state files get auto-migrated on load (Mode set + RecRoomPath
populated = WelcomeSeen + SetupComplete backfilled to true, then
flushed). Settings -> "Re-run setup" clears the flag for users
who want to walk through again.
Raw exception messages ("Access is denied", HttpRequestException
stack traces) gave the user nothing to do. The flow now translates
the common failures into plain language and offers a one-click
retry that re-runs the host/join flow from the top:
- ErrorTranslator handles HttpRequestException (network down),
TaskCanceledException (timeout), SocketException (firewall),
UnauthorizedAccessException + IOException "in use" (Rec Room
open), missing folder / files (re-pick install), cloudflared
download truncation (suggests LAN mode), port 5005 already bound
- Patcher log output goes through TranslateMessage which catches
the same "access denied" / missing files cases
- HostStepsPanel / JoinStepsPanel gain a Retry + "Get help" row
below the steps; visible only on failure
- "Get help" opens README.md#troubleshooting on GitHub
- Stop / new run hides the retry row so a fresh start is clean
The two empty "REALTIME APPID / VOICE APPID" textboxes assumed the
user already knew what Photon was, where to sign up, and which app
type to create. New 5-step wizard fills that gap:
1. What's Photon? — one-paragraph explanation tying back to DorkNet
2. Make an account — Open Dashboard button launches browser
3. Create a Realtime app — numbered click-by-click instructions
4. Create a Voice app — same, with a Skip button (voice is optional)
5. Paste your AppIds — text fields with live regex validation,
region picker, Save commits to AppState
Hyphens optional on input; we normalise to canonical 8-4-4-4-12 form
before writing back. The existing Photon section keeps its three
fields for advanced users, with a "Setup walkthrough" button at the
top opening the wizard prefilled with current values.
The single Copy button left it on the user to format a message and
get the code into Discord/email. Added a layout that makes sharing
obvious:
- QR encodes the join code text so a friend with a phone can scan
the host's screen and forward the code without typing it
- "Copy as message" formats a one-line "Join {server} on DorkNet!
Download at github.com/... and paste this code:" pitch
- "Email to friend" opens the default mail client with subject +
body prefilled, code in the body
- QR built via QRCoder -> WriteableBitmap directly (no
System.Drawing dependency); palette matches the launcher
The single HostStatusText/JoinStatusText line gave no sense of where
the user was in the flow. Replaced with a step list:
- StartupStep model (INotifyPropertyChanged) tracks Title, State
(Pending/Active/Done/Failed), Detail, optional Progress 0..1
- ItemsControl + DataTemplate in App.xaml renders each step as a
status bullet (empty ring -> filled brand -> green check -> red X),
with conditional detail line + restyled mini progress bar
- StartupFlow.NewHostFlow / NewJoinFlow build the canonical 5-step
host and 2-step join lists; host flow swaps the tunnel step label
for LocalNetwork mode
- OnHostStart / OnJoinApply advance steps via StartStep/CompleteStep/
FailStep helpers and route progress callbacks to UpdateStepProgress
so each download phase has its own bar
- Errors mark the active step Failed with the message inline, leaving
the rest of the list visible so the user sees where it broke
- RecRoomPicker.Detect: scan common manual-install paths (Documents/Recnet/dist, Downloads, C:\Games, etc.) on first run; never touches Steam libraries per policy
- RecRoomVersionDetector: three-signal cascade (version.txt -> PE timestamp -> file size) maps install to a manifest branch so version dropdown defaults correctly
- CloudflaredInstaller: download latest cloudflared-windows-amd64.exe from GitHub releases to %LOCALAPPDATA%\DorkNet on first host; resolves side-by-side -> cache -> PATH -> download
- LocalNetwork.GetLanIp + AppState.HostingMode (Internet|LocalNetwork) + Host view "WHO CAN JOIN?" radio: LAN-only mode skips Cloudflare entirely for families on one network
- OnHostStart branches on HostingMode; status text reflects local IP vs public tunnel URL
DorkNet.ClientMod's config reads PhotonCloudRegion alongside the
AppIds, and the joining client needs the host's region to connect
to the right Photon cloud. Adds {PHOTON_REGION} to the substitution
map and threads region through ClientPatcher.ApplyAsync from both
host (AppState.PhotonRegion) and join (JoinPayload.PhotonRegion)
flows.
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.
Initial v0.1 (commit df9dac0) used PhotinoNET wrapping WebView2 + HTML/JS,
but rendered as a blank window on first run — WebView2 + file:// URI
quirks. Switching to native WPF: built into the .NET 9 Windows SDK
(no NuGet), single-file self-contained publish, instant window open.
All Backend/*.cs orchestration logic carried over unchanged (they're
UI-framework-agnostic). Replaced:
- Program.cs (PhotinoNET bootstrap) -> App.xaml(.cs) + MainWindow.xaml(.cs)
- ui/index.html + style.css + app.js -> MainWindow.xaml (XAML views) +
App.xaml resource styles
- Backend/MessageBridge.cs (JS<->C# router) -> direct method calls in
MainWindow.xaml.cs code-behind
RecRoomPicker now uses Microsoft.Win32.OpenFolderDialog (added in .NET 8)
instead of WinForms FolderBrowserDialog — keeps the project WPF-only.
Backend files needed explicit 'using System.IO;' since the WPF SDK
doesn't include it in implicit usings.
Single-download .NET 9 self-contained launcher for hosting / joining
DorkNet servers. Reads versions.json from main to discover supported
client builds, fetches the matching server binary + client patcher
from GitHub Releases per branch, manages the server subprocess +
Cloudflare quick-tunnel, and wraps it all in a PhotinoNET window.
Code layout:
- Program.cs PhotinoNET window + message bridge bootstrap
- Backend/ AppPaths, AppState, VersionsManifest,
ReleaseDownloader, ServerProcess, ClientPatcher,
Tunnel, RecRoomPicker, JoinCode, MessageBridge
- ui/ index.html + style.css + app.js (no build step)
- RELEASES.md GitHub Releases artifact contract per-version
branches must publish to
Per-version branches need to ship two release assets per tag:
- dorknet-server-{version_key}-win-x64.zip
- dorknet-clientpatch-{version_key}.zip
(see launcher/RELEASES.md for the full naming + arg contract)
Photon AppIds, JWT secret, and the picked Rec Room path stay
per-machine; the launcher never bundles them.