diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..801659b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,82 @@ +# Push / PR CI on `main` — fast feedback that the launcher + CLI +# both still build, and that the CLI publishes for the cross-platform +# RIDs the release workflow will package. Does not produce release +# artifacts — only proves things compile. +# +# Runs on: +# - every push to main +# - every pull request targeting main +# - manual workflow_dispatch (so you can re-run after a flake without +# pushing an empty commit) + +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + DOTNET_VERSION: "9.0.x" + DOTNET_NOLOGO: "true" + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + +jobs: + # ── Launcher needs Windows for the WPF target framework ──────────── + build-launcher: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore + run: dotnet restore launcher/DorkNet.Launcher.csproj + + - name: Build + run: dotnet build launcher/DorkNet.Launcher.csproj -c Release --no-restore + + # A self-contained publish is the closest thing to "what we ship" + # without invoking Inno Setup, which would require an extra + # toolchain install for every CI run. + - name: Publish (smoke) + run: dotnet publish launcher/DorkNet.Launcher.csproj -c Release -r win-x64 --self-contained true --no-restore + + # ── CLI builds on Ubuntu — proves the shared Backend/*.cs files + # truly don't have Windows-only dependencies. The release pipeline + # later publishes the matrix; here we just confirm restore + build + # on the canonical Linux runner. + build-cli: + strategy: + fail-fast: false + matrix: + rid: [win-x64, linux-x64, linux-arm64, osx-x64, osx-arm64] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore + run: dotnet restore cli/DorkNet.ServerCli/DorkNet.ServerCli.csproj + + - name: Publish ${{ matrix.rid }} + run: | + dotnet publish cli/DorkNet.ServerCli/DorkNet.ServerCli.csproj \ + -c Release \ + -r ${{ matrix.rid }} \ + --self-contained true \ + --no-restore + + - name: Smoke-test --help (host RID only) + if: matrix.rid == 'linux-x64' + run: | + out=cli/DorkNet.ServerCli/bin/Release/net9.0/${{ matrix.rid }}/publish/dorknet-server + chmod +x "$out" + "$out" --help diff --git a/.github/workflows/release-launcher.yml b/.github/workflows/release-launcher.yml new file mode 100644 index 0000000..0c498f9 --- /dev/null +++ b/.github/workflows/release-launcher.yml @@ -0,0 +1,221 @@ +# Tagged release pipeline for the host-side tooling on `main` — +# the Windows GUI launcher (installer + portable) and the +# cross-platform dorknet-server CLI. Triggers on a SemVer tag like +# v0.2.0 (or v0.2.0-rc1 for pre-releases). +# +# Produces, on each release: +# * dorknet-setup-.exe Inno Setup installer +# * dorknet.exe Portable single-file Windows build +# * dorknet-server-win-x64.zip Cross-platform server CLI, per RID +# * dorknet-server-linux-x64.tar.gz +# * dorknet-server-linux-arm64.tar.gz +# * dorknet-server-osx-x64.tar.gz +# * dorknet-server-osx-arm64.tar.gz +# * SHA256SUMS Checksums of all of the above +# +# All of these get attached to the GitHub release the tag creates. + +name: release-launcher + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-*" + workflow_dispatch: + inputs: + version: + description: "Version to release (e.g. 0.2.0). Leave blank to use the workflow's git tag." + required: false + +permissions: + contents: write # required to create / update GitHub releases + +env: + DOTNET_VERSION: "9.0.x" + DOTNET_NOLOGO: "true" + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + +jobs: + # ── Resolve the version once and pass it to all downstream jobs ───── + resolve-version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.v.outputs.version }} + steps: + - id: v + shell: bash + run: | + if [[ -n "${{ inputs.version }}" ]]; then + v="${{ inputs.version }}" + else + v="${GITHUB_REF_NAME#v}" + fi + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "Releasing version: $v" + + # ── Windows: GUI launcher (installer + portable) ───────────────────── + build-launcher-windows: + needs: resolve-version + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install Inno Setup + run: choco install innosetup --no-progress -y + + - name: Build installer + portable exe + shell: pwsh + run: | + ./installer/build-installer.ps1 -Version "${{ needs.resolve-version.outputs.version }}" + + - name: Stage Windows artifacts + shell: pwsh + run: | + $stage = "artifacts" + New-Item -ItemType Directory -Force -Path $stage | Out-Null + # Installer + Copy-Item "installer/out/dorknet-setup-${{ needs.resolve-version.outputs.version }}.exe" $stage + # Portable single-file build (same publish output the installer wraps) + Copy-Item "launcher/bin/Release/net9.0-windows/win-x64/publish/dorknet.exe" $stage + + - uses: actions/upload-artifact@v4 + with: + name: launcher-windows + path: artifacts/* + + # ── Cross-platform server CLI, one RID per matrix entry ───────────── + build-server-cli: + needs: resolve-version + strategy: + fail-fast: false + matrix: + include: + - rid: win-x64 + os: windows-2022 + ext: zip + - rid: linux-x64 + os: ubuntu-latest + ext: tar.gz + - rid: linux-arm64 + os: ubuntu-latest + ext: tar.gz + - rid: osx-x64 + os: macos-latest + ext: tar.gz + - rid: osx-arm64 + os: macos-latest + ext: tar.gz + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Publish dorknet-server for ${{ matrix.rid }} + shell: bash + run: | + v="${{ needs.resolve-version.outputs.version }}" + dotnet publish cli/DorkNet.ServerCli \ + -c Release \ + -r "${{ matrix.rid }}" \ + --self-contained true \ + /p:Version="$v" \ + /p:AssemblyVersion="${v}.0" \ + /p:FileVersion="${v}.0" \ + /p:InformationalVersion="$v" + + - name: Package + shell: bash + run: | + src="cli/DorkNet.ServerCli/bin/Release/net9.0/${{ matrix.rid }}/publish" + out="artifacts" + mkdir -p "$out" + case "${{ matrix.ext }}" in + zip) + # Use PowerShell on Windows so the archive is consumable + # by Windows Explorer + curl without weird zip flags. + pwsh -Command "Compress-Archive -Path '$src/*' -DestinationPath '$out/dorknet-server-${{ matrix.rid }}.zip' -Force" + ;; + tar.gz) + # chmod +x first — single-file publish drops a binary + # without the executable bit on macOS in some scenarios. + chmod +x "$src"/dorknet-server || true + tar -czf "$out/dorknet-server-${{ matrix.rid }}.tar.gz" -C "$src" . + ;; + esac + + - uses: actions/upload-artifact@v4 + with: + name: server-cli-${{ matrix.rid }} + path: artifacts/* + + # ── Aggregate, checksum, attach to the release ────────────────────── + release: + needs: [resolve-version, build-launcher-windows, build-server-cli] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: List artifacts + compute SHA256SUMS + run: | + cd dist + ls -la + sha256sum * > SHA256SUMS + cat SHA256SUMS + + - name: Build release notes + id: notes + run: | + v="${{ needs.resolve-version.outputs.version }}" + { + echo "## DorkNet launcher \`v$v\`" + echo "" + echo "Host-side release. For the matching server + client patcher per Rec Room build, see the per-version branch tags." + echo "" + echo "### Downloads" + echo "" + echo "| Platform | Use | Asset |" + echo "| --- | --- | --- |" + echo "| Windows | One-click installer (Start menu + auto-updates) | \`dorknet-setup-$v.exe\` |" + echo "| Windows | Portable single-file launcher | \`dorknet.exe\` |" + echo "| Linux x64 | Headless host CLI | \`dorknet-server-linux-x64.tar.gz\` |" + echo "| Linux arm64 | Headless host CLI | \`dorknet-server-linux-arm64.tar.gz\` |" + echo "| macOS Intel | Headless host CLI | \`dorknet-server-osx-x64.tar.gz\` |" + echo "| macOS Apple Silicon | Headless host CLI | \`dorknet-server-osx-arm64.tar.gz\` |" + echo "| Windows | Headless host CLI (same as GUI) | \`dorknet-server-win-x64.zip\` |" + echo "" + echo "Checksums in \`SHA256SUMS\`." + echo "" + echo "### Setup" + echo "" + echo "- Windows GUI: [easy-setup.md](https://github.com/DorkSquadRR/DorkNet/blob/main/docs/easy-setup.md)" + echo "- Linux / macOS: [server-cli.md](https://github.com/DorkSquadRR/DorkNet/blob/main/docs/server-cli.md)" + echo "- Photon setup: [photon-setup.md](https://github.com/DorkSquadRR/DorkNet/blob/main/docs/photon-setup.md)" + } > notes.md + + - name: Create / update GitHub release + uses: softprops/action-gh-release@v2 + with: + # Prerelease if the tag has a -suffix (e.g. v0.2.0-rc1). + prerelease: ${{ contains(github.ref_name, '-') }} + body_path: notes.md + generate_release_notes: false + files: | + dist/* + # The installer registers HKCU\Software\DorkNet\Version on + # install — that means the auto-updater sees this version + # only after the tag actually exists on GitHub, so we let + # the action create the release on first push of the tag. + fail_on_unmatched_files: true diff --git a/README.md b/README.md index 8a69642..623c644 100644 --- a/README.md +++ b/README.md @@ -31,25 +31,38 @@ branches above. --- -## Two ways to run it +## Ways to run it -### 🎮 Easy mode — desktop app +### Windows GUI — `dorknet` launcher -One download. Pick a client version on first launch. Hosts can switch -between supported builds without re-installing the launcher. +One download, two flavours: an **installer** (Start menu shortcut + +rolling auto-updates) or a **portable `dorknet.exe`** (drop anywhere). +Pick a Rec Room build on first launch, host or join. -- **Host a server** — runs a local server + Cloudflare tunnel for the - client version you choose, patches your Rec Room install, and gives +- **Host a server** — runs a local server + Tunnelto tunnel for the + client version you pick, patches your Rec Room install, and gives you a shareable join code. - **Join a server** — paste the code, point at your Rec Room install, - hit play. The launcher fetches the matching client patcher for the - host's server version. + hit play. The launcher fetches the matching patcher. → **[Easy setup guide](docs/easy-setup.md)** — screenshots, ~5 minutes. -[Download the latest Easy app →](https://github.com/DorkSquadRR/DorkNet/releases/latest) +[Download the latest launcher →](https://github.com/DorkSquadRR/DorkNet/releases/latest) -### 🛠️ Advanced mode — Docker / source +### Linux / macOS host — `dorknet-server` CLI + +Headless host-side CLI for Linux (x64/arm64), macOS (Intel/Apple +Silicon), and Windows. Same server + tunnel pipeline as the GUI, +suitable for a VPS or a permanent home server. Joiners still use the +Windows launcher to apply the patch to their own Rec Room install. + +```sh +dorknet-server --photon-id YOUR-APPID --name "Sunday games" +``` + +→ **[Server CLI guide](docs/server-cli.md)** — install, systemd, launchd. + +### Advanced — Docker / source For community-server hosts, modders, and contributors. Pick the branch matching your client build, clone, run. @@ -105,9 +118,11 @@ branch's `BRANCHES.md` row for the exact deltas. ## Docs - [Branch chart](BRANCHES.md) — which branch matches your client -- [Easy setup](docs/easy-setup.md) — desktop-launcher path +- [Easy setup](docs/easy-setup.md) — Windows GUI launcher +- [Server CLI](docs/server-cli.md) — Linux / macOS / Windows headless - [Advanced setup](docs/advanced-setup.md) — Docker / VPS path - [Joining a server](docs/joining-a-server.md) — for players +- [Photon setup](docs/photon-setup.md) — getting your AppIds - [Architecture](docs/architecture.md) — how the code is laid out - [FAQ](docs/faq.md) - [Troubleshooting](docs/troubleshooting.md) diff --git a/cli/DorkNet.ServerCli/DorkNet.ServerCli.csproj b/cli/DorkNet.ServerCli/DorkNet.ServerCli.csproj new file mode 100644 index 0000000..4d61cde --- /dev/null +++ b/cli/DorkNet.ServerCli/DorkNet.ServerCli.csproj @@ -0,0 +1,66 @@ + + + + Exe + + net9.0 + enable + enable + DorkNet.ServerCli + dorknet-server + true + true + true + true + + + + + + + + + + Backend\AppPaths.cs + + + Backend\AppState.cs + + + Backend\JoinCode.cs + + + Backend\LocalNetwork.cs + + + Backend\LocalOverrides.cs + + + Backend\ReleaseDownloader.cs + + + Backend\ServerProcess.cs + + + Backend\TunneltoTunnel.cs + + + Backend\VersionsManifest.cs + + + + + + + versions.json + PreserveNewest + + + + diff --git a/cli/DorkNet.ServerCli/Program.cs b/cli/DorkNet.ServerCli/Program.cs new file mode 100644 index 0000000..32a481e --- /dev/null +++ b/cli/DorkNet.ServerCli/Program.cs @@ -0,0 +1,284 @@ +using System.Diagnostics; +using DorkNet.Launcher.Backend; + +namespace DorkNet.ServerCli; + +/// Cross-platform host-side CLI for DorkNet. Drives the same +/// server + tunnel pipeline the Windows GUI launcher uses, but headless, +/// so Linux and macOS hosts can serve Windows joiners without needing +/// the WPF launcher (or any GUI). Joiners still use the Windows GUI +/// launcher to apply the patch to their copy of Rec Room. +/// +/// Run dorknet-server --help for the option list. +public static class Program +{ + public static async Task Main(string[] args) + { + var parsed = ParseArgs(args); + if (parsed is null) return 1; // ParseArgs already wrote --help or an error. + + // Validate required values up front so the user sees the + // failure before any download / process spin-up. + if (string.IsNullOrWhiteSpace(parsed.PhotonAppId)) + { + Console.Error.WriteLine("error: --photon-id is required."); + Console.Error.WriteLine(" Grab one free at https://dashboard.photonengine.com"); + return 2; + } + + AppPaths.EnsureDirectoriesExist(); + var state = BuildState(parsed); + + Console.WriteLine($"DorkNet server CLI · v{typeof(Program).Assembly.GetName().Version}"); + Console.WriteLine($" mode: {parsed.Mode}"); + Console.WriteLine($" server: {parsed.ServerName}"); + Console.WriteLine($" photon: {Redact(parsed.PhotonAppId)} ({parsed.Region})"); + if (parsed.Mode == HostingMode.RemoteWildcard) + Console.WriteLine($" apex: {state.RemoteWildcardApex}"); + Console.WriteLine(); + + var manifest = await new VersionsManifest().FetchAsync(); + if (manifest is null) + { + Console.Error.WriteLine("error: couldn't fetch versions.json (offline?)."); + Console.Error.WriteLine(" Set DORKNET_LOCAL_MANIFEST=/path/to/versions.json to override."); + return 3; + } + var version = manifest.Branches + .FirstOrDefault(b => b.VersionKey.Equals(parsed.VersionKey, StringComparison.OrdinalIgnoreCase)); + if (version is null) + { + Console.Error.WriteLine($"error: version '{parsed.VersionKey}' not found in manifest."); + Console.Error.WriteLine(" Supported versions:"); + foreach (var b in manifest.Branches.Where(b => b.Supported)) + Console.Error.WriteLine($" {b.VersionKey} (Rec Room {b.ClientBuild})"); + return 4; + } + + var releases = new ReleaseDownloader(); + var server = new ServerProcess(); + TunneltoTunnel? tunnel = null; + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + try + { + // Step 1 — server binary + string serverDir; + if (!string.IsNullOrEmpty(parsed.ServerDir)) + { + if (!Directory.Exists(parsed.ServerDir)) + { + Console.Error.WriteLine($"error: --server-dir not found: {parsed.ServerDir}"); + return 5; + } + serverDir = parsed.ServerDir; + Console.WriteLine($"[server] using local build: {serverDir}"); + } + else + { + Console.WriteLine("[server] downloading…"); + var progress = new Progress(p => + { + if (p.TotalBytes > 0) + Console.Write($"\r {p.Fraction * 100,5:F1}% ({p.BytesRead / (1024.0 * 1024):F1} / {p.TotalBytes / (1024.0 * 1024):F1} MB) "); + }); + serverDir = await releases.EnsureServerAsync(version, progress, cts.Token); + Console.WriteLine($"\n[server] cached at {serverDir}"); + } + + // Step 2 — tunnel (or LAN) + string apex; + if (parsed.Mode == HostingMode.LocalNetwork) + { + var lan = LocalNetwork.GetLanAddress(); + apex = lan.Host; + Console.WriteLine($"[lan] bound on {lan.Ip} (apex={apex})"); + } + else + { + Console.WriteLine("[tunnel] starting tunnelto…"); + tunnel = new TunneltoTunnel(); + var publicUrl = await tunnel.StartAsync( + state.RemoteWildcardApex, + ServerProcess.DefaultLocalPort, + cts.Token); + apex = TunneltoTunnel.NormalizeHost(state.RemoteWildcardApex); + Console.WriteLine($"[tunnel] live at {publicUrl} (apex={apex})"); + } + + // Step 3 — server process + Console.WriteLine("[server] starting…"); + await server.StartAsync(serverDir, state, apex, parsed.Mode, cts.Token); + Console.WriteLine($"[server] listening (logs: {server.StdoutLogPath})"); + + // Step 4 — emit join code + var code = JoinCode.Encode(new JoinPayload + { + Host = apex, + VersionKey = version.VersionKey, + PhotonAppId = state.PhotonAppId, + PhotonVoiceAppId = state.PhotonVoiceAppId, + PhotonRegion = state.PhotonRegion, + Name = state.ServerName, + }); + var scheme = parsed.Mode == HostingMode.LocalNetwork ? "http" : "https"; + Console.WriteLine(); + Console.WriteLine("══════════════════════════════════════════════════════"); + Console.WriteLine($" Server live: {parsed.ServerName}"); + Console.WriteLine($" Address: {scheme}://{apex}"); + Console.WriteLine($" Admin panel: {scheme}://admin.{apex}"); + Console.WriteLine(" (first account on a fresh server becomes admin)"); + Console.WriteLine(); + Console.WriteLine(" Join code (paste this into your friend's launcher):"); + Console.WriteLine(); + Console.WriteLine($" {code}"); + Console.WriteLine(); + Console.WriteLine(" Ctrl-C to stop."); + Console.WriteLine("══════════════════════════════════════════════════════"); + + try { await Task.Delay(Timeout.Infinite, cts.Token); } + catch (OperationCanceledException) { /* clean Ctrl-C */ } + } + catch (Exception ex) + { + Console.Error.WriteLine(); + Console.Error.WriteLine($"error: {ex.Message}"); + return 10; + } + finally + { + Console.WriteLine(); + Console.WriteLine("[shutdown] stopping server…"); + try { await server.StopAsync(); } catch { } + if (tunnel is not null) { try { await tunnel.DisposeAsync(); } catch { } } + Console.WriteLine("[shutdown] done."); + } + return 0; + } + + /// Builds an in-memory matching + /// the CLI args. The CLI is stateless — it never reads or writes + /// the launcher's state.json, so a Windows + Linux user on the + /// same machine don't fight over the same file. + private static AppState BuildState(ParsedArgs p) => new() + { + Mode = AppMode.Host, + PhotonAppId = p.PhotonAppId, + PhotonVoiceAppId = p.PhotonVoiceAppId, + PhotonRegion = p.Region, + HostingMode = p.Mode, + RemoteWildcardApex = p.Mode == HostingMode.RemoteWildcard && !string.IsNullOrEmpty(p.Apex) + ? TunneltoTunnel.NormalizeHost(p.Apex) + : TunneltoTunnel.GenerateBaseHost(), + ServerName = p.ServerName, + SelectedVersion = p.VersionKey, + SetupComplete = true, + WelcomeSeen = true, + }; + + /// Mask all but the last 4 chars of an AppId so it's safe + /// to print to a shared terminal / CI log. + private static string Redact(string s) => + string.IsNullOrEmpty(s) || s.Length < 8 + ? "(missing)" + : new string('*', s.Length - 4) + s[^4..]; + + // ── Arg parsing ───────────────────────────────────────────────────── + + private sealed record ParsedArgs( + string PhotonAppId, string PhotonVoiceAppId, string Region, + HostingMode Mode, string Apex, string ServerName, string VersionKey, + string? ServerDir); + + private static ParsedArgs? ParseArgs(string[] args) + { + if (args.Length == 0 || args.Contains("--help") || args.Contains("-h")) + { + PrintHelp(); + return null; + } + + string photon = "", voice = "", region = "us"; + string mode = "tunnelto", apex = "", name = "DorkNet Server"; + string versionKey = "march_2020_03_10"; + string? serverDir = null; + + for (int i = 0; i < args.Length; i++) + { + string Next() + { + if (i + 1 >= args.Length) + throw new ArgumentException($"missing value for {args[i]}"); + return args[++i]; + } + switch (args[i]) + { + case "--photon-id": photon = Next(); break; + case "--voice-id": voice = Next(); break; + case "--region": region = Next(); break; + case "--mode": mode = Next(); break; + case "--apex": apex = Next(); break; + case "--name": name = Next(); break; + case "--version": versionKey = Next(); break; + case "--server-dir": serverDir = Next(); break; + default: + Console.Error.WriteLine($"error: unknown arg '{args[i]}' (try --help)"); + return null; + } + } + + HostingMode parsedMode = mode.ToLowerInvariant() switch + { + "tunnelto" or "tunnel" or "internet" => HostingMode.Internet, + "wildcard" or "remote" => HostingMode.RemoteWildcard, + "lan" or "local" or "wifi" => HostingMode.LocalNetwork, + _ => throw new ArgumentException($"unknown --mode '{mode}' (try tunnelto, wildcard, lan)"), + }; + + return new ParsedArgs(photon, voice, region, parsedMode, apex, name, versionKey, serverDir); + } + + private static void PrintHelp() + { + Console.WriteLine(""" + dorknet-server — host a private Rec Room server (Linux / macOS / Windows) + + USAGE + dorknet-server --photon-id [options] + + REQUIRED + --photon-id Photon Realtime AppId (free at + dashboard.photonengine.com) + + OPTIONS + --voice-id Photon Voice AppId. Empty = no voice. + --region Photon cloud region (us | eu | asia | jp | + sa | kr | in | au). Default: us. + --mode tunnelto (default) — friends anywhere + wildcard — Tunnelto wildcard + base (needs --apex) + lan — same WiFi only, + binds on 0.0.0.0:80 + --apex Wildcard apex (e.g. dorknet.tunnelto.me). + Required for --mode wildcard. + --name "" Server name shown in the join code. + Default: "DorkNet Server". + --version Rec Room version to host for. + Default: march_2020_03_10. + --server-dir Skip download, use this local server + build. Useful for dev iteration. + + EXAMPLES + # Linux host, default Tunnelto mode + dorknet-server --photon-id 00000000-1111-... --name "Sunday games" + + # Wildcard base, custom region + dorknet-server --photon-id ... --mode wildcard \ + --apex dorknet.example.tunnelto.me --region eu + + # LAN-only on the local network + dorknet-server --photon-id ... --mode lan + """); + } +} diff --git a/docs/README.md b/docs/README.md index 37c3dd2..e72650c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,9 +3,11 @@ | Doc | For who | | ---------------------------------------------- | ------------------------------------------------------ | | [../BRANCHES.md](../BRANCHES.md) | Anyone picking which branch matches their Rec Room build | -| [easy-setup.md](easy-setup.md) | Hosts who want the desktop-app one-click experience | +| [easy-setup.md](easy-setup.md) | Windows hosts using the desktop launcher | +| [server-cli.md](server-cli.md) | Linux / macOS hosts using the headless `dorknet-server` | | [advanced-setup.md](advanced-setup.md) | Hosts running a community server on Docker / a VPS | | [joining-a-server.md](joining-a-server.md) | Players whose friend gave them a join code | +| [photon-setup.md](photon-setup.md) | How to get a free Photon Cloud account + AppIds | | [architecture.md](architecture.md) | Contributors hacking on the code | | [faq.md](faq.md) | Common questions before you ask in issues | | [troubleshooting.md](troubleshooting.md) | Known failure modes + fixes | diff --git a/docs/easy-setup.md b/docs/easy-setup.md index f1f8527..5067f29 100644 --- a/docs/easy-setup.md +++ b/docs/easy-setup.md @@ -1,63 +1,287 @@ # Easy mode — host your own server > Built for non-technical hosts. If you can install Discord, you can do this. +> +> **On Linux or macOS?** This guide is for the Windows GUI launcher. +> For headless hosting from a Linux box / a Mac / a VPS, see +> [server-cli.md](server-cli.md). Joiners on those platforms are not +> supported yet — Rec Room itself is a Windows game. + +DorkNet bundles everything you need to run a private Rec Room server on +your own machine and hand a join code to your friends. The launcher is a +single Windows app that: + +- runs the server, +- opens a public tunnel so friends anywhere can connect, +- patches your local copy of Rec Room to point at the server, +- packages a join code your friends paste into their launcher. ## What you'll need -- A Windows PC (Mac and Linux support coming later) -- A copy of **Rec Room 2020.03.10** — the specific old build DorkNet targets. - You acquire this yourself; see [Getting the 2020 client](#getting-the-2020-client) below. +- Windows 10 or 11 (Mac and Linux support are not planned) +- A copy of **Rec Room 2020.03.10** — the specific old build DorkNet + targets. You acquire this yourself; see + [Getting the 2020 client](#getting-the-2020-client) below. +- A free Photon Cloud account — the launcher walks you through this on + first run. - ~5 GB free disk space -- About 10 minutes +- About 10 minutes for first-run setup, ~30 seconds per subsequent launch -## Step 1 — Download DorkNet +## Download -Grab `dorknet.exe` from the [latest release](https://github.com/YOUR_GH_USERNAME/dorknet/releases/latest). +Two flavours on the +[latest release](https://github.com/DorkSquadRR/DorkNet/releases/latest): -> Windows SmartScreen may warn you. The exe is code-signed; click "More info" -> → "Run anyway". +- **`dorknet-setup-.exe`** *(recommended)* — installer. + Drops the launcher under `%LocalAppData%\Programs\DorkNet`, adds + Start menu + (optional) desktop shortcut, sets up the auto-updater, + registers an uninstaller. No admin rights required. +- **`dorknet.exe`** — single-file portable. Drop it anywhere; state + lives in `%APPDATA%\DorkNet`. No shortcuts, no auto-updates. -## Step 2 — First launch +Either works the same once it's running. Pick the installer if you'd +like Start-menu integration and rolling updates; pick the portable exe +if you'd rather no system changes. -When you open DorkNet for the first time, you'll see a welcome screen -asking what you want to do. +> Windows SmartScreen may warn you on first launch because the binary +> isn't EV-signed. Click "More info" → "Run anyway". + +### Auto-updates + +The installed launcher checks GitHub on every launch for a newer +release. If one's available, a small banner appears in the sidebar +with an **Install & restart** button. Click it; the launcher +downloads the new binary, shuts itself down, swaps the exe in place, +and relaunches. + +Turn this off during setup by un-ticking *Check for updates on launch*, +or any time later by editing `HKCU\Software\DorkNet\AutoUpdate` (0 +disables). The portable `dorknet.exe` doesn't auto-update — pull a new +release yourself when you want one. + +## First launch — the welcome screen + +The very first time you open DorkNet you get a hero screen. ![first-run welcome screen](images/first-run-welcome.png) -Click **🏠 Host a server**. +Click **GET STARTED** to enter the setup wizard. The wizard only runs +once; future launches drop you straight into the main view. -## Step 3 — Patch your Rec Room install +## The setup wizard -DorkNet needs to redirect Rec Room to talk to your server instead of the -official one. This is a one-time patch. +Five steps, top stepper shows your progress. **Back** and **Next** sit +in the bottom bar; the choice cards on the first step auto-advance when +clicked. -1. Click the **Patch game** tab. -2. Click **Browse for Rec Room install...** — point it at the folder where - `Recroom_Release.exe` lives. -3. Click **Patch**. Takes 30 seconds. +### Step 1 — Pick a mode -![patch tab](images/patch-tab.png) +![mode pick — Host a server vs Join a friend](images/first-run-welcome-join.png) -✅ You're done with patching. You won't need to do this again unless you -reinstall Rec Room. +- **Run a server** — you'll host. The launcher will set up the tunnel + and hand you a join code. +- **Use a code** — a friend already hosts and gave you a join code. + See [joining-a-server.md](joining-a-server.md) for that path. -## Step 4 — Start the server + tunnel +This page is also reachable later via **Re-run setup** in the sidebar, +so don't worry about getting it wrong. -1. Click **Start server** on the Status tab. -2. After ~5 seconds you'll see a public URL like - `https://abc-def-ghi.trycloudflare.com`. Anyone with that URL + a - matching join code can connect to you. +### Step 2 — Find your Rec Room install -![status tab running](images/status-running.png) +The wizard auto-detects common manual-install locations (it never +touches your Steam library). If detection works you'll see a green +"Detected install" banner with the path; otherwise click **Browse…** and +point at your `Recroom_Release_Data` folder. -## Step 5 — Share the join code with your friends +The path should look like `…\\Recroom_Release_Data` — the +*_Data sibling of `Recroom_Release.exe`. The launcher validates this +when you continue. -1. Click the **Share** tab. -2. Click **Copy join code**. -3. Paste it to your friend in Discord / iMessage / wherever. +### Step 3 — Set up your server -That's it for hosting. Your friend opens DorkNet, picks "Join a server", -pastes the code, and they're in. +- **Server name** — what shows up in your join code. Use whatever your + friends will recognise ("Adam's Sunday games", "DorkSquad Test", etc.). +- **Who can join?** — three options, only one shows extra inputs: + - **Friends anywhere · Tunnelto** *(default, recommended)* — the + launcher runs a Tunnelto tunnel that gives your server a public + `*.tunnelto.me` URL. Works for anyone, anywhere. No router config. + - **Tunnelto wildcard base** — advanced. If you've got a wildcard + Tunnelto subscription, paste the apex domain. Subdomains like + `api.`, `auth.`, etc. get auto-generated. + - **Same WiFi only · sslip.io** — bind on your LAN only. Friends must + be on the same network. No tunnel, lowest latency. + +### Step 4 — Connect your Photon account + +Photon Cloud runs the in-room realtime sync (positions, voice, room +state). DorkNet doesn't ship one — you bring your own free account. + +Click **OPEN WALKTHROUGH** to launch the in-app Photon wizard. It opens +your browser to `dashboard.photonengine.com` and walks you through +creating two apps (Realtime + Voice). The wizard waits for you to paste +the AppIds back in. Voice is optional; without it, voice chat is +text-only and you can use Discord on the side. + +If you've already got AppIds from a previous setup, paste them directly +into **REALTIME APPID** and **VOICE APPID** in step 4 and skip the +walkthrough. + +> For a standalone screenshot-by-screenshot guide to the Photon +> dashboard, see [photon-setup.md](photon-setup.md). Useful if you +> get stuck mid-walkthrough or want to re-do it later. + +### Step 5 — Done + +Confirmation card. Click **Finish** to enter the main view. + +## The main view + +The launcher's everyday UI: + +![host view with Server and Photon panels](images/patch-tab.png) + +- **Sidebar** (left): **Host a server**, **Join a friend**, **Settings**, + **Re-run setup**. The brand-coloured stripe marks the active section. + The footer at the bottom shows your server's live status (Idle / + Starting / Live / Failed) and the launcher version. +- **Header** (top of content): a short eyebrow describing the view, + the title, and a **Screen mode** checkbox top-right. +- **Content**: two panels — **Server** and **Photon Cloud** — followed + by the action bar. + +Everything in the wizard is editable here. If you change anything mid- +session, the new values get used the next time you hit Start hosting. + +### Server panel + +- **REC ROOM INSTALL** — same picker as the wizard. The path shows in + monospace so you can sanity-check it. +- **SERVER NAME · SHOWN TO JOINERS** — text input. Updates the embedded + name inside your next join code. +- **REC ROOM VERSION TO HOST FOR** — dropdown. Pre-selects the version + matching your install. Picking a different version means the launcher + will fetch that server build the next time you Start hosting. +- **WHO CAN JOIN?** — the same three options as step 3 of the wizard. +- **TUNNELTO BASE HOST** — only visible when you pick the wildcard + option. Auto-generated otherwise; not needed for the default or LAN + modes. + +### Photon Cloud panel + +- **REALTIME APPID** — required. Realtime is the matchmaking + room + sync layer. Click the **WALKTHROUGH** button (top-right of the + panel) any time you want to re-run the in-app Photon wizard. +- **VOICE APPID · OPTIONAL** — if set, voice chat works through Photon + Voice. If empty, players use Discord. +- **CLOUD REGION** — picks the Photon datacenter. All clients must + match. Default is US; change for European / Asian players. + +### Screen mode + +The **Screen mode** checkbox in the header passes `+forcemode:screen` +when launching Rec Room, opening the game in 2D desktop mode instead +of looking for a VR headset. Useful for testing or for friends who +don't have VR. + +## Starting your server + +Hit **START HOSTING** (orange button, bottom of the content panels). +The launcher runs through six steps in order: + +1. **Download server** — fetches the matching server build into + `%LOCALAPPDATA%\DorkNet\servers\\`. ~50 MB the first time; + reused on subsequent launches. +2. **Open tunnel** — starts Tunnelto and assigns a public hostname. + Skipped in LAN mode. +3. **Start server** — boots the server process. SQLite database lives + in `%APPDATA%\DorkNet\dorknet.db`. +4. **Download patcher** — fetches the client-patcher payload that + matches your version. +5. **Strip Steam DRM** — only runs if your install is a Steam build + (Steamless does the unwrap; original `Recroom_Release.exe` is backed + up next to itself). +6. **Apply patch** — rewrites the game's network URLs, Photon AppIds, + and embedded keys in place. + +Progress steps show inline. If any step fails, the row turns red and +**Retry** appears under the steps list (along with **Get help** which +opens the troubleshooting docs). + +## Sharing your server + +When step 6 finishes, the join code panel appears with the orange +"SERVER LIVE" header. + +You get: + +- a **QR code** on a white tile — scan with a phone to get the join + string into your friend's clipboard, +- the join code itself in monospace, +- three share actions: + - **COPY CODE** — copies just the code, + - **Copy as message** — wraps it in a friendly Discord-ready blurb + that links back to the DorkNet download, + - **Email** — opens your mail client with subject + body filled in. + +Your friend opens DorkNet, picks **Join a friend** on first launch +(or from the sidebar later), pastes the code, and gets the matching +patcher. + +## Launching the game + +Once your server is live, the **LAUNCH REC ROOM** button (teal, sits +next to **STOP**) becomes visible. Click it; the launcher starts your +patched copy with the right command-line args. Toggle **Screen mode** +in the header first if you want desktop play. + +## Stopping the server + +**STOP** kills the server process and the tunnel cleanly, hides the +join code panel, and re-enables **START HOSTING**. Your data +(`dorknet.db`) persists; relaunching restores everything. + +The launcher also stops the server automatically when you close the +window. Don't quit mid-game — your friends will get disconnected. + +## Settings + +Sidebar → **Settings**. + +- **Cache locations** — read-only paths to your state file, server + cache, patcher cache, and logs. Useful for "I want to clear the + cache" or "where did my db go". +- **Dev overrides** — only appears when one of the `DORKNET_LOCAL_*` + env vars is set. Lists which overrides are active. Unset the env + vars to return to the normal release-fetching mode. +- **About** — version, license (AGPL-3.0), and a link to the GitHub + repo. + +The sidebar also has a **Re-run setup** entry. It clears the +"setup-complete" flag without losing your saved values, so the wizard +walks again from step 1 — handy if you want to change modes (host +↔ join) or re-do the Photon walkthrough. + +## Switching from host to join + +From the sidebar, click **Join a friend**. Your host settings are +preserved (Photon AppIds, server name, etc.) so switching back later +picks up where you left off. The SQLite database also stays put. + +See [joining-a-server.md](joining-a-server.md) for the join-side flow. + +--- + +## Hosting-mode deep-dive + +| Mode | Tunnel | Public URL | Best for | +| --- | --- | --- | --- | +| Friends anywhere · Tunnelto | yes | `dorknet-.tunnelto.me` | Default — works for everyone, no setup | +| Tunnelto wildcard base | yes | `` + `api.`, `auth.`, … | Power users with a wildcard Tunnelto plan | +| Same WiFi only · sslip.io | no | `.sslip.io` | LAN games, lowest latency, no internet exposure | + +Default mode is the right pick for >95% of cases. The wildcard mode is +only useful if you've already got a stable apex domain set up. LAN mode +is for when everyone's in the same room. --- @@ -80,14 +304,22 @@ DorkNet's authors can't help you obtain the game. Search the web for ## Troubleshooting -See [troubleshooting.md](troubleshooting.md) for common issues. +See [troubleshooting.md](troubleshooting.md) for the full list. -The most common ones: -- **"This site can't be reached" when joining** — your tunnel URL changed. - Get the new one from the Status tab and share it again. (Cloudflare - Quick Tunnels give a new URL each session. See - [advanced-setup.md](advanced-setup.md) if you want a stable one.) -- **Friend can't connect** — they need to use the same Photon region as - you. Settings → Photon region. Default is `eu`. -- **Game launches but stays on the loading screen** — your friend isn't - patched. Make sure they ran the Patch tab in their copy of DorkNet. +The most common problems: + +- **Server starts but friend can't connect** — Cloudflare Quick Tunnels + rotate the URL on every restart. Re-share the join code if you've + restarted hosting. +- **"Photon CustomAuth 401" in the server log** — your Realtime AppId + on the server doesn't match what the launcher patched into the + client. Make sure both sides use the same AppIds. +- **Game launches but stays on the loading screen** — your friend + didn't run the patcher. Their Join view needs to complete step 3 + (Apply Patch) before launching Rec Room. +- **Game launches but goes straight back to the title screen** — your + Photon region setting differs from your friend's. Both clients must + match. Set in Photon Cloud → CLOUD REGION. + +Logs live at `%LOCALAPPDATA%\DorkNet\logs\` and capture the full +server output for each session. diff --git a/docs/images/first-run-welcome-join.png b/docs/images/first-run-welcome-join.png index 27f33e1..2981228 100644 Binary files a/docs/images/first-run-welcome-join.png and b/docs/images/first-run-welcome-join.png differ diff --git a/docs/images/first-run-welcome.png b/docs/images/first-run-welcome.png index 7dc462f..8bc0e56 100644 Binary files a/docs/images/first-run-welcome.png and b/docs/images/first-run-welcome.png differ diff --git a/docs/images/join-preview.png b/docs/images/join-preview.png index 28533e5..d61d1f2 100644 Binary files a/docs/images/join-preview.png and b/docs/images/join-preview.png differ diff --git a/docs/images/patch-tab.png b/docs/images/patch-tab.png index cecf6d5..89c87c1 100644 Binary files a/docs/images/patch-tab.png and b/docs/images/patch-tab.png differ diff --git a/docs/images/status-running.png b/docs/images/status-running.png index cecf6d5..89c87c1 100644 Binary files a/docs/images/status-running.png and b/docs/images/status-running.png differ diff --git a/docs/joining-a-server.md b/docs/joining-a-server.md index d4ab35f..d86443c 100644 --- a/docs/joining-a-server.md +++ b/docs/joining-a-server.md @@ -1,67 +1,178 @@ -# Joining someone's DorkNet server +# Joining a friend's DorkNet server > Built for players whose friend already runs a server and gave them a > code. If you want to *host* instead, see [easy-setup.md](easy-setup.md). +A "join code" is a one-line string your host's launcher generated when +they hit Start hosting. It contains everything DorkNet needs to point +your Rec Room at their server: the public hostname, the Photon AppIds, +the version, and the server's display name. The launcher decodes it +locally — nothing is sent anywhere until you actually launch the game. + ## What you'll need -- A Windows PC -- A copy of **Rec Room 2020.03.10** (your own — see - [Getting the 2020 client](easy-setup.md#getting-the-2020-client)) -- The join code your friend gave you (looks like `dorknet://join?...`) +- Windows 10 or 11 +- A copy of **Rec Room 2020.03.10** — your own, see + [Getting the 2020 client](easy-setup.md#getting-the-2020-client) +- The join code your friend gave you (a base64-looking blob a few hundred + characters long) - About 2 minutes -## Step 1 — Download DorkNet +You **don't** need a Photon account of your own. The join code carries +your friend's AppIds; the launcher rewrites them into your client. -Grab `dorknet.exe` from the [latest release](https://github.com/YOUR_GH_USERNAME/dorknet/releases/latest). +## Download -## Step 2 — First launch +Grab `dorknet.exe` from the +[latest release](https://github.com/DorkSquadRR/DorkNet/releases/latest). +Same single-file binary as the host's. Drop it anywhere; state lives in +`%APPDATA%\DorkNet`. -On the welcome screen, click **🎮 Join a server**. +## First launch -![first-run welcome screen — Join selected](images/first-run-welcome-join.png) +Welcome screen → **GET STARTED** → setup wizard. -## Step 3 — Paste the join code +![welcome screen](images/first-run-welcome.png) -Your friend's code goes in the big text box. DorkNet decodes it and shows -you what you're about to connect to so you can sanity-check: +### Wizard step 1 — Pick "Use a code" -![join code preview](images/join-preview.png) +The right-hand card. -Make sure the server name matches what your friend told you. Codes from -strangers can point anywhere — only paste codes from people you trust. +![mode pick — Host vs Join](images/first-run-welcome-join.png) -## Step 4 — Patch your Rec Room install +Join mode skips the host-only wizard steps (server name + Photon), so +your wizard runs 3 steps total instead of 5. -1. Click **Browse for Rec Room install...** — point at the folder with - `Recroom_Release.exe`. -2. Click **Patch & launch**. +### Wizard step 2 — Find your Rec Room install -DorkNet patches the game (one-time), then launches it pointed at your -friend's server. You'll see the normal Rec Room login screen, log in with -the account you made on your friend's server, and you're in. +The launcher auto-detects common manual-install locations. If detection +worked you'll see a green "Detected install" banner; otherwise click +**Browse…** and point at your `Recroom_Release_Data` folder (the *_Data +sibling of `Recroom_Release.exe`). + +### Wizard step 3 — Done + +That's it for the wizard. Click **Finish** to land in the main view. + +## The main view — Join + +![join view](images/join-preview.png) + +- **Sidebar** (left): same nav as the host's launcher — **Host a + server**, **Join a friend**, **Settings**, **Re-run setup**. You're + on **Join a friend**. +- **Header** (top): JOIN eyebrow, "Join a friend" title, and the + **Screen mode** checkbox top-right. + +### Paste the join code + +Paste the code your friend sent you into the **PASTE THE CODE YOUR +HOST SHARED** textbox. Hit **Preview** — the launcher decodes the code +and pops a "Server preview" panel showing: + +- the server's display name, +- the public host (Tunnelto hostname, sslip.io address, or wildcard + apex), +- the version key (must match your install — the patcher will warn if + not). + +Sanity-check it. **Codes from strangers can point anywhere — only paste +codes from people you trust.** A malicious code wouldn't compromise +your machine, but it could send your Rec Room login traffic to someone +else's server. + +### Confirm your Rec Room install + +Same picker as on the wizard. The path shows in monospace below the +Rec Room install header. Click **Browse…** to change it if needed. + +## Applying the patch + +Click **APPLY PATCH** (the orange button below the install picker; it +stays disabled until you've pasted a valid code). + +The launcher runs three steps: + +1. **Download patcher** — fetches the version-specific patcher payload. + ~10 MB, cached for next time. +2. **Strip Steam DRM** — only runs if your install is a Steam build. +3. **Apply patch** — rewrites network URLs and Photon AppIds in place. + +Progress steps show inline. If anything fails, the row turns red and +you get **Retry** + **Get help** buttons. + +The original `Recroom_Release.exe` and its DRM-stripped copy are +preserved as backups next to the modified files; the patcher is +reversible. + +## Launching Rec Room + +When the patch completes, the **LAUNCH REC ROOM** button (teal) +appears. Click it. The launcher starts your patched Rec Room with the +right command-line. + +Tick **Screen mode** in the top-right of the header first if you want +to play in 2D desktop mode instead of VR. + +You'll see the regular Rec Room login screen — log in with the account +your host gave you (each DorkNet server has its own player database), +and you're in their world. --- -## I already patched for a different server. Can I switch? +## Switching to a different friend's server -Yes — DorkNet's patcher is reversible. Click **Repair / unpatch** in the -Patch tab to undo, then re-patch with a new join code. Or just re-run -**Patch & launch** with the new code; the patcher detects an existing -patch and updates the destination. +You can re-patch any time. Paste the new code, hit **Preview** to +verify, click **APPLY PATCH** again. The patcher detects the existing +patch and updates the destination — no need to repair first. -## My account doesn't exist on the new server +If you want to go back to the official Rec Room servers, the patcher's +backups are still there: rename `Recroom_Release.exe.bak` (or +`.steamless.bak`) back to `Recroom_Release.exe`. Or just reinstall. -Each DorkNet server runs its own account database. You'll need to create -a new account the first time you join a server. Your friend can sign you -up via their admin panel, or you can use the in-game "Create account" -flow if the server has signups enabled. +## Switching to host mode + +Sidebar → **Host a server**. You'll need to fill in server name + +Photon AppIds before you can host. See +[easy-setup.md](easy-setup.md) for that workflow. The launcher +preserves your install path and screen-mode preference when you switch. + +--- + +## My account doesn't exist on the server + +Each DorkNet server runs its own player database. The first time you +join, you need an account on that server. Your friend can sign you up +through their admin panel, or you can use the in-game "Create account" +flow if signups are enabled. + +If you've joined the same server before and switched away, your old +account is still on their database — log in with the same credentials +you used before. ## Voice chat doesn't work -Voice routes through Photon Voice, which requires a separate Photon Voice -AppId. If your friend hasn't set one up, voice chat is text-only for now. +Voice routes through Photon Voice, which is a separate AppId. If your +host hasn't set one up, voice chat won't work in-game. You can still +use Discord or any other voice tool alongside. + +## "Photon CustomAuth 401" or game stuck on the loading screen + +Almost always one of: + +- Your host set up a new Photon AppId since your last join. Paste the + new code, re-apply the patch. +- Your Photon **region** in the code doesn't match the server's. This + is set by the host; ask them to confirm the region. +- The patch didn't actually apply — check the launcher's progress + panel; a red row means the patcher errored. + +## "Check your internet connection" / black screen + +Most often: your host's tunnel URL changed (Tunnelto sometimes rotates +hostnames between sessions). Get a fresh join code from them and +re-apply the patch. ## More troubleshooting -See [troubleshooting.md](troubleshooting.md). +[troubleshooting.md](troubleshooting.md) lists the rest. diff --git a/docs/photon-setup.md b/docs/photon-setup.md new file mode 100644 index 0000000..d86e374 --- /dev/null +++ b/docs/photon-setup.md @@ -0,0 +1,176 @@ +# Getting your Photon AppIds + +DorkNet uses Photon Cloud for in-room realtime sync (positions, voice, +matchmaking). It's free for small private servers and the launcher +never ships one — you bring your own. This guide walks through getting +one if the in-app walkthrough doesn't work for you (e.g. you're using +the Linux/macOS [`dorknet-server`](server-cli.md) CLI). + +You only need to do this **once**. The AppIds are saved on your host +machine and embedded into the join code so your friends never see them. + +## What you're getting + +- **Realtime AppId** — required. Powers position sync, room state, + RPCs. Free Photon plan covers 20 concurrent users which is enough + for a friends-only server. +- **Voice AppId** — optional. Powers in-game voice chat. Skip it if + your group uses Discord on the side. + +Both are GUIDs that look like `abc12345-6789-...`. Treat them like +loose credentials: they're not secret-secret (the join code carries +them to joiners), but don't paste them in public chat. + +--- + +## Step 1 — Sign up + +Go to **[dashboard.photonengine.com](https://dashboard.photonengine.com/)** +and click "Sign up". Email + password; the free tier doesn't ask for a +card. + +![photon dashboard signup screen](images/photon-1-signup.png) + +> If you already have an account from another project, log in instead. +> Multiple DorkNet servers can share the same Photon account, but each +> server needs its own AppId pair so concurrent player counts don't +> collide. + +## Step 2 — Create a Realtime app + +After login, click **CREATE A NEW APP** on the dashboard. The form asks +for: + +- **Photon Type** — pick **Photon Realtime**. Not Fusion, not Quantum, + not Bolt. +- **Name** — anything you'll recognise later ("DorkNet Realtime", + "Sunday Server", etc.). Joiners don't see this. +- **Description** — optional. + +Click **CREATE**. + +![photon "Create a new app" form, Realtime selected](images/photon-2-create-realtime.png) + +## Step 3 — Copy the Realtime AppId + +You land back on the dashboard with your new app at the top. The AppId +is the long string under the app name — click it to copy. + +![photon app card with AppId highlighted](images/photon-3-realtime-appid.png) + +That's your **Realtime AppId**. Paste it into: + +- the launcher's first-run wizard step 4, **REALTIME APPID** field, OR +- the launcher's Host view → Photon Cloud panel → **REALTIME APPID**, OR +- the CLI: `dorknet-server --photon-id ` + +## Step 4 — (Optional) Create a Voice app + +If you want in-game voice chat, repeat Step 2 with **Photon Voice** +selected instead of Realtime. Same flow. + +![photon "Create a new app" form, Voice selected](images/photon-4-create-voice.png) + +Copy the Voice AppId from the dashboard the same way as Step 3 and +paste it into: + +- the wizard's **VOICE APPID** field, OR +- the launcher's Host view → Photon Cloud panel → **VOICE APPID · OPTIONAL**, OR +- the CLI: `--voice-id ` + +## Step 5 — Pick a region + +Both apps default to a global routing layer, but for the lowest latency +you want all your players hitting the same Photon datacenter. + +In the launcher, **CLOUD REGION** sits below the AppId fields: + +| Code | Region | +| --- | --- | +| `us` | US East (default) | +| `eu` | EU Amsterdam | +| `asia` | Asia Singapore | +| `jp` | Japan Tokyo | +| `sa` | South America São Paulo | +| `kr` | Korea Seoul | +| `in` | India Chennai | +| `au` | Australia Sydney | + +Pick the region closest to most of your players. The CLI flag is +`--region `. + +> **All joiners must use the same region as the host.** This is baked +> into the join code, so as long as everyone uses the same code, +> they'll match. If you change the region later, regenerate the code. + +## Step 6 — Paste into DorkNet + +That's it. In the launcher: + +1. Open the Host view. +2. Paste **Realtime AppId** under Photon Cloud. +3. Paste **Voice AppId** (if you made one). +4. Pick the **Cloud Region**. +5. Click **START HOSTING** when ready. + +For the CLI: + +``` +dorknet-server \ + --photon-id \ + --voice-id # optional \ + --region eu # whatever region you picked \ + --name "Sunday games" +``` + +--- + +## What's safe to share + +| Value | Share with friends? | Why | +| --- | --- | --- | +| Realtime AppId | Yes (via join code) | The launcher embeds it into the join code so joiners can connect. | +| Voice AppId | Yes (via join code) | Same. | +| Photon dashboard login | **No** | Anyone with your login can delete your apps + see usage logs. | +| Photon billing details | **No** | Self-explanatory. | + +If you accidentally publish an AppId somewhere public, you can rotate +it: delete the app in the Photon dashboard, create a new one, paste +the new AppId into DorkNet. Existing join codes stop working; share +new ones. + +## Free tier limits + +At time of writing the Photon free tier covers: + +- 20 concurrent users (CCU) — i.e. 20 people simultaneously connected + through your Realtime app +- 60 messages/second/user +- No card required, no expiry + +For a friends-and-family server this is enough. You'll see a warning on +the Photon dashboard if you start hitting limits; the paid plans bump +the CCU cap. + +## Troubleshooting + +### "Photon CustomAuth 401" in the server log + +The Realtime AppId on the server doesn't match what got embedded in +your patched client. Either: +- You changed the AppId in the launcher after applying the patch — + re-share the join code and have joiners re-patch. +- You're running an older patched client from a previous join code — + re-patch with the current one. + +### Joiner can't reach the room (game stuck loading) + +Region mismatch. Both sides must use the same region. Confirm in your +launcher's **CLOUD REGION** dropdown; ask your friend to re-patch with +your latest join code. + +### Voice chat is silent + +You either didn't set a Voice AppId, or you set one but the joiner's +patched client doesn't have it. Re-patch with a fresh join code that +includes both AppIds. diff --git a/docs/server-cli.md b/docs/server-cli.md new file mode 100644 index 0000000..b2ce802 --- /dev/null +++ b/docs/server-cli.md @@ -0,0 +1,275 @@ +# Headless host — Linux & macOS + +> For hosts running a server from a Linux box, a Mac, or a VPS. Joiners +> on Windows still use the [Windows launcher](easy-setup.md) to apply +> the patch to their copy of Rec Room. + +`dorknet-server` is a small command-line tool that does the host side +of what the Windows launcher does: downloads the server build, opens a +[Tunnelto](https://tunnelto.dev) tunnel, runs the server, and prints +a join code. It runs on Linux, macOS, and Windows. + +## What you'll need + +- A machine running Linux (x64 or arm64), macOS (Intel or Apple + Silicon), or Windows +- [Tunnelto](https://github.com/agrinman/tunnelto) on PATH (for the + default mode that exposes your server publicly) +- A Photon Realtime AppId — see [photon-setup.md](photon-setup.md) +- ~500 MB free disk (server binary + SQLite database) + +You **do not** need Rec Room installed on the host machine. The server +doesn't run the game; joiners run their own (Windows) copy. + +## Install + +Grab the matching archive from the +[latest release](https://github.com/DorkSquadRR/DorkNet/releases/latest): + +| Platform | Asset | +| --- | --- | +| Linux x64 | `dorknet-server-linux-x64.tar.gz` | +| Linux arm64 | `dorknet-server-linux-arm64.tar.gz` | +| macOS Apple Silicon | `dorknet-server-osx-arm64.tar.gz` | +| macOS Intel | `dorknet-server-osx-x64.tar.gz` | +| Windows x64 | `dorknet-server-win-x64.zip` | + +Unpack, then either keep it where it is or move the `dorknet-server` +binary onto your `PATH`. + +```sh +# Linux / macOS +curl -L https://github.com/DorkSquadRR/DorkNet/releases/latest/download/dorknet-server-linux-x64.tar.gz | tar xz +chmod +x dorknet-server +./dorknet-server --help +``` + +```pwsh +# Windows (PowerShell) +Invoke-WebRequest https://github.com/DorkSquadRR/DorkNet/releases/latest/download/dorknet-server-win-x64.zip -OutFile dorknet-server.zip +Expand-Archive dorknet-server.zip +.\dorknet-server\dorknet-server.exe --help +``` + +### Tunnelto + +The CLI shells out to `tunnelto` for the public tunnel. Quickest install: + +```sh +# macOS (Homebrew) +brew install tunnelto + +# Linux (cargo) +cargo install tunnelto + +# Or grab a binary from github.com/agrinman/tunnelto/releases +``` + +Skip Tunnelto if you're running in `--mode lan` (LAN-only, no public +URL). + +## Usage + +The minimum invocation is just your Photon AppId: + +```sh +dorknet-server --photon-id 12345678-aaaa-bbbb-cccc-1234567890ab +``` + +The CLI prints progress, then a join code: + +``` +DorkNet server CLI · v0.1.0 + mode: Internet + server: DorkNet Server + photon: ********90ab (us) + +[server] downloading… + 54.2% (27.1 / 50.0 MB) +[server] cached at /home/alex/.local/share/DorkNet/servers/march_2020_03_10 +[tunnel] starting tunnelto… +[tunnel] live at https://dorknet-abc123.tunnelto.me (apex=dorknet-abc123.tunnelto.me) +[server] starting… +[server] listening (logs: /home/alex/.local/share/DorkNet/logs/server-…log) + +══════════════════════════════════════════════════════ + Server live: DorkNet Server + Address: https://dorknet-abc123.tunnelto.me + + Join code (paste this into your friend's launcher): + + eyJob3N0IjoiZG9ya25ldC1hYmMxMjMudHVubmVsdG8ubWUiLCJ2I... + + Ctrl-C to stop. +══════════════════════════════════════════════════════ +``` + +Copy the join code, send it to your friend, they paste it into their +Windows launcher's **Join a friend** view. + +Stop the server with `Ctrl-C` — the CLI shuts the server + tunnel +down cleanly. + +## Common options + +``` +--photon-id Required. Photon Realtime AppId. +--voice-id Optional. Photon Voice AppId (Discord on the side + works fine without this). +--region Photon cloud region — us, eu, asia, jp, sa, kr, + in, au. Default: us. All joiners use the same. +--name "" Server display name in the join code. + Default: "DorkNet Server". +--mode tunnelto (default) — public, friends anywhere + wildcard — your own Tunnelto apex + lan — same WiFi only, no tunnel +--apex Wildcard apex. Required for --mode wildcard. + Example: dorknet.example.tunnelto.me +--version Rec Room version key. Default: march_2020_03_10 + (the only branch currently supported). +--server-dir Skip download — use a local build at this path. + Useful for dev work on the server itself. +``` + +Run with `--help` for the canonical list. + +## Examples + +```sh +# Sunday games server with a custom name, EU region +dorknet-server \ + --photon-id 12345678-aaaa-bbbb-cccc-1234567890ab \ + --name "Sunday games" \ + --region eu + +# LAN party — no tunnel, joiners must be on the same WiFi +dorknet-server --photon-id ... --mode lan + +# Custom Tunnelto wildcard base +dorknet-server \ + --photon-id ... \ + --mode wildcard \ + --apex dorknet.acme.tunnelto.me + +# Voice chat enabled +dorknet-server --photon-id ... --voice-id 87654321-... + +# Iterate on a local server build during dev +dorknet-server --photon-id ... \ + --server-dir /home/alex/src/DorkNet/DorkNet.Server/bin/Debug/net9.0 +``` + +## Run as a systemd service (Linux) + +Make a service so the server restarts on reboot: + +```ini +# /etc/systemd/system/dorknet.service +[Unit] +Description=DorkNet private Rec Room server +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=alex +ExecStart=/home/alex/bin/dorknet-server \ + --photon-id 12345678-aaaa-bbbb-cccc-1234567890ab \ + --region eu \ + --name "Sunday games" +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now dorknet +journalctl -fu dorknet # follow the logs +``` + +> Heads-up: each new tunnel session gets a fresh `*.tunnelto.me` +> hostname, so the join code rotates on restart. Either re-share +> the new code with your friends, or use `--mode wildcard` with a +> stable apex. + +## Run as a launchd agent (macOS) + +```xml + + + + + + Labelcom.dorknet.server + ProgramArguments + + /usr/local/bin/dorknet-server + --photon-idYOUR-APPID + --regionus + + RunAtLoad + KeepAlive + StandardOutPath/tmp/dorknet.log + StandardErrorPath/tmp/dorknet.err + + +``` + +```sh +launchctl load ~/Library/LaunchAgents/com.dorknet.server.plist +``` + +## Files & state + +The CLI follows XDG-ish conventions on Linux/macOS and the standard +`%APPDATA%` / `%LOCALAPPDATA%` paths on Windows: + +| Path | Contents | +| --- | --- | +| `$XDG_DATA_HOME/DorkNet/` (Linux)
`~/Library/Application Support/DorkNet/` (macOS)
`%APPDATA%\DorkNet\` (Windows) | Server SQLite DB (`dorknet.db`), JWT signing secret | +| same prefix `/servers//` | Downloaded server binaries (~50 MB each, cached across runs) | +| same prefix `/logs/` | Per-session server stdout / stderr | + +The CLI does not read or write the Windows launcher's `state.json` — +it's stateless across runs (all config comes from CLI args). + +## Limitations + +- **No client patcher.** Joiners need the Windows launcher to patch + their Rec Room install — there is no patch-from-Linux flow yet + because Rec Room is a Windows game and the patcher manipulates a + Windows PE binary. +- **No GUI.** Use the Windows [`dorknet.exe`](easy-setup.md) if you + want a graphical launcher. +- **No auto-updates.** Pull a new release manually when you want one. + Watch the GitHub releases feed or run a periodic check in your + service. + +## Troubleshooting + +### `tunnelto was not found` + +The CLI couldn't find a `tunnelto` binary. Install it (see above) or +drop the binary next to `dorknet-server`. + +### `couldn't fetch versions.json` + +You're offline or GitHub is blocked. Either fix the connection or +point `DORKNET_LOCAL_MANIFEST` at a local copy of versions.json. + +### `No server binary found in …` + +The downloaded archive doesn't contain the expected `DorkNet.Server` +file. Likely cause: the release for your platform hasn't been +published yet. Workaround: build the server from source and pass +`--server-dir /path/to/built/server`. + +### Same Photon errors as the GUI launcher + +Photon issues (CustomAuth 401, region mismatch, voice silent) have +the same fixes as on Windows. See +[photon-setup.md → Troubleshooting](photon-setup.md#troubleshooting). diff --git a/installer/build-installer.ps1 b/installer/build-installer.ps1 new file mode 100644 index 0000000..cb584d5 --- /dev/null +++ b/installer/build-installer.ps1 @@ -0,0 +1,65 @@ +# Build the DorkNet Windows installer. +# +# Usage: +# pwsh -File installer\build-installer.ps1 # uses default 0.1.0 +# pwsh -File installer\build-installer.ps1 -Version 0.2.0 +# +# Output: +# installer\out\dorknet-setup-.exe +# +# Requirements: +# - .NET 9 SDK (for `dotnet publish`) +# - Inno Setup 6 installed; ISCC must be on PATH or at the default +# install location (C:\Program Files (x86)\Inno Setup 6\ISCC.exe). +# Grab it free from https://jrsoftware.org/isinfo.php + +[CmdletBinding()] +param( + [string]$Version = "0.1.0", + [string]$Configuration = "Release" +) + +$ErrorActionPreference = "Stop" + +$repo = Split-Path -Parent $PSScriptRoot +$launcher = Join-Path $repo "launcher" +$installer = $PSScriptRoot + +Write-Host "==> Publishing launcher (self-contained, single file)..." -ForegroundColor Cyan +& dotnet publish $launcher ` + -c $Configuration ` + -r win-x64 ` + --self-contained true ` + /p:PublishSingleFile=true ` + /p:Version=$Version ` + /p:AssemblyVersion="$Version.0" ` + /p:FileVersion="$Version.0" ` + /p:InformationalVersion=$Version +if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed" } + +$publishDir = Join-Path $launcher "bin\$Configuration\net9.0-windows\win-x64\publish" +$exe = Join-Path $publishDir "dorknet.exe" +if (-not (Test-Path $exe)) { throw "Expected $exe not found after publish" } + +$exeSize = "{0:N1} MB" -f ((Get-Item $exe).Length / 1MB) +Write-Host "==> Published: $exe ($exeSize)" -ForegroundColor Green + +# Resolve ISCC.exe — try PATH first, then the default install dir. +$iscc = (Get-Command "ISCC.exe" -ErrorAction SilentlyContinue).Source +if (-not $iscc) { $iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" } +if (-not (Test-Path $iscc)) { + throw "ISCC.exe not found. Install Inno Setup 6 from https://jrsoftware.org/isinfo.php" +} + +Write-Host "==> Compiling installer..." -ForegroundColor Cyan +$iss = Join-Path $installer "dorknet.iss" +$outDir = Join-Path $installer "out" +New-Item -ItemType Directory -Force -Path $outDir | Out-Null +& $iscc "/DAppVersion=$Version" $iss +if ($LASTEXITCODE -ne 0) { throw "ISCC failed" } + +$installerExe = Join-Path $outDir "dorknet-setup-$Version.exe" +if (-not (Test-Path $installerExe)) { throw "Installer not produced at $installerExe" } + +$instSize = "{0:N1} MB" -f ((Get-Item $installerExe).Length / 1MB) +Write-Host "==> Built: $installerExe ($instSize)" -ForegroundColor Green diff --git a/installer/dorknet.iss b/installer/dorknet.iss new file mode 100644 index 0000000..aa210e9 --- /dev/null +++ b/installer/dorknet.iss @@ -0,0 +1,82 @@ +; DorkNet launcher — Windows installer. +; Produces a per-user installer that drops dorknet.exe under +; %LocalAppData%\Programs\DorkNet, adds Start menu + desktop shortcuts, +; and registers an uninstaller. No admin rights required. +; +; Build: pwsh -File installer\build-installer.ps1 + +#ifndef AppVersion + #define AppVersion "0.1.0" +#endif + +#define AppName "DorkNet" +#define AppPublisher "DorkSquadRR" +#define AppURL "https://github.com/DorkSquadRR/DorkNet" +#define AppExeName "dorknet.exe" +#define PublishDir "..\launcher\bin\Release\net9.0-windows\win-x64\publish" + +[Setup] +AppId={{8C2C7E1A-6B4F-4F8D-9C0B-DorkNet000001} +AppName={#AppName} +AppVersion={#AppVersion} +AppVerName={#AppName} {#AppVersion} +AppPublisher={#AppPublisher} +AppPublisherURL={#AppURL} +AppSupportURL={#AppURL} +AppUpdatesURL={#AppURL}/releases +DefaultDirName={localappdata}\Programs\{#AppName} +DefaultGroupName={#AppName} +DisableProgramGroupPage=yes +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputDir=out +OutputBaseFilename=dorknet-setup-{#AppVersion} +Compression=lzma2/ultra +SolidCompression=yes +WizardStyle=modern +UninstallDisplayIcon={app}\{#AppExeName} +UninstallDisplayName={#AppName} +MinVersion=10.0.17763 +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +CloseApplications=force +RestartApplications=no +; Skip the welcome page — single-file installer, the wizard already +; tells the user what's happening. +DisableWelcomePage=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "Create a &desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: unchecked +Name: "autorunupdates"; Description: "Check for updates on launch"; GroupDescription: "Updates:" + +[Files] +Source: "{#PublishDir}\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion +; Bundle the tunnelto helper if it's been built next to the launcher. +; The launcher auto-falls-back to PATH lookup when not bundled, so this +; is purely a convenience for users who don't already have it installed. +Source: "{#PublishDir}\tunnelto.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist + +[Icons] +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}" +Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}" +Name: "{userdesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon + +[Registry] +; Mark the install location + version so the launcher's updater can +; find where to swap the new exe into. +Root: HKCU; Subkey: "Software\DorkNet"; ValueType: string; ValueName: "InstallPath"; ValueData: "{app}"; Flags: uninsdeletevalue +Root: HKCU; Subkey: "Software\DorkNet"; ValueType: string; ValueName: "Version"; ValueData: "{#AppVersion}"; Flags: uninsdeletevalue +Root: HKCU; Subkey: "Software\DorkNet"; ValueType: dword; ValueName: "AutoUpdate"; ValueData: "1"; Tasks: autorunupdates; Flags: uninsdeletevalue +Root: HKCU; Subkey: "Software\DorkNet"; ValueType: dword; ValueName: "AutoUpdate"; ValueData: "0"; Tasks: not autorunupdates; Flags: uninsdeletevalue + +[Run] +Filename: "{app}\{#AppExeName}"; Description: "Launch {#AppName}"; Flags: nowait postinstall skipifsilent + +[UninstallDelete] +; Leave %AppData%\DorkNet alone — that's user state (Photon AppIds, +; saved server name, etc.) and the user's worlds live in dorknet.db. +; Just clean the program files dir. +Type: filesandordirs; Name: "{app}" diff --git a/launcher/App.xaml b/launcher/App.xaml index 50e69b5..16f0d59 100644 --- a/launcher/App.xaml +++ b/launcher/App.xaml @@ -5,84 +5,103 @@ StartupUri="MainWindow.xaml"> - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + - - - - - - + + - - - - - + + + + - - - - + + + - - - - - - - - + + M2,4 L18,4 L18,8 L2,8 Z M2,10 L18,10 L18,14 L2,14 Z M5,6 L6,6 M5,12 L6,12 M15,6 L16,6 M15,12 L16,12 + M8,12 L12,8 M5,10 A4,4 0 0 1 9,6 L11,6 M11,14 L9,14 A4,4 0 0 1 5,10 M15,10 A4,4 0 0 1 11,14 M11,6 A4,4 0 0 1 15,10 + M10,1 L11,3.5 L13.5,2.5 L14,5 L16.5,5 L15.5,7.5 L18,9 L16,11 L17,13.5 L14.5,13.5 L14,16 L11.5,15 L10,17 L8.5,15 L6,16 L5.5,13.5 L3,13.5 L4,11 L2,9 L4.5,7.5 L3.5,5 L6,5 L6.5,2.5 L9,3.5 Z M10,7 A2,2 0 1 0 10,11 A2,2 0 1 0 10,7 + M2,5 L7,5 L9,7 L18,7 L18,15 L2,15 Z + M5,3 L16,9 L5,15 Z + M3,9 L7,13 L15,5 + M4,4 L14,14 M14,4 L4,14 + M3,9 L15,9 M11,5 L15,9 L11,13 + M15,9 L3,9 M7,5 L3,9 L7,13 + M5,4 L13,4 L13,12 L5,12 Z M3,6 L3,14 L11,14 + M2,5 L18,5 L18,15 L2,15 Z M2,5 L10,11 L18,5 + M11,3 L17,3 L17,9 M17,3 L9,11 M15,11 L15,17 L3,17 L3,5 L9,5 + M10,8 A2,2 0 1 0 10,12 A2,2 0 1 0 10,8 + M7,7 A3,3 0 1 1 10,10 L10,12 M10,15 L10,15.5 + + + + + + - + + + + - + - + - - + + + + + + + + + + + + + + + + status mark; detail/progress are shown only when populated. --> - + - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + @@ -130,18 +522,30 @@ Background="{StaticResource CardBgHost}" Click="OnSetupPickHost"> - - - - + + + + + + + FontSize="12" TextWrapping="Wrap" LineHeight="20" + Text="The launcher runs the server, opens a tunnel, and hands you a join code your friends can paste." /> @@ -149,18 +553,32 @@ Background="{StaticResource CardBgJoin}" Click="OnSetupPickJoin"> - - - - + + + + + + + FontSize="12" TextWrapping="Wrap" LineHeight="20" + Text="Paste a join code from a friend. The launcher fetches the matching patcher and points your client at their server." /> @@ -168,115 +586,142 @@ - - + + - - - + + + + + + + + + + - + Margin="0,0,0,4" /> + - - + + - + + Margin="0,0,0,14" + Text="Already have AppIds? Paste them directly." /> - - - + @@ -286,27 +731,68 @@ Text="Realtime AppId required before you can host." /> - + - + + + + - + - - + + + @@ -316,98 +802,155 @@ - - - - - - - - - - Photon Cloud powers in-room realtime sync. Free to register at + Margin="0,0,0,16" TextWrapping="Wrap" LineHeight="18"> + Powers in-room realtime sync. Free at dashboard.photonengine.com; - DorkNet never bundles one. First time? Click "Setup walkthrough" above. + your AppIds stay local. - - - + @@ -425,38 +968,94 @@ - - - + + + + + - + + Margin="0,0,0,18" Visibility="Collapsed"> - + + + + + Orientation="Horizontal" Margin="0,16,0,0"> + TextWrapping="Wrap" Margin="0,14,0,0" LineHeight="18" + Text="Friend opens DorkNet → Join a friend → pastes. QR encodes the same string for phone forwarding." /> - + @@ -537,97 +1180,188 @@ - - - - - - - - + + + - + + + + + Orientation="Horizontal" Margin="0,16,0,0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + github.com/DorkSquadRR/DorkNet + + + + -