using System.Diagnostics; using System.IO; namespace DorkNet.Launcher.Backend; /// Removes the SteamStub DRM wrapper from /// Recroom_Release.exe so the launcher can spawn it without /// Steam loaded. No-op when the exe isn't packed (Oculus / Quest /// builds, or installs already unwrapped by a prior run). /// /// Detection is a PE-section walk: SteamStub Variant 3.1 /// (the variant used for the 2020 Rec Room build) adds a .bind /// section that contains the encrypted entry-point stub. The presence /// of .bind + the standard packed-PE signature is a reliable /// "needs Steamless" indicator without launching Steamless itself. /// /// When stripping: the original exe is renamed to /// Recroom_Release.exe.steamwrapped as a backup, and /// Steamless's .unpacked.exe output is renamed in its place. /// Re-runs detect the (now-unpacked) exe and skip. public static class SteamDrmStripper { /// Strip Steam DRM from the Rec Room exe in /// if needed. Returns a result the /// caller can use to skip/show progress steps. The Rec Room dir is /// the same path the user picks (i.e. *_Data's parent, /// which contains Recroom_Release.exe). public static async Task StripIfNeededAsync( string recRoomDir, IProgress? downloadProgress = null, CancellationToken ct = default) { // The user picks the *_Data folder; the .exe lives in its // parent. RecRoomPicker.Validate ensures this layout exists // before we ever get here. var dataFolder = new DirectoryInfo(recRoomDir); var installRoot = dataFolder.Parent?.FullName ?? recRoomDir; var exe = Path.Combine(installRoot, "Recroom_Release.exe"); if (!File.Exists(exe)) return StripResult.NotApplicable; if (!HasSteamStub(exe)) return StripResult.AlreadyUnpacked; var steamlessCli = await SteamlessInstaller.EnsureAsync(downloadProgress, ct); var unpacked = exe + ".unpacked.exe"; if (File.Exists(unpacked)) File.Delete(unpacked); var psi = new ProcessStartInfo { FileName = steamlessCli, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = installRoot, }; psi.ArgumentList.Add("--quiet"); psi.ArgumentList.Add(exe); using var proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to spawn Steamless.CLI.exe"); await proc.WaitForExitAsync(ct); if (!File.Exists(unpacked)) { // Steamless prints its findings to stdout, so include that // tail in the error to help users figure out what went wrong. var stdout = await proc.StandardOutput.ReadToEndAsync(ct); var stderr = await proc.StandardError.ReadToEndAsync(ct); throw new InvalidOperationException( "Steamless ran but produced no .unpacked.exe — install may not be the Steam build, " + "or the SteamStub variant isn't supported.\n" + $"stdout: {stdout.TrimEnd()}\nstderr: {stderr.TrimEnd()}"); } var backup = exe + ".steamwrapped"; if (File.Exists(backup)) File.Delete(backup); File.Move(exe, backup); File.Move(unpacked, exe); return StripResult.Stripped; } /// Walk PE section headers looking for a .bind /// section, which SteamStub Variant 3.1 adds during packing. /// Returns false on any read error — the patcher's later steps /// will catch a malformed exe with a clearer message. private static bool HasSteamStub(string exePath) { try { using var fs = File.OpenRead(exePath); using var br = new BinaryReader(fs); // DOS header at offset 0; PE-header offset stored at 0x3C. fs.Position = 0x3C; var peOffset = br.ReadInt32(); fs.Position = peOffset; if (br.ReadUInt32() != 0x00004550) return false; // "PE\0\0" // Skip COFF header (20 bytes) to get to the optional // header; we need its size to find the section table. fs.Position = peOffset + 4 + 2; // skip Machine var sectionCount = br.ReadUInt16(); fs.Position += 12; // skip TimeStamp, PtrToSymTab, NumSyms var optHeaderSize = br.ReadUInt16(); fs.Position += 2; // skip Characteristics // Sections start right after the optional header. fs.Position = peOffset + 4 + 20 + optHeaderSize; // Each section header is 40 bytes, name is the first 8. var nameBytes = new byte[8]; for (var i = 0; i < sectionCount; i++) { fs.Read(nameBytes, 0, 8); var name = System.Text.Encoding.ASCII.GetString(nameBytes).TrimEnd('\0'); if (name == ".bind") return true; fs.Position += 32; // rest of section header } return false; } catch { return false; } } } public enum StripResult { /// No Recroom_Release.exe at the expected path — /// caller should treat this as a soft skip (Oculus install /// shouldn't fail just because the exe name is different). NotApplicable, /// The exe was already unwrapped (no .bind section). No /// work done. Common on second runs and on non-Steam installs. AlreadyUnpacked, /// SteamStub was detected and Steamless successfully /// produced an unwrapped binary. Original backed up to /// Recroom_Release.exe.steamwrapped. Stripped, }