From 52b8d2150d689dd0d29f2005217bbda6c061e889 Mon Sep 17 00:00:00 2001 From: CJ Lees Date: Fri, 2 Oct 2026 04:08:48 +0000 Subject: [PATCH 01/17] feat: add native GPUI desktop alternative and evaluation workflow --- .github/workflows/gpui-build.yml | 64 + .gitignore | 5 +- README.md | 9 + docs/gpui-evaluation.md | 176 + docs/images/gpui-dashboard.png | Bin 0 -> 78252 bytes native/gpui/Cargo.lock | 8047 ++++++++++++++++++++++++++++++ native/gpui/Cargo.toml | 18 + native/gpui/README.md | 89 + native/gpui/rust-toolchain.toml | 4 + native/gpui/src/backend.rs | 243 + native/gpui/src/main.rs | 58 + native/gpui/src/ui.rs | 798 +++ scripts/gpui.py | 125 + scripts/measure_desktop.py | 71 + scripts/package_gpui.py | 29 + scripts/smoke_gpui_linux.py | 92 + scripts/test_gpui.py | 63 + 17 files changed, 9890 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/gpui-build.yml create mode 100644 docs/gpui-evaluation.md create mode 100644 docs/images/gpui-dashboard.png create mode 100644 native/gpui/Cargo.lock create mode 100644 native/gpui/Cargo.toml create mode 100644 native/gpui/README.md create mode 100644 native/gpui/rust-toolchain.toml create mode 100644 native/gpui/src/backend.rs create mode 100644 native/gpui/src/main.rs create mode 100644 native/gpui/src/ui.rs create mode 100644 scripts/gpui.py create mode 100644 scripts/measure_desktop.py create mode 100644 scripts/package_gpui.py create mode 100644 scripts/smoke_gpui_linux.py create mode 100644 scripts/test_gpui.py diff --git a/.github/workflows/gpui-build.yml b/.github/workflows/gpui-build.yml new file mode 100644 index 0000000..814dd6f --- /dev/null +++ b/.github/workflows/gpui-build.yml @@ -0,0 +1,64 @@ +name: GPUI Alternative + +on: + pull_request: + paths: + - 'native/gpui/**' + - 'scripts/*gpui*.py' + - 'scripts/measure_desktop.py' + - 'backend/**' + - '*.py' + - 'requirements.txt' + - 'presets.json' + - 'docs/gpui-evaluation.md' + - '.github/workflows/gpui-build.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + native: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-2022] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: dtolnay/rust-toolchain@master + with: + toolchain: '1.99.0' + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: native/gpui + - name: Linux native dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libvulkan-dev libx11-xcb-dev libxcb1-dev libasound2-dev libclang-dev mesa-vulkan-drivers xvfb xauth openbox xcompmgr xdotool + - name: Python lifecycle tests + run: python -m unittest scripts/test_gpui.py + - name: Format + run: cargo fmt --manifest-path native/gpui/Cargo.toml --check + - name: Lint + run: cargo clippy --locked --manifest-path native/gpui/Cargo.toml --all-targets -- -D warnings + - name: Rust tests + run: python scripts/gpui.py test + - name: Build release + run: python scripts/gpui.py build --release + - name: Linux native window lifecycle regression check + if: runner.os == 'Linux' + run: xvfb-run -a python scripts/smoke_gpui_linux.py --start-session --executable native/gpui/target/release/patchopsiii-gpui + - name: Archive Linux evaluation build + if: runner.os == 'Linux' + run: python scripts/package_gpui.py --executable native/gpui/target/release/patchopsiii-gpui --output dist/gpui/PatchOpsIII-gpui-linux.zip + - name: Archive Windows evaluation build + if: runner.os == 'Windows' + run: python scripts/package_gpui.py --executable native/gpui/target/release/patchopsiii-gpui.exe --output dist/gpui/PatchOpsIII-gpui-windows.zip + - uses: actions/upload-artifact@v4 + with: + name: PatchOpsIII-gpui-${{ runner.os }} + path: dist/gpui/*.zip diff --git a/.gitignore b/.gitignore index 5dc83a9..95593d2 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ node_modules/ *.vdf # codex: -.codex/ \ No newline at end of file +.codex/ + +# Standalone GPUI build output (Cargo.lock is committed) +native/gpui/target/ diff --git a/README.md b/README.md index 6d08e37..dee7975 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - [Advanced Tab](#advanced-tab) - [Terminal & Logging](#terminal--logging) - [Installation](#installation) +- [Native GPUI evaluation](#native-gpui-evaluation) - [Forked Components](#forked-components) - [Quick Start](#quick-start) - [Screenshots](#screenshots) @@ -84,7 +85,15 @@ bun run dev:desktop The browser development server uses Vite on `127.0.0.1:5173` and the Python API on `127.0.0.1:8765`. The Electron desktop command uses Vite on `127.0.0.1:5174` and its Python API on `127.0.0.1:8766`, so both commands can run at the same time. The renderer communicates with Python through HTTP APIs and `/ws` WebSockets; Electron IPC is reserved for desktop-specific bridge actions such as selecting a local game directory. +## Native GPUI evaluation + +An independent Rust/GPUI desktop alternative lives in [native/gpui](native/gpui/README.md). +It uses native controls and the current Python operations alongside Electron. +See [the research and evaluation plan](docs/gpui-evaluation.md) for comparisons +with the Tauri alternatives, supported workflows, build instructions, and parity gaps. + ## Forked Components + - **BO3 Enhanced Proton fork metadata:** [bo3-enhanced-proton/README.md](bo3-enhanced-proton/README.md) - Upstream source: https://github.com/Weather-OS/GDK-Proton - Current base release: `release10-32` diff --git a/docs/gpui-evaluation.md b/docs/gpui-evaluation.md new file mode 100644 index 0000000..b34b88a --- /dev/null +++ b/docs/gpui-evaluation.md @@ -0,0 +1,176 @@ +# GPUI research and evaluation plan + +Research date: 2026-10-02. Baseline: the current `main` Electron/React/Python app. +Related alternatives: [Tauri shell + Rust core PR #36](https://github.com/boggedbrush/PatchOpsIII/pull/36) +and [in-process Rust/Tauri PR #40](https://github.com/boggedbrush/PatchOpsIII/pull/40). +This GPUI branch starts from `main`, independently of both Tauri branches. + +## Findings and architecture decision + +GPUI is Zed's GPU-accelerated Rust framework, combining retained entity state +with declarative view rendering. It is pre-1.0 and APIs can break between +versions. It replaces the renderer as well as the desktop shell: our React, +CSS, browser accessibility, DOM tests, and Electron IPC cannot simply transfer. +The upstream main README now describes a separate `gpui_platform` package; +the published GPUI 0.2.2 selected here still uses `Application::new()` and +includes the platform implementations. Mixing these APIs would break builds. + +GPUI Component supplies keyboard-aware buttons and text inputs, text editing, +focus handling, and the root/theme setup, reducing custom control work. Pinning +the compatible GPUI 0.2.2 / Component 0.5.0 pair and committing the lockfile +avoids adopting an evolving Git revision or silently upgrading the toolkit. +The optional WebView feature is not enabled. Both dependencies use Apache-2.0; +distribution still needs an audit of notices for the complete dependency tree. + +Reusing the current Python API creates a controlled first comparison of desktop +renderers. It retains the existing download, install, backup, hash, Steam, UAC, +and config semantics. It also retains Python startup, packaging cost, and HTTP +serialization; removing Chromium does not prove a smaller or faster complete +application. A later in-process Rust port should use a toolkit-independent core +so Tauri and GPUI can exercise identical business logic. This PR does not import +the unmerged Rust rewrite or duplicate delicate game-file operations in Rust. + +Sources inspected: + +- [GPUI upstream README](https://github.com/zed-industries/zed/blob/main/crates/gpui/README.md): architecture, pre-1.0 status, platform requirements. +- [Published GPUI 0.2.2 source](https://crates.io/crates/gpui/0.2.2): standalone application, async contexts, platform folder prompts. +- [GPUI Component 0.5.0 source](https://crates.io/crates/gpui-component/0.5.0): compatible GPUI dependency, inputs, buttons, Root and theme setup. +- Repository `src/main/main.ts`, `src/renderer/lib/api.ts`, `src/renderer/main.tsx`, and `backend/api.py`: actual lifecycle, payloads, config normalization, state, error responses, and logs. +- PR #36 and #40 metadata: the shell-only migration and the subsequent backend rewrite have different scopes and costs. + +## Implementation plan and resulting slice + +1. Add an isolated Cargo application and compatible native controls. Preserve the + app's dark charcoal/red control-center visual language and use native window + chrome, seven navigation sections, scrollable settings, and a log panel. +2. Launch a supervised Python service on an ephemeral loopback port. Wait for + health before opening GPUI; always reap owned processes on normal/error exits. +3. Execute API calls on one HTTP worker. Reflect backend-reported state, preserve + unsuccessful HTTP-200 responses and depot instructions, disable overlapping + actions, and require an explicit operation confirmation. +4. Implement representative real workflows throughout the app, using existing + payloads and config value mappings. Keep input drafts intact during refresh; + provide an explicit button to load current values. +5. Add meaningful protocol/lifecycle checks, Windows/Linux build CI, downloadable + evaluation archives, a common process-tree sampler, and a parity/manual review + checklist. Keep production release workflows separate from evaluation builds. + +## Stack comparison + +| Criterion | Current Electron | Tauri #36 / #40 | GPUI slice | +|---|---|---|---| +| UI technology | React/CSS + Chromium | React/CSS + OS WebView | Native Rust controls + GPU rendering | +| Backend | Python child + HTTP/WS | #36 Python + small Rust core; #40 in-process Rust | Same Python + HTTP | +| Renderer reuse | Baseline | High | Rewrite required | +| Platform work | Existing Windows/Linux packages | WebView availability and new Rust packaging | GPU/driver compatibility, native UI/input and new packaging | +| Accessibility | Browser infrastructure; app still needs testing | WebView infrastructure; app still needs testing | Must validate controls and screen readers on target OSes | +| Delivery in this branch | Unchanged | Independent open PRs | Source build and prerequisite-based evaluation ZIPs | +| Startup/RAM/package size | Measure | Measure each PR separately | Measure complete stack; no claimed improvement | +| Migration cost | Existing maintenance | Shell changes; #40 also ports operations | Entire UI rewrite; later backend port optional | + +## Workflow coverage + +| Workflow | Implemented in this slice | Remaining evaluation/parity work | +|---|---|---| +| Installation detection/directory | Actual status and native folder picker; manual path entry; existing backend validation | Folder picker on Windows, Wayland portals and Steam Deck | +| Launch/profiles | Steam launch, backend-provided profiles, Workshop open/install request | Workshop progress presentation and detailed profile states | +| T7 Patch | Install/update, uninstall, gamertag/color/password, friends-only | Real install/UAC cancellation, name preview, password editing UX | +| EXE Swapper | Compatible/current/Enhanced restore, hash and integrity state, depot error text | Guided depot dialog/polling/copy action; real restore/backup checks | +| Enhanced | Pick/enter source, validate, install, uninstall | Real Windows/Linux game/proton workflows | +| Graphics | Presets, FPS, FOV, refresh, render %, resolution, display modes, V-Sync and FPS counter | Slider UX, valid hardware-specific mode list, side-by-side visual polish | +| Advanced/QoL | Smoothing, hidden options, CPU reduction, frame latency, VRAM target, intro/compiler/read-only toggles | Real config/backup verification on disposable installs | +| DXVK | Install/uninstall, async/cache/HUD, threads, FPS, latency and tear-free settings | Real download/install/proton behavior | +| Logs/maintenance | Up to 80 recent lines, five-second refresh, clear logs/cache, reset stock, update check/channel | Streaming WS logs, copy/export UX and platform updater integration | +| Delivery | Windows/Linux CI and source-backed release archives | Bundled Python or shared Rust core; signed installers, updater, notices and release integration | + +Status refresh uses the complete existing status endpoint, including executable +hashing. Polling every five seconds can cost more idle CPU than Electron's event +driven updates; include that cost in measurements. No new background game scans +or fabricated installation states are introduced. Actions preserve backend +validation and require confirmation; all game-modifying smoke tests must use +an installation whose backups can be inspected and restored. + +## Fair measurement protocol + +Use the same Windows PC and Steam Deck/Linux PC for all three candidates. Record +OS, GPU/driver, display scale, power profile, game-directory state, and commit +SHA. Build optimized release artifacts. Exclude compilation, debugger and dev +servers. Compare Electron, #36, #40 and GPUI separately; a Python-backed GPUI +versus #40 compares both renderer and backend changes. + +For each candidate collect ten cold launches and ten warm launches. Record +process start to first painted interactive window **and** service-ready status +with screen recording or platform tracing. Include backend startup. Report +median and p95, retaining raw observations. Do not label cargo build time as app +startup time, or GPUI entity creation as first paint. + +Measure idle for 60 seconds after settling, then the same navigation/config and +log workload. Use the supervisor PID that owns both UI and backend processes: + +```sh +python -m pip install psutil +python scripts/measure_desktop.py --pid 12345 --label gpui-release \ + --seconds 60 --artifact dist/gpui/PatchOpsIII-gpui-linux.zip \ + --output measurements/gpui-idle.json +``` + +The sampler captures RSS and CPU for the process tree, including children; RSS +can double-count shared pages and excludes GPU memory. Collect GPU usage with +platform tools. GPUI archives currently omit the Python runtime; report its +installed footprint separately, and do not compare that ZIP directly with a +fully bundled Electron installer. Elevated/detached children must be measured +separately. Evaluate input latency, scrolling, text entry/IME, scaling, keyboard +focus, screen readers, offline/error behavior, and update/install/uninstall +reliability before selecting a winner. There is no winner established by this PR. + +## Validation recorded for this PR + +On the supplied Linux cloud environment: + +- Debug and optimized release builds completed with the locked dependency tree. +- Four Rust protocol tests passed, including an actual local HTTP worker request + and HTTP error response. Clippy with warnings denied and rustfmt checks passed. +- Six Python launcher tests passed, covering child reaping, startup/UI failures, + missing binaries, early service exit, and custom target-directory consistency. +- The existing Electron renderer TypeScript check (`tsc --noEmit`) passed. +- A real Python service bound an ephemeral port, returned health/status with no + game installation, and stopped accepting connections after launcher cleanup. +- The native window rendered under Xvfb/Openbox with Mesa's software Vulkan + driver and an X compositor. Dashboard/Graphics navigation and text entry were + inspected; an edited FPS draft remained unchanged across status refreshes. +- The normal X11 close path exposed a GPUI 0.2.2 callback borrow panic. Quit is + now scheduled on a later event-loop turn. A read-only native lifecycle smoke + check exercises the real platform close callback, and Linux CI runs it too. +- Archive contents and the process-tree sampler were smoke checked. The sampler + captured the supervisor, Python API and native GUI; its debug/software-rendered + samples are not comparative performance evidence. + +![GPUI release dashboard in the Linux smoke session](images/gpui-dashboard.png) + +A GUI Apply-click smoke check was rejected by automatic approval review because +it could activate a configuration/file-changing action. No game-mutating UI +clicks were executed. Confirmation behavior was reviewed in code; actual game +operations, Windows, Wayland, Steam Deck, accessibility, and comparative +performance remain manual/hardware validation gates. Native Windows/Linux CI +is configured in this PR; its hosted results are separate from these local checks. + +## Reviewer smoke checklist + +- Build with the pinned lockfile; open the native window on Windows and Linux. +- Start Electron simultaneously; confirm distinct ports/processes. Close GPUI + and confirm its service exits while Electron stays alive. +- With no game directory, verify status/errors and manual folder input. Browse + to a disposable BO3 install, confirm selection, refresh, and relaunch. +- Navigate every section with mouse and keyboard. Edit a field while refresh + runs; confirm the draft remains. Verify current-values reload is explicit. +- Cancel an operation confirmation and verify no files changed. Verify the + selected operation and target before confirming a config mutation. +- Apply a preset and individual config values; inspect config.ini and backend + logs, including readonly error handling and special CPU/hidden-option values. +- On a test install verify each mod install/uninstall and EXE restore preserves + expected backups. Exercise compatible-depot-required and UAC cancellation. +- Verify API startup failure, disconnect, download errors, and clean shutdown. + Wait for active file operations before closing; cancellation is not implemented. +- Test X11, Wayland, actual Steam Deck, high DPI, IME and screen-reader behavior. +- Measure optimized complete stacks and record results before any replacement + decision. Hardware results and full packaged parity are follow-up gates. diff --git a/docs/images/gpui-dashboard.png b/docs/images/gpui-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..4bfb3a344e5168ff10e93432c6ea5862917628a9 GIT binary patch literal 78252 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, +} + +pub struct Reply { + pub state: Option, + pub message: String, + pub failed: bool, + pub is_status: bool, +} + +pub struct Backend { + pub requests: mpsc::Sender, + pub replies: mpsc::Receiver, +} + +pub fn validate_url(url: &str) -> Result { + let parsed = reqwest::Url::parse(url)?; + if parsed.scheme() != "http" + || parsed.host_str() != Some("127.0.0.1") + || !parsed.username().is_empty() + || parsed.password().is_some() + || parsed.path() != "/" + || parsed.query().is_some() + || parsed.fragment().is_some() + { + bail!("GPUI backend must be an HTTP origin on 127.0.0.1"); + } + Ok(url.trim_end_matches('/').to_owned()) +} + +pub fn decode_reply(path: &str, value: Value) -> Result { + if value.get("ok").and_then(Value::as_bool) == Some(false) + || value.get("valid").and_then(Value::as_bool) == Some(false) + { + let mut message = value["error"] + .as_str() + .or(value["message"].as_str()) + .unwrap_or("Operation failed") + .to_owned(); + if let Some(command) = value["depotCommand"].as_str() { + message.push_str(&format!("\nSteam console command: {command}")); + } + bail!("{message}"); + } + if path != "/api/status" && value.get("ok").and_then(Value::as_bool) != Some(true) { + bail!("Backend returned an invalid operation result"); + } + let state = if path == "/api/status" { + if !value["appVersion"].is_string() || !value["gameDetected"].is_boolean() { + bail!("Backend returned an invalid status document"); + } + Some(value.clone()) + } else { + value.get("state").cloned() + }; + let message = if path == "/api/status" { + "Connected to the local service".to_owned() + } else if let Some(update) = value.get("update") { + format!("Update check: {update}") + } else { + value["message"] + .as_str() + .unwrap_or("Operation completed") + .to_owned() + }; + Ok(Reply { + state, + message, + failed: false, + is_status: path == "/api/status", + }) +} + +fn execute(client: &Client, url: &str, request: Request) -> Result { + let path = request.path; + let builder = match request.body { + Some(body) => client.post(format!("{url}{path}")).json(&body), + None => client + .get(format!("{url}{path}")) + .timeout(Duration::from_secs(30)), + }; + let value = builder + .send() + .context("Local service request failed")? + .error_for_status() + .context("Local service returned an HTTP error")? + .json::() + .context("Local service returned invalid JSON")?; + decode_reply(&path, value) +} + +impl Backend { + pub fn start(url: String) -> Result { + let url = validate_url(&url)?; + // Loopback requests must not use a developer's system HTTP proxy. + let client = Client::builder() + .no_proxy() + .redirect(reqwest::redirect::Policy::none()) + .connect_timeout(Duration::from_secs(3)) + .timeout(Duration::from_secs(600)) + .build()?; + let (requests, incoming) = mpsc::channel::(); + let (outgoing, replies) = mpsc::channel(); + thread::Builder::new() + .name("patchops-gpui-api".into()) + .spawn(move || { + for request in incoming { + let is_status = request.path == "/api/status"; + let reply = execute(&client, &url, request).unwrap_or_else(|error| Reply { + state: None, + message: format!("{error:#}"), + failed: true, + is_status, + }); + if outgoing.send(reply).is_err() { + break; + } + } + })?; + Ok(Self { requests, replies }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn accepts_only_loopback_origins() { + assert_eq!( + validate_url("http://127.0.0.1:8767/").unwrap(), + "http://127.0.0.1:8767" + ); + for url in [ + "https://127.0.0.1", + "http://example.com", + "http://localhost", + "http://127.0.0.1/api", + "http://user@127.0.0.1", + "http://127.0.0.1?x=1", + ] { + assert!(validate_url(url).is_err(), "{url}"); + } + } + + #[test] + fn handles_http_200_operation_failures_and_depot_instructions() { + let error = decode_reply("/api/exe-swap/compatible", json!({"ok": false, "error": "Download depot first", "depotCommand": "download_depot 311210 311211 123"})).err().unwrap(); + assert!( + error + .to_string() + .contains("download_depot 311210 311211 123") + ); + assert!(decode_reply("/api/status", json!({"ok": true})).is_err()); + assert!(decode_reply("/api/config", json!({"state": {}})).is_err()); + assert!( + decode_reply( + "/api/enhanced-validate", + json!({"ok": true, "valid": false, "message": "Missing files"}) + ) + .is_err() + ); + } + + #[test] + fn preserves_backend_state_without_fabricating_success() { + let state = json!({"appVersion": "v1.3.4", "gameDetected": false}); + assert_eq!( + decode_reply("/api/status", state.clone()).unwrap().state, + Some(state.clone()) + ); + assert_eq!( + decode_reply("/api/config", json!({"ok": true, "state": state})) + .unwrap() + .state, + Some(state) + ); + } + + #[test] + fn worker_posts_exact_payload_and_surfaces_http_errors() { + use std::{ + io::{Read, Write}, + net::TcpListener, + }; + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let url = format!("http://{}", listener.local_addr().unwrap()); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(3))) + .unwrap(); + let mut request = Vec::new(); + let mut byte = [0u8; 1]; + while !request.ends_with(b"\r\n\r\n") { + stream.read_exact(&mut byte).unwrap(); + request.push(byte[0]); + } + let headers = String::from_utf8(request).unwrap(); + assert!(headers.starts_with("POST /api/config HTTP/1.1")); + let length: usize = headers + .lines() + .find_map(|line| { + line.to_lowercase() + .strip_prefix("content-length:") + .map(|s| s.trim().parse().unwrap()) + }) + .unwrap(); + let mut body = vec![0; length]; + stream.read_exact(&mut body).unwrap(); + assert_eq!( + serde_json::from_slice::(&body).unwrap(), + json!({"key": "MaxFPS", "value": 144}) + ); + stream.write_all(b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").unwrap(); + }); + let backend = Backend::start(url).unwrap(); + backend + .requests + .send(Request { + path: "/api/config".into(), + body: Some(json!({"key": "MaxFPS", "value": 144})), + }) + .unwrap(); + let reply = backend + .replies + .recv_timeout(Duration::from_secs(5)) + .unwrap(); + assert!(reply.failed); + assert!(reply.message.contains("503")); + assert!(reply.state.is_none()); + server.join().unwrap(); + } +} diff --git a/native/gpui/src/main.rs b/native/gpui/src/main.rs new file mode 100644 index 0000000..3326059 --- /dev/null +++ b/native/gpui/src/main.rs @@ -0,0 +1,58 @@ +mod backend; +mod ui; + +use anyhow::{Context, Result}; +use gpui::{prelude::*, *}; +use gpui_component::{Root, Theme, ThemeMode}; + +fn main() -> Result<()> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + let url = std::env::var("PATCHOPSIII_GPUI_BACKEND_URL") + .context("Start GPUI with: python scripts/gpui.py run (see native/gpui/README.md)")?; + let backend = backend::Backend::start(url)?; + Application::new().run(move |cx: &mut App| { + gpui_component::init(cx); + Theme::change(ThemeMode::Dark, None, cx); + let theme = Theme::global_mut(cx); + theme.colors.primary = rgb(0xd92323).into(); + theme.colors.primary_hover = rgb(0xf03535).into(); + theme.colors.primary_active = rgb(0xa41616).into(); + theme.colors.primary_foreground = rgb(0xffffff).into(); + theme.colors.ring = rgb(0xf03535).into(); + cx.on_window_closed(|cx| { + // GPUI 0.2.2's X11 close callback holds platform state borrowed. + // Quit on a later event-loop turn, after that callback has returned. + cx.spawn(async move |cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(1)) + .await; + let _ = cx.update(|cx| { + if cx.windows().is_empty() { + cx.quit(); + } + }); + }) + .detach(); + }) + .detach(); + let bounds = Bounds::centered(None, size(px(1150.), px(820.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + window_min_size: Some(size(px(800.), px(600.))), + titlebar: Some(TitlebarOptions { + title: Some("PatchOpsIII · GPUI".into()), + ..Default::default() + }), + ..Default::default() + }, + move |window, cx| { + let view = cx.new(|cx| ui::ControlCenter::new(backend, window, cx)); + cx.new(|cx| Root::new(view, window, cx)) + }, + ) + .expect("Unable to open the GPUI window"); + cx.activate(true); + }); + Ok(()) +} diff --git a/native/gpui/src/ui.rs b/native/gpui/src/ui.rs new file mode 100644 index 0000000..d8a18d0 --- /dev/null +++ b/native/gpui/src/ui.rs @@ -0,0 +1,798 @@ +use crate::backend::{Backend, Request}; +use gpui::{prelude::*, *}; +use gpui_component::{ + Disableable, + button::{Button, ButtonVariants}, + input::{Input, InputState}, +}; +use serde_json::{Value, json}; +use std::{ + collections::BTreeMap, + time::{Duration, Instant}, +}; + +const PAGES: [&str; 7] = [ + "Dashboard", + "T7 Patch", + "EXE Swapper", + "Enhanced", + "Graphics", + "DXVK", + "Tools", +]; + +pub struct ControlCenter { + backend: Backend, + state: Value, + page: usize, + busy: bool, + connected: bool, + message: String, + failed: bool, + last_refresh: Instant, + inputs: BTreeMap<&'static str, Entity>, + pending: Option<(String, Request)>, +} + +impl ControlCenter { + pub fn new(backend: Backend, window: &mut Window, cx: &mut Context) -> Self { + let inputs = [ + ("directory", "Game directory"), + ("gamertag", "Gamertag (up to 20 characters)"), + ("password", "Network password"), + ("color", "Color code, e.g. ^1"), + ("dump", "BO3 Enhanced source directory"), + ("fps", "0–1000"), + ("fov", "65–120"), + ("resolution", "1920x1080"), + ("refresh", "Refresh rate"), + ("render", "Render resolution %"), + ("latency", "0–4"), + ("vram", "75–100"), + ("dxvk-threads", "0–64"), + ("dxvk-fps", "0–360"), + ("dxvk-latency", "0–16"), + ] + .into_iter() + .map(|(key, placeholder)| { + let input = cx.new(|cx| { + InputState::new(window, cx) + .placeholder(placeholder) + .masked(key == "password") + }); + (key, input) + }) + .collect(); + let mut view = Self { + backend, + state: Value::Null, + page: 0, + busy: false, + connected: false, + message: "Connecting to the local service…".into(), + failed: false, + last_refresh: Instant::now(), + inputs, + pending: None, + }; + view.send("/api/status", None, cx); + // Drain replies and periodically refresh on GPUI's foreground executor. + // The only blocking work lives on the dedicated HTTP worker. + cx.spawn_in(window, async move |entity, cx| { + loop { + cx.background_executor() + .timer(Duration::from_millis(100)) + .await; + if entity + .update_in(cx, |view, window, cx| { + while let Ok(reply) = view.backend.replies.try_recv() { + view.busy = false; + if reply.failed { + view.failed = true; + if reply.is_status { + view.connected = false; + } + view.message = reply.message; + } else { + let initial = view.state.is_null(); + let reconnected = !view.connected; + if let Some(state) = reply.state { + view.state = state; + view.connected = true; + } + if initial { + view.load_inputs(window, cx); + } + if !reply.is_status + || reconnected + || view.message.starts_with("Connecting") + { + view.failed = false; + view.message = reply.message; + } + } + view.last_refresh = Instant::now(); + cx.notify(); + } + if !view.busy + && view.pending.is_none() + && view.last_refresh.elapsed() >= Duration::from_secs(5) + { + view.send("/api/status", None, cx); + } + }) + .is_err() + { + break; + } + } + }) + .detach(); + view + } + + fn send(&mut self, path: &str, body: Option, cx: &mut Context) { + if self.busy { + return; + } + match self.backend.requests.send(Request { + path: path.into(), + body, + }) { + Ok(()) => { + self.busy = true; + self.last_refresh = Instant::now(); + } + Err(_) => { + self.failed = true; + self.connected = false; + self.message = "Local service worker stopped. Restart GPUI.".into(); + } + } + cx.notify(); + } + + fn stage(&mut self, label: String, path: String, body: Value, cx: &mut Context) { + self.pending = Some(( + label, + Request { + path, + body: Some(body), + }, + )); + cx.notify(); + } + + fn text(&self, key: &str, cx: &App) -> String { + self.inputs[key].read(cx).value().to_string() + } + fn load_inputs(&mut self, window: &mut Window, cx: &mut Context) { + for (key, pointer) in [ + ("directory", "/gameDir"), + ("gamertag", "/t7/plainName"), + ("color", "/t7/colorCode"), + ("dump", "/enhanced/dumpSource"), + ("fps", "/graphics/maxFps"), + ("fov", "/graphics/fov"), + ("resolution", "/graphics/resolution"), + ("refresh", "/graphics/refreshRate"), + ("render", "/graphics/renderResolution"), + ("latency", "/advanced/maxFrameLatency"), + ("vram", "/advanced/vramTarget"), + ("dxvk-threads", "/dxvk/settings/numCompilerThreads"), + ("dxvk-fps", "/dxvk/settings/maxFrameRate"), + ("dxvk-latency", "/dxvk/settings/maxFrameLatency"), + ] { + let value = self.value(pointer); + self.inputs[key].update(cx, |state, cx| { + state.set_value(if value == "—" { String::new() } else { value }, window, cx) + }); + } + } + fn flag(&self, pointer: &str) -> bool { + self.state + .pointer(pointer) + .and_then(Value::as_bool) + .unwrap_or(false) + } + fn value(&self, pointer: &str) -> String { + match self.state.pointer(pointer) { + Some(Value::String(s)) => s.clone(), + Some(Value::Number(n)) => n + .as_f64() + .map(|n| n.to_string()) + .unwrap_or_else(|| n.to_string()), + Some(Value::Null) | None => "—".into(), + Some(v) => v.to_string(), + } + } + + fn action( + &self, + id: String, + label: String, + path: &str, + body: Value, + cx: &mut Context, + ) -> Button { + let path = path.to_owned(); + Button::new(SharedString::from(id)) + .label(label.clone()) + .disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(move |view, _, _, cx| { + view.stage(label.clone(), path.clone(), body.clone(), cx) + })) + } + + fn toggle( + &self, + label: &str, + pointer: &str, + path: &str, + key: &str, + cx: &mut Context, + ) -> AnyElement { + let current = self.flag(pointer); + let body = json!({key: !current}); + div() + .flex() + .justify_between() + .items_center() + .gap_4() + .child(format!("{label}: {}", if current { "On" } else { "Off" })) + .child(self.action( + format!("toggle-{pointer}"), + format!("{} {label}", if current { "Disable" } else { "Enable" }), + path, + body, + cx, + )) + .into_any_element() + } + + fn config_toggle( + &self, + label: &str, + pointer: &str, + key: &str, + cx: &mut Context, + ) -> AnyElement { + let current = self.flag(pointer); + div().flex().justify_between().items_center().gap_4() + .child(format!("{label}: {}", if current { "On" } else { "Off" })) + .child(self.action(format!("config-{key}"), format!("{} {label}", if current { "Disable" } else { "Enable" }), "/api/config", + json!({"key": key, "value": if current {0} else {1}, "comment": "Managed by PatchOpsIII"}), cx)).into_any_element() + } + + fn field(&self, label: &str, key: &str) -> AnyElement { + div() + .flex() + .flex_col() + .gap_2() + .child(label.to_owned()) + .child(Input::new(&self.inputs[key]).disabled(self.busy)) + .into_any_element() + } + + fn numeric_field( + &self, + label: &str, + input: &'static str, + config: &'static str, + min: i64, + max: i64, + cx: &mut Context, + ) -> AnyElement { + div() + .flex() + .items_end() + .gap_3() + .child(div().flex_1().child(self.field(label, input))) + .child( + Button::new(SharedString::from(format!("apply-{input}"))) + .label("Apply") + .disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(move |view, _, _, cx| { + match view.text(input, cx).parse::() { + Ok(value) if (min..=max).contains(&value) => view.stage( + format!("Set {config} to {value}"), + "/api/config".into(), + json!({"key": config, "value": value}), + cx, + ), + _ => { + view.message = format!("Enter a whole number from {min} to {max}."); + view.failed = true; + cx.notify(); + } + } + })), + ) + .into_any_element() + } + + fn choose_directory(&mut self, input: &'static str, window: &Window, cx: &mut Context) { + let paths = cx.prompt_for_paths(PathPromptOptions { + files: false, + directories: true, + multiple: false, + prompt: Some("Select a folder".into()), + }); + cx.spawn_in(window, async move |entity, cx| match paths.await { + Ok(Ok(Some(paths))) => { + if let Some(path) = paths.first() { + let value = path.to_string_lossy().into_owned(); + let _ = entity.update_in(cx, |view, window, cx| { + view.inputs[input] + .update(cx, |state, cx| state.set_value(value, window, cx)); + }); + } + } + Ok(Ok(None)) => {} + _ => { + let _ = entity.update(cx, |view, cx| { + view.message = "Folder picker unavailable. Enter the path directly.".into(); + view.failed = true; + cx.notify(); + }); + } + }) + .detach(); + } + + fn folder_field(&self, label: &str, key: &'static str, cx: &mut Context) -> AnyElement { + div() + .flex() + .items_end() + .gap_3() + .child(div().flex_1().child(self.field(label, key))) + .child( + Button::new(SharedString::from(format!("browse-{key}"))) + .label("Browse…") + .disabled(self.busy) + .on_click(cx.listener(move |view, _, window, cx| { + view.choose_directory(key, window, cx) + })), + ) + .into_any_element() + } + + fn dashboard(&self, cx: &mut Context) -> AnyElement { + panel("Game installation") + .child(format!("Current directory: {}", self.value("/gameDir"))) + .child(format!( + "Platform: {} · Version: {}", + self.value("/platform"), + self.value("/appVersion") + )) + .child(format!( + "Executable: {} · {}", + self.value("/exeSwap/displayLabel"), + self.value("/exeSwap/integrityMessage") + )) + .child(self.folder_field("Black Ops III directory", "directory", cx)) + .child( + Button::new("save-directory") + .label("Use this directory") + .primary() + .disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(|view, _, _, cx| { + let path = view.text("directory", cx); + if path.trim().is_empty() { + view.message = "Select or enter a directory first.".into(); + view.failed = true; + cx.notify(); + return; + } + view.stage( + format!("Use directory {path}"), + "/api/game-directory".into(), + json!({"path": path}), + cx, + ); + })), + ) + .child(div().mt_4().child("Launch profiles")) + .children( + self.state["launchProfiles"] + .as_array() + .into_iter() + .flatten() + .map(|profile| { + let label = profile["label"].as_str().unwrap_or("Profile").to_owned(); + let option = profile["option"].as_str().unwrap_or("").to_owned(); + let id = profile["id"].as_str().unwrap_or("profile").to_owned(); + div() + .flex() + .gap_3() + .items_center() + .child(self.action( + format!("profile-{id}"), + format!( + "{label}{}", + if profile["active"] == true { + " · Active" + } else { + "" + } + ), + "/api/launch-options", + json!({"options": option}), + cx, + )) + .when( + profile["subscribed"].is_boolean() + && id != "default" + && id != "offline", + |row| { + row.child(self.action( + format!("workshop-{id}"), + "Open Workshop".into(), + "/api/workshop-install", + json!({"profileId": id}), + cx, + )) + }, + ) + }), + ) + .into_any_element() + } + + fn t7(&self, cx: &mut Context) -> AnyElement { + panel("T7 Patch") + .child(format!("Installed: {} · Current gamertag: {}", self.flag("/t7/installed"), self.value("/t7/plainName"))) + .child(div().flex().gap_3() + .child(self.action("t7-install".into(), "Install / update T7 Patch".into(), "/api/t7-install", json!({}), cx)) + .child(self.action("t7-uninstall".into(), "Uninstall T7 Patch".into(), "/api/t7-uninstall", json!({}), cx))) + .child(self.field("Gamertag", "gamertag")) + .child(self.field("Gamertag color", "color")) + .child(Button::new("save-gamertag").label("Save gamertag").disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(|view, _, _, cx| view.stage("Update T7 gamertag".into(), "/api/t7-config".into(), json!({"gamertag": view.text("gamertag", cx), "colorCode": view.text("color", cx)}), cx)))) + .child(self.field("Network password (blank clears it)", "password")) + .child(Button::new("save-password").label("Save network password").disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(|view, _, _, cx| view.stage("Update T7 network password".into(), "/api/t7-config".into(), json!({"networkPassword": view.text("password", cx)}), cx)))) + .child(self.toggle("Friends only", "/t7/friendsOnly", "/api/t7-config", "friendsOnly", cx)).into_any_element() + } + + fn exe(&self, cx: &mut Context) -> AnyElement { + panel("Executable profiles") + .child(self.value("/exeSwap/displayLabel")) + .child(self.value("/exeSwap/integrityMessage")) + .child(format!("SHA-256: {}", self.value("/exeSwap/executableHash"))) + .child("Compatible installs may require a Steam depot download. Instructions appear in the operation result below.") + .children([("compatible", "Install compatible build"), ("current", "Restore current Steam build"), ("enhanced", "Restore Enhanced build")].into_iter().map(|(id, label)| + self.action(format!("exe-{id}"), label.into(), &format!("/api/exe-swap/{id}"), json!({}), cx))).into_any_element() + } + + fn enhanced(&self, cx: &mut Context) -> AnyElement { + panel("BO3 Enhanced") + .child(format!( + "Installed: {} · Backup status: {}", + self.flag("/enhanced/installed"), + self.value("/enhanced/backupStatus") + )) + .child(self.folder_field("Enhanced source directory", "dump", cx)) + .children( + [ + ("validate", "Validate source"), + ("install", "Install Enhanced"), + ] + .into_iter() + .map(|(id, label)| { + Button::new(SharedString::from(format!("enhanced-{id}"))) + .label(label) + .disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(move |view, _, _, cx| { + view.stage( + label.into(), + format!("/api/enhanced-{id}"), + json!({"dumpSource": view.text("dump", cx)}), + cx, + ) + })) + }), + ) + .child(self.action( + "enhanced-uninstall".into(), + "Uninstall Enhanced".into(), + "/api/enhanced-uninstall", + json!({}), + cx, + )) + .into_any_element() + } + + fn graphics(&self, cx: &mut Context) -> AnyElement { + panel("Graphics & performance") + .child(format!("Current: {} · {} Hz · FOV {} · FPS cap {}", self.value("/graphics/resolution"), self.value("/graphics/refreshRate"), self.value("/graphics/fov"), self.value("/graphics/maxFps"))) + .children(self.state["presets"].as_array().into_iter().flatten().filter_map(Value::as_str).map(|name| + self.action(format!("preset-{name}"), format!("Apply {name}"), "/api/presets/apply", json!({"name": name}), cx))) + .child(self.numeric_field("FPS cap (0 disables the limiter)", "fps", "MaxFPS", 0, 1000, cx)) + .child(self.numeric_field("Field of view", "fov", "FOV", 65, 120, cx)) + .child(self.numeric_field("Refresh rate", "refresh", "RefreshRate", 1, 1000, cx)) + .child(self.numeric_field("Render resolution %", "render", "ResolutionPercent", 50, 200, cx)) + .child(self.field("Resolution (WIDTHxHEIGHT)", "resolution")) + .child(Button::new("save-resolution").label("Apply resolution").disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(|view, _, _, cx| { + let value = view.text("resolution", cx); + let valid = value.split_once('x').is_some_and(|(w, h)| [w, h].iter().all(|n| n.parse::().is_ok_and(|n| (320..=16384).contains(&n)))); + if valid { view.stage(format!("Set resolution to {value}"), "/api/config".into(), json!({"key": "WindowSize", "value": value}), cx); } + else { view.message = "Enter resolution as WIDTHxHEIGHT, e.g. 1920x1080.".into(); view.failed = true; cx.notify(); } + }))) + .child(div().flex().gap_2().children([(0, "Windowed"), (1, "Fullscreen"), (2, "Borderless")].into_iter().map(|(mode, label)| self.action(format!("display-{mode}"), label.into(), "/api/config", json!({"key": "FullScreenMode", "value": mode}), cx)))) + .child(self.config_toggle("V-Sync", "/graphics/vsync", "Vsync", cx)) + .child(self.config_toggle("FPS counter", "/graphics/drawFps", "DrawFPS", cx)) + .child(self.config_toggle("Frame smoothing", "/advanced/smoothFramerate", "SmoothFramerate", cx)) + .child(self.action("unlock-graphics".into(), "Toggle hidden graphics options".into(), "/api/config", json!({"key": "RestrictGraphicsOptions", "value": if self.flag("/advanced/unlockOptions") {1} else {0}}), cx)) + .child(self.action("reduce-cpu".into(), "Toggle reduced CPU pressure".into(), "/api/config", json!({"key": "SerializeRender", "value": if self.flag("/advanced/reduceCpu") {0} else {2}}), cx)) + .child(self.numeric_field("Maximum frame latency", "latency", "MaxFrameLatency", 0, 4, cx)) + .child(self.field("VRAM target %", "vram")) + .child(Button::new("save-vram").label("Apply VRAM target").disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(|view, _, _, cx| { + match view.text("vram", cx).parse::() { + Ok(target) if (75..=100).contains(&target) => view.stage(format!("Set VRAM target to {target}%"), "/api/vram-target".into(), json!({"limited": target < 100, "target": target}), cx), + _ => { view.message = "VRAM target must be a whole number from 75 to 100.".into(); view.failed = true; cx.notify(); } + } + }))) + .child(self.toggle("Skip intro", "/qol/intro", "/api/intro-skip", "enabled", cx)) + .child(self.toggle("Skip all intros", "/qol/allIntros", "/api/all-intros-skip", "enabled", cx)) + .child(self.toggle("Modern DirectX compiler", "/qol/d3dcompiler", "/api/d3dcompiler", "enabled", cx)) + .child(self.toggle("Read-only config", "/advanced/configReadonly", "/api/config-readonly", "enabled", cx)).into_any_element() + } + + fn dxvk(&self, cx: &mut Context) -> AnyElement { + let settings = self + .state + .pointer("/dxvk/settings") + .cloned() + .unwrap_or(json!({})); + panel("DXVK-GPLAsync") + .child(format!("Installed: {}", self.flag("/dxvk/installed"))) + .child("Install uses the settings reported by the service, or backend defaults for a new installation.") + .child(format!("Current settings: {settings}")) + .child(self.action("dxvk-install".into(), "Install DXVK".into(), "/api/dxvk-install", settings.clone(), cx)) + .child(self.action("dxvk-uninstall".into(), "Uninstall DXVK".into(), "/api/dxvk-uninstall", json!({}), cx)) + .children([("Async compilation", "enableAsync"), ("GPL async cache", "gplAsyncCache"), ("DXVK HUD", "hudEnabled")].into_iter().map(|(label, key)| { + let mut next = settings.clone(); + next[key] = json!(!settings[key].as_bool().unwrap_or(false)); + self.action(format!("dxvk-{key}"), format!("Toggle {label}"), "/api/dxvk-config", next, cx) + })) + .children([("Compiler threads", "dxvk-threads", "numCompilerThreads", 64), ("FPS cap", "dxvk-fps", "maxFrameRate", 360), ("Frame latency", "dxvk-latency", "maxFrameLatency", 16)].into_iter().map(|(label, input, setting, max)| { + div().flex().items_end().gap_3().child(div().flex_1().child(self.field(label, input))) + .child(Button::new(SharedString::from(format!("apply-{input}"))).label("Apply").disabled(self.busy || !self.connected || self.pending.is_some()) + .on_click(cx.listener(move |view, _, _, cx| { + match view.text(input, cx).parse::() { + Ok(value) if value <= max => { let mut settings = view.state["dxvk"]["settings"].clone(); settings[setting] = json!(value); view.stage(format!("Set DXVK {label} to {value}"), "/api/dxvk-config".into(), settings, cx); } + _ => { view.message = format!("Enter a whole number from 0 to {max}."); view.failed = true; cx.notify(); } + } + }))) + })) + .children(["True", "False", "Auto"].into_iter().map(|mode| { let mut next = settings.clone(); next["tearFree"] = json!(mode); self.action(format!("tearfree-{mode}"), format!("Tear-free: {mode}"), "/api/dxvk-config", next, cx) })).into_any_element() + } + + fn tools(&self, cx: &mut Context) -> AnyElement { + panel("Maintenance") + .child(format!("Log file: {}", self.value("/logPath"))) + .children( + [ + ("/api/logs/clear", "Clear logs"), + ("/api/mod-files/clear", "Clear cached mod files"), + ("/api/reset-stock", "Reset game to stock"), + ("/api/update-check", "Check for updates"), + ] + .into_iter() + .map(|(path, label)| self.action(path.into(), label.into(), path, json!({}), cx)), + ) + .child(self.action( + "channel-stable".into(), + "Use stable channel".into(), + "/api/release-channel", + json!({"channel": "stable"}), + cx, + )) + .child(self.action( + "channel-beta".into(), + "Use beta channel".into(), + "/api/release-channel", + json!({"channel": "beta"}), + cx, + )) + .into_any_element() + } +} + +fn panel(title: &str) -> Div { + div() + .flex() + .flex_col() + .gap_4() + .p_6() + .rounded_lg() + .border_1() + .border_color(rgb(0x343438)) + .bg(rgb(0x17171b)) + .child( + div() + .text_xl() + .font_weight(FontWeight::BOLD) + .child(title.to_owned()), + ) +} + +impl Render for ControlCenter { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let content = match self.page { + 0 => self.dashboard(cx), + 1 => self.t7(cx), + 2 => self.exe(cx), + 3 => self.enhanced(cx), + 4 => self.graphics(cx), + 5 => self.dxvk(cx), + _ => self.tools(cx), + }; + let confirmation = self.pending.as_ref().map(|(label, _)| { + panel("Confirm operation") + .child(label.clone()) + .child(format!("Target: {}", self.value("/gameDir"))) + .child( + div() + .flex() + .gap_3() + .child(Button::new("confirm").label("Confirm").primary().on_click( + cx.listener(|view, _, _, cx| { + if let Some((label, request)) = view.pending.take() { + view.message = format!("Running: {label}…"); + view.failed = false; + view.send(&request.path, request.body, cx); + } + }), + )) + .child(Button::new("cancel").label("Cancel").on_click(cx.listener( + |view, _, _, cx| { + view.pending = None; + cx.notify(); + }, + ))), + ) + .into_any_element() + }); + div() + .flex() + .size_full() + .bg(rgb(0x0d0d0f)) + .text_color(rgb(0xf6f6f6)) + .child( + div() + .flex() + .flex_col() + .w(px(200.)) + .p_4() + .gap_3() + .border_r_1() + .border_color(rgb(0x343438)) + .child( + div() + .text_xl() + .font_weight(FontWeight::BOLD) + .child("PATCHOPS III"), + ) + .child( + div() + .text_sm() + .text_color(rgb(0xff4545)) + .child("GPUI evaluation"), + ) + .children(PAGES.into_iter().enumerate().map(|(index, label)| { + Button::new(SharedString::from(format!("nav-{index}"))) + .label(label) + .when(index == self.page, |button| button.primary()) + .on_click(cx.listener(move |view, _, _, cx| { + view.page = index; + cx.notify(); + })) + })), + ) + .child( + div() + .flex() + .flex_col() + .flex_1() + .min_w_0() + .p_5() + .gap_4() + .child( + div() + .flex() + .items_center() + .justify_between() + .gap_3() + .child( + div() + .text_2xl() + .font_weight(FontWeight::BOLD) + .child(PAGES[self.page]), + ) + .child( + div() + .flex() + .gap_2() + .child( + Button::new("load-settings") + .label("Load current values") + .disabled(self.busy || !self.connected) + .on_click(cx.listener(|view, _, window, cx| { + view.load_inputs(window, cx) + })), + ) + .child( + Button::new("refresh") + .label(if self.busy { "Working…" } else { "Refresh" }) + .disabled(self.busy) + .on_click(cx.listener(|view, _, _, cx| { + view.message = + "Connecting to the local service…".into(); + view.send("/api/status", None, cx); + })), + ) + .child(self.action( + "launch".into(), + "Launch game".into(), + "/api/launch", + json!({}), + cx, + )), + ), + ) + .children(confirmation) + .child( + div() + .id("content-scroll") + .flex_1() + .min_h_0() + .overflow_y_scroll() + .flex() + .flex_col() + .gap_4() + .child(content), + ) + .child( + div() + .text_sm() + .text_color(if self.failed { + rgb(0xff9f0a) + } else { + rgb(0xb6b6bb) + }) + .child(self.message.clone()), + ) + .child( + div() + .id("log-scroll") + .h(px(130.)) + .overflow_y_scroll() + .bg(rgb(0x050505)) + .rounded_lg() + .p_3() + .text_sm() + .children( + self.state["logs"] + .as_array() + .into_iter() + .flatten() + .rev() + .take(80) + .map(|entry| { + div() + .text_color(if entry["category"] == "Error" { + rgb(0xff4545) + } else { + rgb(0xb6b6bb) + }) + .child(entry["line"].as_str().unwrap_or("").to_owned()) + }), + ), + ), + ) + } +} diff --git a/scripts/gpui.py b/scripts/gpui.py new file mode 100644 index 0000000..602e6e1 --- /dev/null +++ b/scripts/gpui.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Build/run the GPUI evaluation without changing Electron's processes or ports.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import socket +import subprocess +import sys +import tempfile +import time +from urllib.request import ProxyHandler, build_opener + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "native" / "gpui" / "Cargo.toml" + + +def target_directory() -> Path: + target = Path(os.environ.get("CARGO_TARGET_DIR", MANIFEST.parent / "target")) + return target if target.is_absolute() else ROOT / target + + +def stop(process: subprocess.Popen) -> None: + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + +def serve(ready: Path) -> None: + # Bind once and hand the listening socket directly to Uvicorn. No port race, + # no collision with Electron, and no need to modify the normal API launcher. + import uvicorn + from backend.api import app + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: + listener.bind(("127.0.0.1", 0)) + listener.listen(128) + port = listener.getsockname()[1] + temporary = ready.with_suffix(".tmp") + temporary.write_text(json.dumps({"url": f"http://127.0.0.1:{port}"}), encoding="utf-8") + temporary.replace(ready) + server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning")) + server.run(sockets=[listener]) + + +def wait_ready(process: subprocess.Popen, ready: Path, timeout: float = 30) -> str: + deadline = time.monotonic() + timeout + opener = build_opener(ProxyHandler({})) + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError(f"Local service exited with code {process.returncode}") + if ready.exists(): + url = json.loads(ready.read_text(encoding="utf-8"))["url"] + try: + with opener.open(f"{url}/api/health", timeout=1) as response: + if json.load(response).get("ok") is True: + return url + except OSError: + pass + time.sleep(0.1) + raise RuntimeError("Local service did not become healthy within 30 seconds") + + +def run(executable: Path) -> int: + if not executable.is_file(): + raise RuntimeError(f"GPUI executable not found: {executable}. Run the build command first.") + with tempfile.TemporaryDirectory(prefix="patchops-gpui-") as directory: + ready = Path(directory) / "ready.json" + # Source execution needs the repository root on Python's import path. + env = {**os.environ, "PYTHONPATH": str(ROOT)} + service = subprocess.Popen([sys.executable, str(Path(__file__).resolve()), "serve", "--ready", str(ready)], cwd=ROOT, env=env) + app = None + try: + url = wait_ready(service, ready) + app = subprocess.Popen([str(executable)], cwd=ROOT, env={**env, "PATCHOPSIII_GPUI_BACKEND_URL": url}) + while app.poll() is None: + if service.poll() is not None: + raise RuntimeError("The local service stopped while GPUI was running") + time.sleep(0.2) + return app.returncode + finally: + if app is not None: + stop(app) + stop(service) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=["build", "run", "serve", "test", "check"]) + parser.add_argument("--release", action="store_true") + parser.add_argument("--executable", type=Path, help="Run a prebuilt GPUI executable") + parser.add_argument("--ready", type=Path, help=argparse.SUPPRESS) + args = parser.parse_args() + if args.command == "serve": + if args.ready is None: + parser.error("serve requires --ready") + # When this file is run directly, sys.path starts at scripts/. + sys.path.insert(0, str(ROOT)) + serve(args.ready) + return 0 + if args.command in {"build", "test", "check"}: + command = ["cargo", args.command, "--locked", "--manifest-path", str(MANIFEST)] + if args.release: + command.append("--release") + env = {**os.environ, "CARGO_TARGET_DIR": str(target_directory())} + return subprocess.call(command, cwd=MANIFEST.parent, env=env) + target = target_directory() + executable = args.executable or target / ("release" if args.release else "debug") / ("patchopsiii-gpui.exe" if os.name == "nt" else "patchopsiii-gpui") + return run(executable.resolve()) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + raise SystemExit(130) + except (OSError, RuntimeError) as error: + print(f"GPUI: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/scripts/measure_desktop.py b/scripts/measure_desktop.py new file mode 100644 index 0000000..de5a0b0 --- /dev/null +++ b/scripts/measure_desktop.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Sample an already running app and descendants consistently across desktop stacks. + +Install the optional measurement dependency with: python -m pip install psutil +""" +import argparse +import json +import platform +from pathlib import Path +import statistics +import time + + +def main(): + import psutil + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--pid", type=int, required=True, help="Supervisor PID including backend and GUI descendants") + parser.add_argument("--label", required=True) + parser.add_argument("--seconds", type=int, default=60) + parser.add_argument("--artifact", type=Path, action="append", default=[], help="Complete deliverable(s) to measure, not just the UI binary") + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.seconds < 1: + parser.error("--seconds must be positive") + root = psutil.Process(args.pid) + samples = [] + previous_cpu = None + previous_time = None + for _ in range(args.seconds): + now = time.monotonic() + if not root.is_running(): + raise RuntimeError("App supervisor exited before measurement finished") + processes = [root, *root.children(recursive=True)] + rss = 0 + cpu = 0 + names = [] + skipped = [] + for process in processes: + try: + rss += process.memory_info().rss + times = process.cpu_times() + cpu += times.user + times.system + names.append({"pid": process.pid, "name": process.name()}) + except (psutil.NoSuchProcess, psutil.AccessDenied): + skipped.append(process.pid) + # Aggregate core-percent: 100 is one CPU core. Process churn can make + # cumulative counters fall; omit that interval rather than invent data. + percent = None + if previous_cpu is not None and cpu >= previous_cpu: + percent = 100 * (cpu - previous_cpu) / (now - previous_time) + samples.append({"rssBytes": rss, "cpuCorePercent": percent, "processes": names, "skippedPids": skipped}) + previous_cpu, previous_time = cpu, now + time.sleep(1) + cpu_samples = [s["cpuCorePercent"] for s in samples if s["cpuCorePercent"] is not None] + result = { + "label": args.label, "platform": platform.platform(), "sampleSeconds": args.seconds, + "medianTreeRssBytes": statistics.median(s["rssBytes"] for s in samples), + "peakTreeRssBytes": max(s["rssBytes"] for s in samples), + "medianCpuCorePercent": statistics.median(cpu_samples) if cpu_samples else None, + "artifacts": [{"path": str(p), "bytes": p.stat().st_size} for p in args.artifact], + "notes": ["RSS sums can double-count shared pages; use the same method for every candidate.", "GPU memory and detached/elevated processes are excluded.", "Measure startup separately; this sampler measures an already running app."], + "samples": samples, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + print(args.output) + + +if __name__ == "__main__": + main() diff --git a/scripts/package_gpui.py b/scripts/package_gpui.py new file mode 100644 index 0000000..db911d8 --- /dev/null +++ b/scripts/package_gpui.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Create an evaluation archive; Python and API dependencies remain prerequisites.""" +import argparse +from pathlib import Path +from zipfile import ZIP_DEFLATED, ZipFile + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if not args.executable.is_file(): + parser.error("Build the executable first") + files = ["LICENSE", "package.json", "requirements.txt", "presets.json", "utils.py", "t7_patch.py", "dxvk_manager.py", "bo3_enhanced.py", "scripts/gpui.py", "native/gpui/README.md", "docs/gpui-evaluation.md"] + files += [str(path.relative_to(ROOT)) for path in (ROOT / "backend").glob("*.py")] + files += [str(path.relative_to(ROOT)) for path in (ROOT / "docs" / "images").glob("gpui-*.png")] + args.output.parent.mkdir(parents=True, exist_ok=True) + with ZipFile(args.output, "w", ZIP_DEFLATED) as archive: + archive.write(args.executable, f"bin/{args.executable.name}") + for name in files: + archive.write(ROOT / name, name) + print(args.output) + + +if __name__ == "__main__": + main() diff --git a/scripts/smoke_gpui_linux.py b/scripts/smoke_gpui_linux.py new file mode 100644 index 0000000..1bd8002 --- /dev/null +++ b/scripts/smoke_gpui_linux.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Render a native window and close it through X11 against a read-only fake API. + +Requires an X11 DISPLAY, xdotool, and a compositor/window manager. CI uses +xvfb-run with --start-session. No real game service or game mutations are used. +""" +import argparse +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import os +from pathlib import Path +import subprocess +import threading +import time + + +class ReadOnlyStatus(BaseHTTPRequestHandler): + def do_GET(self): + if self.path != "/api/status": + self.send_error(404) + return + payload = json.dumps({"appVersion": "GPUI smoke", "platform": "Linux", "gameDetected": False, "gameDir": None, "launchProfiles": [], "logs": []}).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def do_POST(self): + self.send_error(405, "Smoke service is read-only") + + def log_message(self, *_): + pass + + +def stop(process): + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--start-session", action="store_true") + args = parser.parse_args() + owned = [] + with ThreadingHTTPServer(("127.0.0.1", 0), ReadOnlyStatus) as server: + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + if args.start_session: + owned.append(subprocess.Popen(["openbox", "--sm-disable"])) + owned.append(subprocess.Popen(["xcompmgr"])) + time.sleep(1) + env = {**os.environ, "PATCHOPSIII_GPUI_BACKEND_URL": f"http://127.0.0.1:{server.server_port}"} + app = subprocess.Popen([str(args.executable.resolve())], env=env) + owned.append(app) + deadline = time.monotonic() + 30 + window = None + while time.monotonic() < deadline: + if app.poll() is not None: + raise RuntimeError(f"GPUI exited during startup: {app.returncode}") + result = subprocess.run(["xdotool", "search", "--onlyvisible", "--pid", str(app.pid), "--name", "PatchOpsIII"], capture_output=True, text=True) + if result.returncode == 0 and result.stdout.strip(): + window = result.stdout.splitlines()[0] + break + time.sleep(0.1) + if window is None: + raise RuntimeError("GPUI did not map its native window") + time.sleep(1) + # Close this exact owned window through its WM keyboard shortcut. + # This reproduces the platform callback path that TestAppContext + # cannot exercise; direct XDestroyWindow is not normal app closure. + subprocess.run(["xdotool", "windowactivate", "--sync", window, "key", "alt+F4"], check=True, timeout=5) + result = app.wait(timeout=10) + if result != 0: + raise RuntimeError(f"Native window close failed with exit code {result}") + print("GPUI native window startup and normal X11 close passed") + finally: + for process in reversed(owned): + stop(process) + server.shutdown() + thread.join(timeout=5) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_gpui.py b/scripts/test_gpui.py new file mode 100644 index 0000000..ae4a975 --- /dev/null +++ b/scripts/test_gpui.py @@ -0,0 +1,63 @@ +"""Lifecycle checks use harmless stand-ins; no game installation is modified.""" +import importlib.util +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location("gpui_launcher", Path(__file__).with_name("gpui.py")) +launcher = importlib.util.module_from_spec(spec) +spec.loader.exec_module(launcher) + + +class LifecycleTests(unittest.TestCase): + def test_relative_target_directory_is_consistent_for_build_and_run(self): + with patch.dict(os.environ, {"CARGO_TARGET_DIR": "build/custom-gpui"}): + self.assertEqual(launcher.target_directory(), launcher.ROOT / "build/custom-gpui") + + def test_stop_reaps_child(self): + process = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) + launcher.stop(process) + self.assertIsNotNone(process.poll()) + launcher.stop(process) # idempotent + + def test_startup_failure_cleans_service(self): + process = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) + try: + with patch.object(launcher.subprocess, "Popen", return_value=process), patch.object(launcher, "wait_ready", side_effect=RuntimeError("unhealthy")): + with self.assertRaisesRegex(RuntimeError, "unhealthy"): + launcher.run(Path(sys.executable)) + self.assertIsNotNone(process.poll()) + finally: + launcher.stop(process) + + def test_early_service_exit_is_reported(self): + process = subprocess.Popen([sys.executable, "-c", "raise SystemExit(7)"]) + process.wait() + with tempfile.TemporaryDirectory() as directory: + with self.assertRaisesRegex(RuntimeError, "code 7"): + launcher.wait_ready(process, Path(directory) / "ready.json") + + def test_missing_executable_does_not_start_service(self): + with patch.object(launcher.subprocess, "Popen") as popen: + with self.assertRaisesRegex(RuntimeError, "not found"): + launcher.run(Path("/missing/patchopsiii-gpui")) + popen.assert_not_called() + + def test_ui_failure_stops_owned_service(self): + service = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) + app = subprocess.Popen([sys.executable, "-c", "raise SystemExit(9)"]) + app.wait() + try: + with patch.object(launcher.subprocess, "Popen", side_effect=[service, app]), patch.object(launcher, "wait_ready", return_value="http://127.0.0.1:12345"): + self.assertEqual(launcher.run(Path(sys.executable)), 9) + self.assertIsNotNone(service.poll()) + finally: + launcher.stop(service) + + +if __name__ == "__main__": + unittest.main() From 770f46eba373eb8da65a8a784456e3b9e3552a75 Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:21:14 -0400 Subject: [PATCH 02/17] feat(gpui): match Electron UI and promote native app to primary Restyle the GPUI client after the Electron renderer (theme tokens, nav, header, game directory bar, cards, activity log, Lucide icons), and have the native binary supervise a bundled PyInstaller backend. Package self-contained Linux/Windows archives and attach them to releases. --- .github/workflows/gpui-build.yml | 90 +- .github/workflows/release-beta.yml | 27 + .github/workflows/release-stable.yml | 27 + .github/workflows/virustotal-release-scan.yml | 5 +- README.md | 41 +- ...pui-evaluation.md => gpui-architecture.md} | 69 +- native/gpui/README.md | 137 +- native/gpui/assets/icons/check.svg | 3 + native/gpui/assets/icons/chevron-down.svg | 3 + native/gpui/assets/icons/chevron-right.svg | 3 + native/gpui/assets/icons/chevron-up.svg | 3 + native/gpui/assets/icons/circle-check.svg | 4 + native/gpui/assets/icons/clipboard.svg | 4 + native/gpui/assets/icons/close.svg | 4 + native/gpui/assets/icons/cpu.svg | 16 + native/gpui/assets/icons/download.svg | 5 + native/gpui/assets/icons/eraser.svg | 4 + native/gpui/assets/icons/external-link.svg | 5 + native/gpui/assets/icons/eye-off.svg | 6 + native/gpui/assets/icons/eye.svg | 4 + native/gpui/assets/icons/folder-open.svg | 3 + native/gpui/assets/icons/gem.svg | 5 + native/gpui/assets/icons/image.svg | 5 + native/gpui/assets/icons/key-round.svg | 4 + native/gpui/assets/icons/layout-dashboard.svg | 6 + native/gpui/assets/icons/refresh-cw.svg | 6 + native/gpui/assets/icons/rotate-ccw.svg | 4 + native/gpui/assets/icons/save.svg | 5 + native/gpui/assets/icons/shield-check.svg | 4 + native/gpui/assets/icons/square-play.svg | 4 + native/gpui/assets/icons/trash-2.svg | 7 + native/gpui/assets/icons/triangle-alert.svg | 5 + native/gpui/assets/icons/wrench.svg | 3 + native/gpui/assets/icons/x.svg | 4 + native/gpui/assets/images/logo.png | Bin 0 -> 53168 bytes native/gpui/src/backend.rs | 313 +++- native/gpui/src/main.rs | 13 +- native/gpui/src/ui.rs | 1610 +++++++++++------ native/gpui/src/ui/assets.rs | 135 ++ native/gpui/src/ui/components.rs | 843 +++++++++ native/gpui/src/ui/dashboard.rs | 269 +++ native/gpui/src/ui/enhanced.rs | 404 +++++ native/gpui/src/ui/exe.rs | 335 ++++ native/gpui/src/ui/graphics.rs | 874 +++++++++ native/gpui/src/ui/modals.rs | 150 ++ native/gpui/src/ui/t7.rs | 533 ++++++ native/gpui/src/ui/theme.rs | 208 +++ native/gpui/src/ui/tools.rs | 222 +++ package.json | 5 +- scripts/gpui.py | 2 +- scripts/package_gpui.py | 75 +- scripts/smoke_gpui_linux.py | 10 +- scripts/test_gpui.py | 43 +- 53 files changed, 5852 insertions(+), 717 deletions(-) rename docs/{gpui-evaluation.md => gpui-architecture.md} (78%) create mode 100644 native/gpui/assets/icons/check.svg create mode 100644 native/gpui/assets/icons/chevron-down.svg create mode 100644 native/gpui/assets/icons/chevron-right.svg create mode 100644 native/gpui/assets/icons/chevron-up.svg create mode 100644 native/gpui/assets/icons/circle-check.svg create mode 100644 native/gpui/assets/icons/clipboard.svg create mode 100644 native/gpui/assets/icons/close.svg create mode 100644 native/gpui/assets/icons/cpu.svg create mode 100644 native/gpui/assets/icons/download.svg create mode 100644 native/gpui/assets/icons/eraser.svg create mode 100644 native/gpui/assets/icons/external-link.svg create mode 100644 native/gpui/assets/icons/eye-off.svg create mode 100644 native/gpui/assets/icons/eye.svg create mode 100644 native/gpui/assets/icons/folder-open.svg create mode 100644 native/gpui/assets/icons/gem.svg create mode 100644 native/gpui/assets/icons/image.svg create mode 100644 native/gpui/assets/icons/key-round.svg create mode 100644 native/gpui/assets/icons/layout-dashboard.svg create mode 100644 native/gpui/assets/icons/refresh-cw.svg create mode 100644 native/gpui/assets/icons/rotate-ccw.svg create mode 100644 native/gpui/assets/icons/save.svg create mode 100644 native/gpui/assets/icons/shield-check.svg create mode 100644 native/gpui/assets/icons/square-play.svg create mode 100644 native/gpui/assets/icons/trash-2.svg create mode 100644 native/gpui/assets/icons/triangle-alert.svg create mode 100644 native/gpui/assets/icons/wrench.svg create mode 100644 native/gpui/assets/icons/x.svg create mode 100644 native/gpui/assets/images/logo.png create mode 100644 native/gpui/src/ui/assets.rs create mode 100644 native/gpui/src/ui/components.rs create mode 100644 native/gpui/src/ui/dashboard.rs create mode 100644 native/gpui/src/ui/enhanced.rs create mode 100644 native/gpui/src/ui/exe.rs create mode 100644 native/gpui/src/ui/graphics.rs create mode 100644 native/gpui/src/ui/modals.rs create mode 100644 native/gpui/src/ui/t7.rs create mode 100644 native/gpui/src/ui/theme.rs create mode 100644 native/gpui/src/ui/tools.rs diff --git a/.github/workflows/gpui-build.yml b/.github/workflows/gpui-build.yml index 814dd6f..9bb8f10 100644 --- a/.github/workflows/gpui-build.yml +++ b/.github/workflows/gpui-build.yml @@ -1,4 +1,4 @@ -name: GPUI Alternative +name: PatchOpsIII Native Build on: pull_request: @@ -10,25 +10,60 @@ on: - '*.py' - 'requirements.txt' - 'presets.json' - - 'docs/gpui-evaluation.md' + - 'package.json' + - 'docs/gpui-architecture.md' - '.github/workflows/gpui-build.yml' workflow_dispatch: + inputs: + ref: + description: Git ref to build + required: false + type: string + default: '' + workflow_call: + inputs: + ref: + description: Git ref to build + required: false + type: string + default: '' permissions: contents: read jobs: native: + name: Native ${{ matrix.os }} archive strategy: fail-fast: false matrix: - os: [ubuntu-24.04, windows-2022] + include: + - os: ubuntu-24.04 + executable: native/gpui/target/release/patchopsiii-gpui + backend: dist/backend/patchops-backend + archive: dist/native/PatchOpsIII-native-linux-x64.zip + artifact: PatchOpsIII-native-Linux + - os: windows-2022 + executable: native/gpui/target/release/patchopsiii-gpui.exe + backend: dist/backend/patchops-backend.exe + archive: dist/native/PatchOpsIII-native-windows-x64.zip + artifact: PatchOpsIII-native-Windows runs-on: ${{ matrix.os }} + env: + PYTHON_VERSION: '3.12' + PYINSTALLER_VERSION: '6.20.0' steps: - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.13' - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: requirements.txt - uses: dtolnay/rust-toolchain@master with: toolchain: '1.99.0' @@ -36,10 +71,24 @@ jobs: - uses: Swatinem/rust-cache@v2 with: workspaces: native/gpui - - name: Linux native dependencies + - name: Linux native and packaging dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y binutils libfontconfig1-dev libfreetype6-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libvulkan-dev libx11-xcb-dev libxcb1-dev libasound2-dev libclang-dev mesa-vulkan-drivers xvfb xauth openbox xcompmgr xdotool + - name: Install Python backend dependencies (Linux) if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libvulkan-dev libx11-xcb-dev libxcb1-dev libasound2-dev libclang-dev mesa-vulkan-drivers xvfb xauth openbox xcompmgr xdotool - - name: Python lifecycle tests + run: | + python -m venv .venv + .venv/bin/python -m pip install -U pip + .venv/bin/python -m pip install -r requirements.txt + .venv/bin/python -m pip install pyinstaller==${{ env.PYINSTALLER_VERSION }} + - name: Install Python backend dependencies (Windows) + if: runner.os == 'Windows' + run: | + python -m venv .venv + .\.venv\Scripts\python -m pip install -U pip + .\.venv\Scripts\python -m pip install -r requirements.txt + .\.venv\Scripts\python -m pip install pyinstaller==${{ env.PYINSTALLER_VERSION }} + - name: Python lifecycle and packaging tests run: python -m unittest scripts/test_gpui.py - name: Format run: cargo fmt --manifest-path native/gpui/Cargo.toml --check @@ -47,18 +96,25 @@ jobs: run: cargo clippy --locked --manifest-path native/gpui/Cargo.toml --all-targets -- -D warnings - name: Rust tests run: python scripts/gpui.py test - - name: Build release + - name: Build native release run: python scripts/gpui.py build --release - - name: Linux native window lifecycle regression check - if: runner.os == 'Linux' - run: xvfb-run -a python scripts/smoke_gpui_linux.py --start-session --executable native/gpui/target/release/patchopsiii-gpui - - name: Archive Linux evaluation build + - name: Build bundled backend (Linux) if: runner.os == 'Linux' - run: python scripts/package_gpui.py --executable native/gpui/target/release/patchopsiii-gpui --output dist/gpui/PatchOpsIII-gpui-linux.zip - - name: Archive Windows evaluation build + run: bun run build:backend:linux + - name: Build bundled backend (Windows) if: runner.os == 'Windows' - run: python scripts/package_gpui.py --executable native/gpui/target/release/patchopsiii-gpui.exe --output dist/gpui/PatchOpsIII-gpui-windows.zip + run: bun run build:backend:win + - name: Create self-contained native archive + run: python scripts/package_gpui.py --executable ${{ matrix.executable }} --backend ${{ matrix.backend }} --output ${{ matrix.archive }} + - name: Smoke test the self-contained Linux archive + if: runner.os == 'Linux' + run: | + python -m zipfile -e ${{ matrix.archive }} "${RUNNER_TEMP}/patchops-native" + chmod +x "${RUNNER_TEMP}/patchops-native/PatchOpsIII/patchopsiii-gpui" + chmod +x "${RUNNER_TEMP}/patchops-native/PatchOpsIII/resources/backend-bin/patchops-backend" + xvfb-run -a python scripts/smoke_gpui_linux.py --start-session --use-owned-backend --executable "${RUNNER_TEMP}/patchops-native/PatchOpsIII/patchopsiii-gpui" - uses: actions/upload-artifact@v4 with: - name: PatchOpsIII-gpui-${{ runner.os }} - path: dist/gpui/*.zip + name: ${{ matrix.artifact }} + path: ${{ matrix.archive }} + if-no-files-found: error diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 25e97cd..28e6304 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -309,6 +309,13 @@ jobs: ref: ${{ needs.prepare.outputs.tag }} secrets: inherit + build-native: + needs: + - prepare + uses: ./.github/workflows/gpui-build.yml + with: + ref: ${{ needs.prepare.outputs.tag }} + release: name: Publish beta release runs-on: ubuntu-latest @@ -318,6 +325,7 @@ jobs: - prepare - build-windows - build-linux + - build-native steps: - name: Checkout repository uses: actions/checkout@v4 @@ -334,6 +342,16 @@ jobs: with: name: ${{ needs.build-linux.outputs.artifact_name }} path: artifacts/linux + - name: Download native Linux archive + uses: actions/download-artifact@v4 + with: + name: PatchOpsIII-native-Linux + path: artifacts/native-linux + - name: Download native Windows archive + uses: actions/download-artifact@v4 + with: + name: PatchOpsIII-native-Windows + path: artifacts/native-windows - name: Inspect downloaded artifacts run: | set -euo pipefail @@ -468,6 +486,15 @@ jobs: if [ -f "$(dirname "$linux_exec")/PatchOpsIII.AppImage.sig" ]; then cp "$(dirname "$linux_exec")/PatchOpsIII.AppImage.sig" "release/PatchOpsIII-Beta.AppImage.sig" fi + native_linux=$(find artifacts/native-linux -type f -name 'PatchOpsIII-native-linux-x64.zip' -print -quit) + native_windows=$(find artifacts/native-windows -type f -name 'PatchOpsIII-native-windows-x64.zip' -print -quit) + if [ -z "$native_linux" ] || [ -z "$native_windows" ]; then + echo "Native desktop archives were not produced" >&2 + find artifacts/native-linux artifacts/native-windows -type f -print >&2 + exit 1 + fi + cp "$native_linux" "release/PatchOpsIII-native-linux-x64.zip" + cp "$native_windows" "release/PatchOpsIII-native-windows-x64.zip" - name: Publish GitHub release uses: softprops/action-gh-release@v2 with: diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 9ada3dc..8706315 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -303,6 +303,13 @@ jobs: update_information: gh-releases-zsync|boggedbrush|PatchOpsIII|latest|PatchOpsIII.AppImage.zsync secrets: inherit + build-native: + needs: + - prepare + uses: ./.github/workflows/gpui-build.yml + with: + ref: ${{ needs.prepare.outputs.tag }} + release: name: Publish stable release runs-on: ubuntu-latest @@ -312,6 +319,7 @@ jobs: - prepare - build-windows - build-linux + - build-native steps: - name: Checkout repository uses: actions/checkout@v4 @@ -328,6 +336,16 @@ jobs: with: name: ${{ needs.build-linux.outputs.artifact_name }} path: artifacts/linux + - name: Download native Linux archive + uses: actions/download-artifact@v4 + with: + name: PatchOpsIII-native-Linux + path: artifacts/native-linux + - name: Download native Windows archive + uses: actions/download-artifact@v4 + with: + name: PatchOpsIII-native-Windows + path: artifacts/native-windows - name: Inspect downloaded artifacts run: | set -euo pipefail @@ -462,6 +480,15 @@ jobs: if [ -f "$(dirname "$linux_exec")/PatchOpsIII.AppImage.sig" ]; then cp "$(dirname "$linux_exec")/PatchOpsIII.AppImage.sig" "release/PatchOpsIII.AppImage.sig" fi + native_linux=$(find artifacts/native-linux -type f -name 'PatchOpsIII-native-linux-x64.zip' -print -quit) + native_windows=$(find artifacts/native-windows -type f -name 'PatchOpsIII-native-windows-x64.zip' -print -quit) + if [ -z "$native_linux" ] || [ -z "$native_windows" ]; then + echo "Native desktop archives were not produced" >&2 + find artifacts/native-linux artifacts/native-windows -type f -print >&2 + exit 1 + fi + cp "$native_linux" "release/PatchOpsIII-native-linux-x64.zip" + cp "$native_windows" "release/PatchOpsIII-native-windows-x64.zip" - name: Publish GitHub release uses: softprops/action-gh-release@v2 with: diff --git a/.github/workflows/virustotal-release-scan.yml b/.github/workflows/virustotal-release-scan.yml index 3d397bd..40f5c02 100644 --- a/.github/workflows/virustotal-release-scan.yml +++ b/.github/workflows/virustotal-release-scan.yml @@ -39,6 +39,7 @@ jobs: --repo "$GITHUB_REPOSITORY" \ --pattern "*.exe" \ --pattern "*.AppImage" \ + --pattern "PatchOpsIII-native-*.zip" \ --dir release-assets find release-assets -maxdepth 1 -type f -print - name: Determine VirusTotal CLI release @@ -107,9 +108,9 @@ jobs: printf "| `%s` | `%s` | [Report](%s) | `%s` |\n" \ "$(basename "$asset")" "$hash" "$vt_url" "$analysis_id" >> "$GITHUB_STEP_SUMMARY" printf "%s %s %s\n" "$(basename "$asset")" "$hash" "$analysis_id" - done < <(find release-assets -maxdepth 1 -type f \( -name "*.exe" -o -name "*.AppImage" \) -print0) + done < <(find release-assets -maxdepth 1 -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "PatchOpsIII-native-*.zip" \) -print0) if [ "$found" -eq 0 ]; then - echo "No .exe or .AppImage assets found for ${{ inputs.tag }}." >&2 + echo "No executable or native archive assets found for ${{ inputs.tag }}." >&2 exit 1 fi diff --git a/README.md b/README.md index dee7975..aac1cdc 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ - [Advanced Tab](#advanced-tab) - [Terminal & Logging](#terminal--logging) - [Installation](#installation) -- [Native GPUI evaluation](#native-gpui-evaluation) +- [Native desktop architecture](#native-desktop-architecture) +- [Legacy Electron app](#legacy-electron-app) - [Forked Components](#forked-components) - [Quick Start](#quick-start) - [Screenshots](#screenshots) @@ -33,7 +34,7 @@ - [Star History](#star-history) ## Overview -PatchOpsIII streamlines the setup and upkeep of Black Ops III by surfacing popular community tools and quality-of-life tweaks in a single polished Electron interface backed by a local Python API. Whether you are securing your game with T7 Patch, smoothing shader compilation stutter with DXVK, or fine-tuning launch options, PatchOpsIII consolidates every workflow into one cohesive experience. +PatchOpsIII streamlines the setup and upkeep of Black Ops III by surfacing popular community tools and quality-of-life tweaks in a native Rust/GPUI desktop interface backed by a local Python API. Whether you are securing your game with T7 Patch, smoothing shader compilation stutter with DXVK, or fine-tuning launch options, PatchOpsIII consolidates every workflow into one cohesive experience. The previous Electron/React client remains available in this repository as a legacy fallback. ## Key Features @@ -60,12 +61,15 @@ PatchOpsIII streamlines the setup and upkeep of Black Ops III by surfacing popul ## Installation 1. **Download:** Grab the latest release from the [Releases page](https://github.com/boggedbrush/PatchOpsIII/releases). -2. **Windows:** Run `PatchOpsIII.msi`, follow the installer, then launch PatchOpsIII. The `PatchOpsIII.exe` installer remains available for compatibility with older updaters. -3. **Linux & Steam Deck:** Download `PatchOpsIII.AppImage`, make it executable, then run it. -4. **Dependencies:** The packaged build bundles all required Python dependencies; no additional setup is needed. +2. **Windows:** Download `PatchOpsIII-native-windows-x64.zip`, extract the entire `PatchOpsIII` folder, and run `patchopsiii-gpui.exe` from that folder. +3. **Linux & Steam Deck:** Download `PatchOpsIII-native-linux-x64.zip`, extract it, run `chmod +x PatchOpsIII/patchopsiii-gpui`, then launch that executable from a desktop session. +4. **Dependencies:** Keep the executable and its `resources` directory together. The archive bundles the Python backend, presets, and icons; Python is not required on the destination system. Linux still requires a supported X11/Wayland session, system fonts, and working graphics drivers. + +The existing `PatchOpsIII.msi`, `PatchOpsIII.exe`, and `PatchOpsIII.AppImage` release assets are the legacy Electron fallback while native installer/updater work remains open. ### Developer Setup -PatchOpsIII uses a React + TypeScript renderer wrapped by Electron. Bun is the JavaScript runtime/package manager, while Python runs the local backend API. + +The primary desktop app uses Rust 1.99.0, GPUI 0.2.2, and GPUI Component 0.5.0. Python runs the existing local operations API. ```bash # install Python service dependencies @@ -73,24 +77,21 @@ python -m venv .venv source .venv/bin/activate # On Windows use: .venv\Scripts\activate pip install -r requirements.txt -# install desktop frontend dependencies -bun install +# build and run the native app with the source backend +python scripts/gpui.py build +python scripts/gpui.py run +# Equivalent package scripts: bun run gpui:build / bun run gpui:run +``` -# run Vite and the Python local API -bun run dev +See [native/gpui/README.md](native/gpui/README.md) for platform build packages, release packaging, validation commands, and known parity gaps. The development launcher gives its Python API an OS-assigned loopback port. A packaged native binary locates and owns the bundled backend itself. -# run the Electron desktop app -bun run dev:desktop -``` +## Native desktop architecture -The browser development server uses Vite on `127.0.0.1:5173` and the Python API on `127.0.0.1:8765`. The Electron desktop command uses Vite on `127.0.0.1:5174` and its Python API on `127.0.0.1:8766`, so both commands can run at the same time. The renderer communicates with Python through HTTP APIs and `/ws` WebSockets; Electron IPC is reserved for desktop-specific bridge actions such as selecting a local game directory. +The primary Rust/GPUI client lives in [native/gpui](native/gpui/README.md). It uses native controls without Chromium, React, a WebView, or Tauri, and supervises the packaged Python operations service on an ephemeral loopback port. See [the native desktop architecture and migration record](docs/gpui-architecture.md) for the original research, comparison data, supported workflows, and remaining parity work. -## Native GPUI evaluation +## Legacy Electron app -An independent Rust/GPUI desktop alternative lives in [native/gpui](native/gpui/README.md). -It uses native controls and the current Python operations alongside Electron. -See [the research and evaluation plan](docs/gpui-evaluation.md) for comparisons -with the Tauri alternatives, supported workflows, build instructions, and parity gaps. +The Electron/React application in `src/` remains buildable as a fallback. To work on it, install Bun dependencies with `bun install`, then use `bun run dev:desktop`; `bun run dist:linux` and `bun run dist:win` retain the established AppImage/MSI/NSIS builds. The browser development server remains available through `bun run dev`. Electron and GPUI share backend settings, logs, caches, and game files, so do not perform overlapping game mutations in both clients. ## Forked Components @@ -108,6 +109,8 @@ with the Tauri alternatives, supported workflows, build instructions, and parity 5. Visit the **Advanced** tab to unlock VRAM, tweak frame latency, and set your config to read-only once satisfied. ## Screenshots + +These screenshots show the legacy Electron client; updated native screenshots are tracked with the native UI work. diff --git a/docs/gpui-evaluation.md b/docs/gpui-architecture.md similarity index 78% rename from docs/gpui-evaluation.md rename to docs/gpui-architecture.md index b34b88a..b593cd1 100644 --- a/docs/gpui-evaluation.md +++ b/docs/gpui-architecture.md @@ -1,9 +1,11 @@ -# GPUI research and evaluation plan +# Native GPUI architecture and migration record -Research date: 2026-10-02. Baseline: the current `main` Electron/React/Python app. +Research date: 2026-10-02. Original baseline: the Electron/React/Python app, +which now remains as the legacy fallback. Related alternatives: [Tauri shell + Rust core PR #36](https://github.com/boggedbrush/PatchOpsIII/pull/36) and [in-process Rust/Tauri PR #40](https://github.com/boggedbrush/PatchOpsIII/pull/40). -This GPUI branch starts from `main`, independently of both Tauri branches. +The GPUI implementation started from `main`, independently of both Tauri branches, +and has since been selected as PatchOpsIII's primary desktop client. ## Findings and architecture decision @@ -22,14 +24,19 @@ avoids adopting an evolving Git revision or silently upgrading the toolkit. The optional WebView feature is not enabled. Both dependencies use Apache-2.0; distribution still needs an audit of notices for the complete dependency tree. -Reusing the current Python API creates a controlled first comparison of desktop -renderers. It retains the existing download, install, backup, hash, Steam, UAC, +Reusing the current Python API provided a controlled first comparison of desktop +renderers and remains the production architecture today. It retains the existing download, install, backup, hash, Steam, UAC, and config semantics. It also retains Python startup, packaging cost, and HTTP serialization; removing Chromium does not prove a smaller or faster complete application. A later in-process Rust port should use a toolkit-independent core so Tauri and GPUI can exercise identical business logic. This PR does not import the unmerged Rust rewrite or duplicate delicate game-file operations in Rust. +The promotion decision changes product ownership, not the evidence: GPUI is now +the primary client, native archives bundle the Python service, and Electron stays +buildable as a fallback. The hardware, accessibility, updater, and richer UX gaps +listed below remain follow-up work rather than being treated as completed parity. + Sources inspected: - [GPUI upstream README](https://github.com/zed-industries/zed/blob/main/crates/gpui/README.md): architecture, pre-1.0 status, platform requirements. @@ -38,39 +45,42 @@ Sources inspected: - Repository `src/main/main.ts`, `src/renderer/lib/api.ts`, `src/renderer/main.tsx`, and `backend/api.py`: actual lifecycle, payloads, config normalization, state, error responses, and logs. - PR #36 and #40 metadata: the shell-only migration and the subsequent backend rewrite have different scopes and costs. -## Implementation plan and resulting slice +## Implemented architecture 1. Add an isolated Cargo application and compatible native controls. Preserve the app's dark charcoal/red control-center visual language and use native window chrome, seven navigation sections, scrollable settings, and a log panel. -2. Launch a supervised Python service on an ephemeral loopback port. Wait for - health before opening GPUI; always reap owned processes on normal/error exits. +2. Launch a supervised Python service on an ephemeral loopback port. Packaged + binaries locate the PyInstaller service in `resources/backend-bin`; source + binaries fall back to `backend/api.py`. Wait for health before opening GPUI + and always reap owned processes on normal/error exits. 3. Execute API calls on one HTTP worker. Reflect backend-reported state, preserve unsuccessful HTTP-200 responses and depot instructions, disable overlapping actions, and require an explicit operation confirmation. 4. Implement representative real workflows throughout the app, using existing payloads and config value mappings. Keep input drafts intact during refresh; provide an explicit button to load current values. -5. Add meaningful protocol/lifecycle checks, Windows/Linux build CI, downloadable - evaluation archives, a common process-tree sampler, and a parity/manual review - checklist. Keep production release workflows separate from evaluation builds. +5. Add meaningful protocol/lifecycle checks, Windows/Linux build CI, + self-contained archives, a common process-tree sampler, and a parity/manual + review checklist. Stable and beta releases attach native archives alongside + the unchanged legacy Electron deliverables. ## Stack comparison -| Criterion | Current Electron | Tauri #36 / #40 | GPUI slice | +| Criterion | Legacy Electron | Tauri #36 / #40 | Primary GPUI client | |---|---|---|---| | UI technology | React/CSS + Chromium | React/CSS + OS WebView | Native Rust controls + GPU rendering | | Backend | Python child + HTTP/WS | #36 Python + small Rust core; #40 in-process Rust | Same Python + HTTP | | Renderer reuse | Baseline | High | Rewrite required | | Platform work | Existing Windows/Linux packages | WebView availability and new Rust packaging | GPU/driver compatibility, native UI/input and new packaging | | Accessibility | Browser infrastructure; app still needs testing | WebView infrastructure; app still needs testing | Must validate controls and screen readers on target OSes | -| Delivery in this branch | Unchanged | Independent open PRs | Source build and prerequisite-based evaluation ZIPs | +| Delivery | Existing MSI/NSIS/AppImage fallback | Independent open PRs | Self-contained Windows/Linux ZIPs with a bundled PyInstaller backend | | Startup/RAM/package size | Measure | Measure each PR separately | Measure complete stack; no claimed improvement | | Migration cost | Existing maintenance | Shell changes; #40 also ports operations | Entire UI rewrite; later backend port optional | ## Workflow coverage -| Workflow | Implemented in this slice | Remaining evaluation/parity work | +| Workflow | Native implementation | Remaining parity work | |---|---|---| | Installation detection/directory | Actual status and native folder picker; manual path entry; existing backend validation | Folder picker on Windows, Wayland portals and Steam Deck | | Launch/profiles | Steam launch, backend-provided profiles, Workshop open/install request | Workshop progress presentation and detailed profile states | @@ -81,7 +91,7 @@ Sources inspected: | Advanced/QoL | Smoothing, hidden options, CPU reduction, frame latency, VRAM target, intro/compiler/read-only toggles | Real config/backup verification on disposable installs | | DXVK | Install/uninstall, async/cache/HUD, threads, FPS, latency and tear-free settings | Real download/install/proton behavior | | Logs/maintenance | Up to 80 recent lines, five-second refresh, clear logs/cache, reset stock, update check/channel | Streaming WS logs, copy/export UX and platform updater integration | -| Delivery | Windows/Linux CI and source-backed release archives | Bundled Python or shared Rust core; signed installers, updater, notices and release integration | +| Delivery | Windows/Linux CI and self-contained release archives | Signed native installers, updater, dependency notices and broader release testing | Status refresh uses the complete existing status endpoint, including executable hashing. Polling every five seconds can cost more idle CPU than Electron's event @@ -90,7 +100,7 @@ or fabricated installation states are introduced. Actions preserve backend validation and require confirmation; all game-modifying smoke tests must use an installation whose backups can be inspected and restored. -## Fair measurement protocol +## Measurement protocol retained from the selection process Use the same Windows PC and Steam Deck/Linux PC for all three candidates. Record OS, GPU/driver, display scale, power profile, game-directory state, and commit @@ -98,7 +108,8 @@ SHA. Build optimized release artifacts. Exclude compilation, debugger and dev servers. Compare Electron, #36, #40 and GPUI separately; a Python-backed GPUI versus #40 compares both renderer and backend changes. -For each candidate collect ten cold launches and ten warm launches. Record +For regression tracking and any future comparison, collect ten cold launches and +ten warm launches per candidate. Record process start to first painted interactive window **and** service-ready status with screen recording or platform tracing. Include backend startup. Report median and p95, retaining raw observations. Do not label cargo build time as app @@ -110,28 +121,29 @@ log workload. Use the supervisor PID that owns both UI and backend processes: ```sh python -m pip install psutil python scripts/measure_desktop.py --pid 12345 --label gpui-release \ - --seconds 60 --artifact dist/gpui/PatchOpsIII-gpui-linux.zip \ + --seconds 60 --artifact dist/native/PatchOpsIII-native-linux-x64.zip \ --output measurements/gpui-idle.json ``` The sampler captures RSS and CPU for the process tree, including children; RSS can double-count shared pages and excludes GPU memory. Collect GPU usage with -platform tools. GPUI archives currently omit the Python runtime; report its -installed footprint separately, and do not compare that ZIP directly with a -fully bundled Electron installer. Elevated/detached children must be measured +platform tools. Native archives include the PyInstaller backend, so measure the +complete extracted deliverable. Elevated/detached children must be measured separately. Evaluate input latency, scrolling, text entry/IME, scaling, keyboard focus, screen readers, offline/error behavior, and update/install/uninstall -reliability before selecting a winner. There is no winner established by this PR. +reliability when comparing future changes. GPUI is the selected product client; +that decision does not substitute for collecting hardware measurements. -## Validation recorded for this PR +## Validation history and current gates On the supplied Linux cloud environment: - Debug and optimized release builds completed with the locked dependency tree. - Four Rust protocol tests passed, including an actual local HTTP worker request and HTTP error response. Clippy with warnings denied and rustfmt checks passed. -- Six Python launcher tests passed, covering child reaping, startup/UI failures, - missing binaries, early service exit, and custom target-directory consistency. +- Eight Python launcher/packaging tests pass, covering child reaping, startup/UI + failures, missing binaries, early service exit, custom target-directory + consistency, required bundle inputs, and the self-contained archive layout. - The existing Electron renderer TypeScript check (`tsc --noEmit`) passed. - A real Python service bound an ephemeral port, returned health/status with no game installation, and stopped accepting connections after launcher cleanup. @@ -141,7 +153,7 @@ On the supplied Linux cloud environment: - The normal X11 close path exposed a GPUI 0.2.2 callback borrow panic. Quit is now scheduled on a later event-loop turn. A read-only native lifecycle smoke check exercises the real platform close callback, and Linux CI runs it too. -- Archive contents and the process-tree sampler were smoke checked. The sampler +- The original archive contents and process-tree sampler were smoke checked. The sampler captured the supervisor, Python API and native GUI; its debug/software-rendered samples are not comparative performance evidence. @@ -172,5 +184,6 @@ is configured in this PR; its hosted results are separate from these local check - Verify API startup failure, disconnect, download errors, and clean shutdown. Wait for active file operations before closing; cancellation is not implemented. - Test X11, Wayland, actual Steam Deck, high DPI, IME and screen-reader behavior. -- Measure optimized complete stacks and record results before any replacement - decision. Hardware results and full packaged parity are follow-up gates. +- Measure optimized complete stacks and record results as native delivery evolves. + Hardware results and full packaged parity remain follow-up gates after the + product-level promotion. diff --git a/native/gpui/README.md b/native/gpui/README.md index 89ff7e0..0332592 100644 --- a/native/gpui/README.md +++ b/native/gpui/README.md @@ -1,22 +1,59 @@ -# PatchOpsIII GPUI alternative +# PatchOpsIII native desktop app + +This is the primary PatchOpsIII desktop client for Windows x64 and Linux x64, +including Steam Deck desktop mode. It renders Rust/GPUI controls without +Chromium, React, Tauri, or a WebView. The established Python API still performs +game detection, downloads, backups, configuration, and launch operations. The +Electron/React client remains in the repository as a legacy fallback. + +The original framework research, architecture decision, comparison criteria, +implemented workflow inventory, and remaining parity work are retained in +[the native desktop migration record](../../docs/gpui-architecture.md). + +## Install a release archive + +The native build produces `PatchOpsIII-native-windows-x64.zip` and +`PatchOpsIII-native-linux-x64.zip`. Each archive has this layout: + +```text +PatchOpsIII/ +├── patchopsiii-gpui[.exe] +├── resources/ +│ ├── backend-bin/patchops-backend[.exe] +│ ├── package.json +│ ├── presets.json +│ ├── PatchOpsIII.ico +│ └── icon-512.png +├── LICENSE +├── THIRD-PARTY-NOTICES/ +├── README.md +└── NATIVE-README.md +``` -A native Rust/GPUI evaluation app alongside the Electron app, targeting Windows -x64 and Linux x64 (including a future Steam Deck evaluation). It renders native -controls without Chromium, React, Tauri, or a WebView. The existing Python API -still performs game operations. This is an experimental alternative, not a -feature-complete replacement or a production installer. +Extract the whole folder and keep `resources` beside the native executable. On +Linux, make the native executable executable after extraction if the archive +tool did not preserve its mode: -Research, implementation plan, comparison criteria, remaining work, and review -checks are in [the evaluation document](../../docs/gpui-evaluation.md). +```sh +chmod +x PatchOpsIII/patchopsiii-gpui +./PatchOpsIII/patchopsiii-gpui +``` + +On Windows, run `PatchOpsIII\patchopsiii-gpui.exe`. The native executable starts +the bundled PyInstaller backend on an OS-assigned loopback port, waits up to 30 +seconds for `/api/health`, opens the UI only after the service is ready, and +terminates and reaps the service on normal exit. No Python installation is +needed. Linux still needs a working X11 or Wayland desktop, fonts, and a +Vulkan-capable driver. ## Build and run from source -Install Rust with rustup, Python 3.12, and the normal backend requirements. -The crate pins GPUI 0.2.2 and GPUI Component 0.5.0 with a committed Cargo.lock; -its toolchain is Rust 1.99.0. GPUI's upstream main branch has since changed its -platform API; use the pinned published API when editing this app. +Install Rust with rustup, Python 3.12, and the normal backend requirements. The +crate pins GPUI 0.2.2 and GPUI Component 0.5.0 with a committed `Cargo.lock` and +Rust 1.99.0. GPUI's upstream main branch has since changed its platform API; use +the pinned published APIs when editing this app. -Linux build dependencies (Ubuntu 24.04): +Linux build dependencies on Ubuntu 24.04: ```sh sudo apt-get install libfontconfig1-dev libfreetype6-dev libxkbcommon-dev \ @@ -24,12 +61,10 @@ sudo apt-get install libfontconfig1-dev libfreetype6-dev libxkbcommon-dev \ libxcb1-dev libasound2-dev libclang-dev ``` -Windows needs Visual Studio Build Tools with Desktop development with C++ and -a Windows SDK, plus Rust's MSVC toolchain. Linux runtime needs a working X11 or -Wayland session, fonts, and a Vulkan-capable driver. Software rendering is useful -for smoke checks but does not establish Steam Deck/GPU performance. +Windows needs Visual Studio Build Tools with Desktop development with C++, a +Windows SDK, and Rust's MSVC toolchain. -From the repository root, use your Python environment's interpreter: +From the repository root: ```sh python -m venv .venv @@ -40,39 +75,43 @@ python scripts/gpui.py build python scripts/gpui.py run ``` -The launcher waits for a healthy API on an OS-assigned loopback port, launches -the compiled GPUI binary, and stops/reaps both processes on exit, Ctrl+C, or -startup failure. Build separately so launch measurements exclude compilation. -For an optimized build, pass `--release` to both commands. - -Electron and GPUI use separate processes and ports, but deliberately share -backend settings, logs, caches, and game files. Do not perform overlapping game -mutations in the two apps. Native actions display the selected operation and -game directory for confirmation. HTTP requests run sequentially on a worker; -long downloads do not freeze navigation. GET requests time out after 30 seconds, -operations after 10 minutes. A timeout reports failure and does **not** guarantee -the Python operation stopped; check logs and refresh before retrying. Closing the -launcher stops its backend, so wait for file operations to finish before closing. +The launcher remains the recommended development command: it starts the source +API with an inherited bound socket, waits for health, injects the resulting URL, +and reaps both processes. A directly started development binary can also find +`backend/api.py` from the repository and uses `.venv` or +`PATCHOPSIII_PYTHON`. `PATCHOPSIII_GPUI_BACKEND_URL` selects an externally owned +loopback API; `PATCHOPSIII_BACKEND_PATH` selects a packaged backend executable. -## Evaluation archives +Electron and GPUI deliberately share backend settings, logs, caches, and game +files. Do not perform overlapping game mutations in the two apps. Native actions +display the selected operation and game directory for confirmation. HTTP +requests run sequentially on a worker; long downloads do not freeze navigation. +GET requests time out after 30 seconds and operations after 10 minutes. A timeout +does not guarantee that the Python operation stopped, so check logs and refresh +before retrying. Wait for file operations to finish before closing the app. -The GPUI workflow checks formatting/lints/tests and builds release executables -for Linux and Windows. Its ZIP artifacts include the native executable, Python -backend source, presets, launcher, and documentation. They require Python and -installed requirements; no Python runtime is bundled yet. They do not publish -releases or replace Electron's MSI/AppImage workflows. +## Build a distributable archive -After extracting an archive: +The backend build commands are shared with the legacy Electron packaging so the +PyInstaller flags cannot drift. Install Bun plus PyInstaller in `.venv`, then: ```sh -python -m venv .venv -# Activate the environment as above. -python -m pip install -r requirements.txt -# Linux ZIP extraction may require: chmod +x bin/patchopsiii-gpui -python scripts/gpui.py run --executable bin/patchopsiii-gpui -# Windows: python scripts/gpui.py run --executable bin/patchopsiii-gpui.exe +# Linux +bun run gpui:build:release +bun run build:backend:linux +python scripts/package_gpui.py \ + --executable native/gpui/target/release/patchopsiii-gpui \ + --backend dist/backend/patchops-backend \ + --output dist/native/PatchOpsIII-native-linux-x64.zip + +# Windows PowerShell uses build:backend:win and the corresponding .exe paths. ``` +`.github/workflows/gpui-build.yml` runs formatting, Clippy with warnings denied, +Rust and Python tests, the Linux window-close smoke check, both release builds, +and archive creation. Stable and beta release workflows attach both native ZIPs +without changing the existing Electron artifact names or tag behavior. + ## Validation ```sh @@ -86,4 +125,12 @@ xvfb-run -a python scripts/smoke_gpui_linux.py --start-session \ --executable native/gpui/target/release/patchopsiii-gpui ``` -See the evaluation document for the hardware/manual checks that remain necessary. +## Remaining gaps + +The native app is the primary implementation, but promotion does not imply full +UI parity. Native installer integration, signing, automatic updates, streaming +WebSocket logs, some richer progress/detail states, accessibility/IME coverage, +and real-device Windows, Wayland, and Steam Deck validation remain open. The +legacy Electron installers stay available while those delivery and parity gaps +are closed. See the migration record for the detailed workflow matrix and manual +review checklist. diff --git a/native/gpui/assets/icons/check.svg b/native/gpui/assets/icons/check.svg new file mode 100644 index 0000000..41ab05b --- /dev/null +++ b/native/gpui/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/icons/chevron-down.svg b/native/gpui/assets/icons/chevron-down.svg new file mode 100644 index 0000000..9c9685e --- /dev/null +++ b/native/gpui/assets/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/icons/chevron-right.svg b/native/gpui/assets/icons/chevron-right.svg new file mode 100644 index 0000000..812cb6e --- /dev/null +++ b/native/gpui/assets/icons/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/icons/chevron-up.svg b/native/gpui/assets/icons/chevron-up.svg new file mode 100644 index 0000000..abf0556 --- /dev/null +++ b/native/gpui/assets/icons/chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/icons/circle-check.svg b/native/gpui/assets/icons/circle-check.svg new file mode 100644 index 0000000..edf1f6a --- /dev/null +++ b/native/gpui/assets/icons/circle-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/clipboard.svg b/native/gpui/assets/icons/clipboard.svg new file mode 100644 index 0000000..f111f42 --- /dev/null +++ b/native/gpui/assets/icons/clipboard.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/close.svg b/native/gpui/assets/icons/close.svg new file mode 100644 index 0000000..2f94dee --- /dev/null +++ b/native/gpui/assets/icons/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/cpu.svg b/native/gpui/assets/icons/cpu.svg new file mode 100644 index 0000000..c482a2e --- /dev/null +++ b/native/gpui/assets/icons/cpu.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/native/gpui/assets/icons/download.svg b/native/gpui/assets/icons/download.svg new file mode 100644 index 0000000..86e2ccd --- /dev/null +++ b/native/gpui/assets/icons/download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/native/gpui/assets/icons/eraser.svg b/native/gpui/assets/icons/eraser.svg new file mode 100644 index 0000000..fcacf82 --- /dev/null +++ b/native/gpui/assets/icons/eraser.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/external-link.svg b/native/gpui/assets/icons/external-link.svg new file mode 100644 index 0000000..23e238c --- /dev/null +++ b/native/gpui/assets/icons/external-link.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/native/gpui/assets/icons/eye-off.svg b/native/gpui/assets/icons/eye-off.svg new file mode 100644 index 0000000..bc7af7b --- /dev/null +++ b/native/gpui/assets/icons/eye-off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/native/gpui/assets/icons/eye.svg b/native/gpui/assets/icons/eye.svg new file mode 100644 index 0000000..5731d78 --- /dev/null +++ b/native/gpui/assets/icons/eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/folder-open.svg b/native/gpui/assets/icons/folder-open.svg new file mode 100644 index 0000000..3777d2c --- /dev/null +++ b/native/gpui/assets/icons/folder-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/icons/gem.svg b/native/gpui/assets/icons/gem.svg new file mode 100644 index 0000000..43a0285 --- /dev/null +++ b/native/gpui/assets/icons/gem.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/native/gpui/assets/icons/image.svg b/native/gpui/assets/icons/image.svg new file mode 100644 index 0000000..4a6e34c --- /dev/null +++ b/native/gpui/assets/icons/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/native/gpui/assets/icons/key-round.svg b/native/gpui/assets/icons/key-round.svg new file mode 100644 index 0000000..324eaba --- /dev/null +++ b/native/gpui/assets/icons/key-round.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/layout-dashboard.svg b/native/gpui/assets/icons/layout-dashboard.svg new file mode 100644 index 0000000..c23621e --- /dev/null +++ b/native/gpui/assets/icons/layout-dashboard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/native/gpui/assets/icons/refresh-cw.svg b/native/gpui/assets/icons/refresh-cw.svg new file mode 100644 index 0000000..191be9b --- /dev/null +++ b/native/gpui/assets/icons/refresh-cw.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/native/gpui/assets/icons/rotate-ccw.svg b/native/gpui/assets/icons/rotate-ccw.svg new file mode 100644 index 0000000..acb3eb5 --- /dev/null +++ b/native/gpui/assets/icons/rotate-ccw.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/save.svg b/native/gpui/assets/icons/save.svg new file mode 100644 index 0000000..1b9fc74 --- /dev/null +++ b/native/gpui/assets/icons/save.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/native/gpui/assets/icons/shield-check.svg b/native/gpui/assets/icons/shield-check.svg new file mode 100644 index 0000000..ea22670 --- /dev/null +++ b/native/gpui/assets/icons/shield-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/square-play.svg b/native/gpui/assets/icons/square-play.svg new file mode 100644 index 0000000..74098f1 --- /dev/null +++ b/native/gpui/assets/icons/square-play.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/icons/trash-2.svg b/native/gpui/assets/icons/trash-2.svg new file mode 100644 index 0000000..1d16f15 --- /dev/null +++ b/native/gpui/assets/icons/trash-2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/native/gpui/assets/icons/triangle-alert.svg b/native/gpui/assets/icons/triangle-alert.svg new file mode 100644 index 0000000..9edd41e --- /dev/null +++ b/native/gpui/assets/icons/triangle-alert.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/native/gpui/assets/icons/wrench.svg b/native/gpui/assets/icons/wrench.svg new file mode 100644 index 0000000..693acf5 --- /dev/null +++ b/native/gpui/assets/icons/wrench.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/icons/x.svg b/native/gpui/assets/icons/x.svg new file mode 100644 index 0000000..2f94dee --- /dev/null +++ b/native/gpui/assets/icons/x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/native/gpui/assets/images/logo.png b/native/gpui/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b4c6a6eb309b44e14c684210c730a9cb13ade2ec GIT binary patch literal 53168 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 literal 0 HcmV?d00001 diff --git a/native/gpui/src/backend.rs b/native/gpui/src/backend.rs index 3f3c015..64b79e4 100644 --- a/native/gpui/src/backend.rs +++ b/native/gpui/src/backend.rs @@ -2,7 +2,318 @@ use anyhow::{Context, Result, bail}; use reqwest::blocking::Client; use serde_json::Value; -use std::{sync::mpsc, thread, time::Duration}; +use std::{ + env, + ffi::OsString, + fs, + net::TcpListener, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + sync::mpsc, + thread, + time::{Duration, Instant}, +}; + +const BACKEND_HOST: &str = "127.0.0.1"; +const BACKEND_START_TIMEOUT: Duration = Duration::from_secs(30); + +struct LaunchSpec { + command: PathBuf, + args: Vec, + app_root: PathBuf, + packaged: bool, +} + +/// Owns the local API process for the lifetime of the desktop application. +/// +/// `PATCHOPSIII_GPUI_BACKEND_URL` remains an escape hatch for the development +/// launcher and tests. In that mode the process that supplied the URL retains +/// ownership of the service. +pub struct BackendService { + url: String, + child: Option, +} + +impl BackendService { + pub fn launch() -> Result { + if let Ok(url) = env::var("PATCHOPSIII_GPUI_BACKEND_URL") { + return Ok(Self { + url: validate_url(&url)?, + child: None, + }); + } + + let spec = resolve_launch_spec()?; + let port = reserve_loopback_port()?; + let url = format!("http://{BACKEND_HOST}:{port}"); + let mut command = Command::new(&spec.command); + command + .args(&spec.args) + .current_dir(&spec.app_root) + .env("PATCHOPSIII_BACKEND_HOST", BACKEND_HOST) + .env("PATCHOPSIII_BACKEND_PORT", port.to_string()) + .env("PYTHONUNBUFFERED", "1") + .stdin(Stdio::null()); + if let Some(version) = application_version(&spec.app_root) { + command.env("PATCHOPSIII_VERSION", version); + } + if spec.packaged { + command.stdout(Stdio::null()).stderr(Stdio::null()); + } else { + command.stdout(Stdio::inherit()).stderr(Stdio::inherit()); + } + configure_child_process(&mut command); + + let mut child = command.spawn().with_context(|| { + format!( + "Unable to start the PatchOpsIII backend at {}", + spec.command.display() + ) + })?; + if let Err(error) = wait_until_healthy(&mut child, &url, BACKEND_START_TIMEOUT) { + stop_child(&mut child); + return Err(error); + } + Ok(Self { + url, + child: Some(child), + }) + } + + pub fn url(&self) -> &str { + &self.url + } +} + +impl Drop for BackendService { + fn drop(&mut self) { + if let Some(child) = self.child.as_mut() { + stop_child(child); + } + } +} + +fn backend_executable_name() -> &'static str { + if cfg!(windows) { + "patchops-backend.exe" + } else { + "patchops-backend" + } +} + +fn resolve_launch_spec() -> Result { + if let Some(path) = env::var_os("PATCHOPSIII_BACKEND_PATH").map(PathBuf::from) { + let path = if path.is_absolute() { + path + } else { + env::current_dir() + .context("Unable to resolve PATCHOPSIII_BACKEND_PATH")? + .join(path) + }; + if !path.is_file() { + bail!( + "PATCHOPSIII_BACKEND_PATH does not name a file: {}", + path.display() + ); + } + return packaged_launch(path); + } + + let executable = env::current_exe().context("Unable to locate the GPUI executable")?; + let executable_dir = executable + .parent() + .context("The GPUI executable has no parent directory")?; + let name = backend_executable_name(); + let packaged_candidates = [ + executable_dir + .join("resources") + .join("backend-bin") + .join(name), + executable_dir.join("backend-bin").join(name), + executable_dir + .parent() + .unwrap_or(executable_dir) + .join("Resources") + .join("backend-bin") + .join(name), + ]; + for path in packaged_candidates { + if path.is_file() { + return packaged_launch(path); + } + } + + let app_root = find_source_root(&executable)?; + let python = python_command(&app_root); + Ok(LaunchSpec { + command: python, + args: vec![app_root.join("backend").join("api.py").into_os_string()], + app_root, + packaged: false, + }) +} + +fn packaged_launch(path: PathBuf) -> Result { + ensure_backend_executable(&path)?; + let app_root = path + .parent() + .and_then(Path::parent) + .context("Packaged backend has no resources directory")? + .to_path_buf(); + Ok(LaunchSpec { + command: path, + args: Vec::new(), + app_root, + packaged: true, + }) +} + +#[cfg(unix)] +fn ensure_backend_executable(path: &Path) -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let mut permissions = fs::metadata(path)?.permissions(); + let mode = permissions.mode(); + if mode & 0o100 == 0 { + permissions.set_mode(mode | 0o100); + fs::set_permissions(path, permissions).with_context(|| { + format!( + "Unable to make the packaged backend executable: {}", + path.display() + ) + })?; + } + Ok(()) +} + +#[cfg(not(unix))] +fn ensure_backend_executable(_path: &Path) -> Result<()> { + Ok(()) +} + +fn find_source_root(executable: &Path) -> Result { + let current_dir = env::current_dir().context("Unable to read the working directory")?; + for start in [Some(current_dir.as_path()), executable.parent()] + .into_iter() + .flatten() + { + for candidate in start.ancestors() { + if candidate.join("backend").join("api.py").is_file() + && candidate.join("presets.json").is_file() + { + return Ok(candidate.to_path_buf()); + } + } + } + bail!( + "PatchOpsIII backend not found. Install resources/backend-bin/{} next to the app, or run from the repository.", + backend_executable_name() + ) +} + +fn python_command(app_root: &Path) -> PathBuf { + if let Some(python) = env::var_os("PATCHOPSIII_PYTHON") { + return PathBuf::from(python); + } + let venv_python = if cfg!(windows) { + app_root.join(".venv").join("Scripts").join("python.exe") + } else { + app_root.join(".venv").join("bin").join("python") + }; + if venv_python.is_file() { + venv_python + } else { + PathBuf::from(if cfg!(windows) { "python" } else { "python3" }) + } +} + +fn application_version(app_root: &Path) -> Option { + if let Ok(version) = env::var("PATCHOPSIII_VERSION") + && !version.trim().is_empty() + { + return Some(version); + } + let package = fs::read_to_string(app_root.join("package.json")).ok()?; + serde_json::from_str::(&package).ok()?["version"] + .as_str() + .map(str::to_owned) +} + +fn reserve_loopback_port() -> Result { + let listener = TcpListener::bind((BACKEND_HOST, 0)) + .context("Unable to reserve a loopback port for the backend")?; + Ok(listener.local_addr()?.port()) +} + +fn wait_until_healthy(child: &mut Child, url: &str, timeout: Duration) -> Result<()> { + let client = Client::builder() + .no_proxy() + .redirect(reqwest::redirect::Policy::none()) + .connect_timeout(Duration::from_secs(1)) + .timeout(Duration::from_secs(1)) + .build()?; + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if let Some(status) = child.try_wait().context("Unable to inspect the backend")? { + bail!("PatchOpsIII backend exited during startup with {status}"); + } + if let Ok(response) = client.get(format!("{url}/api/health")).send() + && response.status().is_success() + && response + .json::() + .ok() + .and_then(|body| body["ok"].as_bool()) + == Some(true) + { + return Ok(()); + } + thread::sleep(Duration::from_millis(100)); + } + bail!("PatchOpsIII backend did not become healthy within 30 seconds") +} + +#[cfg(windows)] +fn configure_child_process(command: &mut Command) { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); +} + +#[cfg(not(windows))] +fn configure_child_process(_command: &mut Command) {} + +fn stop_child(child: &mut Child) { + if matches!(child.try_wait(), Ok(Some(_))) { + return; + } + #[cfg(windows)] + { + let pid = child.id().to_string(); + let _ = Command::new("taskkill") + .args(["/pid", pid.as_str(), "/t", "/f"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } + #[cfg(not(windows))] + { + let pid = child.id().to_string(); + let _ = Command::new("kill") + .args(["-TERM", pid.as_str()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + let deadline = Instant::now() + Duration::from_secs(3); + while Instant::now() < deadline { + if matches!(child.try_wait(), Ok(Some(_))) { + return; + } + thread::sleep(Duration::from_millis(50)); + } + } + let _ = child.kill(); + let _ = child.wait(); +} pub struct Request { pub path: String, diff --git a/native/gpui/src/main.rs b/native/gpui/src/main.rs index 3326059..d8349cf 100644 --- a/native/gpui/src/main.rs +++ b/native/gpui/src/main.rs @@ -1,16 +1,16 @@ mod backend; mod ui; -use anyhow::{Context, Result}; +use anyhow::Result; use gpui::{prelude::*, *}; use gpui_component::{Root, Theme, ThemeMode}; fn main() -> Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); - let url = std::env::var("PATCHOPSIII_GPUI_BACKEND_URL") - .context("Start GPUI with: python scripts/gpui.py run (see native/gpui/README.md)")?; - let backend = backend::Backend::start(url)?; - Application::new().run(move |cx: &mut App| { + let service = backend::BackendService::launch()?; + let backend = backend::Backend::start(service.url().to_owned())?; + let app = Application::new().with_assets(ui::Assets); + app.run(move |cx: &mut App| { gpui_component::init(cx); Theme::change(ThemeMode::Dark, None, cx); let theme = Theme::global_mut(cx); @@ -41,7 +41,7 @@ fn main() -> Result<()> { window_bounds: Some(WindowBounds::Windowed(bounds)), window_min_size: Some(size(px(800.), px(600.))), titlebar: Some(TitlebarOptions { - title: Some("PatchOpsIII · GPUI".into()), + title: Some("PatchOpsIII".into()), ..Default::default() }), ..Default::default() @@ -54,5 +54,6 @@ fn main() -> Result<()> { .expect("Unable to open the GPUI window"); cx.activate(true); }); + drop(service); Ok(()) } diff --git a/native/gpui/src/ui.rs b/native/gpui/src/ui.rs index d8a18d0..0827f68 100644 --- a/native/gpui/src/ui.rs +++ b/native/gpui/src/ui.rs @@ -1,56 +1,202 @@ -use crate::backend::{Backend, Request}; +//! The PatchOpsIII desktop UI. The layout mirrors the Electron renderer +//! (`src/renderer/main.tsx` + `styles/app.css`); each page lives in its own module. +mod assets; +mod components; +mod dashboard; +mod enhanced; +mod exe; +mod graphics; +mod modals; +mod t7; +mod theme; +mod tools; + +pub use assets::Assets; + +use crate::backend::{Backend, Reply, Request}; +use components::{Btn, Glyph, icon}; use gpui::{prelude::*, *}; use gpui_component::{ - Disableable, - button::{Button, ButtonVariants}, - input::{Input, InputState}, + ActiveTheme, + input::{Input, InputEvent, InputState}, + scroll::ScrollableElement, + slider::{SliderEvent, SliderState}, }; use serde_json::{Value, json}; use std::{ - collections::BTreeMap, - time::{Duration, Instant}, + collections::{BTreeMap, VecDeque}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; -const PAGES: [&str; 7] = [ - "Dashboard", - "T7 Patch", - "EXE Swapper", - "Enhanced", - "Graphics", - "DXVK", - "Tools", -]; +const POLL_INTERVAL: Duration = Duration::from_secs(5); +const DEPOT_COMMAND_MARKER: &str = "Steam console command: "; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Page { + Dashboard, + T7, + Exe, + Enhanced, + Graphics, + Tools, +} + +impl Page { + const ALL: [Page; 6] = [ + Page::Dashboard, + Page::T7, + Page::Exe, + Page::Enhanced, + Page::Graphics, + Page::Tools, + ]; + + fn label(self) -> &'static str { + match self { + Page::Dashboard => "Dashboard", + Page::T7 => "T7 Patch", + Page::Exe => "EXE Swapper", + Page::Enhanced => "Enhanced", + Page::Graphics => "Graphics", + Page::Tools => "Tools", + } + } + + fn glyph(self) -> Glyph { + match self { + Page::Dashboard => Glyph::Dashboard, + Page::T7 => Glyph::Shield, + Page::Exe => Glyph::Refresh, + Page::Enhanced => Glyph::Gem, + Page::Graphics => Glyph::Image, + Page::Tools => Glyph::Wrench, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum GraphicsTab { + Settings, + Dxvk, +} + +/// Result of the last "Validate Source" run (Enhanced page). +struct Validation { + label: String, + ok: Option, + checked_at: Option, + source: String, +} + +impl Validation { + fn not_run() -> Self { + Self { + label: "Not run".into(), + ok: None, + checked_at: None, + source: String::new(), + } + } +} + +/// The Steam console prompt shown when the compatible depot is missing. +struct Depot { + command: String, + copied: bool, + watching: bool, + last_poll: Instant, +} + +type Staged = (String, Vec); pub struct ControlCenter { backend: Backend, state: Value, - page: usize, + page: Page, + graphics_tab: GraphicsTab, busy: bool, connected: bool, message: String, failed: bool, last_refresh: Instant, + /// Path of the request currently owned by the backend worker. + inflight: String, inputs: BTreeMap<&'static str, Entity>, - pending: Option<(String, Request)>, + sliders: BTreeMap<&'static str, Entity>, + /// An action waiting for the user's confirmation. Multi-step actions + /// (such as "Apply All") stage several requests that run in order. + pending: Option, + queue: VecDeque, + selected_profile: String, + seen_profile: String, + t7_color: String, + t7_password_enabled: bool, + t7_password_touched: bool, + show_current_password: bool, + show_new_password: bool, + validation: Validation, + depot: Option, + dxvk_draft: Value, + dxvk_synced: Value, + advanced_open: bool, + danger_open: bool, + content_scroll: ScrollHandle, + log_scroll: ScrollHandle, + log_count: usize, +} + +fn dxvk_recommended() -> Value { + json!({ + "enableAsync": true, + "gplAsyncCache": true, + "numCompilerThreads": 0, + "maxFrameRate": 0, + "maxFrameLatency": 1, + "tearFree": "True", + "hudEnabled": false, + }) +} + +/// `formatTimestamp` from the Electron renderer, without a locale database. +fn format_timestamp(value: &str) -> String { + if value.is_empty() { + return "Never".into(); + } + value + .split(['.', '+', 'Z']) + .next() + .unwrap_or(value) + .replace('T', " ") +} + +fn clock_now() -> String { + let secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|elapsed| elapsed.as_secs()) + .unwrap_or_default(); + let day = secs % 86_400; + format!( + "{:02}:{:02}:{:02} UTC", + day / 3600, + day % 3600 / 60, + day % 60 + ) } impl ControlCenter { pub fn new(backend: Backend, window: &mut Window, cx: &mut Context) -> Self { - let inputs = [ - ("directory", "Game directory"), + theme::apply(cx); + let inputs: BTreeMap<_, _> = [ + ("directory", "Black Ops III directory"), ("gamertag", "Gamertag (up to 20 characters)"), - ("password", "Network password"), - ("color", "Color code, e.g. ^1"), - ("dump", "BO3 Enhanced source directory"), + ("password", "Enter network password"), + ("dump", "Path to DUMP.zip or an extracted folder"), ("fps", "0–1000"), - ("fov", "65–120"), ("resolution", "1920x1080"), - ("refresh", "Refresh rate"), - ("render", "Render resolution %"), + ("refresh", "1–1000"), ("latency", "0–4"), ("vram", "75–100"), ("dxvk-threads", "0–64"), - ("dxvk-fps", "0–360"), ("dxvk-latency", "0–16"), ] .into_iter() @@ -63,17 +209,81 @@ impl ControlCenter { (key, input) }) .collect(); + for input in inputs.values() { + // Drafts feed buttons and badges, so repaint while the user types. + cx.subscribe_in( + input, + window, + |_, _, event: &InputEvent, _, cx| match event { + InputEvent::Change => cx.notify(), + InputEvent::PressEnter { .. } => {} + InputEvent::Focus | InputEvent::Blur => {} + }, + ) + .detach(); + } + cx.subscribe_in( + &inputs["directory"], + window, + |view, _, event: &InputEvent, _, cx| { + if matches!(event, InputEvent::PressEnter { .. }) { + view.stage_directory(cx); + } + }, + ) + .detach(); + let sliders: BTreeMap<_, _> = [ + ("render", 50., 200., 100.), + ("fov", 65., 120., 80.), + ("dxvk-fps", 0., 360., 0.), + ] + .into_iter() + .map(|(key, min, max, value)| { + let slider = cx.new(|_| { + SliderState::new() + .min(min) + .max(max) + .step(1.) + .default_value(value) + }); + (key, slider) + }) + .collect(); + for slider in sliders.values() { + cx.subscribe_in(slider, window, |_, _, _: &SliderEvent, _, cx| cx.notify()) + .detach(); + } let mut view = Self { backend, state: Value::Null, - page: 0, + page: Page::Dashboard, + graphics_tab: GraphicsTab::Settings, busy: false, connected: false, message: "Connecting to the local service…".into(), failed: false, last_refresh: Instant::now(), + inflight: String::new(), inputs, + sliders, pending: None, + queue: VecDeque::new(), + selected_profile: "default".into(), + seen_profile: String::new(), + t7_color: String::new(), + t7_password_enabled: false, + t7_password_touched: false, + show_current_password: false, + show_new_password: false, + validation: Validation::not_run(), + depot: None, + dxvk_draft: dxvk_recommended(), + dxvk_synced: Value::Null, + advanced_open: false, + danger_open: false, + content_scroll: ScrollHandle::new(), + log_scroll: ScrollHandle::new(), + log_count: 0, }; view.send("/api/status", None, cx); // Drain replies and periodically refresh on GPUI's foreground executor. @@ -86,40 +296,9 @@ impl ControlCenter { if entity .update_in(cx, |view, window, cx| { while let Ok(reply) = view.backend.replies.try_recv() { - view.busy = false; - if reply.failed { - view.failed = true; - if reply.is_status { - view.connected = false; - } - view.message = reply.message; - } else { - let initial = view.state.is_null(); - let reconnected = !view.connected; - if let Some(state) = reply.state { - view.state = state; - view.connected = true; - } - if initial { - view.load_inputs(window, cx); - } - if !reply.is_status - || reconnected - || view.message.starts_with("Connecting") - { - view.failed = false; - view.message = reply.message; - } - } - view.last_refresh = Instant::now(); - cx.notify(); - } - if !view.busy - && view.pending.is_none() - && view.last_refresh.elapsed() >= Duration::from_secs(5) - { - view.send("/api/status", None, cx); + view.handle_reply(reply, window, cx); } + view.poll(cx); }) .is_err() { @@ -131,6 +310,123 @@ impl ControlCenter { view } + fn handle_reply(&mut self, reply: Reply, window: &mut Window, cx: &mut Context) { + self.busy = false; + let path = std::mem::take(&mut self.inflight); + if reply.failed { + self.queue.clear(); + self.failed = true; + if reply.is_status { + self.connected = false; + } + self.on_failure(&path, &reply.message); + self.message = reply.message; + } else { + let initial = self.state.is_null(); + let reconnected = !self.connected; + if let Some(state) = reply.state { + self.state = state; + self.connected = true; + if initial { + self.load_inputs(window, cx); + } else { + self.sync_drafts(window, cx); + } + } + self.on_success(&path, &reply.message, window, cx); + if !reply.is_status || reconnected || self.message.starts_with("Connecting") { + self.failed = false; + self.message = reply.message; + } + } + self.last_refresh = Instant::now(); + let entries = self.visible_logs().len(); + if entries != self.log_count { + self.log_count = entries; + self.log_scroll.scroll_to_bottom(); + } + if let Some(next) = self.queue.pop_front() { + self.send(&next.path, next.body, cx); + } + cx.notify(); + } + + /// Per-endpoint bookkeeping for a failed request. + fn on_failure(&mut self, path: &str, message: &str) { + match path { + "/api/enhanced-validate" => { + self.validation = Validation { + label: message.to_owned(), + ok: Some(false), + checked_at: Some(clock_now()), + source: self.validation.source.clone(), + }; + } + "/api/exe-swap/compatible" => { + if let Some((_, command)) = message.split_once(DEPOT_COMMAND_MARKER) { + // The depot is not downloaded yet: this is a prompt, not an error. + let watching = self.depot.as_ref().is_some_and(|depot| depot.watching); + let copied = self.depot.as_ref().is_some_and(|depot| depot.copied); + self.depot = Some(Depot { + command: command.trim().to_owned(), + copied, + watching, + last_poll: Instant::now(), + }); + self.failed = false; + } else if let Some(depot) = self.depot.as_mut() { + depot.watching = false; + } + } + _ => {} + } + } + + /// Per-endpoint bookkeeping for a successful request. + fn on_success( + &mut self, + path: &str, + message: &str, + window: &mut Window, + cx: &mut Context, + ) { + match path { + "/api/enhanced-validate" => { + self.validation = Validation { + label: message.to_owned(), + ok: Some(true), + checked_at: Some(clock_now()), + source: self.text("dump", cx).trim().to_owned(), + }; + } + "/api/exe-swap/compatible" => self.depot = None, + "/api/t7-config" => { + self.t7_password_touched = false; + self.load_t7(window, cx); + } + _ => {} + } + } + + /// Background work: status refresh and depot watching. + fn poll(&mut self, cx: &mut Context) { + if self.busy || self.pending.is_some() { + return; + } + let watching = self + .depot + .as_ref() + .is_some_and(|depot| depot.watching && depot.last_poll.elapsed() >= POLL_INTERVAL); + if watching { + if let Some(depot) = self.depot.as_mut() { + depot.last_poll = Instant::now(); + } + self.send("/api/exe-swap/compatible", Some(json!({})), cx); + } else if self.last_refresh.elapsed() >= POLL_INTERVAL { + self.send("/api/status", None, cx); + } + } + fn send(&mut self, path: &str, body: Option, cx: &mut Context) { if self.busy { return; @@ -141,174 +437,263 @@ impl ControlCenter { }) { Ok(()) => { self.busy = true; + self.inflight = path.to_owned(); self.last_refresh = Instant::now(); } Err(_) => { self.failed = true; self.connected = false; - self.message = "Local service worker stopped. Restart GPUI.".into(); + self.message = "Local service worker stopped. Restart PatchOpsIII.".into(); } } cx.notify(); } fn stage(&mut self, label: String, path: String, body: Value, cx: &mut Context) { - self.pending = Some(( + self.stage_all( label, - Request { + vec![Request { path, body: Some(body), - }, - )); + }], + cx, + ); + } + + fn stage_all(&mut self, label: String, requests: Vec, cx: &mut Context) { + self.pending = Some((label, requests)); + cx.notify(); + } + + fn confirm(&mut self, cx: &mut Context) { + if self.busy { + return; + } + if let Some((label, mut requests)) = self.pending.take() { + self.message = format!("Running: {label}…"); + self.failed = false; + if requests.is_empty() { + cx.notify(); + return; + } + let first = requests.remove(0); + self.queue = requests.into(); + self.send(&first.path, first.body, cx); + } + } + + fn reject(&mut self, message: impl Into, cx: &mut Context) { + self.message = message.into(); + self.failed = true; cx.notify(); } fn text(&self, key: &str, cx: &App) -> String { self.inputs[key].read(cx).value().to_string() } + + fn set_text(&self, key: &str, value: String, window: &mut Window, cx: &mut Context) { + self.inputs[key].update(cx, |state, cx| state.set_value(value, window, cx)); + } + + fn set_slider(&self, key: &str, value: i64, window: &mut Window, cx: &mut Context) { + self.sliders[key].update(cx, |state, cx| state.set_value(value as f32, window, cx)); + } + + fn slider_value(&self, key: &str, cx: &App) -> i64 { + self.sliders[key].read(cx).value().start().round() as i64 + } + + /// Copy every draft control from the backend state (initial load and + /// "Reload Values"). fn load_inputs(&mut self, window: &mut Window, cx: &mut Context) { for (key, pointer) in [ ("directory", "/gameDir"), - ("gamertag", "/t7/plainName"), - ("color", "/t7/colorCode"), ("dump", "/enhanced/dumpSource"), ("fps", "/graphics/maxFps"), - ("fov", "/graphics/fov"), ("resolution", "/graphics/resolution"), ("refresh", "/graphics/refreshRate"), - ("render", "/graphics/renderResolution"), ("latency", "/advanced/maxFrameLatency"), ("vram", "/advanced/vramTarget"), - ("dxvk-threads", "/dxvk/settings/numCompilerThreads"), - ("dxvk-fps", "/dxvk/settings/maxFrameRate"), - ("dxvk-latency", "/dxvk/settings/maxFrameLatency"), ] { - let value = self.value(pointer); - self.inputs[key].update(cx, |state, cx| { - state.set_value(if value == "—" { String::new() } else { value }, window, cx) - }); + let value = self.string(pointer); + self.set_text(key, value, window, cx); + } + for (key, pointer) in [ + ("render", "/graphics/renderResolution"), + ("fov", "/graphics/fov"), + ] { + let value = self.int(pointer, 0); + self.set_slider(key, value, window, cx); } + self.load_t7(window, cx); + self.t7_password_touched = false; + self.seen_profile.clear(); + self.dxvk_synced = Value::Null; + self.sync_drafts(window, cx); } + + fn load_t7(&mut self, window: &mut Window, cx: &mut Context) { + let name = self.string("/t7/plainName"); + self.set_text("gamertag", name, window, cx); + self.t7_color = self.string("/t7/colorCode"); + } + + /// Drafts that follow the backend unless the user is editing them. + fn sync_drafts(&mut self, window: &mut Window, cx: &mut Context) { + let active = self.string("/activeLaunchProfile"); + if active != self.seen_profile { + self.selected_profile = if active.is_empty() || active == "custom" { + "default".into() + } else { + active.clone() + }; + self.seen_profile = active; + } + if !self.t7_password_touched { + self.t7_password_enabled = !self.string("/t7/networkPassword").is_empty(); + } + let settings = self + .state + .pointer("/dxvk/settings") + .cloned() + .unwrap_or(Value::Null); + if settings != self.dxvk_synced && settings.is_object() { + self.dxvk_draft = settings.clone(); + self.dxvk_synced = settings; + self.show_dxvk_draft(window, cx); + } + } + + fn show_dxvk_draft(&mut self, window: &mut Window, cx: &mut Context) { + let threads = self.dxvk_int("numCompilerThreads").to_string(); + let latency = self.dxvk_int("maxFrameLatency").to_string(); + let fps = self.dxvk_int("maxFrameRate"); + self.set_text("dxvk-threads", threads, window, cx); + self.set_text("dxvk-latency", latency, window, cx); + self.set_slider("dxvk-fps", fps, window, cx); + } + fn flag(&self, pointer: &str) -> bool { self.state .pointer(pointer) .and_then(Value::as_bool) .unwrap_or(false) } - fn value(&self, pointer: &str) -> String { + + /// A state value as display text; absent and null values become "". + fn string(&self, pointer: &str) -> String { match self.state.pointer(pointer) { Some(Value::String(s)) => s.clone(), Some(Value::Number(n)) => n .as_f64() .map(|n| n.to_string()) .unwrap_or_else(|| n.to_string()), - Some(Value::Null) | None => "—".into(), + Some(Value::Null) | None => String::new(), Some(v) => v.to_string(), } } - fn action( + /// A state value as display text with a visible fallback. + fn value(&self, pointer: &str) -> String { + let value = self.string(pointer); + if value.is_empty() { + "—".into() + } else { + value + } + } + + fn int(&self, pointer: &str, default: i64) -> i64 { + self.state + .pointer(pointer) + .and_then(Value::as_f64) + .map_or(default, |n| n.round() as i64) + } + + fn dxvk_flag(&self, key: &str) -> bool { + self.dxvk_draft[key].as_bool().unwrap_or(false) + } + + fn dxvk_int(&self, key: &str) -> i64 { + self.dxvk_draft[key].as_i64().unwrap_or(0) + } + + fn can_act(&self) -> bool { + !self.busy && self.connected && self.pending.is_none() + } + + /// A click handler that asks for confirmation before sending `path`. + fn stage_click( &self, - id: String, - label: String, + cx: &mut Context, + label: impl Into, path: &str, body: Value, - cx: &mut Context, - ) -> Button { + ) -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static { + let label = label.into(); let path = path.to_owned(); - Button::new(SharedString::from(id)) - .label(label.clone()) - .disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(move |view, _, _, cx| { - view.stage(label.clone(), path.clone(), body.clone(), cx) - })) + cx.listener(move |view, _: &ClickEvent, _, cx| { + view.stage(label.clone(), path.clone(), body.clone(), cx) + }) } - fn toggle( + /// A button that stages one request for confirmation. + fn action( &self, + cx: &mut Context, + id: impl Into, label: &str, - pointer: &str, path: &str, - key: &str, - cx: &mut Context, - ) -> AnyElement { - let current = self.flag(pointer); - let body = json!({key: !current}); - div() - .flex() - .justify_between() - .items_center() - .gap_4() - .child(format!("{label}: {}", if current { "On" } else { "Off" })) - .child(self.action( - format!("toggle-{pointer}"), - format!("{} {label}", if current { "Disable" } else { "Enable" }), - path, - body, - cx, - )) - .into_any_element() + body: Value, + ) -> Btn { + Btn::new(id, label.to_owned()) + .disabled(!self.can_act()) + .on_click(self.stage_click(cx, label, path, body)) } - fn config_toggle( - &self, - label: &str, - pointer: &str, - key: &str, - cx: &mut Context, - ) -> AnyElement { - let current = self.flag(pointer); - div().flex().justify_between().items_center().gap_4() - .child(format!("{label}: {}", if current { "On" } else { "Off" })) - .child(self.action(format!("config-{key}"), format!("{} {label}", if current { "Disable" } else { "Enable" }), "/api/config", - json!({"key": key, "value": if current {0} else {1}, "comment": "Managed by PatchOpsIII"}), cx)).into_any_element() + fn config_body(key: &str, value: Value) -> Value { + let comment = match key { + "MaxFPS" => "Maximum FPS cap", + "FOV" => "Field of view", + "FullScreenMode" => "0=Windowed,1=Fullscreen,2=Fullscreen Windowed", + "WindowSize" => "any text", + "RefreshRate" => "1 to 240", + "ResolutionPercent" => "50 to 200", + "Vsync" => "Vertical sync", + "DrawFPS" => "FPS counter", + "SmoothFramerate" => "Frame smoothing", + "RestrictGraphicsOptions" => "Expose all graphics options", + "SerializeRender" => "Reduce CPU pressure", + "MaxFrameLatency" => "Maximum frame latency", + _ => "Managed by PatchOpsIII", + }; + json!({"key": key, "value": value, "comment": comment}) } - fn field(&self, label: &str, key: &str) -> AnyElement { - div() - .flex() - .flex_col() - .gap_2() - .child(label.to_owned()) - .child(Input::new(&self.inputs[key]).disabled(self.busy)) - .into_any_element() + /// A text input styled like the Electron `input` elements. + fn field(&self, key: &str) -> Input { + Input::new(&self.inputs[key]) + .disabled(self.busy) + .min_h(px(theme::CONTROL_HEIGHT - 2.)) + .bg(theme::field()) + .rounded(theme::radius_card()) + .border_color(theme::border()) } - fn numeric_field( - &self, - label: &str, - input: &'static str, - config: &'static str, - min: i64, - max: i64, - cx: &mut Context, - ) -> AnyElement { - div() - .flex() - .items_end() - .gap_3() - .child(div().flex_1().child(self.field(label, input))) - .child( - Button::new(SharedString::from(format!("apply-{input}"))) - .label("Apply") - .disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(move |view, _, _, cx| { - match view.text(input, cx).parse::() { - Ok(value) if (min..=max).contains(&value) => view.stage( - format!("Set {config} to {value}"), - "/api/config".into(), - json!({"key": config, "value": value}), - cx, - ), - _ => { - view.message = format!("Enter a whole number from {min} to {max}."); - view.failed = true; - cx.notify(); - } - } - })), - ) - .into_any_element() + fn stage_directory(&mut self, cx: &mut Context) { + let path = self.text("directory", cx); + let path = path.trim().to_owned(); + if path.is_empty() { + self.reject("Select or enter a directory first.", cx); + } else if path != self.string("/gameDir") { + self.stage( + format!("Use directory {path}"), + "/api/game-directory".into(), + json!({"path": path}), + cx, + ); + } } fn choose_directory(&mut self, input: &'static str, window: &Window, cx: &mut Context) { @@ -323,476 +708,543 @@ impl ControlCenter { if let Some(path) = paths.first() { let value = path.to_string_lossy().into_owned(); let _ = entity.update_in(cx, |view, window, cx| { - view.inputs[input] - .update(cx, |state, cx| state.set_value(value, window, cx)); + view.set_text(input, value, window, cx); + view.after_pick(input, cx); }); } } Ok(Ok(None)) => {} _ => { let _ = entity.update(cx, |view, cx| { - view.message = "Folder picker unavailable. Enter the path directly.".into(); - view.failed = true; - cx.notify(); + view.reject("Folder picker unavailable. Enter the path directly.", cx); }); } }) .detach(); } - fn folder_field(&self, label: &str, key: &'static str, cx: &mut Context) -> AnyElement { + /// What picking a folder means for each input. + fn after_pick(&mut self, input: &str, cx: &mut Context) { + match input { + "directory" => self.stage_directory(cx), + "dump" => { + self.validation = Validation::not_run(); + cx.notify(); + } + _ => {} + } + } + + /// Latest log lines, oldest first, without the service start-up notice. + fn visible_logs(&self) -> Vec<(String, String)> { + let mut seen = std::collections::HashSet::new(); + let mut entries: Vec<(String, String)> = self.state["logs"] + .as_array() + .into_iter() + .flatten() + .filter(|entry| entry["message"] != "PatchOpsIII local API started.") + .filter(|entry| { + seen.insert(format!( + "{}|{}|{}", + entry["line"], entry["category"], entry["message"] + )) + }) + .map(|entry| { + ( + entry["category"].as_str().unwrap_or("Info").to_owned(), + entry["message"].as_str().unwrap_or("").to_owned(), + ) + }) + .collect(); + if entries.len() > 120 { + entries.drain(..entries.len() - 120); + } + entries + } + + fn page_view(&self, cx: &mut Context) -> AnyElement { + match self.page { + Page::Dashboard => self.dashboard_view(cx), + Page::T7 => self.t7_view(cx), + Page::Exe => self.exe_view(cx), + Page::Enhanced => self.enhanced_view(cx), + Page::Graphics => self.graphics_view(cx), + Page::Tools => self.tools_view(cx), + } + } + + /// `.titlebar`: logo, name, version (click to check for updates) and + /// the connection state. + fn header(&self, cx: &mut Context) -> impl IntoElement { + let version = self.string("/appVersion"); + let version = if version.to_lowercase().starts_with('v') || version.is_empty() { + version + } else { + format!("v{version}") + }; + let can_act = self.can_act(); div() .flex() - .items_end() - .gap_3() - .child(div().flex_1().child(self.field(label, key))) + .items_center() + .flex_none() + .h(px(32.)) + .px(px(10.)) + .gap(px(8.)) + .bg(theme::bg_deep()) + .border_b_1() + .border_color(theme::white(0.08)) + .child( + img("images/logo.png") + .size(px(18.)) + .flex_none() + .rounded(px(4.)), + ) + .child(div().text_size(px(theme::FONT_SM)).child("PatchOpsIII")) + .child( + div() + .id("check-updates") + .flex() + .items_center() + .gap(px(5.)) + .h(px(20.)) + .px(px(8.)) + .rounded(px(6.)) + .text_size(px(11.)) + .font_weight(FontWeight::BOLD) + .text_color(theme::muted()) + .when(!can_act, |this| this.opacity(0.7).cursor_not_allowed()) + .when(can_act, |this| { + this.cursor_pointer() + .hover(|style| style.bg(theme::white(0.07)).text_color(theme::text())) + .on_click(self.stage_click( + cx, + "Check for updates", + "/api/update-check", + json!({}), + )) + }) + .child(version) + .child(icon(Glyph::Refresh, 12., theme::muted())), + ) + .child(div().flex_1()) + .child(self.connection_badge()) .child( - Button::new(SharedString::from(format!("browse-{key}"))) - .label("Browse…") + Btn::new("reload-values", "Reload Values") + .tiny() + .disabled(self.busy || !self.connected) + .on_click(cx.listener(|view, _: &ClickEvent, window, cx| { + view.load_inputs(window, cx); + cx.notify(); + })), + ) + .child( + Btn::new("refresh", if self.busy { "Working…" } else { "Refresh" }) + .tiny() + .icon(Glyph::Refresh) .disabled(self.busy) - .on_click(cx.listener(move |view, _, window, cx| { - view.choose_directory(key, window, cx) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + view.message = "Connecting to the local service…".into(); + view.send("/api/status", None, cx); })), ) - .into_any_element() } - fn dashboard(&self, cx: &mut Context) -> AnyElement { - panel("Game installation") - .child(format!("Current directory: {}", self.value("/gameDir"))) - .child(format!( - "Platform: {} · Version: {}", - self.value("/platform"), - self.value("/appVersion") - )) - .child(format!( - "Executable: {} · {}", - self.value("/exeSwap/displayLabel"), - self.value("/exeSwap/integrityMessage") - )) - .child(self.folder_field("Black Ops III directory", "directory", cx)) + fn connection_badge(&self) -> impl IntoElement { + let (tone, label) = if self.connected { + (theme::ok(), "Connected") + } else { + (theme::danger(), "Offline") + }; + div() + .flex() + .items_center() + .gap(px(6.)) + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::BOLD) + .text_color(tone) + .child(div().size(px(8.)).rounded_full().bg(tone)) + .child(label) + } + + /// `.directory-row`. + fn directory_row(&self, cx: &mut Context) -> impl IntoElement { + let draft = self.text("directory", cx); + let draft = draft.trim(); + let dirty = !draft.is_empty() && draft != self.string("/gameDir"); + div() + .flex() + .items_end() + .gap(px(10.)) + .flex_none() .child( - Button::new("save-directory") - .label("Use this directory") - .primary() - .disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(|view, _, _, cx| { - let path = view.text("directory", cx); - if path.trim().is_empty() { - view.message = "Select or enter a directory first.".into(); - view.failed = true; - cx.notify(); - return; - } - view.stage( - format!("Use directory {path}"), - "/api/game-directory".into(), - json!({"path": path}), - cx, - ); - })), + div() + .flex() + .flex_col() + .flex_1() + .min_w_0() + .gap(px(6.)) + .child(components::field_label("Game Directory:")) + .child(self.field("directory")), ) - .child(div().mt_4().child("Launch profiles")) - .children( - self.state["launchProfiles"] - .as_array() - .into_iter() - .flatten() - .map(|profile| { - let label = profile["label"].as_str().unwrap_or("Profile").to_owned(); - let option = profile["option"].as_str().unwrap_or("").to_owned(); - let id = profile["id"].as_str().unwrap_or("profile").to_owned(); - div() - .flex() - .gap_3() - .items_center() - .child(self.action( - format!("profile-{id}"), - format!( - "{label}{}", - if profile["active"] == true { - " · Active" - } else { - "" - } - ), - "/api/launch-options", - json!({"options": option}), - cx, - )) - .when( - profile["subscribed"].is_boolean() - && id != "default" - && id != "offline", - |row| { - row.child(self.action( - format!("workshop-{id}"), - "Open Workshop".into(), - "/api/workshop-install", - json!({"profileId": id}), - cx, - )) - }, - ) - }), + .when(dirty, |row| { + row.child( + Btn::new("use-directory", "Use Directory") + .disabled(!self.can_act()) + .on_click( + cx.listener(|view, _: &ClickEvent, _, cx| view.stage_directory(cx)), + ), + ) + }) + .child( + Btn::new("browse-directory", "Browse...") + .icon(Glyph::FolderOpen) + .disabled(self.busy) + .on_click(cx.listener(|view, _: &ClickEvent, window, cx| { + view.choose_directory("directory", window, cx) + })), ) - .into_any_element() - } - - fn t7(&self, cx: &mut Context) -> AnyElement { - panel("T7 Patch") - .child(format!("Installed: {} · Current gamertag: {}", self.flag("/t7/installed"), self.value("/t7/plainName"))) - .child(div().flex().gap_3() - .child(self.action("t7-install".into(), "Install / update T7 Patch".into(), "/api/t7-install", json!({}), cx)) - .child(self.action("t7-uninstall".into(), "Uninstall T7 Patch".into(), "/api/t7-uninstall", json!({}), cx))) - .child(self.field("Gamertag", "gamertag")) - .child(self.field("Gamertag color", "color")) - .child(Button::new("save-gamertag").label("Save gamertag").disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(|view, _, _, cx| view.stage("Update T7 gamertag".into(), "/api/t7-config".into(), json!({"gamertag": view.text("gamertag", cx), "colorCode": view.text("color", cx)}), cx)))) - .child(self.field("Network password (blank clears it)", "password")) - .child(Button::new("save-password").label("Save network password").disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(|view, _, _, cx| view.stage("Update T7 network password".into(), "/api/t7-config".into(), json!({"networkPassword": view.text("password", cx)}), cx)))) - .child(self.toggle("Friends only", "/t7/friendsOnly", "/api/t7-config", "friendsOnly", cx)).into_any_element() - } - - fn exe(&self, cx: &mut Context) -> AnyElement { - panel("Executable profiles") - .child(self.value("/exeSwap/displayLabel")) - .child(self.value("/exeSwap/integrityMessage")) - .child(format!("SHA-256: {}", self.value("/exeSwap/executableHash"))) - .child("Compatible installs may require a Steam depot download. Instructions appear in the operation result below.") - .children([("compatible", "Install compatible build"), ("current", "Restore current Steam build"), ("enhanced", "Restore Enhanced build")].into_iter().map(|(id, label)| - self.action(format!("exe-{id}"), label.into(), &format!("/api/exe-swap/{id}"), json!({}), cx))).into_any_element() - } - - fn enhanced(&self, cx: &mut Context) -> AnyElement { - panel("BO3 Enhanced") - .child(format!( - "Installed: {} · Backup status: {}", - self.flag("/enhanced/installed"), - self.value("/enhanced/backupStatus") - )) - .child(self.folder_field("Enhanced source directory", "dump", cx)) - .children( - [ - ("validate", "Validate source"), - ("install", "Install Enhanced"), - ] - .into_iter() - .map(|(id, label)| { - Button::new(SharedString::from(format!("enhanced-{id}"))) - .label(label) - .disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(move |view, _, _, cx| { - view.stage( - label.into(), - format!("/api/enhanced-{id}"), - json!({"dumpSource": view.text("dump", cx)}), - cx, - ) - })) - }), + .child( + self.action(cx, "launch", "Launch Game", "/api/launch", json!({})) + .icon(Glyph::Play) + .primary(), ) - .child(self.action( - "enhanced-uninstall".into(), - "Uninstall Enhanced".into(), - "/api/enhanced-uninstall", - json!({}), - cx, - )) - .into_any_element() - } - - fn graphics(&self, cx: &mut Context) -> AnyElement { - panel("Graphics & performance") - .child(format!("Current: {} · {} Hz · FOV {} · FPS cap {}", self.value("/graphics/resolution"), self.value("/graphics/refreshRate"), self.value("/graphics/fov"), self.value("/graphics/maxFps"))) - .children(self.state["presets"].as_array().into_iter().flatten().filter_map(Value::as_str).map(|name| - self.action(format!("preset-{name}"), format!("Apply {name}"), "/api/presets/apply", json!({"name": name}), cx))) - .child(self.numeric_field("FPS cap (0 disables the limiter)", "fps", "MaxFPS", 0, 1000, cx)) - .child(self.numeric_field("Field of view", "fov", "FOV", 65, 120, cx)) - .child(self.numeric_field("Refresh rate", "refresh", "RefreshRate", 1, 1000, cx)) - .child(self.numeric_field("Render resolution %", "render", "ResolutionPercent", 50, 200, cx)) - .child(self.field("Resolution (WIDTHxHEIGHT)", "resolution")) - .child(Button::new("save-resolution").label("Apply resolution").disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(|view, _, _, cx| { - let value = view.text("resolution", cx); - let valid = value.split_once('x').is_some_and(|(w, h)| [w, h].iter().all(|n| n.parse::().is_ok_and(|n| (320..=16384).contains(&n)))); - if valid { view.stage(format!("Set resolution to {value}"), "/api/config".into(), json!({"key": "WindowSize", "value": value}), cx); } - else { view.message = "Enter resolution as WIDTHxHEIGHT, e.g. 1920x1080.".into(); view.failed = true; cx.notify(); } - }))) - .child(div().flex().gap_2().children([(0, "Windowed"), (1, "Fullscreen"), (2, "Borderless")].into_iter().map(|(mode, label)| self.action(format!("display-{mode}"), label.into(), "/api/config", json!({"key": "FullScreenMode", "value": mode}), cx)))) - .child(self.config_toggle("V-Sync", "/graphics/vsync", "Vsync", cx)) - .child(self.config_toggle("FPS counter", "/graphics/drawFps", "DrawFPS", cx)) - .child(self.config_toggle("Frame smoothing", "/advanced/smoothFramerate", "SmoothFramerate", cx)) - .child(self.action("unlock-graphics".into(), "Toggle hidden graphics options".into(), "/api/config", json!({"key": "RestrictGraphicsOptions", "value": if self.flag("/advanced/unlockOptions") {1} else {0}}), cx)) - .child(self.action("reduce-cpu".into(), "Toggle reduced CPU pressure".into(), "/api/config", json!({"key": "SerializeRender", "value": if self.flag("/advanced/reduceCpu") {0} else {2}}), cx)) - .child(self.numeric_field("Maximum frame latency", "latency", "MaxFrameLatency", 0, 4, cx)) - .child(self.field("VRAM target %", "vram")) - .child(Button::new("save-vram").label("Apply VRAM target").disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(|view, _, _, cx| { - match view.text("vram", cx).parse::() { - Ok(target) if (75..=100).contains(&target) => view.stage(format!("Set VRAM target to {target}%"), "/api/vram-target".into(), json!({"limited": target < 100, "target": target}), cx), - _ => { view.message = "VRAM target must be a whole number from 75 to 100.".into(); view.failed = true; cx.notify(); } - } - }))) - .child(self.toggle("Skip intro", "/qol/intro", "/api/intro-skip", "enabled", cx)) - .child(self.toggle("Skip all intros", "/qol/allIntros", "/api/all-intros-skip", "enabled", cx)) - .child(self.toggle("Modern DirectX compiler", "/qol/d3dcompiler", "/api/d3dcompiler", "enabled", cx)) - .child(self.toggle("Read-only config", "/advanced/configReadonly", "/api/config-readonly", "enabled", cx)).into_any_element() - } - - fn dxvk(&self, cx: &mut Context) -> AnyElement { - let settings = self - .state - .pointer("/dxvk/settings") - .cloned() - .unwrap_or(json!({})); - panel("DXVK-GPLAsync") - .child(format!("Installed: {}", self.flag("/dxvk/installed"))) - .child("Install uses the settings reported by the service, or backend defaults for a new installation.") - .child(format!("Current settings: {settings}")) - .child(self.action("dxvk-install".into(), "Install DXVK".into(), "/api/dxvk-install", settings.clone(), cx)) - .child(self.action("dxvk-uninstall".into(), "Uninstall DXVK".into(), "/api/dxvk-uninstall", json!({}), cx)) - .children([("Async compilation", "enableAsync"), ("GPL async cache", "gplAsyncCache"), ("DXVK HUD", "hudEnabled")].into_iter().map(|(label, key)| { - let mut next = settings.clone(); - next[key] = json!(!settings[key].as_bool().unwrap_or(false)); - self.action(format!("dxvk-{key}"), format!("Toggle {label}"), "/api/dxvk-config", next, cx) - })) - .children([("Compiler threads", "dxvk-threads", "numCompilerThreads", 64), ("FPS cap", "dxvk-fps", "maxFrameRate", 360), ("Frame latency", "dxvk-latency", "maxFrameLatency", 16)].into_iter().map(|(label, input, setting, max)| { - div().flex().items_end().gap_3().child(div().flex_1().child(self.field(label, input))) - .child(Button::new(SharedString::from(format!("apply-{input}"))).label("Apply").disabled(self.busy || !self.connected || self.pending.is_some()) - .on_click(cx.listener(move |view, _, _, cx| { - match view.text(input, cx).parse::() { - Ok(value) if value <= max => { let mut settings = view.state["dxvk"]["settings"].clone(); settings[setting] = json!(value); view.stage(format!("Set DXVK {label} to {value}"), "/api/dxvk-config".into(), settings, cx); } - _ => { view.message = format!("Enter a whole number from 0 to {max}."); view.failed = true; cx.notify(); } - } - }))) - })) - .children(["True", "False", "Auto"].into_iter().map(|mode| { let mut next = settings.clone(); next["tearFree"] = json!(mode); self.action(format!("tearfree-{mode}"), format!("Tear-free: {mode}"), "/api/dxvk-config", next, cx) })).into_any_element() - } - - fn tools(&self, cx: &mut Context) -> AnyElement { - panel("Maintenance") - .child(format!("Log file: {}", self.value("/logPath"))) - .children( - [ - ("/api/logs/clear", "Clear logs"), - ("/api/mod-files/clear", "Clear cached mod files"), - ("/api/reset-stock", "Reset game to stock"), - ("/api/update-check", "Check for updates"), - ] - .into_iter() - .map(|(path, label)| self.action(path.into(), label.into(), path, json!({}), cx)), + } + + /// `.error-strip`. + fn error_strip(&self, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .items_center() + .flex_none() + .gap(px(8.)) + .min_h(px(34.)) + .px(px(12.)) + .py(px(6.)) + .border_1() + .border_color(theme::danger_alpha(0.38)) + .rounded(px(9.)) + .bg(theme::danger_alpha(0.12)) + .text_color(theme::danger_text()) + .child(icon(Glyph::Alert, 16., theme::danger_text())) + .child(div().flex_1().min_w_0().child(self.message.clone())) + .child( + div() + .id("dismiss-error") + .flex() + .items_center() + .justify_center() + .size(px(26.)) + .rounded(px(7.)) + .cursor_pointer() + .hover(|style| style.bg(theme::white(0.12))) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + view.failed = false; + cx.notify(); + })) + .child(icon(Glyph::Close, 15., theme::danger_text())), ) - .child(self.action( - "channel-stable".into(), - "Use stable channel".into(), - "/api/release-channel", - json!({"channel": "stable"}), - cx, - )) - .child(self.action( - "channel-beta".into(), - "Use beta channel".into(), - "/api/release-channel", - json!({"channel": "beta"}), - cx, - )) - .into_any_element() } -} -fn panel(title: &str) -> Div { - div() - .flex() - .flex_col() - .gap_4() - .p_6() - .rounded_lg() - .border_1() - .border_color(rgb(0x343438)) - .bg(rgb(0x17171b)) - .child( - div() - .text_xl() - .font_weight(FontWeight::BOLD) - .child(title.to_owned()), - ) -} + /// `.nav-panel`. + fn nav(&self, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .flex_none() + .gap(px(7.)) + .w(px(theme::NAV_WIDTH)) + .p(px(10.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius()) + .bg(theme::white(0.045)) + .children(Page::ALL.into_iter().enumerate().map(|(index, page)| { + let active = self.page == page; + let color = if active { + theme::text() + } else { + theme::muted_strong() + }; + div() + .id(("nav", index)) + .flex() + .items_center() + .gap(px(10.)) + .min_h(px(theme::ROW_HEIGHT)) + .px(px(12.)) + .border_1() + .border_color(if active { + theme::accent_alpha(0.22) + } else { + theme::white(0.) + }) + .rounded(theme::radius_control()) + .bg(if active { + linear_gradient( + 135., + linear_color_stop(theme::accent_alpha(0.22), 0.), + linear_color_stop(theme::white(0.07), 1.), + ) + } else { + solid_background(theme::white(0.)) + }) + .text_size(px(theme::FONT_MD)) + .font_weight(FontWeight::BOLD) + .text_color(color) + .cursor_pointer() + .when(!active, |this| { + this.hover(|style| style.bg(theme::white(0.06)).text_color(theme::text())) + }) + .on_click(cx.listener(move |view, _: &ClickEvent, _, cx| { + view.page = page; + view.content_scroll.set_offset(point(px(0.), px(0.))); + cx.notify(); + })) + .child(icon(page.glyph(), 18., color)) + .child(div().truncate().child(page.label())) + })) + } -impl Render for ControlCenter { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let content = match self.page { - 0 => self.dashboard(cx), - 1 => self.t7(cx), - 2 => self.exe(cx), - 3 => self.enhanced(cx), - 4 => self.graphics(cx), - 5 => self.dxvk(cx), - _ => self.tools(cx), + /// `.log-panel`: the last lines of the backend's activity log. + fn log_panel(&self, cx: &mut Context) -> impl IntoElement { + let entries = self.visible_logs(); + let status_color = if self.failed { + theme::warning() + } else { + theme::muted() }; - let confirmation = self.pending.as_ref().map(|(label, _)| { - panel("Confirm operation") - .child(label.clone()) - .child(format!("Target: {}", self.value("/gameDir"))) - .child( - div() - .flex() - .gap_3() - .child(Button::new("confirm").label("Confirm").primary().on_click( - cx.listener(|view, _, _, cx| { - if let Some((label, request)) = view.pending.take() { - view.message = format!("Running: {label}…"); - view.failed = false; - view.send(&request.path, request.body, cx); - } - }), - )) - .child(Button::new("cancel").label("Cancel").on_click(cx.listener( - |view, _, _, cx| { - view.pending = None; - cx.notify(); - }, - ))), - ) - .into_any_element() - }); div() .flex() - .size_full() - .bg(rgb(0x0d0d0f)) - .text_color(rgb(0xf6f6f6)) + .flex_col() + .flex_none() + .h(px(180.)) .child( div() .flex() - .flex_col() - .w(px(200.)) - .p_4() - .gap_3() - .border_r_1() - .border_color(rgb(0x343438)) + .items_baseline() + .justify_between() + .gap(px(12.)) + .mb(px(8.)) .child( div() - .text_xl() + .text_size(px(theme::FONT_LG)) .font_weight(FontWeight::BOLD) - .child("PATCHOPS III"), + .child("Activity Log"), ) .child( div() - .text_sm() - .text_color(rgb(0xff4545)) - .child("GPUI evaluation"), - ) - .children(PAGES.into_iter().enumerate().map(|(index, label)| { - Button::new(SharedString::from(format!("nav-{index}"))) - .label(label) - .when(index == self.page, |button| button.primary()) - .on_click(cx.listener(move |view, _, _, cx| { - view.page = index; - cx.notify(); - })) - })), + .min_w_0() + .truncate() + .text_size(px(theme::FONT_XS)) + .text_color(status_color) + .child(self.message.clone()), + ), ) .child( div() - .flex() - .flex_col() .flex_1() - .min_w_0() - .p_5() - .gap_4() + .min_h_0() + .p(px(10.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius()) + .bg(theme::panel()) .child( div() - .flex() - .items_center() - .justify_between() - .gap_3() + .relative() + .size_full() + .border_1() + .border_color(theme::white(0.)) + .rounded(px(9.)) + .bg(theme::bg_deep().alpha(0.5)) .child( div() - .text_2xl() - .font_weight(FontWeight::BOLD) - .child(PAGES[self.page]), + .id("log-scroll") + .size_full() + .overflow_y_scroll() + .track_scroll(&self.log_scroll) + .px(px(10.)) + .py(px(8.)) + .font_family(cx.theme().mono_font_family.clone()) + .text_size(px(theme::FONT_XS)) + .when(entries.is_empty(), |this| { + this.text_color(theme::muted()).child("No activity yet.") + }) + .children(entries.into_iter().map(|(category, message)| { + let tone = match category.as_str() { + "Success" => theme::ok(), + "Warning" => theme::warning(), + "Error" => theme::danger(), + _ => theme::muted(), + }; + div() + .flex() + .gap(px(10.)) + .mb(px(4.)) + .text_color(theme::text().alpha(0.76)) + .child( + div() + .w(px(72.)) + .flex_none() + .font_weight(FontWeight::BOLD) + .text_color(tone) + .child(category), + ) + .child(div().flex_1().min_w_0().child(message)) + })), ) - .child( - div() - .flex() - .gap_2() - .child( - Button::new("load-settings") - .label("Load current values") - .disabled(self.busy || !self.connected) - .on_click(cx.listener(|view, _, window, cx| { - view.load_inputs(window, cx) - })), - ) - .child( - Button::new("refresh") - .label(if self.busy { "Working…" } else { "Refresh" }) - .disabled(self.busy) - .on_click(cx.listener(|view, _, _, cx| { - view.message = - "Connecting to the local service…".into(); - view.send("/api/status", None, cx); - })), - ) - .child(self.action( - "launch".into(), - "Launch game".into(), - "/api/launch", - json!({}), - cx, - )), - ), - ) - .children(confirmation) - .child( - div() - .id("content-scroll") - .flex_1() - .min_h_0() - .overflow_y_scroll() - .flex() - .flex_col() - .gap_4() - .child(content), - ) + .vertical_scrollbar(&self.log_scroll), + ), + ) + } + + /// `.startup-screen`, shown until the first status document arrives. + fn startup_screen(&self, cx: &mut Context) -> impl IntoElement { + let failed = self.failed; + div() + .flex() + .flex_col() + .flex_1() + .items_center() + .justify_center() + .gap(px(14.)) + .child(img("images/logo.png").size(px(64.)).rounded(px(14.))) + .child( + div() + .flex() + .flex_col() + .items_center() + .gap(px(5.)) + .max_w(px(360.)) + .text_center() .child( div() - .text_sm() - .text_color(if self.failed { - rgb(0xff9f0a) + .text_size(px(theme::FONT_LG)) + .font_weight(FontWeight::BOLD) + .child(if failed { + "Startup took too long" } else { - rgb(0xb6b6bb) - }) - .child(self.message.clone()), + "Opening PatchOpsIII" + }), ) .child( div() - .id("log-scroll") - .h(px(130.)) - .overflow_y_scroll() - .bg(rgb(0x050505)) - .rounded_lg() - .p_3() - .text_sm() - .children( - self.state["logs"] - .as_array() - .into_iter() - .flatten() - .rev() - .take(80) - .map(|entry| { - div() - .text_color(if entry["category"] == "Error" { - rgb(0xff4545) - } else { - rgb(0xb6b6bb) - }) - .child(entry["line"].as_str().unwrap_or("").to_owned()) - }), - ), + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted()) + .child(if failed { + self.message.clone() + } else { + "Loading your settings...".to_owned() + }), ), ) + .when(!failed, |this| { + this.child( + div() + .w(px(132.)) + .h(px(3.)) + .rounded_full() + .bg(theme::white(0.1)) + .child(div().w(px(55.)).h_full().rounded_full().bg(theme::accent())), + ) + }) + .when(failed, |this| { + this.child( + Btn::new("retry", "Try Again") + .compact() + .icon(Glyph::Refresh) + .disabled(self.busy) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + view.message = "Connecting to the local service…".into(); + view.failed = false; + view.send("/api/status", None, cx); + })), + ) + }) + } +} + +impl Render for ControlCenter { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let ready = !self.state.is_null(); + let body = if ready { + div() + .flex() + .flex_col() + .flex_1() + .min_h_0() + .gap(px(theme::APP_GAP)) + .child(self.directory_row(cx)) + .when(self.failed, |this| this.child(self.error_strip(cx))) + .child( + div() + .flex() + .flex_col() + .flex_1() + .min_h_0() + .gap(px(theme::PANEL_GAP)) + .child( + div() + .flex() + .flex_1() + .min_h_0() + .gap(px(theme::PANEL_GAP)) + .child(self.nav(cx)) + .child( + div() + .relative() + .flex_1() + .min_w_0() + .child( + div() + .id("content-scroll") + .size_full() + .overflow_y_scroll() + .track_scroll(&self.content_scroll) + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .pr(px(8.)) + .child(self.page_view(cx)), + ) + .vertical_scrollbar(&self.content_scroll), + ), + ) + .child(self.log_panel(cx)), + ) + } else { + div() + .flex() + .flex_col() + .flex_1() + .min_h_0() + .child(self.startup_screen(cx)) + }; + div() + .relative() + .flex() + .flex_col() + .size_full() + .bg(linear_gradient( + 180., + linear_color_stop(theme::bg_deep(), 0.), + linear_color_stop(theme::bg(), 1.), + )) + .font(theme::ui_font()) + .text_color(theme::text()) + .text_size(px(theme::FONT_MD)) + .child(self.header(cx)) + .child( + div() + .flex() + .flex_col() + .flex_1() + .min_h_0() + .p(px(theme::APP_PAD)) + .child(body), + ) + .children(self.confirm_modal(cx)) + .children(self.depot_modal(cx)) } } diff --git a/native/gpui/src/ui/assets.rs b/native/gpui/src/ui/assets.rs new file mode 100644 index 0000000..06c70cb --- /dev/null +++ b/native/gpui/src/ui/assets.rs @@ -0,0 +1,135 @@ +//! Embedded assets: Lucide icons (the same set the Electron UI uses) and the app logo. +use anyhow::Result; +use gpui::{AssetSource, SharedString}; +use std::borrow::Cow; + +const ASSETS: &[(&str, &[u8])] = &[ + ( + "icons/check.svg", + include_bytes!("../../assets/icons/check.svg"), + ), + ( + "icons/chevron-down.svg", + include_bytes!("../../assets/icons/chevron-down.svg"), + ), + ( + "icons/chevron-right.svg", + include_bytes!("../../assets/icons/chevron-right.svg"), + ), + ( + "icons/chevron-up.svg", + include_bytes!("../../assets/icons/chevron-up.svg"), + ), + ( + "icons/circle-check.svg", + include_bytes!("../../assets/icons/circle-check.svg"), + ), + ( + "icons/clipboard.svg", + include_bytes!("../../assets/icons/clipboard.svg"), + ), + ( + "icons/close.svg", + include_bytes!("../../assets/icons/close.svg"), + ), + ( + "icons/cpu.svg", + include_bytes!("../../assets/icons/cpu.svg"), + ), + ( + "icons/download.svg", + include_bytes!("../../assets/icons/download.svg"), + ), + ( + "icons/eraser.svg", + include_bytes!("../../assets/icons/eraser.svg"), + ), + ( + "icons/external-link.svg", + include_bytes!("../../assets/icons/external-link.svg"), + ), + ( + "icons/eye.svg", + include_bytes!("../../assets/icons/eye.svg"), + ), + ( + "icons/eye-off.svg", + include_bytes!("../../assets/icons/eye-off.svg"), + ), + ( + "icons/folder-open.svg", + include_bytes!("../../assets/icons/folder-open.svg"), + ), + ( + "icons/gem.svg", + include_bytes!("../../assets/icons/gem.svg"), + ), + ( + "icons/image.svg", + include_bytes!("../../assets/icons/image.svg"), + ), + ( + "icons/key-round.svg", + include_bytes!("../../assets/icons/key-round.svg"), + ), + ( + "icons/layout-dashboard.svg", + include_bytes!("../../assets/icons/layout-dashboard.svg"), + ), + ( + "icons/refresh-cw.svg", + include_bytes!("../../assets/icons/refresh-cw.svg"), + ), + ( + "icons/rotate-ccw.svg", + include_bytes!("../../assets/icons/rotate-ccw.svg"), + ), + ( + "icons/save.svg", + include_bytes!("../../assets/icons/save.svg"), + ), + ( + "icons/shield-check.svg", + include_bytes!("../../assets/icons/shield-check.svg"), + ), + ( + "icons/square-play.svg", + include_bytes!("../../assets/icons/square-play.svg"), + ), + ( + "icons/trash-2.svg", + include_bytes!("../../assets/icons/trash-2.svg"), + ), + ( + "icons/triangle-alert.svg", + include_bytes!("../../assets/icons/triangle-alert.svg"), + ), + ( + "icons/wrench.svg", + include_bytes!("../../assets/icons/wrench.svg"), + ), + ("icons/x.svg", include_bytes!("../../assets/icons/x.svg")), + ( + "images/logo.png", + include_bytes!("../../assets/images/logo.png"), + ), +]; + +pub struct Assets; + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result>> { + Ok(ASSETS + .iter() + .find(|(name, _)| *name == path) + .map(|(_, bytes)| Cow::Borrowed(*bytes))) + } + + fn list(&self, path: &str) -> Result> { + Ok(ASSETS + .iter() + .filter(|(name, _)| name.starts_with(path)) + .map(|(name, _)| SharedString::from(*name)) + .collect()) + } +} diff --git a/native/gpui/src/ui/components.rs b/native/gpui/src/ui/components.rs new file mode 100644 index 0000000..745dc9c --- /dev/null +++ b/native/gpui/src/ui/components.rs @@ -0,0 +1,843 @@ +//! Small building blocks that reproduce the Electron app's controls +//! (`.tool-button`, `.panel`, `.status-row`, `.toggle`, `.check-row`, ...). +use super::theme; +use gpui::{prelude::*, *}; + +pub type ClickHandler = Box; + +pub fn handler(f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> ClickHandler { + Box::new(f) +} + +/// Lucide icons bundled in `assets/icons` (see `assets.rs`). +#[derive(Clone, Copy)] +pub enum Glyph { + Alert, + Check, + CheckCircle, + ChevronDown, + ChevronRight, + Clipboard, + Close, + Cpu, + Dashboard, + Download, + Eraser, + ExternalLink, + EyeOff, + FolderOpen, + Gem, + Image, + Key, + Play, + Refresh, + Rotate, + Save, + Shield, + Trash, + Wrench, +} + +impl Glyph { + fn path(self) -> &'static str { + match self { + Self::Alert => "icons/triangle-alert.svg", + Self::Check => "icons/check.svg", + Self::CheckCircle => "icons/circle-check.svg", + Self::ChevronDown => "icons/chevron-down.svg", + Self::ChevronRight => "icons/chevron-right.svg", + Self::Clipboard => "icons/clipboard.svg", + Self::Close => "icons/x.svg", + Self::Cpu => "icons/cpu.svg", + Self::Dashboard => "icons/layout-dashboard.svg", + Self::Download => "icons/download.svg", + Self::Eraser => "icons/eraser.svg", + Self::ExternalLink => "icons/external-link.svg", + Self::EyeOff => "icons/eye-off.svg", + Self::FolderOpen => "icons/folder-open.svg", + Self::Gem => "icons/gem.svg", + Self::Image => "icons/image.svg", + Self::Key => "icons/key-round.svg", + Self::Play => "icons/square-play.svg", + Self::Refresh => "icons/refresh-cw.svg", + Self::Rotate => "icons/rotate-ccw.svg", + Self::Save => "icons/save.svg", + Self::Shield => "icons/shield-check.svg", + Self::Trash => "icons/trash-2.svg", + Self::Wrench => "icons/wrench.svg", + } + } +} + +pub fn icon(glyph: Glyph, size: f32, color: Hsla) -> Svg { + svg() + .path(glyph.path()) + .size(px(size)) + .flex_none() + .text_color(color) +} + +pub fn text_xs(label: impl Into, color: Hsla) -> Div { + div() + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(color) + .child(label.into()) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Tone { + Plain, + Primary, + Danger, +} + +/// `.tool-button` / `.small-button`. +#[derive(IntoElement)] +pub struct Btn { + id: ElementId, + label: SharedString, + glyph: Option, + tone: Tone, + compact: bool, + tiny: bool, + grow: bool, + disabled: bool, + on_click: Option, +} + +impl Btn { + pub fn new(id: impl Into, label: impl Into) -> Self { + Self { + id: id.into(), + label: label.into(), + glyph: None, + tone: Tone::Plain, + compact: false, + tiny: false, + grow: false, + disabled: false, + on_click: None, + } + } + + pub fn icon(mut self, glyph: Glyph) -> Self { + self.glyph = Some(glyph); + self + } + + pub fn primary(mut self) -> Self { + self.tone = Tone::Primary; + self + } + + pub fn danger(mut self) -> Self { + self.tone = Tone::Danger; + self + } + + /// The shorter `.small-button` variant. + pub fn compact(mut self) -> Self { + self.compact = true; + self + } + + /// The 24px variant used in the title bar. + pub fn tiny(mut self) -> Self { + self.tiny = true; + self + } + + /// Share the row evenly with sibling buttons. + pub fn grow(mut self) -> Self { + self.grow = true; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl RenderOnce for Btn { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + let (fill, border, hover_fill, hover_border, color) = match self.tone { + Tone::Primary => ( + linear_gradient( + 135., + linear_color_stop(theme::accent(), 0.), + linear_color_stop(theme::accent_dark(), 1.), + ), + theme::accent_alpha(0.42), + theme::accent(), + theme::accent_alpha(0.7), + theme::text(), + ), + Tone::Danger => ( + solid_background(theme::danger_alpha(0.12)), + theme::danger_alpha(0.34), + theme::danger_alpha(0.2), + theme::danger_alpha(0.5), + theme::danger_text(), + ), + Tone::Plain => ( + solid_background(theme::panel_strong()), + theme::border_strong(), + theme::white(0.12), + theme::white(0.24), + theme::text(), + ), + }; + let enabled = !self.disabled; + let handler = self.on_click.filter(|_| enabled); + div() + .id(self.id) + .flex() + .items_center() + .justify_center() + .gap(px(7.)) + .min_w_0() + .min_h(px(if self.tiny { + 24. + } else if self.compact { + 34. + } else { + theme::CONTROL_HEIGHT + })) + .px(px(if self.tiny { + 8. + } else if self.compact { + 10. + } else { + 14. + })) + .border_1() + .border_color(border) + .rounded(theme::radius_control()) + .bg(fill) + .text_color(color) + .text_size(px(if self.tiny { + theme::FONT_XS + } else if self.compact { + theme::FONT_SM + } else { + theme::FONT_MD + })) + .font_weight(FontWeight::BOLD) + .when(self.grow, |this| this.flex_1()) + .when_some(self.glyph, |this, glyph| { + this.child(icon(glyph, if self.compact { 15. } else { 16. }, color)) + }) + .child(self.label) + .when(!enabled, |this| this.opacity(0.62).cursor_not_allowed()) + .when(enabled, |this| { + this.cursor_pointer() + .hover(|style| style.bg(hover_fill).border_color(hover_border)) + }) + .when_some(handler, |this, handler| this.on_click(handler)) + } +} + +/// `.icon-action`: a square icon-only button. +pub fn icon_button( + id: impl Into, + glyph: Glyph, + enabled: bool, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, +) -> Stateful
{ + div() + .id(id) + .flex() + .items_center() + .justify_center() + .flex_none() + .size(px(theme::CONTROL_HEIGHT)) + .border_1() + .border_color(theme::border_strong()) + .rounded(theme::radius_control()) + .bg(theme::panel_strong()) + .child(icon(glyph, 15., theme::text())) + .when(!enabled, |this| this.opacity(0.62).cursor_not_allowed()) + .when(enabled, |this| { + this.cursor_pointer() + .hover(|style| style.bg(theme::white(0.12))) + .on_click(on_click) + }) +} + +/// `.panel`: a 16px heading above a bordered body. +pub fn panel(title: impl Into, body: impl IntoElement) -> Div { + div() + .flex() + .flex_col() + .flex_none() + .min_w_0() + .child( + div() + .mb(px(8.)) + .text_size(px(theme::FONT_LG)) + .font_weight(FontWeight::BOLD) + .child(title.into()), + ) + .child( + div() + .flex() + .flex_col() + .min_w_0() + .border_1() + .border_color(theme::border()) + .rounded(theme::radius()) + .bg(theme::panel()) + .overflow_hidden() + .child(body), + ) +} + +/// A heading above a body that has no surrounding chrome (the EXE swapper, +/// Enhanced, Graphics and DXVK panels draw their own cards). +pub fn bare_panel(title: impl Into, body: impl IntoElement) -> Div { + div() + .flex() + .flex_col() + .flex_none() + .min_w_0() + .child( + div() + .mb(px(8.)) + .text_size(px(theme::FONT_LG)) + .font_weight(FontWeight::BOLD) + .child(title.into()), + ) + .child(body) +} + +/// `.enhanced-card` / `.tool-card` / `.graphics-settings-group`. +pub fn card() -> Div { + div() + .flex() + .flex_col() + .gap(px(10.)) + .min_w_0() + .p(px(12.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.025)) +} + +pub fn card_title(glyph: Option, title: impl Into) -> Div { + div() + .flex() + .items_center() + .gap(px(9.)) + .text_size(px(theme::FONT_MD)) + .font_weight(FontWeight::BOLD) + .when_some(glyph, |this, glyph| { + this.child(icon(glyph, 16., theme::muted())) + }) + .child(title.into()) +} + +/// A row of equally sized columns that wraps when the window is narrow, +/// approximating `grid-template-columns: repeat(auto-fit, minmax(..))`. +pub fn columns() -> Div { + div().flex().flex_wrap().gap(px(theme::PANEL_GAP)).w_full() +} + +/// A flex child that participates in [`columns`]. +pub fn column(min_width: f32) -> Div { + div().flex().flex_col().flex_1().min_w(px(min_width)) +} + +pub fn hairline() -> Div { + div().h(px(1.)).w_full().flex_none().bg(theme::border()) +} + +/// `.toggle`: the 52x28 pill switch. +pub fn switch( + id: impl Into, + on: bool, + enabled: bool, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, +) -> Stateful
{ + div() + .id(id) + .relative() + .flex_none() + .w(px(52.)) + .h(px(28.)) + .rounded_full() + .border_1() + .border_color(if on { + theme::ok_alpha(0.38) + } else { + theme::border_strong() + }) + .bg(if on { + theme::ok_alpha(0.16) + } else { + theme::field() + }) + .child( + div() + .absolute() + .top(px(4.)) + .left(px(if on { 28. } else { 4. })) + .size(px(18.)) + .rounded_full() + .bg(if on { theme::ok() } else { theme::muted() }), + ) + .when(!enabled, |this| this.opacity(0.62).cursor_not_allowed()) + .when(enabled, |this| this.cursor_pointer().on_click(on_click)) +} + +fn check_box(checked: bool) -> Div { + div() + .flex() + .items_center() + .justify_center() + .flex_none() + .size(px(18.)) + .rounded(px(4.)) + .border_1() + .border_color(if checked { + theme::accent() + } else { + theme::white(0.5) + }) + .bg(if checked { + theme::accent() + } else { + theme::field() + }) + .when(checked, |this| { + this.child(icon(Glyph::Check, 14., theme::white(1.))) + }) +} + +/// `.check-row`. +pub fn check_row( + id: impl Into, + label: impl Into, + checked: bool, + enabled: bool, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, +) -> Stateful
{ + div() + .id(id) + .flex() + .items_center() + .gap(px(10.)) + .min_h(px(34.)) + .min_w_0() + .text_size(px(theme::FONT_MD)) + .font_weight(FontWeight::MEDIUM) + .child(check_box(checked)) + .child(div().flex_1().min_w_0().child(label.into())) + .when(!enabled, |this| this.opacity(0.62).cursor_not_allowed()) + .when(enabled, |this| this.cursor_pointer().on_click(on_click)) +} + +/// `.radio-row`, with the optional trailing state label (`.profile-state`). +pub fn radio_row( + id: impl Into, + label: impl Into, + selected: bool, + trailing: Option<(SharedString, Hsla)>, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, +) -> Stateful
{ + div() + .id(id) + .flex() + .items_center() + .gap(px(10.)) + .min_h(px(32.)) + .min_w_0() + .border_b_1() + .border_color(theme::border()) + .text_size(px(theme::FONT_MD)) + .font_weight(FontWeight::MEDIUM) + .cursor_pointer() + .on_click(on_click) + .child( + div() + .flex() + .items_center() + .justify_center() + .flex_none() + .size(px(18.)) + .rounded_full() + .border_1() + .border_color(if selected { + theme::accent() + } else { + theme::white(0.5) + }) + .bg(theme::field()) + .when(selected, |this| { + this.child(div().size(px(10.)).rounded_full().bg(theme::accent())) + }), + ) + .child(div().flex_1().min_w_0().child(label.into())) + .when_some(trailing, |this, (state, color)| { + this.child( + div() + .ml_auto() + .min_w_0() + .truncate() + .text_size(px(theme::FONT_SM)) + .text_color(color) + .child(state), + ) + }) +} + +/// `.status-row`: a label with a coloured status dot on the right. +pub fn status_row(label: &str, ok: bool, detail: Option<&str>, last: bool) -> Div { + let shown = if ok { + detail.unwrap_or("Configured") + } else { + "Not configured" + }; + let tone = if ok { theme::ok() } else { theme::muted() }; + div() + .flex() + .items_center() + .justify_between() + .gap(px(12.)) + .min_h(px(34.)) + .text_size(px(theme::FONT_MD)) + .text_color(theme::muted_strong()) + .when(!last, |this| { + this.border_b_1().border_color(theme::border()) + }) + .child(div().flex_1().min_w_0().child(label.to_owned())) + .child( + div() + .flex() + .items_center() + .gap(px(7.)) + .min_w_0() + .font_weight(FontWeight::BOLD) + .text_color(tone) + .child(div().size(px(9.)).flex_none().rounded_full().bg(if ok { + theme::ok() + } else { + theme::dot_idle() + })) + .child(div().truncate().child(shown.to_owned())), + ) +} + +/// `.module-row`: icon, title and a configured/not configured label. +pub fn module_row(glyph: Glyph, title: &str, active: bool) -> Div { + div() + .flex() + .items_center() + .gap(px(12.)) + .px(px(14.)) + .min_h(px(46.)) + .text_size(px(theme::FONT_MD)) + .child(icon(glyph, 22., theme::text())) + .child(div().flex_1().min_w_0().child(title.to_owned())) + .child( + div() + .font_weight(FontWeight::BOLD) + .text_color(if active { theme::ok() } else { theme::muted() }) + .child(if active { + "Configured" + } else { + "Not configured" + }), + ) +} + +/// `.status-pill`: a small label above a value inside a bordered box. +pub fn status_pill( + label: impl Into, + value: impl Into, + ok: bool, +) -> Div { + div() + .flex() + .flex_col() + .justify_center() + .gap(px(4.)) + .flex_1() + .min_w_0() + .min_h(px(56.)) + .px(px(14.)) + .py(px(8.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_control()) + .bg(theme::white(0.035)) + .child(text_xs(label, theme::muted()).truncate()) + .child( + div() + .truncate() + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::BOLD) + .text_color(if ok { + theme::ok() + } else { + theme::muted_strong() + }) + .child(value.into()), + ) +} + +/// The borderless label-left / value-right variant used inside the T7 +/// overview and DXVK control bars. +pub fn inline_pill( + label: impl Into, + value: impl Into, + ok: bool, +) -> Div { + div() + .flex() + .items_center() + .justify_between() + .gap(px(12.)) + .min_w_0() + .px(px(14.)) + .min_h(px(54.)) + .child(text_xs(label, theme::muted()).truncate()) + .child( + div() + .truncate() + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::BOLD) + .text_color(if ok { + theme::ok() + } else { + theme::muted_strong() + }) + .child(value.into()), + ) +} + +/// `.enhanced-metric` / `.t7-state-grid > div`: small label, value below. +pub fn metric(label: impl Into, value: impl Into, tone: Hsla) -> Div { + div() + .flex() + .flex_col() + .gap(px(4.)) + .flex_1() + .min_w_0() + .child(text_xs(label, theme::muted())) + .child( + div() + .truncate() + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(tone) + .child(value.into()), + ) +} + +/// `.t7-state-grid > div`: a [`metric`] inside its own bordered box. +pub fn boxed_metric( + label: impl Into, + value: impl Into, + tone: Hsla, +) -> Div { + metric(label, value, tone) + .p(px(10.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.026)) +} + +/// `.tool-metric`: a muted label with its value on the same line. +pub fn tool_metric(label: &str, value: impl Into, ok: bool) -> Div { + div() + .flex() + .items_baseline() + .gap(px(10.)) + .min_w_0() + .text_size(px(theme::FONT_SM)) + .child( + div() + .w(px(96.)) + .flex_none() + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted()) + .child(label.to_owned()), + ) + .child( + div() + .flex_1() + .min_w_0() + .truncate() + .font_weight(FontWeight::BOLD) + .text_color(if ok { + gpui::rgb(0x24ff66).into() + } else { + theme::text() + }) + .child(value.into()), + ) +} + +/// `.segmented-control` / `.subtab-bar`: mutually exclusive options. +pub fn segmented(items: Vec<(SharedString, bool, ClickHandler)>, enabled: bool) -> Div { + div() + .flex() + .gap(px(4.)) + .p(px(4.)) + .min_w_0() + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_control()) + .bg(theme::white(0.035)) + .children( + items + .into_iter() + .enumerate() + .map(|(index, (label, active, handler))| { + div() + .id(("segment", index)) + .flex() + .items_center() + .justify_center() + .flex_1() + .min_w_0() + .min_h(px(30.)) + .px(px(10.)) + .rounded(px(5.)) + .border_1() + .border_color(if active { + theme::accent_alpha(0.28) + } else { + theme::white(0.) + }) + .bg(if active { + theme::accent_alpha(0.16) + } else { + theme::white(0.) + }) + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::BOLD) + .text_color(if active { + theme::text() + } else { + theme::muted_strong() + }) + .child(label) + .when(!enabled, |this| this.opacity(0.62).cursor_not_allowed()) + .when(enabled, |this| { + this.cursor_pointer() + .hover(|style| style.bg(theme::white(0.06))) + .on_click(handler) + }) + }), + ) +} + +/// `.color-chip` / `.recommended-chip`. +pub fn chip( + id: impl Into, + label: impl Into, + swatch: Option, + active: bool, + enabled: bool, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, +) -> Stateful
{ + div() + .id(id) + .flex() + .items_center() + .gap(px(5.)) + .min_w_0() + .min_h(px(32.)) + .px(px(6.)) + .border_1() + .border_color(if active { + theme::accent_alpha(0.42) + } else { + theme::border() + }) + .rounded(theme::radius_card()) + .bg(if active { + theme::accent_alpha(0.14) + } else { + theme::white(0.04) + }) + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::BOLD) + .text_color(if active { + theme::text() + } else { + theme::muted_strong() + }) + .when_some(swatch, |this, color| { + this.child( + div() + .size(px(12.)) + .flex_none() + .rounded_full() + .border_1() + .border_color(theme::white(0.34)) + .bg(color), + ) + }) + .child(div().truncate().child(label.into())) + .when(!enabled, |this| this.opacity(0.62).cursor_not_allowed()) + .when(enabled, |this| { + this.cursor_pointer() + .hover(|style| style.bg(theme::accent_alpha(0.1))) + .on_click(on_click) + }) +} + +/// A dimmed uppercase-free field caption (`.field-grid label`). +pub fn field_label(label: impl Into) -> Div { + div() + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted()) + .child(label.into()) +} + +/// The scrim and centring wrapper used by modal dialogs. +pub fn modal(content: impl IntoElement) -> Div { + div() + .absolute() + .top_0() + .left_0() + .size_full() + .flex() + .items_center() + .justify_center() + .p(px(28.)) + .bg(theme::scrim()) + .occlude() + .child(content) +} + +/// `.modal` surface: rounded, red glow in the corner approximated by a border. +pub fn modal_surface() -> Div { + div() + .flex() + .flex_col() + .gap(px(14.)) + .w(px(520.)) + .max_w_full() + .p(px(20.)) + .border_1() + .border_color(theme::border_strong()) + .rounded(px(16.)) + .bg(theme::modal_surface()) + .shadow_lg() +} diff --git a/native/gpui/src/ui/dashboard.rs b/native/gpui/src/ui/dashboard.rs new file mode 100644 index 0000000..c9f2e51 --- /dev/null +++ b/native/gpui/src/ui/dashboard.rs @@ -0,0 +1,269 @@ +//! Dashboard: status overview, quality-of-life options and launch options. +use super::components::{Btn, check_row, column, columns, hairline, panel, radio_row, status_row}; +use super::{ControlCenter, theme}; +use crate::backend::Request; +use gpui::{prelude::*, *}; +use serde_json::{Value, json}; + +impl ControlCenter { + fn active_launch_label(&self) -> Option { + match self.string("/activeLaunchProfile").as_str() { + "default" | "" => None, + "custom" => Some("Custom".into()), + id => self.state["launchProfiles"] + .as_array() + .into_iter() + .flatten() + .find(|profile| profile["id"] == id) + .and_then(|profile| profile["label"].as_str()) + .map(str::to_owned), + } + } + + fn status_overview(&self) -> Div { + let qol_any = + self.flag("/qol/d3dcompiler") || self.flag("/qol/intro") || self.flag("/qol/allIntros"); + let launch = self.active_launch_label(); + panel( + "Status Overview", + div() + .flex() + .flex_col() + .px(px(16.)) + .py(px(10.)) + .child(status_row( + "T7 Patch", + self.flag("/mods/t7Patch"), + None, + false, + )) + .child(status_row( + "DXVK-GPLAsync", + self.flag("/mods/dxvk"), + None, + false, + )) + .child(status_row( + "BO3 Enhanced", + self.flag("/mods/enhanced"), + None, + false, + )) + .child(status_row( + "Launch Option", + self.string("/activeLaunchProfile") != "default", + launch.as_deref(), + false, + )) + .child(status_row("Quality of Life", qol_any, None, true)), + ) + } + + fn qol_panel(&self, cx: &mut Context) -> Div { + let enabled = self.can_act(); + let options = [ + ( + "d3dcompiler", + "/qol/d3dcompiler", + "/api/d3dcompiler", + "Use latest d3dcompiler (d3dcompiler_46.dll)", + "the latest d3dcompiler", + ), + ( + "intro", + "/qol/intro", + "/api/intro-skip", + "Skip Intro (BO3_Global_Logo_LogoSequence.mkv)", + "intro skipping", + ), + ( + "all-intros", + "/qol/allIntros", + "/api/all-intros-skip", + "Skip All Intros (Campaign, Zombies, etc.)", + "skipping all intros", + ), + ]; + let all = options.iter().all(|(_, pointer, ..)| self.flag(pointer)); + let apply_all = { + let next = !all; + let label = format!( + "{} all quality-of-life options", + if next { "Enable" } else { "Disable" } + ); + let paths: Vec = options + .iter() + .map(|(_, _, path, ..)| (*path).to_owned()) + .collect(); + cx.listener(move |view, _: &ClickEvent, _, cx| { + let requests = paths + .iter() + .map(|path| Request { + path: path.clone(), + body: Some(json!({"enabled": next})), + }) + .collect(); + view.stage_all(label.clone(), requests, cx); + }) + }; + panel( + "Quality of Life", + div() + .flex() + .flex_col() + .gap(px(4.)) + .p(px(14.)) + .child( + check_row("qol-all", "Apply All", all, enabled, apply_all) + .font_weight(FontWeight::BOLD) + .text_color(theme::muted_strong()), + ) + .child(hairline().mb(px(4.))) + .children( + options + .into_iter() + .map(|(id, pointer, path, label, summary)| { + let current = self.flag(pointer); + check_row( + id, + label, + current, + enabled, + self.stage_click( + cx, + format!( + "{} {summary}", + if current { "Disable" } else { "Enable" } + ), + path, + json!({"enabled": !current}), + ), + ) + }), + ), + ) + } + + fn launch_options_panel(&self, cx: &mut Context) -> Div { + let profiles: Vec = self.state["launchProfiles"] + .as_array() + .cloned() + .unwrap_or_default(); + let selected = profiles + .iter() + .find(|profile| profile["id"] == self.selected_profile.as_str()); + let installable = selected + .is_some_and(|profile| profile["id"] != "default" && profile["id"] != "offline"); + let selected_option = selected + .and_then(|profile| profile["option"].as_str()) + .unwrap_or("") + .to_owned(); + let selected_label = selected + .and_then(|profile| profile["label"].as_str()) + .unwrap_or("Default") + .to_owned(); + let can_act = self.can_act(); + panel( + "Launch Options", + div() + .flex() + .flex_col() + .gap(px(10.)) + .p(px(14.)) + .child( + div() + .flex() + .flex_col() + .children(profiles.iter().enumerate().map(|(index, profile)| { + let id = profile["id"].as_str().unwrap_or("profile").to_owned(); + let mut label = + profile["label"].as_str().unwrap_or("Profile").to_owned(); + if profile["active"] == true { + label.push_str(" · Active"); + } + let trailing = (id != "default" && id != "offline").then(|| { + let installed = profile["installed"] == true; + let subscribed = profile["subscribed"] == true; + let tone = if installed { + theme::ok() + } else if subscribed { + theme::warning() + } else { + theme::danger() + }; + ( + SharedString::from( + profile["state"].as_str().unwrap_or("").to_owned(), + ), + tone, + ) + }); + let choice = id.clone(); + radio_row( + ("profile", index), + label, + self.selected_profile == id, + trailing, + cx.listener(move |view, _: &ClickEvent, _, cx| { + view.selected_profile = choice.clone(); + cx.notify(); + }), + ) + })), + ) + .child( + div() + .flex() + .gap(px(8.)) + .child( + Btn::new("install-profile", "Install Selected Mod") + .compact() + .grow() + .disabled(!can_act || !installable) + .on_click(self.stage_click( + cx, + format!("Install {selected_label}"), + "/api/workshop-install", + json!({"profileId": self.selected_profile}), + )), + ) + .child( + Btn::new("refresh-profiles", "Refresh") + .compact() + .grow() + .disabled(self.busy) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + view.send("/api/status", None, cx) + })), + ) + .child( + Btn::new("apply-profile", "Apply") + .compact() + .grow() + .disabled(!can_act || selected.is_none()) + .on_click(self.stage_click( + cx, + format!("Apply launch option: {selected_label}"), + "/api/launch-options", + json!({"options": selected_option, "preserve_fs_game": false}), + )), + ), + ), + ) + } + + pub(super) fn dashboard_view(&self, cx: &mut Context) -> AnyElement { + div() + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .child(self.status_overview()) + .child( + columns() + .items_start() + .child(column(300.).child(self.qol_panel(cx))) + .child(column(300.).child(self.launch_options_panel(cx))), + ) + .into_any_element() + } +} diff --git a/native/gpui/src/ui/enhanced.rs b/native/gpui/src/ui/enhanced.rs new file mode 100644 index 0000000..8eda52a --- /dev/null +++ b/native/gpui/src/ui/enhanced.rs @@ -0,0 +1,404 @@ +//! BO3 Enhanced: dump validation, install, diagnostics and uninstall. +use super::components::{ + Btn, Glyph, bare_panel, card, card_title, column, columns, field_label, icon, metric, +}; +use super::{ControlCenter, Validation, format_timestamp, theme}; +use gpui::{prelude::*, *}; +use serde_json::json; + +const DUMP_GUIDE_URL: &str = "https://youtu.be/rBZZTcSJ9_s?si=41p0r_Enten3h5AQ"; + +fn ok_tone(ok: bool) -> Hsla { + if ok { + theme::ok() + } else { + theme::muted_strong() + } +} + +fn requirement(label: &str, glyph: Glyph, color: Hsla) -> Div { + div() + .flex() + .items_center() + .gap(px(8.)) + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted_strong()) + .child(icon(glyph, 15., color)) + .child(label.to_owned()) +} + +fn summary_line(topic: &str, detail: &str) -> Div { + div() + .flex() + .gap(px(4.)) + .child( + div() + .font_weight(FontWeight::EXTRA_BOLD) + .text_color(theme::text()) + .child(format!("{topic}:")), + ) + .child(detail.to_owned()) +} + +impl ControlCenter { + fn enhanced_status_card(&self) -> Div { + let installed = self.flag("/enhanced/installed"); + let launch = self.flag("/enhanced/launchOptionsActive"); + let detected = self.flag("/gameDetected"); + let tracked = !self.string("/enhanced/detectedAt").is_empty(); + card() + .child(card_title(Some(Glyph::Gem), "BO3 Enhanced Status")) + .child( + div() + .flex() + .flex_wrap() + .gap(px(14.)) + .child(metric( + "Status", + if installed { + "Installed" + } else { + "Not installed" + }, + ok_tone(installed), + )) + .child(metric( + "Launch override", + if launch { "Active" } else { "Inactive" }, + ok_tone(launch), + )) + .child(metric( + "Game", + if detected { "Detected" } else { "Not detected" }, + ok_tone(detected), + )) + .child(metric( + "Tracking", + if tracked { "Tracked" } else { "Not tracked" }, + ok_tone(tracked), + )), + ) + } + + fn enhanced_source_card(&self, cx: &mut Context) -> Div { + let source = self.text("dump", cx); + let source = source.trim().to_owned(); + let validated = self.validation.ok == Some(true) && self.validation.source == source; + let can_act = self.can_act(); + let validation_label = match &self.validation.checked_at { + Some(at) => format!("{} ({at})", self.validation.label), + None => self.validation.label.clone(), + }; + let validation_color = match self.validation.ok { + Some(true) => theme::ok(), + Some(false) => theme::caution(), + None => theme::muted_strong(), + }; + let drop_zone = div() + .id("dump-drop-zone") + .flex() + .flex_col() + .items_center() + .justify_center() + .gap(px(4.)) + .flex_1() + .min_w(px(220.)) + .min_h(px(120.)) + .p(px(14.)) + .border_1() + .border_dashed() + .border_color(theme::white(0.2)) + .rounded(theme::radius_card()) + .bg(theme::white(0.02)) + .text_center() + .drag_over::(|style, _, _, _| { + style + .bg(theme::accent_alpha(0.08)) + .border_color(theme::accent_alpha(0.6)) + }) + .on_drop(cx.listener(|view, paths: &ExternalPaths, window, cx| { + if let Some(path) = paths.paths().first() { + view.set_text("dump", path.to_string_lossy().into_owned(), window, cx); + view.validation = Validation::not_run(); + cx.notify(); + } + })) + .child( + div() + .font_weight(FontWeight::BOLD) + .child("Drop DUMP.zip or extracted folder here"), + ) + .child( + div() + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted()) + .child("or browse to the dump source manually"), + ); + let controls = div() + .flex() + .flex_col() + .gap(px(10.)) + .flex_1() + .min_w(px(240.)) + .child( + div() + .flex() + .gap(px(10.)) + .child( + Btn::new("browse-dump", "Browse") + .icon(Glyph::FolderOpen) + .grow() + .disabled(self.busy) + .on_click(cx.listener(|view, _: &ClickEvent, window, cx| { + view.choose_directory("dump", window, cx) + })), + ) + .child( + self.action( + cx, + "validate-dump", + "Validate Source", + "/api/enhanced-validate", + json!({"dumpSource": source}), + ) + .icon(Glyph::CheckCircle) + .grow() + .disabled(!can_act || source.is_empty()), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap(px(4.)) + .child(field_label("Source")) + .child(self.field("dump")), + ) + .child( + div() + .flex() + .flex_col() + .gap(px(4.)) + .child(field_label("Validation")) + .child( + div() + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(validation_color) + .child(validation_label), + ), + ) + .child( + self.action( + cx, + "install-enhanced", + "Install / Update BO3 Enhanced", + "/api/enhanced-install", + json!({"dumpSource": source}), + ) + .icon(Glyph::Download) + .disabled(!can_act || source.is_empty() || !validated), + ) + .when(!validated, |this| { + this.child( + div() + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted()) + .child("Install requires a valid DUMP.zip or extracted UWP dump folder."), + ) + }); + card() + .child(card_title(Some(Glyph::Download), "Install Source")) + .child( + div() + .flex() + .flex_wrap() + .gap(px(14.)) + .child(drop_zone) + .child(controls), + ) + } + + fn enhanced_help_card(&self, cx: &mut Context) -> Div { + let detected = self.flag("/gameDetected"); + let dump_ok = self.validation.ok == Some(true); + card() + .child(card_title(Some(Glyph::Alert), "Help / Requirements")) + .child( + div() + .flex() + .flex_col() + .gap(px(7.)) + .child(requirement( + "Game detected", + Glyph::CheckCircle, + if detected { + theme::ok() + } else { + theme::caution() + }, + )) + .child(requirement( + "Dump required", + Glyph::Alert, + if dump_ok { + theme::ok() + } else { + theme::caution() + }, + )), + ) + .child( + div() + .flex() + .flex_col() + .gap(px(6.)) + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted_strong()) + .child(summary_line("Sources", "DUMP.zip or extracted folder")) + .child(summary_line("Checks", "files, read/write, version")), + ) + .child( + div().flex().child( + Btn::new("dump-guide", "Open Dump Guide") + .icon(Glyph::ExternalLink) + .on_click( + cx.listener(|_, _: &ClickEvent, _, cx| cx.open_url(DUMP_GUIDE_URL)), + ), + ), + ) + } + + fn enhanced_details_card(&self) -> Div { + let installed = self.flag("/enhanced/installed"); + let backup = { + let backup = self.string("/enhanced/backupStatus"); + if backup.is_empty() { + "Not created".to_owned() + } else { + backup + } + }; + let game_dir = self.string("/gameDir"); + let install_path = if installed && !game_dir.is_empty() { + game_dir + } else { + "Not created".to_owned() + }; + let last_install = format_timestamp(&self.string("/enhanced/detectedAt")); + let last_validation = self + .validation + .checked_at + .clone() + .unwrap_or_else(|| "Never".into()); + let cells = [ + ("Last install", last_install), + ("Last validation", last_validation), + ( + "Files installed", + self.int("/enhanced/filesInstalled", 0).to_string(), + ), + ("Backup status", backup.clone()), + ("Install path", install_path), + ( + "Launch target", + if self.flag("/enhanced/launchOptionsActive") { + "BO3 Enhanced" + } else { + "Stock BO3" + } + .to_owned(), + ), + ( + "Config file", + if installed { "Configured" } else { "Missing" }.to_owned(), + ), + ( + "Restore point", + if backup == "Created" { + "Available" + } else { + "Not available" + } + .to_owned(), + ), + ]; + card() + .child(card_title(None, "Install Details / Diagnostics")) + .child(div().flex().flex_wrap().gap(px(14.)).children( + cells.into_iter().map(|(label, value)| { + metric(label, value, theme::muted_strong()).min_w(px(180.)) + }), + )) + } + + fn enhanced_danger_zone(&self, cx: &mut Context) -> Div { + let open = self.danger_open; + card() + .border_color(theme::accent_alpha(0.34)) + .bg(theme::accent_alpha(0.035)) + .child( + div() + .id("danger-zone") + .flex() + .items_center() + .gap(px(8.)) + .cursor_pointer() + .font_weight(FontWeight::BOLD) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + view.danger_open = !view.danger_open; + cx.notify(); + })) + .child(icon( + if open { + Glyph::ChevronDown + } else { + Glyph::ChevronRight + }, + 15., + theme::muted(), + )) + .child("Danger Zone"), + ) + .when(open, |this| { + this.child( + div().flex().justify_end().child( + self.action( + cx, + "uninstall-enhanced", + "Uninstall BO3 Enhanced", + "/api/enhanced-uninstall", + json!({}), + ) + .icon(Glyph::Trash) + .danger() + .disabled(!self.can_act() || !self.flag("/enhanced/installed")), + ), + ) + }) + } + + pub(super) fn enhanced_view(&self, cx: &mut Context) -> AnyElement { + bare_panel( + "Enhanced", + div() + .flex() + .flex_col() + .gap(px(10.)) + .child(self.enhanced_status_card()) + .child( + columns() + .items_start() + .child(column(420.).child(self.enhanced_source_card(cx))) + .child(column(280.).child(self.enhanced_help_card(cx))), + ) + .child(self.enhanced_details_card()) + .child(self.enhanced_danger_zone(cx)), + ) + .into_any_element() + } +} diff --git a/native/gpui/src/ui/exe.rs b/native/gpui/src/ui/exe.rs new file mode 100644 index 0000000..b140bc4 --- /dev/null +++ b/native/gpui/src/ui/exe.rs @@ -0,0 +1,335 @@ +//! EXE Swapper: the three selectable executables and their trade-offs. +use super::components::{Btn, Glyph, bare_panel, columns, icon, status_pill, text_xs}; +use super::{ControlCenter, theme}; +use gpui::{prelude::*, *}; +use serde_json::json; + +#[derive(Clone, Copy)] +enum Verdict { + Pro, + Warn, + Con, +} + +fn verdict_line(verdict: Verdict, topic: &str, detail: &str) -> Div { + let (glyph, color) = match verdict { + Verdict::Pro => (Glyph::CheckCircle, theme::ok()), + Verdict::Warn => (Glyph::Alert, theme::caution()), + Verdict::Con => (Glyph::Close, theme::accent()), + }; + div() + .flex() + .items_center() + .gap(px(8.)) + .min_w_0() + .text_size(px(theme::FONT_SM)) + .child(icon(glyph, 15., color)) + .child( + div().flex_1().min_w_0().child( + div() + .flex() + .gap(px(4.)) + .child( + div() + .font_weight(FontWeight::BOLD) + .child(format!("{topic}:")), + ) + .child(detail.to_owned()), + ), + ) +} + +struct ExeOption<'a> { + title: &'a str, + subtitle: String, + badge: &'a str, + active: bool, + disabled: bool, + verdicts: [(Verdict, &'a str, &'a str); 3], +} + +fn exe_option(option: ExeOption, button: Btn) -> Div { + div() + .flex() + .flex_col() + .gap(px(16.)) + .flex_1() + .min_w(px(240.)) + .p(px(16.)) + .when(option.active, |this| { + this.border_t_4() + .border_color(theme::ok()) + .bg(theme::ok_alpha(0.045)) + }) + .when(option.disabled, |this| this.opacity(0.72)) + .child( + div() + .flex() + .items_start() + .justify_between() + .gap(px(12.)) + .child( + div() + .flex_1() + .min_w_0() + .child( + div() + .text_size(px(theme::FONT_LG)) + .font_weight(FontWeight::BOLD) + .child(option.title.to_owned()), + ) + .child( + div() + .text_size(px(theme::FONT_SM)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted()) + .child(option.subtitle), + ), + ) + .child( + div() + .flex_none() + .px(px(9.)) + .py(px(4.)) + .border_1() + .border_color(if option.active { + theme::ok_alpha(0.42) + } else { + theme::border() + }) + .rounded_full() + .bg(if option.active { + theme::ok_alpha(0.13) + } else { + theme::white(0.) + }) + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::EXTRA_BOLD) + .text_color(if option.active { + theme::ok() + } else { + theme::muted() + }) + .child(option.badge.to_owned()), + ), + ) + .child( + div().flex().flex_col().gap(px(10.)).children( + option + .verdicts + .into_iter() + .map(|(verdict, topic, detail)| verdict_line(verdict, topic, detail)), + ), + ) + .child(button) +} + +impl ControlCenter { + pub(super) fn exe_view(&self, cx: &mut Context) -> AnyElement { + let profile = self.string("/exeSwap/profile"); + let trusted = self.flag("/exeSwap/trustedExecutable"); + let detected = self.flag("/gameDetected"); + let build_id = self.value("/exeSwap/activeBuildId"); + let current_date = self.value("/exeSwap/currentBuildDate"); + let compatible_date = self.value("/exeSwap/compatibleBuildDate"); + let active_title = match (trusted, profile.as_str()) { + (true, "compatible") => format!("Compatible Build ({build_id})"), + (true, "current") => format!("Latest Build ({build_id})"), + (true, "enhanced") => format!("BO3 Enhanced ({build_id})"), + _ => "Unverified EXE".to_owned(), + }; + let enhanced_available = self.flag("/exeSwap/enhancedAvailable"); + let enhanced_value = if self.flag("/exeSwap/enhancedExeActive") { + "Active" + } else if enhanced_available { + "Available" + } else { + "Not found" + }; + let compatible_active = profile == "compatible" && trusted; + let latest_active = profile == "current" && trusted; + let enhanced_active = profile == "enhanced" && trusted; + let compatible_disabled = !detected || profile == "compatible"; + let latest_disabled = + !detected || profile == "current" || !self.flag("/exeSwap/latestAvailable"); + let enhanced_disabled = !detected || profile == "enhanced" || !enhanced_available; + let can_act = self.can_act(); + + let hero = columns() + .child( + div() + .flex() + .items_center() + .gap(px(14.)) + .flex_none() + .w(px(330.)) + .p(px(14.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.025)) + .child(icon(Glyph::Refresh, 20., theme::accent())) + .child( + div() + .flex_1() + .min_w_0() + .child(text_xs("Active build", theme::muted())) + .child( + div() + .truncate() + .text_size(px(16.)) + .font_weight(FontWeight::EXTRA_BOLD) + .child(active_title), + ), + ), + ) + .child( + div() + .flex() + .gap(px(8.)) + .flex_1() + .min_w(px(420.)) + .child(status_pill( + format!("Latest - {current_date}"), + self.value("/exeSwap/currentBuildId"), + latest_active, + )) + .child(status_pill( + format!("Compatible - {compatible_date}"), + self.value("/exeSwap/compatibleBuildId"), + self.flag("/exeSwap/compatibleActive"), + )) + .child(status_pill("Enhanced", enhanced_value, enhanced_available)), + ); + + let options = div() + .flex() + .flex_wrap() + .border_1() + .border_color(theme::border()) + .rounded(theme::radius()) + .bg(theme::panel()) + .overflow_hidden() + .child( + exe_option( + ExeOption { + title: "Compatible Build", + subtitle: "March 3, 2023 Steam depot".into(), + badge: if compatible_active { + "Active" + } else { + "Best for mods" + }, + active: compatible_active, + disabled: compatible_disabled, + verdicts: [ + (Verdict::Pro, "Vanilla experience", "Supported"), + (Verdict::Pro, "Modded experience", "Best support"), + (Verdict::Warn, "Performance", "Standard Steam performance"), + ], + }, + self.action( + cx, + "exe-compatible", + "Use Compatible Build", + "/api/exe-swap/compatible", + json!({}), + ) + .icon(Glyph::Download) + .primary() + .disabled(!can_act || compatible_disabled), + ) + .border_r_1() + .border_color(theme::border()), + ) + .child( + exe_option( + ExeOption { + title: "Latest Build", + subtitle: format!("{current_date} Steam build"), + badge: if latest_active { + "Active" + } else { + "Steam default" + }, + active: latest_active, + disabled: latest_disabled, + verdicts: [ + (Verdict::Pro, "Vanilla experience", "Best Steam match"), + (Verdict::Warn, "Modded experience", "May or may not work"), + (Verdict::Warn, "Performance", "Standard Steam performance"), + ], + }, + self.action( + cx, + "exe-current", + "Use Latest Build", + "/api/exe-swap/current", + json!({}), + ) + .icon(Glyph::Rotate) + .disabled(!can_act || latest_disabled), + ) + .border_r_1() + .border_color(theme::border()), + ) + .child(exe_option( + ExeOption { + title: "BO3 Enhanced", + subtitle: "Windows Store build modded for Steam".into(), + badge: if enhanced_active { + "Active" + } else if enhanced_available { + "Best performance" + } else { + "Not installed" + }, + active: enhanced_active, + disabled: enhanced_disabled, + verdicts: [ + (Verdict::Pro, "Vanilla experience", "Supported"), + (Verdict::Con, "Modded experience", "Most mods unlikely"), + (Verdict::Pro, "Performance", "Highest performance"), + ], + }, + self.action( + cx, + "exe-enhanced", + "Use BO3 Enhanced", + "/api/exe-swap/enhanced", + json!({}), + ) + .icon(Glyph::Gem) + .disabled(!can_act || enhanced_disabled), + )); + + let details = div() + .flex() + .flex_col() + .gap(px(4.)) + .p(px(12.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_control()) + .bg(theme::scrim().alpha(0.18)) + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::muted_strong()) + .child(self.value("/exeSwap/displayLabel")) + .child(self.value("/exeSwap/integrityMessage")) + .child(format!("SHA-256: {}", self.value("/exeSwap/executableHash"))) + .child("Compatible installs may need a Steam depot download. The command appears in a dialog when it is required."); + + bare_panel( + "EXE Swapper", + div() + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .child(hero) + .child(options) + .child(details), + ) + .into_any_element() + } +} diff --git a/native/gpui/src/ui/graphics.rs b/native/gpui/src/ui/graphics.rs new file mode 100644 index 0000000..81167bf --- /dev/null +++ b/native/gpui/src/ui/graphics.rs @@ -0,0 +1,874 @@ +//! Graphics settings and DXVK. Both write `config.ini` / `dxvk.conf` through +//! the local service; every write asks for confirmation first. +use super::components::{ + Btn, Glyph, bare_panel, chip, column, columns, handler, icon, inline_pill, segmented, switch, + text_xs, +}; +use super::{ControlCenter, GraphicsTab, dxvk_recommended, theme}; +use gpui::{prelude::*, *}; +use gpui_component::slider::Slider; +use serde_json::{Value, json}; + +const DISPLAY_MODES: [(i64, &str); 3] = [(0, "Windowed"), (1, "Fullscreen"), (2, "Borderless")]; + +fn dxvk_none() -> Value { + json!({ + "enableAsync": true, + "gplAsyncCache": false, + "numCompilerThreads": 0, + "maxFrameRate": 0, + "maxFrameLatency": 0, + "tearFree": "Auto", + "hudEnabled": false, + }) +} + +/// `.graphics-quick-grid .setting-row`: a bordered tile with a label and a control. +fn quick_row(label: &str, control: impl IntoElement) -> Div { + div() + .flex() + .items_center() + .gap(px(12.)) + .flex_1() + .min_w(px(300.)) + .min_h(px(50.)) + .px(px(14.)) + .py(px(6.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.04)) + .child( + div() + .w(px(104.)) + .flex_none() + .text_size(px(theme::FONT_MD)) + .child(label.to_owned()), + ) + .child( + div() + .flex() + .items_center() + .flex_wrap() + .gap(px(8.)) + .flex_1() + .min_w_0() + .child(control), + ) +} + +/// A row inside a settings group or the advanced drawer. +fn group_row(label: &str, control: impl IntoElement) -> Div { + div() + .flex() + .items_center() + .justify_between() + .gap(px(12.)) + .min_h(px(40.)) + .px(px(14.)) + .py(px(6.)) + .child( + div() + .min_w(px(110.)) + .flex_none() + .text_size(px(theme::FONT_MD)) + .child(label.to_owned()), + ) + .child( + div() + .flex() + .items_center() + .justify_end() + .gap(px(8.)) + .flex_1() + .min_w_0() + .child(control), + ) +} + +/// `.graphics-settings-group`: a titled stack of rows separated by hairlines. +fn group(title: &str, rows: Vec
) -> Div { + let count = rows.len(); + div() + .flex() + .flex_col() + .min_w_0() + .overflow_hidden() + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.03)) + .child( + div() + .px(px(14.)) + .pt(px(12.)) + .pb(px(6.)) + .font_weight(FontWeight::BOLD) + .child(title.to_owned()), + ) + .children(rows.into_iter().enumerate().map(|(index, row)| { + row.when(index + 1 < count, |row| { + row.border_b_1().border_color(theme::border()) + }) + })) +} + +impl ControlCenter { + /// An input with an `Apply` button that stages a whole-number setting. + fn number_control( + &self, + cx: &mut Context, + input: &'static str, + config: &'static str, + min: i64, + max: i64, + ) -> Div { + div() + .flex() + .items_center() + .gap(px(8.)) + .flex_1() + .min_w_0() + .child(div().flex_1().min_w_0().child(self.field(input))) + .child( + Btn::new(SharedString::from(format!("apply-{input}")), "Apply") + .compact() + .disabled(!self.can_act()) + .on_click(cx.listener(move |view, _: &ClickEvent, _, cx| { + match view.text(input, cx).trim().parse::() { + Ok(value) if (min..=max).contains(&value) => view.stage( + format!("Set {config} to {value}"), + "/api/config".into(), + ControlCenter::config_body(config, json!(value)), + cx, + ), + _ => view + .reject(format!("Enter a whole number from {min} to {max}."), cx), + } + })), + ) + } + + /// A slider with its current value and an optional `Apply` button. + fn slider_control( + &self, + cx: &mut Context, + key: &'static str, + apply: Option<(&'static str, &'static str)>, + ) -> Div { + let value = self.slider_value(key, cx); + div() + .flex() + .items_center() + .gap(px(10.)) + .flex_1() + .min_w(px(160.)) + .child( + div() + .flex() + .flex_1() + .min_w(px(100.)) + .child(Slider::new(&self.sliders[key]).disabled(self.busy)), + ) + .child( + div() + .w(px(36.)) + .flex_none() + .text_right() + .font_weight(FontWeight::BOLD) + .child(value.to_string()), + ) + .when_some(apply, |this, (config, label)| { + this.child( + Btn::new(SharedString::from(format!("apply-{key}")), "Apply") + .compact() + .disabled(!self.can_act()) + .on_click(cx.listener(move |view, _: &ClickEvent, _, cx| { + let value = view.slider_value(key, cx); + view.stage( + format!("Set {label} to {value}"), + "/api/config".into(), + ControlCenter::config_body(config, json!(value)), + cx, + ); + })), + ) + }) + } + + /// A switch that writes `value` to a `config.ini` key once confirmed. + fn config_switch( + &self, + cx: &mut Context, + id: &'static str, + label: &str, + current: bool, + key: &str, + next_value: i64, + ) -> Div { + group_row( + label, + switch( + id, + current, + self.can_act(), + self.stage_click( + cx, + format!("{} {label}", if current { "Disable" } else { "Enable" }), + "/api/config", + ControlCenter::config_body(key, json!(next_value)), + ), + ), + ) + } + + fn graphics_quick_grid(&self, cx: &mut Context) -> Div { + let can_act = self.can_act(); + let presets: Vec = self.state["presets"] + .as_array() + .into_iter() + .flatten() + .filter_map(Value::as_str) + .map(str::to_owned) + .collect(); + let display_mode = self.int("/graphics/displayMode", 1); + let preset_chips = + div() + .flex() + .flex_wrap() + .gap(px(6.)) + .children(presets.into_iter().enumerate().map(|(index, name)| { + let click = self.stage_click( + cx, + format!("Apply preset {name}"), + "/api/presets/apply", + json!({"name": name}), + ); + chip(("preset", index), name, None, false, can_act, click) + })); + let modes = segmented( + DISPLAY_MODES + .iter() + .map(|(mode, label)| { + ( + SharedString::from(*label), + display_mode == *mode, + handler(self.stage_click( + cx, + format!("Set display mode to {label}"), + "/api/config", + ControlCenter::config_body("FullScreenMode", json!(mode)), + )), + ) + }) + .collect(), + can_act, + ) + .flex_1(); + columns() + .child(quick_row("Preset", preset_chips)) + .child(quick_row("Display Mode", modes)) + .child(quick_row( + "Resolution", + div() + .flex() + .items_center() + .gap(px(8.)) + .flex_1() + .min_w_0() + .child(div().flex_1().min_w_0().child(self.field("resolution"))) + .child( + Btn::new("apply-resolution", "Apply") + .compact() + .disabled(!can_act) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + let value = view.text("resolution", cx).trim().to_owned(); + let valid = value.split_once('x').is_some_and(|(w, h)| { + [w, h].iter().all(|n| { + n.parse::().is_ok_and(|n| (320..=16384).contains(&n)) + }) + }); + if valid { + view.stage( + format!("Set resolution to {value}"), + "/api/config".into(), + ControlCenter::config_body("WindowSize", json!(value)), + cx, + ); + } else { + view.reject( + "Enter resolution as WIDTHxHEIGHT, e.g. 1920x1080.", + cx, + ); + } + })), + ), + )) + .child(quick_row( + "Refresh Rate", + self.number_control(cx, "refresh", "RefreshRate", 1, 1000), + )) + } + + fn graphics_advanced(&self, cx: &mut Context) -> Div { + let open = self.advanced_open; + let limited = self.flag("/advanced/vramLimited"); + let target = self.int("/advanced/vramTarget", 75).clamp(75, 100); + let readonly = self.flag("/advanced/configReadonly"); + let unlocked = self.flag("/advanced/unlockOptions"); + let reduce_cpu = self.flag("/advanced/reduceCpu"); + let can_act = self.can_act(); + let rows = div() + .flex() + .flex_wrap() + .border_t_1() + .border_color(theme::border()) + .child( + self.config_switch( + cx, + "smooth-framerate", + "Smooth framerate", + self.flag("/advanced/smoothFramerate"), + "SmoothFramerate", + if self.flag("/advanced/smoothFramerate") { + 0 + } else { + 1 + }, + ) + .flex_1() + .min_w(px(300.)) + .border_b_1() + .border_color(theme::border()), + ) + .child( + self.config_switch( + cx, + "unlock-graphics", + "Expose hidden graphics", + unlocked, + "RestrictGraphicsOptions", + if unlocked { 1 } else { 0 }, + ) + .flex_1() + .min_w(px(300.)) + .border_b_1() + .border_color(theme::border()), + ) + .child( + self.config_switch( + cx, + "reduce-cpu", + "Reduce CPU pressure", + reduce_cpu, + "SerializeRender", + if reduce_cpu { 0 } else { 2 }, + ) + .flex_1() + .min_w(px(300.)) + .border_b_1() + .border_color(theme::border()), + ) + .child( + group_row( + "Frame latency", + self.number_control(cx, "latency", "MaxFrameLatency", 0, 4), + ) + .flex_1() + .min_w(px(300.)) + .border_b_1() + .border_color(theme::border()), + ) + .child( + group_row( + "Limit VRAM target", + switch( + "limit-vram", + limited, + can_act, + self.stage_click( + cx, + format!( + "{} the VRAM target", + if limited { "Remove" } else { "Limit" } + ), + "/api/vram-target", + json!({"limited": !limited, "target": target}), + ), + ), + ) + .flex_1() + .min_w(px(300.)) + .border_b_1() + .border_color(theme::border()), + ) + .child( + group_row( + "VRAM target %", + div() + .flex() + .items_center() + .gap(px(8.)) + .flex_1() + .min_w_0() + .child(div().flex_1().min_w_0().child(self.field("vram"))) + .child( + Btn::new("apply-vram", "Apply") + .compact() + .disabled(!can_act) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + match view.text("vram", cx).trim().parse::() { + Ok(target) if (75..=100).contains(&target) => view.stage( + format!("Set VRAM target to {target}%"), + "/api/vram-target".into(), + json!({"limited": target < 100, "target": target}), + cx, + ), + _ => view.reject( + "VRAM target must be a whole number from 75 to 100.", + cx, + ), + } + })), + ), + ) + .flex_1() + .min_w(px(300.)) + .border_b_1() + .border_color(theme::border()), + ) + .child( + group_row( + "Lock config.ini", + switch( + "config-readonly", + readonly, + can_act, + self.stage_click( + cx, + format!( + "{} config.ini read-only mode", + if readonly { "Disable" } else { "Enable" } + ), + "/api/config-readonly", + json!({"enabled": !readonly}), + ), + ), + ) + .flex_1() + .min_w(px(300.)) + .border_b_1() + .border_color(theme::border()), + ); + div() + .flex() + .flex_col() + .overflow_hidden() + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.025)) + .child( + div() + .id("advanced-drawer") + .flex() + .items_center() + .justify_between() + .min_h(px(46.)) + .px(px(14.)) + .cursor_pointer() + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + view.advanced_open = !view.advanced_open; + cx.notify(); + })) + .child( + div() + .flex() + .flex_col() + .gap(px(2.)) + .child(div().font_weight(FontWeight::BOLD).child("Advanced")) + .child(text_xs( + "Config, CPU, latency, and VRAM controls", + theme::muted(), + )), + ) + .child(icon( + if open { + Glyph::ChevronDown + } else { + Glyph::ChevronRight + }, + 16., + theme::muted(), + )), + ) + .when(open, |this| this.child(rows)) + } + + fn graphics_settings(&self, cx: &mut Context) -> Div { + let vsync = self.flag("/graphics/vsync"); + let draw_fps = self.flag("/graphics/drawFps"); + bare_panel( + "Graphics Settings", + div() + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .child(self.graphics_quick_grid(cx)) + .child( + columns() + .items_start() + .child(column(340.).child(group( + "Performance", + vec![ + group_row( + "Max FPS", + self.number_control(cx, "fps", "MaxFPS", 0, 1000), + ), + self.config_switch( + cx, + "vsync", + "Vertical sync", + vsync, + "Vsync", + if vsync { 0 } else { 1 }, + ), + self.config_switch( + cx, + "draw-fps", + "FPS counter", + draw_fps, + "DrawFPS", + if draw_fps { 0 } else { 1 }, + ), + ], + ))) + .child(column(340.).child(group( + "Quality", + vec![ + group_row( + "Render Resolution", + self.slider_control( + cx, + "render", + Some(("ResolutionPercent", "render resolution")), + ), + ), + group_row( + "Field of View", + self.slider_control(cx, "fov", Some(("FOV", "field of view"))), + ), + ], + ))), + ) + .child(self.graphics_advanced(cx)), + ) + } + + /// The current DXVK draft with the number inputs and slider folded in. + fn dxvk_payload(&self, cx: &App) -> Result { + let parse = |key: &str, label: &str, max: i64| -> Result { + self.text(key, cx) + .trim() + .parse::() + .ok() + .filter(|value| (0..=max).contains(value)) + .ok_or_else(|| format!("{label} must be a whole number from 0 to {max}.")) + }; + let mut payload = self.dxvk_draft.clone(); + payload["numCompilerThreads"] = json!(parse("dxvk-threads", "Compiler threads", 64)?); + payload["maxFrameLatency"] = json!(parse("dxvk-latency", "Frame latency", 16)?); + payload["maxFrameRate"] = json!(self.slider_value("dxvk-fps", cx)); + Ok(payload) + } + + /// Stage a DXVK install/apply after folding the draft controls in. + fn dxvk_click( + &self, + cx: &mut Context, + label: &'static str, + path: &'static str, + ) -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static { + cx.listener( + move |view, _: &ClickEvent, _, cx| match view.dxvk_payload(cx) { + Ok(payload) => view.stage(label.into(), path.into(), payload, cx), + Err(message) => view.reject(message, cx), + }, + ) + } + + fn dxvk_toggle( + &self, + cx: &mut Context, + id: &'static str, + label: &str, + key: &'static str, + ) -> Div { + group_row( + label, + switch( + id, + self.dxvk_flag(key), + !self.busy, + cx.listener(move |view, _: &ClickEvent, _, cx| { + let next = !view.dxvk_flag(key); + view.dxvk_draft[key] = json!(next); + cx.notify(); + }), + ), + ) + } + + fn dxvk_view(&self, cx: &mut Context) -> Div { + let installed = self.flag("/dxvk/installed"); + let conf = self.flag("/dxvk/confExists"); + let can_act = self.can_act(); + let detected_threads = std::thread::available_parallelism() + .ok() + .map(|cores| cores.get().saturating_sub(2).max(1)); + let pill = |label: &str, value: &str, ok: bool| { + div() + .flex_1() + .min_w(px(180.)) + .min_h(px(58.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.04)) + .child(inline_pill(label.to_owned(), value.to_owned(), ok)) + }; + let control_bar = div() + .flex() + .flex_wrap() + .gap(px(theme::PANEL_GAP)) + .child(pill( + "Status", + if installed { + "Installed" + } else { + "Not installed" + }, + installed, + )) + .child(pill( + "dxvk.conf", + if conf { "Configured" } else { "Missing" }, + conf, + )) + .child( + div() + .flex() + .items_center() + .gap(px(8.)) + .flex_1() + .min_w(px(300.)) + .min_h(px(58.)) + .p(px(8.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.04)) + .child( + Btn::new("dxvk-install", "Install") + .compact() + .icon(Glyph::Download) + .grow() + .disabled(!can_act) + .on_click(self.dxvk_click(cx, "Install DXVK", "/api/dxvk-install")), + ) + .child( + self.action( + cx, + "dxvk-uninstall", + "Uninstall", + "/api/dxvk-uninstall", + json!({}), + ) + .compact() + .icon(Glyph::Trash) + .grow() + .disabled(!can_act || !installed), + ) + .child( + Btn::new("dxvk-apply", "Apply") + .compact() + .icon(Glyph::Save) + .grow() + .disabled(!can_act) + .on_click(self.dxvk_click( + cx, + "Apply DXVK settings", + "/api/dxvk-config", + )), + ), + ); + let presets = div().flex().gap(px(6.)).children( + [("Recommended", dxvk_recommended()), ("None", dxvk_none())] + .into_iter() + .enumerate() + .map(|(index, (name, preset))| { + chip( + ("dxvk-preset", index), + name, + None, + self.dxvk_draft == preset, + !self.busy, + cx.listener(move |view, _: &ClickEvent, window, cx| { + view.dxvk_draft = preset.clone(); + view.show_dxvk_draft(window, cx); + cx.notify(); + }), + ) + }), + ); + let threads = div() + .flex() + .flex_wrap() + .items_center() + .gap(px(8.)) + .flex_1() + .min_w_0() + .child(div().w(px(80.)).child(self.field("dxvk-threads"))) + .child(chip( + "threads-auto", + "Recommended: Auto (0)", + None, + false, + !self.busy, + cx.listener(|view, _: &ClickEvent, window, cx| { + view.set_text("dxvk-threads", "0".into(), window, cx) + }), + )) + .when_some(detected_threads, |this, threads| { + this.child(chip( + "threads-manual", + format!("Manual: {threads}"), + None, + false, + !self.busy, + cx.listener(move |view, _: &ClickEvent, window, cx| { + view.set_text("dxvk-threads", threads.to_string(), window, cx) + }), + )) + }); + let tear_free = segmented( + ["Auto", "True", "False"] + .into_iter() + .map(|mode| { + ( + SharedString::from(mode), + self.dxvk_draft["tearFree"] == mode, + handler(cx.listener(move |view, _: &ClickEvent, _, cx| { + view.dxvk_draft["tearFree"] = json!(mode); + cx.notify(); + })), + ) + }) + .collect(), + !self.busy, + ) + .flex_1(); + let fps = self.slider_value("dxvk-fps", cx); + bare_panel( + "DXVK-GPLAsync", + div() + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .child(control_bar) + .child( + columns() + .child(quick_row("Preset", presets)) + .child(quick_row("Compiler threads", threads)), + ) + .child( + columns() + .items_start() + .child(column(340.).child(group( + "Shader Pipeline", + vec![ + self.dxvk_toggle( + cx, + "dxvk-async", + "Async shader compilation", + "enableAsync", + ), + self.dxvk_toggle( + cx, + "dxvk-gpl", + "GPL async cache", + "gplAsyncCache", + ), + self.dxvk_toggle(cx, "dxvk-hud", "FPS/GPU HUD", "hudEnabled"), + ], + ))) + .child(column(340.).child(group( + "Frame Pacing", + vec![ + group_row( + "Frame rate cap", + div() + .flex() + .items_center() + .gap(px(10.)) + .flex_1() + .min_w(px(160.)) + .child( + div().flex().flex_1().min_w(px(100.)).child( + Slider::new(&self.sliders["dxvk-fps"]) + .disabled(self.busy), + ), + ) + .child( + div() + .w(px(36.)) + .flex_none() + .text_right() + .font_weight(FontWeight::BOLD) + .child(fps.to_string()), + ), + ), + group_row( + "Frame latency", + div().w(px(120.)).child(self.field("dxvk-latency")), + ), + group_row("Tear Free", tear_free), + ], + ))), + ), + ) + } + + pub(super) fn graphics_view(&self, cx: &mut Context) -> AnyElement { + let tab = self.graphics_tab; + let tabs = segmented( + [ + (GraphicsTab::Settings, "Graphics Settings"), + (GraphicsTab::Dxvk, "DXVK"), + ] + .into_iter() + .map(|(target, label)| { + ( + SharedString::from(label), + tab == target, + handler(cx.listener(move |view, _: &ClickEvent, _, cx| { + view.graphics_tab = target; + cx.notify(); + })), + ) + }) + .collect(), + true, + ); + div() + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .child(tabs) + .child(match tab { + GraphicsTab::Settings => self.graphics_settings(cx), + GraphicsTab::Dxvk => self.dxvk_view(cx), + }) + .into_any_element() + } +} diff --git a/native/gpui/src/ui/modals.rs b/native/gpui/src/ui/modals.rs new file mode 100644 index 0000000..3bba448 --- /dev/null +++ b/native/gpui/src/ui/modals.rs @@ -0,0 +1,150 @@ +//! Confirmation and Steam-depot dialogs (`.modal-backdrop`, `.depot-modal`). +use super::components::{Btn, Glyph, icon, modal, modal_surface}; +use super::{ControlCenter, theme}; +use gpui::{prelude::*, *}; +use std::time::Instant; + +fn code_box(text: impl Into) -> Div { + div() + .p(px(12.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::scrim().alpha(0.35)) + .text_size(px(theme::FONT_SM)) + .child(text.into()) +} + +impl ControlCenter { + /// Every staged action asks for confirmation before it reaches the service. + pub(super) fn confirm_modal(&self, cx: &mut Context) -> Option
{ + let (label, requests) = self.pending.as_ref()?; + let steps = requests.len(); + let target = self.value("/gameDir"); + Some(modal( + modal_surface() + .child( + div() + .text_size(px(theme::FONT_LG + 2.)) + .font_weight(FontWeight::BOLD) + .child("Confirm operation"), + ) + .child(div().text_color(theme::muted_strong()).child(if steps > 1 { + format!("{label} ({steps} steps)") + } else { + label.clone() + })) + .child(code_box(format!("Target: {target}"))) + .child( + div() + .flex() + .justify_end() + .gap(px(10.)) + .child(Btn::new("cancel", "Cancel").on_click(cx.listener( + |view, _: &ClickEvent, _, cx| { + view.pending = None; + cx.notify(); + }, + ))) + .child( + Btn::new("confirm", "Confirm") + .primary() + .disabled(self.busy) + .on_click( + cx.listener(|view, _: &ClickEvent, _, cx| view.confirm(cx)), + ), + ), + ), + )) + } + + /// Shown when the compatible EXE needs the Steam depot first. + pub(super) fn depot_modal(&self, cx: &mut Context) -> Option
{ + let depot = self.depot.as_ref()?; + let watching = depot.watching; + let copied = depot.copied; + Some(modal( + modal_surface() + .child( + div() + .flex() + .items_center() + .justify_center() + .size(px(54.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .child(icon(Glyph::Clipboard, 28., theme::accent())), + ) + .child( + div() + .flex() + .flex_col() + .gap(px(10.)) + .child( + div() + .text_size(px(theme::FONT_LG + 2.)) + .font_weight(FontWeight::BOLD) + .child("Copy this"), + ) + .child( + div().child( + "Run this command in the Steam console to download the compatible build.", + ), + ) + .child(code_box(depot.command.clone())) + .when(watching, |this| { + this.child( + div() + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme::ok()) + .child( + "Watching for the depot. PatchOpsIII will install it when ready.", + ), + ) + }), + ) + .child( + div() + .flex() + .gap(px(10.)) + .child( + Btn::new("depot-cancel", "Cancel").grow().on_click(cx.listener( + |view, _: &ClickEvent, _, cx| { + view.depot = None; + cx.notify(); + }, + )), + ) + .child( + Btn::new("depot-copy", if copied { "Copied" } else { "Copy" }) + .grow() + .icon(Glyph::Clipboard) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + if let Some(depot) = view.depot.as_mut() { + depot.copied = true; + cx.write_to_clipboard(ClipboardItem::new_string( + depot.command.clone(), + )); + } + cx.notify(); + })), + ) + .child( + Btn::new("depot-continue", "Continue") + .grow() + .primary() + .icon(Glyph::ExternalLink) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + cx.open_url("steam://open/console"); + if let Some(depot) = view.depot.as_mut() { + depot.watching = true; + depot.last_poll = Instant::now(); + } + cx.notify(); + })), + ), + ), + )) + } +} diff --git a/native/gpui/src/ui/t7.rs b/native/gpui/src/ui/t7.rs new file mode 100644 index 0000000..7657bab --- /dev/null +++ b/native/gpui/src/ui/t7.rs @@ -0,0 +1,533 @@ +//! T7 Patch: install state, gamertag and network security. +use super::components::{ + Btn, Glyph, boxed_metric, chip, column, columns, field_label, icon_button, inline_pill, + module_row, panel, switch, text_xs, +}; +use super::{ControlCenter, theme}; +use gpui::{prelude::*, *}; +use serde_json::json; + +const GAMERTAG_COLORS: [(&str, &str, u32); 10] = [ + ("", "Default", 0xf6f6f6), + ("^1", "Red", 0xff453a), + ("^2", "Green", 0x34c759), + ("^3", "Yellow", 0xffd60a), + ("^4", "Blue", 0x5ac8fa), + ("^5", "Cyan", 0x64d2ff), + ("^6", "Pink", 0xff2d55), + ("^8", "Mid Blue", 0x0a84ff), + ("^9", "Cinnabar", 0xff6b35), + ("^0", "Black", 0x151518), +]; + +fn color_for(code: &str) -> (&'static str, Hsla) { + let (_, label, hex) = GAMERTAG_COLORS + .iter() + .find(|(candidate, ..)| *candidate == code) + .unwrap_or(&GAMERTAG_COLORS[0]); + (label, rgb(*hex).into()) +} + +/// `.compact-setting`: a bordered row with a label and a switch. +fn compact_setting(label: &str, control: impl IntoElement) -> Div { + div() + .flex() + .items_center() + .justify_between() + .gap(px(12.)) + .min_h(px(44.)) + .px(px(14.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.032)) + .child(div().flex_1().min_w_0().child(label.to_owned())) + .child(control) +} + +fn tone(ok: bool) -> Hsla { + if ok { + theme::ok() + } else { + theme::muted_strong() + } +} + +impl ControlCenter { + fn t7_overview(&self, cx: &mut Context) -> Div { + let installed = self.flag("/t7/installed"); + let conf = self.flag("/t7/confExists"); + let mode = self.value("/t7/mode"); + let divider = |content: Div, min: f32| { + content + .flex_1() + .min_w(px(min)) + .border_r_1() + .border_color(theme::border()) + }; + panel( + "T7 Patch", + div() + .flex() + .flex_wrap() + .items_center() + .child(divider( + module_row(Glyph::Shield, "T7 Patch", installed), + 240., + )) + .child(divider( + inline_pill( + "Config", + if conf { + "t7patch.conf found" + } else { + "Config missing" + }, + conf, + ), + 230., + )) + .child(divider( + inline_pill("Game Mode", mode.clone(), mode != "Unknown" && mode != "—"), + 170., + )) + .child( + div() + .flex() + .flex_1() + .min_w(px(440.)) + .gap(px(10.)) + .p(px(10.)) + .child( + self.action( + cx, + "t7-install", + "Install / Update T7 Patch", + "/api/t7-install", + json!({}), + ) + .icon(Glyph::Download) + .primary() + .grow(), + ) + .child( + self.action( + cx, + "t7-uninstall", + "Uninstall T7 Patch", + "/api/t7-uninstall", + json!({}), + ) + .icon(Glyph::Trash) + .disabled(!self.can_act() || !installed) + .grow(), + ), + ), + ) + } + + fn t7_gamertag_panel(&self, cx: &mut Context) -> Div { + let conf = self.flag("/t7/confExists"); + let saved_name = self.string("/t7/plainName"); + let saved_color = self.string("/t7/colorCode"); + let draft = self.text("gamertag", cx); + let preview = if draft.trim().is_empty() { + if saved_name.is_empty() { + "None".to_owned() + } else { + saved_name.clone() + } + } else { + draft.trim().to_owned() + }; + let pending = draft != saved_name || self.t7_color != saved_color; + let (_, preview_color) = color_for(&self.t7_color); + let (_, saved_swatch) = color_for(&saved_color); + let saved_display = if saved_name.is_empty() { + "None".to_owned() + } else { + saved_name.clone() + }; + let enabled = conf && !self.busy; + panel( + "Gamertag", + div() + .flex() + .flex_col() + .gap(px(14.)) + .p(px(14.)) + .child( + div() + .flex() + .items_center() + .justify_center() + .min_h(px(82.)) + .p(px(14.)) + .border_1() + .border_color(theme::white(0.12)) + .rounded(theme::radius_card()) + .bg(theme::white(0.035)) + .child( + div() + .max_w_full() + .truncate() + .text_size(px(30.)) + .font_weight(FontWeight::EXTRA_BOLD) + .text_color(preview_color) + .child(preview), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap(px(6.)) + .child(field_label("Name")) + .child(self.field("gamertag").disabled(!enabled)), + ) + .child(div().flex().flex_col().gap(px(7.)).children( + GAMERTAG_COLORS.chunks(5).enumerate().map(|(row, colors)| { + div() + .flex() + .gap(px(7.)) + .children(colors.iter().enumerate().map(|(col, (code, label, hex))| { + let code = (*code).to_owned(); + chip( + ("color", row * 5 + col), + *label, + Some(rgb(*hex).into()), + self.t7_color == code, + enabled, + cx.listener(move |view, _: &ClickEvent, _, cx| { + view.t7_color = code.clone(); + cx.notify(); + }), + ) + .flex_1() + .text_size(px(11.)) + })) + }), + )) + .child( + div() + .flex() + .gap(px(8.)) + .child( + div() + .flex() + .items_center() + .gap(px(12.)) + .flex_1() + .min_w_0() + .p(px(10.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::white(0.026)) + .child(text_xs("Currently Saved", theme::muted()).flex_none()) + .child( + div() + .flex() + .items_center() + .flex_1() + .min_w_0() + .gap(px(10.)) + .child( + div() + .truncate() + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::BOLD) + .text_color(theme::muted_strong()) + .child(saved_display), + ) + .child( + div() + .size(px(12.)) + .flex_none() + .rounded_full() + .border_1() + .border_color(theme::white(0.42)) + .bg(saved_swatch), + ) + .child( + div() + .ml_auto() + .flex_none() + .text_size(px(theme::FONT_XS)) + .font_weight(FontWeight::BOLD) + .text_color(theme::muted_strong()) + .child(format!( + "{}/20", + saved_name.chars().count() + )), + ), + ), + ) + .child( + boxed_metric( + "Status", + if pending { "Unsaved" } else { "Saved" }, + if pending { + theme::warning() + } else { + theme::ok() + }, + ) + .flex_none() + .w(px(120.)), + ), + ) + .child( + div() + .flex() + .gap(px(10.)) + .child( + Btn::new("t7-reset-name", "Reset Edits") + .compact() + .grow() + .disabled(!pending || self.busy) + .on_click(cx.listener(|view, _: &ClickEvent, window, cx| { + view.load_t7(window, cx); + cx.notify(); + })), + ) + .child( + Btn::new("t7-save-name", "Update Gamertag") + .compact() + .icon(Glyph::Save) + .grow() + .disabled(!self.can_act() || !conf) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + let name = view.text("gamertag", cx); + let name = name.trim().to_owned(); + if name.is_empty() { + view.reject("Gamertag cannot be empty.", cx); + } else if name.chars().count() > 20 { + view.reject("Gamertag cannot exceed 20 characters.", cx); + } else { + let color = view.t7_color.clone(); + view.stage( + "Update T7 gamertag".into(), + "/api/t7-config".into(), + json!({"gamertag": name, "colorCode": color}), + cx, + ); + } + })), + ), + ), + ) + } + + fn t7_security_panel(&self, cx: &mut Context) -> Div { + let conf = self.flag("/t7/confExists"); + let friends = self.flag("/t7/friendsOnly"); + let saved_password = self.string("/t7/networkPassword"); + let has_password = !saved_password.is_empty(); + let enabled = self.t7_password_enabled; + let controls = conf && enabled; + let pending = enabled != has_password || !self.text("password", cx).is_empty(); + let show_current = self.show_current_password; + let show_new = self.show_new_password; + let current_display = if saved_password.is_empty() { + ("None".to_owned(), theme::muted()) + } else if show_current { + (saved_password.clone(), theme::text()) + } else { + ( + "•".repeat(saved_password.chars().count().min(24)), + theme::text(), + ) + }; + panel( + "Security", + div() + .flex() + .flex_col() + .gap(px(14.)) + .p(px(14.)) + .child(compact_setting( + "Friends Only Mode", + switch( + "t7-friends", + friends, + conf && self.can_act(), + self.stage_click( + cx, + format!( + "{} Friends Only mode", + if friends { "Disable" } else { "Enable" } + ), + "/api/t7-config", + json!({"friendsOnly": !friends}), + ), + ), + )) + .child(compact_setting( + "Network Password", + switch( + "t7-password-toggle", + enabled, + conf && !self.busy && self.pending.is_none(), + cx.listener(|view, _: &ClickEvent, _, cx| { + if view.t7_password_enabled { + view.stage( + "Remove the T7 network password".into(), + "/api/t7-config".into(), + json!({"networkPassword": ""}), + cx, + ); + } else { + view.t7_password_enabled = true; + view.t7_password_touched = true; + cx.notify(); + } + }), + ), + )) + .child( + columns() + .child( + column(220.) + .gap(px(6.)) + .child(field_label("Current Password")) + .child( + div() + .flex() + .gap(px(8.)) + .child( + div() + .flex() + .items_center() + .flex_1() + .min_w_0() + .min_h(px(theme::CONTROL_HEIGHT - 2.)) + .px(px(10.)) + .border_1() + .border_color(theme::border()) + .rounded(theme::radius_card()) + .bg(theme::field()) + .text_color(current_display.1) + .when(!controls, |this| this.opacity(0.58)) + .child(div().truncate().child(current_display.0)), + ) + .child(icon_button( + "show-current-password", + if show_current { Glyph::EyeOff } else { Glyph::Key }, + controls, + cx.listener(|view, _: &ClickEvent, _, cx| { + view.show_current_password = !view.show_current_password; + cx.notify(); + }), + )), + ), + ) + .child( + column(220.) + .gap(px(6.)) + .child(field_label("Password")) + .child( + div() + .flex() + .gap(px(8.)) + .child( + div() + .flex_1() + .min_w_0() + .child(self.field("password").disabled(self.busy || !controls)), + ) + .child(icon_button( + "show-new-password", + if show_new { Glyph::EyeOff } else { Glyph::Key }, + controls, + cx.listener(|view, _: &ClickEvent, window, cx| { + view.show_new_password = !view.show_new_password; + let masked = !view.show_new_password; + view.inputs["password"].update(cx, |state, cx| { + state.set_masked(masked, window, cx) + }); + cx.notify(); + }), + )), + ), + ), + ) + .child( + div() + .flex() + .gap(px(8.)) + .child(boxed_metric( + "Friends Only", + if friends { "Enabled" } else { "Disabled" }, + tone(friends), + )) + .child(boxed_metric( + "Password", + if has_password { "Set" } else { "Not set" }, + tone(has_password), + )) + .child(boxed_metric( + "Status", + if pending { "Unsaved" } else { "Saved" }, + if pending { theme::warning() } else { theme::ok() }, + )), + ) + .child( + div() + .flex() + .gap(px(10.)) + .child( + Btn::new("t7-reset-security", "Reset Edits") + .compact() + .grow() + .disabled(!pending || self.busy) + .on_click(cx.listener(|view, _: &ClickEvent, window, cx| { + view.t7_password_touched = false; + view.t7_password_enabled = !view.string("/t7/networkPassword").is_empty(); + view.set_text("password", String::new(), window, cx); + cx.notify(); + })), + ) + .child( + Btn::new("t7-save-password", "Update Security") + .compact() + .icon(Glyph::Save) + .grow() + .disabled(!self.can_act() || !controls) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| { + let password = view.text("password", cx); + let password = password.trim().to_owned(); + if password.is_empty() { + view.reject( + "Enter a network password, or turn the toggle off to remove it.", + cx, + ); + } else { + view.stage( + "Update T7 network password".into(), + "/api/t7-config".into(), + json!({"networkPassword": password}), + cx, + ); + } + })), + ), + ), + ) + } + + pub(super) fn t7_view(&self, cx: &mut Context) -> AnyElement { + div() + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .child(self.t7_overview(cx)) + .child( + columns() + .items_start() + .child(column(340.).child(self.t7_gamertag_panel(cx))) + .child(column(340.).child(self.t7_security_panel(cx))), + ) + .into_any_element() + } +} diff --git a/native/gpui/src/ui/theme.rs b/native/gpui/src/ui/theme.rs new file mode 100644 index 0000000..4d7c54f --- /dev/null +++ b/native/gpui/src/ui/theme.rs @@ -0,0 +1,208 @@ +//! Design tokens ported from `src/renderer/styles/app.css`. +//! +//! The Electron app computes most sizes with `clamp()`; the values here are the +//! ones that CSS resolves to at the default 1150x820 window. +use gpui::{App, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Pixels, px, rgb}; +use gpui_component::Theme; + +pub const FONT_XS: f32 = 12.; +pub const FONT_SM: f32 = 13.; +pub const FONT_MD: f32 = 14.; +pub const FONT_LG: f32 = 16.; + +pub const CONTROL_HEIGHT: f32 = 36.; +pub const ROW_HEIGHT: f32 = 38.; +pub const NAV_WIDTH: f32 = 190.; +pub const APP_PAD: f32 = 14.; +pub const APP_GAP: f32 = 10.; +pub const PANEL_GAP: f32 = 12.; + +/// `--radius`. +pub fn radius() -> Pixels { + px(9.) +} + +/// `calc(var(--radius) - 3px)`: buttons and pills. +pub fn radius_control() -> Pixels { + px(6.) +} + +/// `calc(var(--radius) - 4px)`: inputs, cards and chips. +pub fn radius_card() -> Pixels { + px(5.) +} + +fn hex(value: u32) -> Hsla { + rgb(value).into() +} + +fn tint(value: u32, alpha: f32) -> Hsla { + hex(value).alpha(alpha) +} + +pub fn white(alpha: f32) -> Hsla { + tint(0xffffff, alpha) +} + +pub fn bg() -> Hsla { + hex(0x0d0d0f) +} + +pub fn bg_deep() -> Hsla { + hex(0x050505) +} + +pub fn panel() -> Hsla { + white(0.055) +} + +pub fn panel_strong() -> Hsla { + white(0.08) +} + +pub fn field() -> Hsla { + tint(0x000000, 0.26) +} + +pub fn border() -> Hsla { + white(0.12) +} + +pub fn border_strong() -> Hsla { + white(0.18) +} + +pub fn text() -> Hsla { + hex(0xf6f6f6) +} + +pub fn muted() -> Hsla { + tint(0xf6f6f6, 0.68) +} + +pub fn muted_strong() -> Hsla { + tint(0xf6f6f6, 0.82) +} + +pub fn accent() -> Hsla { + hex(0xfd0100) +} + +pub fn accent_dark() -> Hsla { + hex(0x9a0f0e) +} + +pub fn accent_alpha(alpha: f32) -> Hsla { + tint(0xfd0100, alpha) +} + +pub fn ok() -> Hsla { + hex(0x34c759) +} + +pub fn ok_alpha(alpha: f32) -> Hsla { + tint(0x34c759, alpha) +} + +pub fn warning() -> Hsla { + hex(0xff9f0a) +} + +/// The `#ffb020` amber used by validation and requirement warnings. +pub fn caution() -> Hsla { + hex(0xffb020) +} + +pub fn danger() -> Hsla { + hex(0xff453a) +} + +pub fn danger_alpha(alpha: f32) -> Hsla { + tint(0xff453a, alpha) +} + +/// `#ffd8d4`: text on danger-tinted surfaces. +pub fn danger_text() -> Hsla { + hex(0xffd8d4) +} + +/// Status dot for "not configured" rows. +pub fn dot_idle() -> Hsla { + hex(0xa8b2be) +} + +pub fn modal_surface() -> Hsla { + hex(0x101011) +} + +pub fn scrim() -> Hsla { + tint(0x000000, 0.62) +} + +/// The Electron app's `ui-sans-serif, system-ui, ...` stack. GPUI's +/// `.SystemUIFont` resolves to IBM Plex Sans on Linux, which most systems lack. +pub fn ui_font() -> Font { + let (family, fallbacks): (&str, &[&str]) = if cfg!(target_os = "macos") { + (".SystemUIFont", &[]) + } else if cfg!(target_os = "windows") { + ("Segoe UI", &["Arial"]) + } else { + ( + "Noto Sans", + &["Roboto", "DejaVu Sans", "Liberation Sans", "Arial"], + ) + }; + Font { + family: family.into(), + features: FontFeatures::default(), + fallbacks: Some(FontFallbacks::from_fonts( + fallbacks.iter().map(|name| (*name).to_owned()).collect(), + )), + weight: FontWeight::NORMAL, + style: FontStyle::Normal, + } +} + +/// Mirror the CSS variables onto gpui-component's theme so its widgets +/// (inputs, sliders, scrollbars) match the hand-built controls. +pub fn apply(cx: &mut App) { + let theme = Theme::global_mut(cx); + theme.font_family = ui_font().family; + theme.font_size = px(FONT_MD); + theme.mono_font_size = px(FONT_XS); + theme.radius = radius_card(); + theme.radius_lg = radius(); + theme.shadow = false; + let colors = &mut theme.colors; + colors.background = bg(); + colors.foreground = text(); + colors.border = border(); + colors.input = border(); + colors.caret = text(); + colors.selection = accent_alpha(0.32); + colors.muted = panel_strong(); + colors.muted_foreground = muted(); + colors.accent = white(0.07); + colors.accent_foreground = text(); + colors.primary = accent(); + colors.primary_hover = hex(0xf03535); + colors.primary_active = accent_dark(); + colors.primary_foreground = hex(0xffffff); + colors.secondary = panel_strong(); + colors.secondary_hover = white(0.12); + colors.secondary_active = white(0.16); + colors.secondary_foreground = text(); + colors.ring = accent_alpha(0.72); + colors.danger = danger(); + colors.success = ok(); + colors.warning = warning(); + colors.popover = modal_surface(); + colors.popover_foreground = text(); + colors.slider_bar = accent(); + colors.slider_thumb = text(); + colors.scrollbar = white(0.045); + colors.scrollbar_thumb = white(0.26); + colors.scrollbar_thumb_hover = accent_alpha(0.58); + colors.switch = field(); + colors.switch_thumb = muted(); +} diff --git a/native/gpui/src/ui/tools.rs b/native/gpui/src/ui/tools.rs new file mode 100644 index 0000000..6a5fb6b --- /dev/null +++ b/native/gpui/src/ui/tools.rs @@ -0,0 +1,222 @@ +//! Tools: system information, updates, caches and logs. +use super::components::{ + Btn, Glyph, bare_panel, card, card_title, columns, handler, segmented, tool_metric, +}; +use super::{ControlCenter, theme}; +use gpui::{prelude::*, *}; +use serde_json::json; + +impl ControlCenter { + fn tool_card(&self, glyph: Glyph, title: &str) -> Div { + card() + .flex_1() + .min_w(px(300.)) + .min_h(px(154.)) + .child(card_title(Some(glyph), title.to_owned())) + } + + fn copy_logs(&mut self, cx: &mut Context) { + let payload = self.string("/maintenance/logPayload"); + let payload = if payload.is_empty() { + self.visible_logs() + .into_iter() + .map(|(category, message)| format!("[{category}] {message}")) + .collect::>() + .join("\n") + } else { + payload + }; + if payload.is_empty() { + self.reject("There are no log entries to copy.", cx); + return; + } + cx.write_to_clipboard(ClipboardItem::new_string(payload)); + self.failed = false; + self.message = "Copied the logs to the clipboard.".into(); + cx.notify(); + } + + pub(super) fn tools_view(&self, cx: &mut Context) -> AnyElement { + let can_act = self.can_act(); + let channel = self.string("/releaseChannel"); + let channel_label = if channel == "beta" { "Beta" } else { "Stable" }; + let platform = self.string("/platform"); + let steam_id = self.string("/steamUserId"); + let mod_files = self.string("/maintenance/modFilesDir"); + let log_path = self.string("/logPath"); + let entries = self.visible_logs(); + let last_error = entries.iter().any(|(category, _)| category == "Error"); + let channels = segmented( + [("stable", "Stable"), ("beta", "Beta")] + .into_iter() + .map(|(id, label)| { + ( + SharedString::from(label), + channel == id, + handler(self.stage_click( + cx, + format!("Use the {label} release channel"), + "/api/release-channel", + json!({"channel": id}), + )), + ) + }) + .collect(), + can_act, + ); + let system = self + .tool_card(Glyph::Cpu, "System") + .child(tool_metric( + "Platform", + if platform.is_empty() { + "Unknown".to_owned() + } else { + platform.clone() + }, + !platform.is_empty(), + )) + .child(tool_metric( + "Steam ID", + if steam_id.is_empty() { + "Not found".to_owned() + } else { + steam_id.clone() + }, + !steam_id.is_empty(), + )) + .child(tool_metric("PatchOps", self.value("/appVersion"), true)); + let updates = self + .tool_card(Glyph::Refresh, "Updates") + .child(channels) + .child(tool_metric("Current", channel_label, true)) + .child(tool_metric("Last checked", "On demand", false)) + .child( + self.action( + cx, + "check-updates", + "Check for Updates", + "/api/update-check", + json!({}), + ) + .compact() + .icon(Glyph::Refresh), + ); + let cache = self + .tool_card(Glyph::Download, "Mod Cache") + .child(tool_metric( + "Status", + if mod_files.is_empty() { + "Missing" + } else { + "Configured" + }, + !mod_files.is_empty(), + )) + .child(tool_metric( + "Directory", + if mod_files.is_empty() { + "Not found" + } else { + "Ready" + }, + !mod_files.is_empty(), + )); + let cache_actions = self.tool_card(Glyph::Trash, "Cache Actions").child( + div() + .flex() + .flex_wrap() + .gap(px(8.)) + .child( + self.action( + cx, + "clear-mod-files", + "Clear Mod Files", + "/api/mod-files/clear", + json!({}), + ) + .compact() + .icon(Glyph::Trash) + .grow(), + ) + .child( + self.action( + cx, + "reset-stock", + "Reset to Stock", + "/api/reset-stock", + json!({}), + ) + .compact() + .icon(Glyph::Rotate) + .danger() + .grow() + .disabled(!can_act || !self.flag("/gameDetected")), + ), + ); + let logs = self + .tool_card(Glyph::Clipboard, "Logs") + .child(tool_metric( + "Log file", + if log_path.is_empty() { + "Missing" + } else { + "Available" + }, + !log_path.is_empty(), + )) + .child(tool_metric( + "Last error", + if last_error { + "See Activity Log" + } else { + "None" + }, + !last_error, + )) + .child(tool_metric( + "Entries", + entries.len().to_string(), + !entries.is_empty(), + )) + .when(!log_path.is_empty(), |this| { + this.child( + div() + .truncate() + .text_size(px(theme::FONT_XS)) + .text_color(theme::muted()) + .child(log_path.clone()), + ) + }); + let log_actions = self.tool_card(Glyph::Eraser, "Log Actions").child( + div() + .flex() + .flex_wrap() + .gap(px(8.)) + .child( + Btn::new("copy-logs", "Copy Logs") + .compact() + .icon(Glyph::Clipboard) + .grow() + .disabled(!self.connected) + .on_click(cx.listener(|view, _: &ClickEvent, _, cx| view.copy_logs(cx))), + ) + .child( + self.action(cx, "clear-logs", "Clear Logs", "/api/logs/clear", json!({})) + .compact() + .icon(Glyph::Eraser) + .grow(), + ), + ); + bare_panel( + "Tools", + div() + .flex() + .flex_col() + .gap(px(theme::PANEL_GAP)) + .child(columns().child(system).child(updates)) + .child(columns().child(cache).child(cache_actions)) + .child(columns().child(logs).child(log_actions)), + ) + .into_any_element() + } +} diff --git a/package.json b/package.json index 7bda4cf..2c836cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "patchopsiii-electron", "version": "v1.3.4", - "description": "Electron control center for Call of Duty: Black Ops III maintenance, mod setup, and performance tuning.", + "description": "Native GPUI control center for Call of Duty: Black Ops III maintenance, mod setup, and performance tuning (with a legacy Electron client).", "author": "boggedbrush", "private": true, "type": "module", @@ -15,6 +15,9 @@ "dist": "bun run build && bun x electron-builder@26.8.1", "build:backend:linux": "bun scripts/check-backend-build.ts linux && .venv/bin/python -m PyInstaller --onefile --name patchops-backend --distpath dist/backend --workpath build/pyinstaller --specpath build/pyinstaller --hidden-import uvicorn.logging --hidden-import uvicorn.loops --hidden-import uvicorn.loops.auto --hidden-import uvicorn.protocols --hidden-import uvicorn.protocols.http --hidden-import uvicorn.protocols.http.auto --hidden-import uvicorn.protocols.websockets --hidden-import uvicorn.protocols.websockets.auto --hidden-import uvicorn.lifespan --hidden-import uvicorn.lifespan.on backend/api.py", "build:backend:win": "bun scripts/check-backend-build.ts win && .venv/Scripts/python.exe -m PyInstaller --onefile --name patchops-backend --distpath dist/backend --workpath build/pyinstaller --specpath build/pyinstaller --hidden-import uvicorn.logging --hidden-import uvicorn.loops --hidden-import uvicorn.loops.auto --hidden-import uvicorn.protocols --hidden-import uvicorn.protocols.http --hidden-import uvicorn.protocols.http.auto --hidden-import uvicorn.protocols.websockets --hidden-import uvicorn.protocols.websockets.auto --hidden-import uvicorn.lifespan --hidden-import uvicorn.lifespan.on backend/api.py", + "gpui:build": "python scripts/gpui.py build", + "gpui:build:release": "python scripts/gpui.py build --release", + "gpui:run": "python scripts/gpui.py run", "dist:linux": "bun run build && bun run build:backend:linux && bun x electron-builder@26.8.1 --linux AppImage --publish never", "dist:win": "bun run build && bun run build:backend:win && bun x electron-builder@26.8.1 --win --publish never --config.win.signAndEditExecutable=false", "typecheck": "tsc --noEmit", diff --git a/scripts/gpui.py b/scripts/gpui.py index 602e6e1..8306c01 100644 --- a/scripts/gpui.py +++ b/scripts/gpui.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build/run the GPUI evaluation without changing Electron's processes or ports.""" +"""Build or run the GPUI desktop app with a source-backed development service.""" from __future__ import annotations import argparse diff --git a/scripts/package_gpui.py b/scripts/package_gpui.py index db911d8..4ee1b47 100644 --- a/scripts/package_gpui.py +++ b/scripts/package_gpui.py @@ -1,28 +1,75 @@ #!/usr/bin/env python3 -"""Create an evaluation archive; Python and API dependencies remain prerequisites.""" +"""Create a self-contained PatchOpsIII native desktop archive.""" +from __future__ import annotations + import argparse -from pathlib import Path +from pathlib import Path, PurePosixPath from zipfile import ZIP_DEFLATED, ZipFile ROOT = Path(__file__).resolve().parents[1] +ARCHIVE_ROOT = PurePosixPath("PatchOpsIII") + + +def archive_manifest(executable: Path, backend: Path) -> list[tuple[Path, PurePosixPath]]: + required = [ + ROOT / "LICENSE", + ROOT / "README.md", + ROOT / "package.json", + ROOT / "presets.json", + ROOT / "PatchOpsIII.ico", + ROOT / "website" / "assets" / "img" / "icon-512.png", + ROOT / "native" / "gpui" / "assets" / "icons" / "LICENSE-lucide.txt", + ROOT / "native" / "gpui" / "README.md", + ROOT / "docs" / "gpui-architecture.md", + ] + missing = [path for path in [executable, backend, *required] if not path.is_file()] + if missing: + names = ", ".join(str(path) for path in missing) + raise FileNotFoundError(f"Required packaging input missing: {names}") + + return [ + (executable, ARCHIVE_ROOT / executable.name), + (backend, ARCHIVE_ROOT / "resources" / "backend-bin" / backend.name), + (ROOT / "presets.json", ARCHIVE_ROOT / "resources" / "presets.json"), + (ROOT / "package.json", ARCHIVE_ROOT / "resources" / "package.json"), + (ROOT / "PatchOpsIII.ico", ARCHIVE_ROOT / "resources" / "PatchOpsIII.ico"), + ( + ROOT / "website" / "assets" / "img" / "icon-512.png", + ARCHIVE_ROOT / "resources" / "icon-512.png", + ), + (ROOT / "LICENSE", ARCHIVE_ROOT / "LICENSE"), + ( + ROOT / "native" / "gpui" / "assets" / "icons" / "LICENSE-lucide.txt", + ARCHIVE_ROOT / "THIRD-PARTY-NOTICES" / "Lucide.txt", + ), + (ROOT / "README.md", ARCHIVE_ROOT / "README.md"), + (ROOT / "native" / "gpui" / "README.md", ARCHIVE_ROOT / "NATIVE-README.md"), + ( + ROOT / "docs" / "gpui-architecture.md", + ARCHIVE_ROOT / "docs" / "native-desktop.md", + ), + ] + + +def create_archive(executable: Path, backend: Path, output: Path) -> Path: + output.parent.mkdir(parents=True, exist_ok=True) + with ZipFile(output, "w", ZIP_DEFLATED) as archive: + for source, destination in archive_manifest(executable, backend): + archive.write(source, destination.as_posix()) + return output -def main(): +def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--backend", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() - if not args.executable.is_file(): - parser.error("Build the executable first") - files = ["LICENSE", "package.json", "requirements.txt", "presets.json", "utils.py", "t7_patch.py", "dxvk_manager.py", "bo3_enhanced.py", "scripts/gpui.py", "native/gpui/README.md", "docs/gpui-evaluation.md"] - files += [str(path.relative_to(ROOT)) for path in (ROOT / "backend").glob("*.py")] - files += [str(path.relative_to(ROOT)) for path in (ROOT / "docs" / "images").glob("gpui-*.png")] - args.output.parent.mkdir(parents=True, exist_ok=True) - with ZipFile(args.output, "w", ZIP_DEFLATED) as archive: - archive.write(args.executable, f"bin/{args.executable.name}") - for name in files: - archive.write(ROOT / name, name) - print(args.output) + try: + output = create_archive(args.executable, args.backend, args.output) + except FileNotFoundError as error: + parser.error(str(error)) + print(output) if __name__ == "__main__": diff --git a/scripts/smoke_gpui_linux.py b/scripts/smoke_gpui_linux.py index 1bd8002..de307a3 100644 --- a/scripts/smoke_gpui_linux.py +++ b/scripts/smoke_gpui_linux.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -"""Render a native window and close it through X11 against a read-only fake API. +"""Render a native window and close it through X11 against a safe local API. Requires an X11 DISPLAY, xdotool, and a compositor/window manager. CI uses -xvfb-run with --start-session. No real game service or game mutations are used. +xvfb-run with --start-session. By default a read-only fake API is injected; +--use-owned-backend exercises the packaged backend. No mutations are requested. """ import argparse from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer @@ -47,6 +48,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--executable", type=Path, required=True) parser.add_argument("--start-session", action="store_true") + parser.add_argument("--use-owned-backend", action="store_true") args = parser.parse_args() owned = [] with ThreadingHTTPServer(("127.0.0.1", 0), ReadOnlyStatus) as server: @@ -57,7 +59,9 @@ def main(): owned.append(subprocess.Popen(["openbox", "--sm-disable"])) owned.append(subprocess.Popen(["xcompmgr"])) time.sleep(1) - env = {**os.environ, "PATCHOPSIII_GPUI_BACKEND_URL": f"http://127.0.0.1:{server.server_port}"} + env = dict(os.environ) + if not args.use_owned_backend: + env["PATCHOPSIII_GPUI_BACKEND_URL"] = f"http://127.0.0.1:{server.server_port}" app = subprocess.Popen([str(args.executable.resolve())], env=env) owned.append(app) deadline = time.monotonic() + 30 diff --git a/scripts/test_gpui.py b/scripts/test_gpui.py index ae4a975..3011942 100644 --- a/scripts/test_gpui.py +++ b/scripts/test_gpui.py @@ -1,4 +1,4 @@ -"""Lifecycle checks use harmless stand-ins; no game installation is modified.""" +"""Lifecycle and packaging checks use stand-ins; no game installation is modified.""" import importlib.util import os from pathlib import Path @@ -7,11 +7,16 @@ import tempfile import unittest from unittest.mock import patch +from zipfile import ZipFile spec = importlib.util.spec_from_file_location("gpui_launcher", Path(__file__).with_name("gpui.py")) launcher = importlib.util.module_from_spec(spec) spec.loader.exec_module(launcher) +package_spec = importlib.util.spec_from_file_location("gpui_packager", Path(__file__).with_name("package_gpui.py")) +packager = importlib.util.module_from_spec(package_spec) +package_spec.loader.exec_module(packager) + class LifecycleTests(unittest.TestCase): def test_relative_target_directory_is_consistent_for_build_and_run(self): @@ -59,5 +64,41 @@ def test_ui_failure_stops_owned_service(self): launcher.stop(service) +class PackagingTests(unittest.TestCase): + def test_archive_is_self_contained_and_matches_native_lookup_layout(self): + with tempfile.TemporaryDirectory() as directory: + temporary = Path(directory) + executable = temporary / ("patchopsiii-gpui.exe" if os.name == "nt" else "patchopsiii-gpui") + backend = temporary / ("patchops-backend.exe" if os.name == "nt" else "patchops-backend") + executable.write_bytes(b"native") + backend.write_bytes(b"backend") + output = temporary / "PatchOpsIII-native.zip" + + packager.create_archive(executable, backend, output) + + with ZipFile(output) as archive: + names = set(archive.namelist()) + self.assertIn(f"PatchOpsIII/{executable.name}", names) + self.assertIn(f"PatchOpsIII/resources/backend-bin/{backend.name}", names) + self.assertIn("PatchOpsIII/resources/presets.json", names) + self.assertIn("PatchOpsIII/resources/package.json", names) + self.assertIn("PatchOpsIII/resources/PatchOpsIII.ico", names) + self.assertIn("PatchOpsIII/resources/icon-512.png", names) + self.assertIn("PatchOpsIII/THIRD-PARTY-NOTICES/Lucide.txt", names) + self.assertFalse(any(name.endswith(".py") for name in names)) + self.assertEqual( + archive.read(f"PatchOpsIII/resources/backend-bin/{backend.name}"), + b"backend", + ) + + def test_archive_requires_both_product_executables(self): + with tempfile.TemporaryDirectory() as directory: + temporary = Path(directory) + executable = temporary / "patchopsiii-gpui" + executable.write_bytes(b"native") + with self.assertRaisesRegex(FileNotFoundError, "patchops-backend"): + packager.archive_manifest(executable, temporary / "patchops-backend") + + if __name__ == "__main__": unittest.main() From 2f289c572e70b806f4c14ab443c6fc3ba163bf6f Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:02:51 -0400 Subject: [PATCH 03/17] feat(native): run GPUI on in-process patchops-core Rust backend Extract PR #40's operations into a toolkit-independent patchops-core crate in a native/ workspace, replace the Python/HTTP supervisor with an in-process dispatcher that keeps the existing request/response shapes, port the September 2026 EXE build and T7 release-asset fixes from main, and ship native archives without Python. --- .github/workflows/gpui-build.yml | 62 +- .github/workflows/release-beta.yml | 1 + .github/workflows/release-stable.yml | 1 + .gitignore | 3 +- README.md | 30 +- docs/gpui-architecture.md | 170 +- native/{gpui => }/Cargo.lock | 272 +- native/Cargo.toml | 8 + native/core/Cargo.toml | 26 + native/core/build.rs | 11 + native/core/src/app.rs | 880 +++++ native/core/src/dxvk.rs | 1498 +++++++++ native/core/src/engine.rs | 787 +++++ native/core/src/enhanced.rs | 2340 ++++++++++++++ native/core/src/exe.rs | 863 +++++ native/core/src/fs_ops.rs | 655 ++++ native/core/src/lib.rs | 14 + native/core/src/models.rs | 220 ++ native/core/src/steam.rs | 4315 +++++++++++++++++++++++++ native/core/src/t7.rs | 2814 ++++++++++++++++ native/gpui/Cargo.toml | 7 +- native/gpui/README.md | 150 +- native/gpui/src/backend.rs | 523 +-- native/{gpui => }/rust-toolchain.toml | 0 scripts/gpui.py | 86 +- scripts/package_gpui.py | 12 +- scripts/smoke_gpui_linux.py | 65 +- scripts/test_gpui.py | 76 +- 28 files changed, 15098 insertions(+), 791 deletions(-) rename native/{gpui => }/Cargo.lock (97%) create mode 100644 native/Cargo.toml create mode 100644 native/core/Cargo.toml create mode 100644 native/core/build.rs create mode 100644 native/core/src/app.rs create mode 100644 native/core/src/dxvk.rs create mode 100644 native/core/src/engine.rs create mode 100644 native/core/src/enhanced.rs create mode 100644 native/core/src/exe.rs create mode 100644 native/core/src/fs_ops.rs create mode 100644 native/core/src/lib.rs create mode 100644 native/core/src/models.rs create mode 100644 native/core/src/steam.rs create mode 100644 native/core/src/t7.rs rename native/{gpui => }/rust-toolchain.toml (100%) diff --git a/.github/workflows/gpui-build.yml b/.github/workflows/gpui-build.yml index 9bb8f10..94f9a33 100644 --- a/.github/workflows/gpui-build.yml +++ b/.github/workflows/gpui-build.yml @@ -3,12 +3,9 @@ name: PatchOpsIII Native Build on: pull_request: paths: - - 'native/gpui/**' + - 'native/**' - 'scripts/*gpui*.py' - 'scripts/measure_desktop.py' - - 'backend/**' - - '*.py' - - 'requirements.txt' - 'presets.json' - 'package.json' - 'docs/gpui-architecture.md' @@ -39,80 +36,59 @@ jobs: matrix: include: - os: ubuntu-24.04 - executable: native/gpui/target/release/patchopsiii-gpui - backend: dist/backend/patchops-backend + executable: native/target/release/patchopsiii-gpui archive: dist/native/PatchOpsIII-native-linux-x64.zip artifact: PatchOpsIII-native-Linux - os: windows-2022 - executable: native/gpui/target/release/patchopsiii-gpui.exe - backend: dist/backend/patchops-backend.exe + executable: native/target/release/patchopsiii-gpui.exe archive: dist/native/PatchOpsIII-native-windows-x64.zip artifact: PatchOpsIII-native-Windows runs-on: ${{ matrix.os }} env: PYTHON_VERSION: '3.12' - PYINSTALLER_VERSION: '6.20.0' steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} - - uses: oven-sh/setup-bun@v2 - with: - bun-version: '1.3.13' - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - cache: pip - cache-dependency-path: requirements.txt - uses: dtolnay/rust-toolchain@master with: toolchain: '1.99.0' components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 with: - workspaces: native/gpui + workspaces: native - name: Linux native and packaging dependencies if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y binutils libfontconfig1-dev libfreetype6-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libvulkan-dev libx11-xcb-dev libxcb1-dev libasound2-dev libclang-dev mesa-vulkan-drivers xvfb xauth openbox xcompmgr xdotool - - name: Install Python backend dependencies (Linux) - if: runner.os == 'Linux' - run: | - python -m venv .venv - .venv/bin/python -m pip install -U pip - .venv/bin/python -m pip install -r requirements.txt - .venv/bin/python -m pip install pyinstaller==${{ env.PYINSTALLER_VERSION }} - - name: Install Python backend dependencies (Windows) - if: runner.os == 'Windows' - run: | - python -m venv .venv - .\.venv\Scripts\python -m pip install -U pip - .\.venv\Scripts\python -m pip install -r requirements.txt - .\.venv\Scripts\python -m pip install pyinstaller==${{ env.PYINSTALLER_VERSION }} + run: sudo apt-get update && sudo apt-get install -y binutils libfontconfig1-dev libfreetype6-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libvulkan-dev libx11-xcb-dev libxcb1-dev libasound2-dev libclang-dev mesa-vulkan-drivers xvfb xauth openbox xcompmgr xdotool bubblewrap - name: Python lifecycle and packaging tests run: python -m unittest scripts/test_gpui.py - name: Format - run: cargo fmt --manifest-path native/gpui/Cargo.toml --check + run: cargo fmt --manifest-path native/Cargo.toml --all --check - name: Lint - run: cargo clippy --locked --manifest-path native/gpui/Cargo.toml --all-targets -- -D warnings - - name: Rust tests - run: python scripts/gpui.py test + run: cargo clippy --locked --manifest-path native/Cargo.toml --workspace --all-targets -- -D warnings + - name: Core unit tests + run: cargo test --locked --manifest-path native/Cargo.toml -p patchops-core --lib + - name: Python-oracle parity tests (Linux isolation harness) + if: runner.os == 'Linux' + run: cargo test --locked --manifest-path native/Cargo.toml -p patchops-core --test parity + - name: GPUI tests + run: cargo test --locked --manifest-path native/Cargo.toml -p patchopsiii-gpui - name: Build native release run: python scripts/gpui.py build --release - - name: Build bundled backend (Linux) - if: runner.os == 'Linux' - run: bun run build:backend:linux - - name: Build bundled backend (Windows) - if: runner.os == 'Windows' - run: bun run build:backend:win - name: Create self-contained native archive - run: python scripts/package_gpui.py --executable ${{ matrix.executable }} --backend ${{ matrix.backend }} --output ${{ matrix.archive }} + run: python scripts/package_gpui.py --executable ${{ matrix.executable }} --output ${{ matrix.archive }} + - name: Verify in-process status (Windows) + if: runner.os == 'Windows' + run: python scripts/smoke_gpui_linux.py --status-only --executable ${{ matrix.executable }} - name: Smoke test the self-contained Linux archive if: runner.os == 'Linux' run: | python -m zipfile -e ${{ matrix.archive }} "${RUNNER_TEMP}/patchops-native" chmod +x "${RUNNER_TEMP}/patchops-native/PatchOpsIII/patchopsiii-gpui" - chmod +x "${RUNNER_TEMP}/patchops-native/PatchOpsIII/resources/backend-bin/patchops-backend" - xvfb-run -a python scripts/smoke_gpui_linux.py --start-session --use-owned-backend --executable "${RUNNER_TEMP}/patchops-native/PatchOpsIII/patchopsiii-gpui" + xvfb-run -a python scripts/smoke_gpui_linux.py --start-session --executable "${RUNNER_TEMP}/patchops-native/PatchOpsIII/patchopsiii-gpui" - uses: actions/upload-artifact@v4 with: name: ${{ matrix.artifact }} diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 28e6304..e2ccdc2 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -310,6 +310,7 @@ jobs: secrets: inherit build-native: + # GPUI + patchops-core archives; no Python backend bundle in native assets. needs: - prepare uses: ./.github/workflows/gpui-build.yml diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 8706315..8594bf1 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -304,6 +304,7 @@ jobs: secrets: inherit build-native: + # GPUI + patchops-core archives; no Python backend bundle in native assets. needs: - prepare uses: ./.github/workflows/gpui-build.yml diff --git a/.gitignore b/.gitignore index 95593d2..28d92aa 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,6 @@ node_modules/ # codex: .codex/ -# Standalone GPUI build output (Cargo.lock is committed) +# Rust native workspace build output (Cargo.lock is committed) +native/target/ native/gpui/target/ diff --git a/README.md b/README.md index aac1cdc..3277a89 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ - [Star History](#star-history) ## Overview -PatchOpsIII streamlines the setup and upkeep of Black Ops III by surfacing popular community tools and quality-of-life tweaks in a native Rust/GPUI desktop interface backed by a local Python API. Whether you are securing your game with T7 Patch, smoothing shader compilation stutter with DXVK, or fine-tuning launch options, PatchOpsIII consolidates every workflow into one cohesive experience. The previous Electron/React client remains available in this repository as a legacy fallback. +PatchOpsIII streamlines the setup and upkeep of Black Ops III by surfacing popular community tools and quality-of-life tweaks in a native Rust/GPUI desktop interface with in-process Rust operations. Whether you are securing your game with T7 Patch, smoothing shader compilation stutter with DXVK, or fine-tuning launch options, PatchOpsIII consolidates every workflow into one cohesive experience. The previous Electron/React client remains available in this repository as a legacy fallback. ## Key Features @@ -63,35 +63,41 @@ PatchOpsIII streamlines the setup and upkeep of Black Ops III by surfacing popul 1. **Download:** Grab the latest release from the [Releases page](https://github.com/boggedbrush/PatchOpsIII/releases). 2. **Windows:** Download `PatchOpsIII-native-windows-x64.zip`, extract the entire `PatchOpsIII` folder, and run `patchopsiii-gpui.exe` from that folder. 3. **Linux & Steam Deck:** Download `PatchOpsIII-native-linux-x64.zip`, extract it, run `chmod +x PatchOpsIII/patchopsiii-gpui`, then launch that executable from a desktop session. -4. **Dependencies:** Keep the executable and its `resources` directory together. The archive bundles the Python backend, presets, and icons; Python is not required on the destination system. Linux still requires a supported X11/Wayland session, system fonts, and working graphics drivers. +4. **Dependencies:** Keep the executable and its `resources` directory together. The archive bundles one Rust executable, presets, metadata, and icons; Python is not required on the destination system. Linux still requires a supported X11/Wayland session, system fonts, and working graphics drivers. The existing `PatchOpsIII.msi`, `PatchOpsIII.exe`, and `PatchOpsIII.AppImage` release assets are the legacy Electron fallback while native installer/updater work remains open. ### Developer Setup -The primary desktop app uses Rust 1.99.0, GPUI 0.2.2, and GPUI Component 0.5.0. Python runs the existing local operations API. +The primary desktop app uses Rust 1.99.0, GPUI 0.2.2, GPUI Component 0.5.0, +and the `patchops-core` library in the `native/` Cargo workspace. Install Rust +through rustup and the platform build packages listed in the native README. ```bash -# install Python service dependencies -python -m venv .venv -source .venv/bin/activate # On Windows use: .venv\Scripts\activate -pip install -r requirements.txt - -# build and run the native app with the source backend +cargo +1.99.0 build --locked --manifest-path native/Cargo.toml -p patchopsiii-gpui +./native/target/debug/patchopsiii-gpui +# Optional Python standard-library launchers (no backend dependencies): python scripts/gpui.py build python scripts/gpui.py run # Equivalent package scripts: bun run gpui:build / bun run gpui:run ``` -See [native/gpui/README.md](native/gpui/README.md) for platform build packages, release packaging, validation commands, and known parity gaps. The development launcher gives its Python API an OS-assigned loopback port. A packaged native binary locates and owns the bundled backend itself. +See [native/gpui/README.md](native/gpui/README.md) for release packaging, +validation commands, and remaining hardware checks. Rust operations run on a +worker thread within the native app; no Python service or HTTP listener starts. ## Native desktop architecture -The primary Rust/GPUI client lives in [native/gpui](native/gpui/README.md). It uses native controls without Chromium, React, a WebView, or Tauri, and supervises the packaged Python operations service on an ephemeral loopback port. See [the native desktop architecture and migration record](docs/gpui-architecture.md) for the original research, comparison data, supported workflows, and remaining parity work. +The primary Rust/GPUI client lives in [native/gpui](native/gpui/README.md) and +shares toolkit-independent operations from [native/core](native/core). +The worker dispatches existing `/api/...` action identifiers directly to Rust, +preserving the JSON shapes consumed by the UI. Downloads use outbound HTTPS. +See [the architecture and migration record](docs/gpui-architecture.md) for the +progress callback contract, migration details, and remaining validation work. ## Legacy Electron app -The Electron/React application in `src/` remains buildable as a fallback. To work on it, install Bun dependencies with `bun install`, then use `bun run dev:desktop`; `bun run dist:linux` and `bun run dist:win` retain the established AppImage/MSI/NSIS builds. The browser development server remains available through `bun run dev`. Electron and GPUI share backend settings, logs, caches, and game files, so do not perform overlapping game mutations in both clients. +The Electron/React application in `src/` remains buildable as a fallback. To work on it, install Python 3.12 dependencies with `python -m pip install -r requirements.txt` and Bun dependencies with `bun install`, then use `bun run dev:desktop`; `bun run dist:linux` and `bun run dist:win` retain the established AppImage/MSI/NSIS builds. The browser development server remains available through `bun run dev`. Electron and GPUI share backend settings, logs, caches, and game files, so do not perform overlapping game mutations in both clients. ## Forked Components diff --git a/docs/gpui-architecture.md b/docs/gpui-architecture.md index b593cd1..a4b8fdc 100644 --- a/docs/gpui-architecture.md +++ b/docs/gpui-architecture.md @@ -24,18 +24,16 @@ avoids adopting an evolving Git revision or silently upgrading the toolkit. The optional WebView feature is not enabled. Both dependencies use Apache-2.0; distribution still needs an audit of notices for the complete dependency tree. -Reusing the current Python API provided a controlled first comparison of desktop -renderers and remains the production architecture today. It retains the existing download, install, backup, hash, Steam, UAC, -and config semantics. It also retains Python startup, packaging cost, and HTTP -serialization; removing Chromium does not prove a smaller or faster complete -application. A later in-process Rust port should use a toolkit-independent core -so Tauri and GPUI can exercise identical business logic. This PR does not import -the unmerged Rust rewrite or duplicate delicate game-file operations in Rust. - -The promotion decision changes product ownership, not the evidence: GPUI is now -the primary client, native archives bundle the Python service, and Electron stays -buildable as a fallback. The hardware, accessibility, updater, and richer UX gaps -listed below remain follow-up work rather than being treated as completed parity. +The original GPUI baseline reused Python to compare desktop renderers. The +native client now imports PR #40's Rust operations into `native/core`, a library +with no Tauri/webview dependency. GPUI and operations run in one Rust process; +legacy Electron retains its existing Python service. There is no local HTTP +server or Python supervision in the native binary or its archives. + +The core retains verified downloads, ownership manifests, transactional backup +and rollback logic, Steam/VDF handling, and platform launch helpers from PR #40. +UI, hardware, accessibility and installer/updater validation remain separate +from this backend migration. No comparative performance improvement is claimed. Sources inspected: @@ -47,36 +45,113 @@ Sources inspected: ## Implemented architecture -1. Add an isolated Cargo application and compatible native controls. Preserve the - app's dark charcoal/red control-center visual language and use native window - chrome, seven navigation sections, scrollable settings, and a log panel. -2. Launch a supervised Python service on an ephemeral loopback port. Packaged - binaries locate the PyInstaller service in `resources/backend-bin`; source - binaries fall back to `backend/api.py`. Wait for health before opening GPUI - and always reap owned processes on normal/error exits. -3. Execute API calls on one HTTP worker. Reflect backend-reported state, preserve - unsuccessful HTTP-200 responses and depot instructions, disable overlapping - actions, and require an explicit operation confirmation. -4. Implement representative real workflows throughout the app, using existing - payloads and config value mappings. Keep input drafts intact during refresh; - provide an explicit button to load current values. -5. Add meaningful protocol/lifecycle checks, Windows/Linux build CI, - self-contained archives, a common process-tree sampler, and a parity/manual - review checklist. Stable and beta releases attach native archives alongside - the unchanged legacy Electron deliverables. +`native/` is a Cargo workspace with `patchops-core` and `patchopsiii-gpui`, a +committed workspace lockfile, and Rust 1.99.0. GPUI/Component stay pinned to +0.2.2/0.5.0. Presets and package.json version are embedded at build time, so +stable/beta release preparation applies to both Electron and GPUI. + +`Backend::start()` creates a `patchops-gpui-core` worker. Public `requests: +Sender` and `replies: Receiver` retain the existing UI contract. +`Request { path: String, body: Option }` uses the existing +`/api/...` identifiers without making HTTP requests. `Reply { state: +Option, message: String, failed: bool, is_status: bool }` retains status, +operation messages, and the Steam depot command marker used by the UI. +Long operations and status assembly run sequentially on the worker; neither +blocks the UI thread. Closing a window does not cancel or join active operations. + +`patchops_core::Engine::dispatch(path, body)` returns the same status keys and +`{ok, state}`, `{ok, error}`, validation, update and depot response envelopes as +the Python API. The Python flag is `ok`, rather than `success`. Internal Rust +errors become failed operation results/replies. Built-in launch profiles omit +`path`; Workshop profiles include it. The UI uses its native folder picker, so +Python's `/api/browse` endpoint is intentionally not implemented. + +Update metadata retains `available`, `channel`, `currentVersion`, +`latestVersion`, `name`, `body`, `pageUrl`, and `asset {name, url, size, +contentType}`. Asset selection now chooses `PatchOpsIII-native-{linux,windows}-x64.zip`; +it does not point the GPUI user to legacy installers. Checks do not install an +update automatically. Draft and stable/prerelease checks are preserved. + +Status hashing is cached by executable path, file size and modification time, +including preserved EXE availability probes. The bounded cache is display-only: +mutation validation always rereads bytes, verifies staging, and checks the active +target after replacement. Files without modification times are not cache hits. + +### Progress and log hook + +PR #40 emits `patchops-log` entries. `AppState::new(data_dir, resource_dir, +on_log)` replaces its AppHandle with an optional `EventCallback = Arc`. `resource_dir: Option` replaces +the Steam module's Tauri resource-path lookup. `set_progress_callback` adds +`ProgressCallback = Arc`. +Callbacks run on the worker and must forward data to the UI thread. + +The GPUI bridge exposes `events: Receiver`, with: + +```rust +pub enum BackendEvent { + Log(patchops_core::models::LogEntry), // category, message, line: String + Progress(OperationProgress), +} +pub struct OperationProgress { + pub op: String, // full request path, e.g. /api/t7-install + pub stage: String, // started/running/download/verify/completed/failed + pub fraction: Option, // stage-local [0,1], None = indeterminate + pub message: String, +} +``` + +`OperationProgress` is re-exported from `backend`; it also derives Serialize and +Deserialize in core. Logs stream as they occur. Operations emit started and +terminal events; downloads emit throttled byte fractions when Content-Length is +known and SHA-256 verification stages. Status/depot polls emit no operation +progress. The UI agent's `on_progress(ProgressEvent, cx)` can map `op` through +`Operation::from_path`, forward Log entries, and map stages/fractions; wiring +that receiver belongs to the UI integration. Existing `send(path, body)` call +sites need no changes. Errors and validation failures are still failed replies; +compatible-depot failures retain `Steam console command: ...` in their message. + +### Main-branch changes reconciled + +- `aadb32d` (#43): September 10, 2026 BuildID 24784313 and SHA-256 are recognized; + T7 uses the current profile. Unit tests cover the constants and hash classification. +- `20e1dbe` (#46), `ca9af04` (#47): prefer exact/versioned T7 release archives, + then unambiguous platform/universal renamed packages with GitHub SHA-256 + digests. Tests cover discovery and ambiguity; unsigned candidates are rejected. +- `8888db6` (#50), `2503485` (#51): Electron AppImage tooling, SquashFS directory + permissions, Firejail validation, UI compact layout, and update metadata do + not apply to native ZIP archives. Their legacy workflows remain untouched. +- `c582a62`, `58f7ba3` and versions from #50/#51: package.json is the embedded + runtime release version; no duplicate hard-coded desktop version is introduced. + +Core intentionally keeps PR #40's stronger archive/path validation and ownership +rules. It may refuse uninstall/adoption of files Python would modify without +proof of ownership. Config validation also limits unsupported keys/values. +Rust config writes retain CRLF line endings and explicitly reject read-only files; +Python normalizes line endings and exposes platform permission errors. +Steam writes retain the first original backup and create the PR #40 sibling backup; +VDF formatting and logs differ from Python. Enhanced status does not adopt +legacy timestamps/file counts without matching game-directory provenance. +Exact log text and filesystem behavior are tracked by the concurrently maintained +Python-oracle parity suite; passing core unit tests alone is not full parity. + +Native archives contain the Rust binary, resources and docs only. The optional +Python scripts use the standard library as build/package/smoke helpers. Native CI +runs core and GPUI tests, formatting, Clippy, archive checks and a fake-game +window/status smoke. Stable/beta workflows retain native artifact names. ## Stack comparison | Criterion | Legacy Electron | Tauri #36 / #40 | Primary GPUI client | |---|---|---|---| | UI technology | React/CSS + Chromium | React/CSS + OS WebView | Native Rust controls + GPU rendering | -| Backend | Python child + HTTP/WS | #36 Python + small Rust core; #40 in-process Rust | Same Python + HTTP | +| Backend | Python child + HTTP/WS | #36 Python + small Rust core; #40 in-process Rust | In-process patchops-core + worker channels | | Renderer reuse | Baseline | High | Rewrite required | | Platform work | Existing Windows/Linux packages | WebView availability and new Rust packaging | GPU/driver compatibility, native UI/input and new packaging | | Accessibility | Browser infrastructure; app still needs testing | WebView infrastructure; app still needs testing | Must validate controls and screen readers on target OSes | -| Delivery | Existing MSI/NSIS/AppImage fallback | Independent open PRs | Self-contained Windows/Linux ZIPs with a bundled PyInstaller backend | +| Delivery | Existing MSI/NSIS/AppImage fallback | Independent open PRs | Self-contained Windows/Linux ZIPs with one Rust executable | | Startup/RAM/package size | Measure | Measure each PR separately | Measure complete stack; no claimed improvement | -| Migration cost | Existing maintenance | Shell changes; #40 also ports operations | Entire UI rewrite; later backend port optional | +| Migration cost | Existing maintenance | Shell changes; #40 also ports operations | UI rewrite plus shared Rust operations | ## Workflow coverage @@ -93,30 +168,28 @@ Sources inspected: | Logs/maintenance | Up to 80 recent lines, five-second refresh, clear logs/cache, reset stock, update check/channel | Streaming WS logs, copy/export UX and platform updater integration | | Delivery | Windows/Linux CI and self-contained release archives | Signed native installers, updater, dependency notices and broader release testing | -Status refresh uses the complete existing status endpoint, including executable -hashing. Polling every five seconds can cost more idle CPU than Electron's event -driven updates; include that cost in measurements. No new background game scans -or fabricated installation states are introduced. Actions preserve backend -validation and require confirmation; all game-modifying smoke tests must use -an installation whose backups can be inspected and restored. +Status refresh polls the worker every five seconds. Executable hashes are reused +while size/mtime remain unchanged. Other status work (Steam config, mod state and +small config files) still runs on each poll; measure its idle CPU cost. All +game-modifying validation must use disposable installations and inspect backups. ## Measurement protocol retained from the selection process Use the same Windows PC and Steam Deck/Linux PC for all three candidates. Record OS, GPU/driver, display scale, power profile, game-directory state, and commit SHA. Build optimized release artifacts. Exclude compilation, debugger and dev -servers. Compare Electron, #36, #40 and GPUI separately; a Python-backed GPUI -versus #40 compares both renderer and backend changes. +servers. Compare Electron, #36, #40 and GPUI separately; the original Python-backed GPUI baseline and the current Rust-backed client +must be measured separately. For regression tracking and any future comparison, collect ten cold launches and ten warm launches per candidate. Record -process start to first painted interactive window **and** service-ready status +process start to first painted interactive window **and** first loaded status with screen recording or platform tracing. Include backend startup. Report median and p95, retaining raw observations. Do not label cargo build time as app startup time, or GPUI entity creation as first paint. Measure idle for 60 seconds after settling, then the same navigation/config and -log workload. Use the supervisor PID that owns both UI and backend processes: +log workload. Use the native executable PID (Electron still has a process tree): ```sh python -m pip install psutil @@ -127,15 +200,16 @@ python scripts/measure_desktop.py --pid 12345 --label gpui-release \ The sampler captures RSS and CPU for the process tree, including children; RSS can double-count shared pages and excludes GPU memory. Collect GPU usage with -platform tools. Native archives include the PyInstaller backend, so measure the -complete extracted deliverable. Elevated/detached children must be measured +platform tools. Measure the complete extracted native deliverable. Elevated/detached children must be measured separately. Evaluate input latency, scrolling, text entry/IME, scaling, keyboard focus, screen readers, offline/error behavior, and update/install/uninstall reliability when comparing future changes. GPUI is the selected product client; that decision does not substitute for collecting hardware measurements. -## Validation history and current gates +## Historical baseline validation and current gates +The following results describe the original Python-backed baseline, before the +in-process backend migration; they are not current Rust-core parity evidence. On the supplied Linux cloud environment: - Debug and optimized release builds completed with the locked dependency tree. @@ -169,8 +243,8 @@ is configured in this PR; its hosted results are separate from these local check ## Reviewer smoke checklist - Build with the pinned lockfile; open the native window on Windows and Linux. -- Start Electron simultaneously; confirm distinct ports/processes. Close GPUI - and confirm its service exits while Electron stays alive. +- Start Electron simultaneously; confirm GPUI has no Python child or HTTP + listener. Close GPUI while idle and confirm Electron stays alive. - With no game directory, verify status/errors and manual folder input. Browse to a disposable BO3 install, confirm selection, refresh, and relaunch. - Navigate every section with mouse and keyboard. Edit a field while refresh @@ -181,7 +255,7 @@ is configured in this PR; its hosted results are separate from these local check logs, including readonly error handling and special CPU/hidden-option values. - On a test install verify each mod install/uninstall and EXE restore preserves expected backups. Exercise compatible-depot-required and UAC cancellation. -- Verify API startup failure, disconnect, download errors, and clean shutdown. +- Verify core initialization failure, download errors, and clean shutdown. Wait for active file operations before closing; cancellation is not implemented. - Test X11, Wayland, actual Steam Deck, high DPI, IME and screen-reader behavior. - Measure optimized complete stacks and record results as native delivery evolves. diff --git a/native/gpui/Cargo.lock b/native/Cargo.lock similarity index 97% rename from native/gpui/Cargo.lock rename to native/Cargo.lock index e8595d9..77c8649 100644 --- a/native/gpui/Cargo.lock +++ b/native/Cargo.lock @@ -139,6 +139,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arc-swap" @@ -393,13 +396,13 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -791,6 +794,25 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "calloop" version = "0.13.0" @@ -846,9 +868,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "jobserver", @@ -1394,6 +1416,23 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1447,6 +1486,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -1454,7 +1502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -1466,10 +1514,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.3", + "windows-sys 0.61.2", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1885,9 +1945,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "font-types" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" +checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274" dependencies = [ "bytemuck", ] @@ -2796,6 +2856,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -3614,9 +3675,9 @@ checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "log", @@ -3835,6 +3896,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -3896,6 +3963,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -4152,6 +4228,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "patchops-core" +version = "1.3.4" +dependencies = [ + "dirs 6.0.0", + "flate2", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2 0.10.9", + "tar", + "time", + "windows-sys 0.59.0", + "winreg", + "zip", +] + [[package]] name = "patchopsiii-gpui" version = "0.1.0" @@ -4160,7 +4254,8 @@ dependencies = [ "env_logger", "gpui", "gpui-component", - "reqwest", + "log", + "patchops-core", "serde_json", ] @@ -4379,6 +4474,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4837,6 +4938,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + [[package]] name = "ref-cast" version = "1.0.27" @@ -4901,16 +5012,21 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -4918,6 +5034,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -6005,6 +6122,17 @@ dependencies = [ "objc", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr 1.6.1", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -6092,6 +6220,38 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -6145,9 +6305,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ "bytes", "libc", @@ -6608,9 +6768,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.26.1" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -6918,6 +7078,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.12" @@ -7544,6 +7713,16 @@ dependencies = [ "libc", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.5", +] + [[package]] name = "xcb" version = "1.7.1" @@ -7742,7 +7921,7 @@ dependencies = [ "libc", "pin-project", "redox_syscall 0.2.16", - "xattr", + "xattr 0.2.3", ] [[package]] @@ -7968,6 +8147,25 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "bzip2", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.21", + "zopfli", + "zstd", +] + [[package]] name = "zlib-rs" version = "0.6.8" @@ -7980,6 +8178,46 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.5.3" diff --git a/native/Cargo.toml b/native/Cargo.toml new file mode 100644 index 0000000..7f8d004 --- /dev/null +++ b/native/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = ["core", "gpui"] +resolver = "3" + +[profile.release] +lto = "thin" +strip = "debuginfo" + diff --git a/native/core/Cargo.toml b/native/core/Cargo.toml new file mode 100644 index 0000000..da37eb1 --- /dev/null +++ b/native/core/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "patchops-core" +version = "1.3.4" +edition = "2024" +publish = false +license = "MIT" + +[dependencies] +dirs = "6" +flate2 = "1" +regex = "1" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["preserve_order"] } +sha2 = "0.10" +tar = "0.4" +time = { version = "0.3", features = ["formatting", "local-offset"] } +zip = { version = "2", default-features = false, features = ["deflate", "bzip2", "zstd"] } + +[build-dependencies] +serde_json = "1" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_UI_Shell"] } +winreg = "0.55" + diff --git a/native/core/build.rs b/native/core/build.rs new file mode 100644 index 0000000..e90fbdb --- /dev/null +++ b/native/core/build.rs @@ -0,0 +1,11 @@ +fn main() { + // Release workflows update package.json before building all deliverables. + // Embed the same stable/beta version without a runtime resource lookup. + println!("cargo:rerun-if-changed=../../package.json"); + let package: serde_json::Value = serde_json::from_str(include_str!("../../package.json")) + .expect("package.json must contain valid release metadata"); + let version = package["version"] + .as_str() + .expect("package.json version is required"); + println!("cargo:rustc-env=PATCHOPSIII_VERSION={version}"); +} diff --git a/native/core/src/app.rs b/native/core/src/app.rs new file mode 100644 index 0000000..8598792 --- /dev/null +++ b/native/core/src/app.rs @@ -0,0 +1,880 @@ +use std::{ + collections::VecDeque, + fs::{self, OpenOptions}, + io::Write, + path::{Path, PathBuf}, + sync::{Arc, Mutex, OnceLock}, +}; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use time::OffsetDateTime; + +use crate::{ + fs_ops::{atomic_write, backup_path, existing_backup}, + models::{AdvancedState, GraphicsState, LogEntry, OperationProgress, QolState}, +}; + +pub const APP_VERSION: &str = env!("PATCHOPSIII_VERSION"); + +/// Toolkit-neutral hook for forwarding structured log entries to a UI. +pub type EventCallback = Arc; +pub type ProgressCallback = Arc; + +#[derive(Clone)] +pub struct AppState(Arc); + +struct AppStateInner { + data_dir: PathBuf, + resource_dir: Option, + on_log: Option, + on_progress: Mutex>, + active_operation: Mutex>, + logs: Mutex>, + operation: Mutex<()>, +} + +pub fn has_game_executable(directory: &Path) -> bool { + directory.is_dir() + && ["BlackOpsIII.exe", "BlackOps3.exe"] + .iter() + .any(|name| directory.join(name).is_file()) +} + +pub fn set_game_directory(state: &AppState, path: &str) -> Result<(), String> { + if !has_game_executable(Path::new(path)) { + return Err("BlackOps3.exe or BlackOpsIII.exe was not found.".into()); + } + let candidate = Path::new(path) + .canonicalize() + .map_err(|error| format!("Unable to open the selected directory: {error}"))?; + if !has_game_executable(&candidate) { + return Err("BlackOps3.exe or BlackOpsIII.exe was not found.".into()); + } + let mut settings = state.load_settings(); + settings.game_dir = Some(candidate.to_string_lossy().into_owned()); + state.save_settings(&settings)?; + state.log( + "Success", + format!("Game directory set to {}", candidate.display()), + ); + Ok(()) +} + +pub fn release_channel(settings: &Settings) -> &str { + match settings.release_channel.as_deref() { + Some("beta") => "beta", + _ => "stable", + } +} + +pub fn set_release_channel(state: &AppState, channel: &str) -> Result<(), String> { + if !matches!(channel, "stable" | "beta") { + return Err("Release channel must be beta or stable.".into()); + } + let mut settings = state.load_settings(); + settings.release_channel = Some(channel.to_owned()); + state.save_settings(&settings)?; + state.log( + "Success", + format!("Release channel set to {}.", title_case(channel)), + ); + Ok(()) +} + +fn title_case(value: &str) -> String { + let mut chars = value.chars(); + chars + .next() + .map(|first| first.to_uppercase().collect::() + chars.as_str()) + .unwrap_or_default() +} + +pub fn config_path(game_dir: &Path) -> PathBuf { + game_dir.join("players").join("config.ini") +} + +pub fn read_config(game_dir: &Path) -> String { + fs::read(config_path(game_dir)) + .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) + .unwrap_or_default() +} + +fn config_value<'a>(content: &'a str, key: &str) -> Option<&'a str> { + content.lines().find_map(|line| { + let line = line.trim_start(); + let rest = line.strip_prefix(key)?; + if !rest.starts_with(char::is_whitespace) && !rest.starts_with('=') { + return None; + } + let value = rest.trim_start().strip_prefix('=')?.trim_start(); + let value = value.strip_prefix('"')?; + value.find('"').map(|end| &value[..end]) + }) +} + +fn config_i32(content: &str, key: &str, default: i32) -> i32 { + config_value(content, key) + .and_then(|value| value.parse::().ok()) + .map(|value| value as i32) + .unwrap_or(default) +} + +fn config_f64(content: &str, key: &str, default: f64) -> f64 { + config_value(content, key) + .and_then(|value| value.parse().ok()) + .unwrap_or(default) +} + +fn config_bool(content: &str, key: &str, enabled: &str, default: &str) -> bool { + config_value(content, key).unwrap_or(default) == enabled +} + +pub fn graphics_state(content: &str) -> GraphicsState { + GraphicsState { + max_fps: config_i32(content, "MaxFPS", 165), + fov: config_i32(content, "FOV", 80), + display_mode: config_i32(content, "FullScreenMode", 1), + resolution: config_value(content, "WindowSize") + .unwrap_or("1920x1080") + .to_owned(), + refresh_rate: config_f64(content, "RefreshRate", 60.0), + render_resolution: config_i32(content, "ResolutionPercent", 100), + vsync: config_bool(content, "Vsync", "1", "1"), + draw_fps: config_bool(content, "DrawFPS", "1", "0"), + } +} + +pub fn advanced_state(game_dir: Option<&Path>, content: &str) -> AdvancedState { + let video_memory = config_value(content, "VideoMemory").unwrap_or("1"); + let stream_min = config_value(content, "StreamMinResident").unwrap_or("0"); + AdvancedState { + smooth_framerate: config_bool(content, "SmoothFramerate", "1", "0"), + unlock_options: config_bool(content, "RestrictGraphicsOptions", "0", "1"), + reduce_cpu: config_bool(content, "SerializeRender", "2", "0"), + max_frame_latency: config_i32(content, "MaxFrameLatency", 1), + vram_limited: video_memory != "1" || stream_min != "0", + vram_target: (config_f64(content, "VideoMemory", 0.75) * 100.0) as i32, + config_readonly: game_dir + .map(config_path) + .and_then(|path| path.metadata().ok()) + .is_some_and(|metadata| metadata.permissions().readonly()), + } +} + +fn valid_config_key(key: &str) -> bool { + !key.is_empty() + && key + .chars() + .all(|character| character.is_ascii_alphanumeric() || character == '_') +} + +pub fn write_config_values( + game_dir: &Path, + updates: &[(String, String, String)], +) -> Result<(), String> { + let path = config_path(game_dir); + if !path.is_file() { + return Err(format!("config.ini not found at {}", path.display())); + } + if path + .metadata() + .map_err(|error| error.to_string())? + .permissions() + .readonly() + { + return Err("config.ini is read-only. Unlock it before making changes.".into()); + } + let mut content = read_config(game_dir); + let line_ending = if content.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + let mut lines: Vec = content.lines().map(str::to_owned).collect(); + + for (key, value, comment) in updates { + if !valid_config_key(key) + || value.chars().any(char::is_control) + || comment + .chars() + .any(|character| matches!(character, '\r' | '\n')) + { + return Err("Invalid configuration value.".into()); + } + let replacement = format!("{key} = \"{value}\" // {comment}"); + let mut matched = false; + for line in &mut lines { + let trimmed = line.trim_start(); + let Some(rest) = trimmed.strip_prefix(key) else { + continue; + }; + if rest.starts_with(char::is_whitespace) || rest.starts_with('=') { + *line = replacement.clone(); + matched = true; + } + } + if !matched { + lines.push(replacement); + } + } + content = lines.join(line_ending); + content.push_str(line_ending); + atomic_write(&path, content.as_bytes()) +} + +fn config_input(key: &str, value: &Value) -> Result<(String, &'static str), String> { + let raw = match value { + Value::String(value) => value.clone(), + Value::Number(value) => value.to_string(), + _ => return Err("Invalid configuration value.".into()), + }; + let number = || { + raw.parse::() + .map_err(|_| "Expected a whole number.".to_string()) + }; + let ranged = |minimum, maximum| { + let value = number()?; + if (minimum..=maximum).contains(&value) { + Ok(value.to_string()) + } else { + Err(format!("Value must be between {minimum} and {maximum}.")) + } + }; + let result = match key { + "MaxFPS" => (ranged(0, 1000)?, "Maximum FPS cap"), + "FOV" => (ranged(65, 120)?, "Field of view"), + "FullScreenMode" => ( + ranged(0, 2)?, + "0=Windowed,1=Fullscreen,2=Fullscreen Windowed", + ), + "RefreshRate" => (ranged(1, 240)?, "1 to 240"), + "ResolutionPercent" => (ranged(50, 200)?, "50 to 200"), + "MaxFrameLatency" => (ranged(0, 4)?, "Maximum frame latency"), + "Vsync" if matches!(raw.as_str(), "0" | "1") => (raw, "Vertical sync"), + "DrawFPS" if matches!(raw.as_str(), "0" | "1") => (raw, "FPS counter"), + "SmoothFramerate" if matches!(raw.as_str(), "0" | "1") => (raw, "Frame smoothing"), + "RestrictGraphicsOptions" if matches!(raw.as_str(), "0" | "1") => { + (raw, "Expose all graphics options") + } + "SerializeRender" if matches!(raw.as_str(), "0" | "2") => (raw, "Reduce CPU pressure"), + "WindowSize" => { + let mut parts = raw.split('x'); + let width = parts.next().and_then(|part| part.parse::().ok()); + let height = parts.next().and_then(|part| part.parse::().ok()); + if parts.next().is_some() + || !width.is_some_and(|value| (320..=16384).contains(&value)) + || !height.is_some_and(|value| (200..=16384).contains(&value)) + { + return Err("Resolution must look like 1920x1080.".into()); + } + (raw, "Screen resolution") + } + _ => return Err("Unsupported configuration key or value.".into()), + }; + Ok(result) +} + +pub fn set_config_value( + state: &AppState, + game_dir: &Path, + key: &str, + value: Value, + comment: Option<&str>, +) -> Result<(), String> { + let (value, default_comment) = config_input(key, &value)?; + let comment = comment.unwrap_or(default_comment); + write_config_values( + game_dir, + &[(key.to_owned(), value.clone(), comment.to_owned())], + )?; + state.log("Success", format!("Set {key} to {value}.")); + Ok(()) +} + +pub fn set_vram_target( + state: &AppState, + game_dir: &Path, + limited: bool, + target: i32, +) -> Result<(), String> { + if limited && !(75..=100).contains(&target) { + return Err("VRAM target must be between 75 and 100.".into()); + } + let updates = if limited { + vec![ + ( + "VideoMemory".into(), + serde_json::json!(target as f64 / 100.0).to_string(), + "0.75 to 1".into(), + ), + ("StreamMinResident".into(), "1".into(), "0 or 1".into()), + ] + } else { + vec![ + ("VideoMemory".into(), "1".into(), "0.75 to 1".into()), + ("StreamMinResident".into(), "0".into(), "0 or 1".into()), + ] + }; + write_config_values(game_dir, &updates)?; + state.log( + "Success", + if limited { + format!("Limited VRAM usage set to {target}%.") + } else { + "Enabled full VRAM usage.".into() + }, + ); + Ok(()) +} + +pub fn set_config_readonly(state: &AppState, game_dir: &Path, enabled: bool) -> Result<(), String> { + let path = config_path(game_dir); + let metadata = path + .metadata() + .map_err(|_| "config.ini was not found.".to_string())?; + let mut permissions = metadata.permissions(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = permissions.mode(); + permissions.set_mode(if enabled { mode & !0o222 } else { mode | 0o200 }); + } + #[cfg(not(unix))] + permissions.set_readonly(enabled); + fs::set_permissions(&path, permissions).map_err(|error| error.to_string())?; + state.log( + "Success", + format!( + "config.ini set to {}.", + if enabled { "read-only" } else { "writable" } + ), + ); + Ok(()) +} + +pub fn qol_state(game_dir: Option<&Path>) -> QolState { + let Some(game_dir) = game_dir else { + return QolState::default(); + }; + let video_dir = game_dir.join("video"); + let intro = video_dir.join("BO3_Global_Logo_LogoSequence.mkv"); + let d3dcompiler = game_dir.join("d3dcompiler_46.dll"); + let mut mkv = 0; + let mut backups = 0; + if let Ok(entries) = fs::read_dir(video_dir) { + for path in entries.flatten().map(|entry| entry.path()) { + let name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + if name.ends_with(".mkv") { + mkv += 1; + } + if name.ends_with(".mkv.patchops.bak") || name.ends_with(".mkv.bak") { + backups += 1; + } + } + } + QolState { + d3dcompiler: existing_backup(&d3dcompiler).is_some(), + intro: existing_backup(&intro).is_some(), + all_intros: backups > 0 && mkv == 0, + } +} + +pub fn set_intro_skip(state: &AppState, game_dir: &Path, enabled: bool) -> Result<(), String> { + let target = game_dir + .join("video") + .join("BO3_Global_Logo_LogoSequence.mkv"); + toggle_backup(&target, enabled)?; + state.log( + "Success", + if enabled { + "Intro video skipped." + } else { + "Intro video restored." + }, + ); + Ok(()) +} + +pub fn set_d3dcompiler(state: &AppState, game_dir: &Path, enabled: bool) -> Result<(), String> { + let target = game_dir.join("d3dcompiler_46.dll"); + let already_skipped = enabled && existing_backup(&target).is_some(); + toggle_backup(&target, enabled)?; + state.log( + "Success", + if already_skipped { + "Already using latest d3dcompiler." + } else if enabled { + "Renamed d3dcompiler_46.dll to reduce stuttering." + } else { + "Restored d3dcompiler_46.dll." + }, + ); + Ok(()) +} + +fn toggle_backup(target: &Path, enabled: bool) -> Result<(), String> { + if enabled { + if existing_backup(target).is_some() { + return Ok(()); + } + if !target.is_file() { + return Err(format!("{} was not found.", target.display())); + } + fs::rename(target, backup_path(target)).map_err(|error| error.to_string()) + } else { + let backup = existing_backup(target) + .ok_or_else(|| format!("No backup was found for {}.", target.display()))?; + if target.exists() { + return Err(format!("Refusing to overwrite {}.", target.display())); + } + fs::rename(backup, target).map_err(|error| error.to_string()) + } +} + +pub fn set_all_intro_skip(state: &AppState, game_dir: &Path, enabled: bool) -> Result<(), String> { + let video_dir = game_dir.join("video"); + if !video_dir.is_dir() { + return Err("Video directory was not found.".into()); + } + let mut operations = Vec::new(); + for path in fs::read_dir(&video_dir) + .map_err(|error| error.to_string())? + .flatten() + .map(|entry| entry.path()) + { + let name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + if enabled && name.ends_with(".mkv") && existing_backup(&path).is_none() { + operations.push((path.clone(), backup_path(&path))); + } else if !enabled && (name.ends_with(".mkv.patchops.bak") || name.ends_with(".mkv.bak")) { + let suffix = if name.ends_with(".patchops.bak") { + ".patchops.bak" + } else { + ".bak" + }; + let target = path.with_file_name(&name[..name.len() - suffix.len()]); + if !target.exists() { + operations.push((path.clone(), target)); + } + } + } + let mut completed = Vec::new(); + for (source, destination) in &operations { + if let Err(error) = fs::rename(source, destination) { + for (source, destination) in completed.into_iter().rev() { + let _ = fs::rename(destination, source); + } + return Err(error.to_string()); + } + completed.push((source.clone(), destination.clone())); + } + state.log( + "Success", + if enabled && operations.is_empty() { + "Intro videos were already skipped." + } else if enabled { + "All intro videos skipped." + } else { + "Intro videos restored." + }, + ); + Ok(()) +} + +static PRESETS: OnceLock> = OnceLock::new(); + +fn presets() -> &'static Map { + PRESETS.get_or_init(|| { + serde_json::from_str(include_str!("../../../presets.json")) + .expect("bundled presets.json must be valid") + }) +} + +pub fn preset_names() -> Vec { + presets().keys().cloned().collect() +} + +pub fn apply_preset(state: &AppState, game_dir: &Path, name: &str) -> Result<(), String> { + let preset = presets() + .get(name) + .and_then(Value::as_object) + .ok_or_else(|| "Unknown preset.".to_string())?; + let mut updates = Vec::with_capacity(preset.len()); + for (key, item) in preset { + if key == "ReduceStutter" { + continue; + } + let values = item + .as_array() + .filter(|values| values.len() == 2) + .ok_or_else(|| format!("Preset value for {key} is invalid."))?; + let value = values[0] + .as_str() + .map(str::to_owned) + .unwrap_or_else(|| values[0].to_string()); + let comment = values[1] + .as_str() + .ok_or_else(|| format!("Preset comment for {key} is invalid."))?; + updates.push((key.clone(), value, comment.to_owned())); + } + write_config_values(game_dir, &updates)?; + state.log("Success", format!("Applied preset '{name}'.")); + Ok(()) +} + +pub fn platform_name() -> &'static str { + match std::env::consts::OS { + "windows" => "Windows", + "linux" => "Linux", + "macos" => "Darwin", + other => other, + } +} + +fn kernel_release() -> String { + #[cfg(target_os = "linux")] + { + fs::read_to_string("/proc/sys/kernel/osrelease") + .unwrap_or_default() + .trim() + .to_owned() + } + #[cfg(not(target_os = "linux"))] + { + String::new() + } +} + +pub fn log_payload(state: &AppState) -> String { + let body = fs::read_to_string(state.log_path()).unwrap_or_default(); + format!( + "PatchOpsIII {APP_VERSION} - {} {} ({}) logs:\n```\n{}\n```", + platform_name(), + kernel_release(), + std::env::consts::ARCH, + if body.trim().is_empty() { + "(no log entries found)" + } else { + body.trim() + } + ) +} + +pub fn clear_mod_files(state: &AppState) -> Result<(), String> { + let directory = state.mod_files_dir(); + fs::create_dir_all(&directory).map_err(|error| error.to_string())?; + for entry in fs::read_dir(&directory).map_err(|error| error.to_string())? { + let path = entry.map_err(|error| error.to_string())?.path(); + let metadata = fs::symlink_metadata(&path).map_err(|error| error.to_string())?; + if metadata.is_dir() && !metadata.file_type().is_symlink() { + fs::remove_dir_all(path).map_err(|error| error.to_string())?; + } else { + fs::remove_file(path).map_err(|error| error.to_string())?; + } + } + state.log( + "Success", + format!("Cleared mod files in {}", directory.display()), + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static NEXT: AtomicUsize = AtomicUsize::new(0); + + fn temp_dir(label: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "patchops-app-{label}-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn config_reader_uses_first_quoted_value() { + let content = "MaxFPS = \"120\" // first\nMaxFPS = \"240\" // second\nLoose=12\n"; + assert_eq!(config_value(content, "MaxFPS"), Some("120")); + assert_eq!(config_value(content, "Loose"), None); + } + + #[test] + fn only_known_config_inputs_are_accepted() { + assert!(config_input("MaxFPS", &Value::from(240)).is_ok()); + assert!(config_input("MaxFPS", &Value::from(1001)).is_err()); + assert!(config_input("WindowSize", &Value::from("1920x1080")).is_ok()); + assert!(config_input("WindowSize", &Value::from("1920x1080\nInjected=1")).is_err()); + assert!(config_input("Unknown", &Value::from(1)).is_err()); + } + + #[test] + fn legacy_settings_round_trip_without_losing_unknown_fields() { + let settings: Settings = serde_json::from_str( + r#"{"game_dir":"C:\\Games\\BO3","release_channel":"beta","future":true}"#, + ) + .unwrap(); + assert_eq!(settings.game_dir.as_deref(), Some(r"C:\Games\BO3")); + assert_eq!(release_channel(&settings), "beta"); + + let saved = serde_json::to_value(settings).unwrap(); + assert_eq!(saved["game_dir"], r"C:\Games\BO3"); + assert_eq!(saved["release_channel"], "beta"); + assert_eq!(saved["future"], true); + } + + #[test] + fn readonly_config_cannot_be_replaced() { + let game = temp_dir("readonly-config"); + let path = config_path(&game); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let original = b"MaxFPS = \"120\" // Maximum FPS cap\n"; + fs::write(&path, original).unwrap(); + let mut permissions = path.metadata().unwrap().permissions(); + permissions.set_readonly(true); + fs::set_permissions(&path, permissions).unwrap(); + + let updates = vec![("MaxFPS".into(), "240".into(), "Maximum FPS cap".into())]; + let error = write_config_values(&game, &updates).unwrap_err(); + assert!(error.contains("read-only")); + assert_eq!(fs::read(&path).unwrap(), original); + + let mut permissions = path.metadata().unwrap().permissions(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + permissions.set_mode(0o644); + } + #[cfg(not(unix))] + permissions.set_readonly(false); + fs::set_permissions(&path, permissions).unwrap(); + fs::remove_dir_all(game).unwrap(); + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Settings { + #[serde(default)] + pub game_dir: Option, + #[serde(default)] + pub release_channel: Option, + #[serde(default)] + pub enhanced_dump_source: Option, + #[serde(default)] + pub enhanced_exe_hash: Option, + #[serde(default)] + pub enhanced_exe_hashes: Map, + #[serde(flatten)] + pub extra: Map, +} + +impl AppState { + pub fn new( + data_dir: PathBuf, + resource_dir: Option, + on_log: Option, + ) -> Result { + fs::create_dir_all(&data_dir).map_err(|error| error.to_string())?; + Ok(Self(Arc::new(AppStateInner { + data_dir, + resource_dir, + on_log, + on_progress: Mutex::new(None), + active_operation: Mutex::new(None), + logs: Mutex::new(VecDeque::with_capacity(300)), + operation: Mutex::new(()), + }))) + } + + /// Construct the normal desktop state without depending on a GUI toolkit. + pub fn for_desktop(on_log: Option) -> Result { + let data_dir = + match std::env::var_os("PATCHOPSIII_DATA_DIR").filter(|path| !path.is_empty()) { + Some(path) => PathBuf::from(path), + None => dirs::data_dir() + .ok_or_else(|| "Unable to resolve the application data directory.".to_string())? + .join("PatchOpsIII"), + }; + let resource_dir = std::env::current_exe().ok().and_then(|executable| { + let directory = executable.parent()?; + let candidates = [directory.join("resources"), directory.to_path_buf()]; + candidates.into_iter().find(|path| path.is_dir()) + }); + Self::new(data_dir, resource_dir, on_log) + } + + /// Callbacks run on the operation worker. Forward to a channel; never touch + /// GUI entities here. Installing this callback does not require a toolkit. + pub fn set_progress_callback(&self, callback: ProgressCallback) { + *self.0.on_progress.lock().expect("progress callback lock") = Some(callback); + } + + pub(crate) fn begin_progress(&self, op: &str) { + *self + .0 + .active_operation + .lock() + .expect("operation progress lock") = Some(op.into()); + self.progress("started", None, "Operation started"); + } + + pub(crate) fn end_progress(&self, error: Option<&str>) { + self.progress( + if error.is_some() { + "failed" + } else { + "completed" + }, + error.is_none().then_some(1.0), + error.unwrap_or("Operation completed"), + ); + *self + .0 + .active_operation + .lock() + .expect("operation progress lock") = None; + } + + pub(crate) fn progress(&self, stage: &str, fraction: Option, message: &str) { + let op = self + .0 + .active_operation + .lock() + .ok() + .and_then(|op| op.clone()); + let callback = self.0.on_progress.lock().ok().and_then(|cb| cb.clone()); + if let (Some(op), Some(callback)) = (op, callback) { + callback(OperationProgress { + op, + stage: stage.into(), + fraction, + message: message.into(), + }); + } + } + + pub fn resource_dir(&self) -> Option<&Path> { + self.0.resource_dir.as_deref() + } + + pub fn data_dir(&self) -> &Path { + &self.0.data_dir + } + + pub fn settings_path(&self) -> PathBuf { + self.data_dir().join("electron-settings.json") + } + + pub fn mod_files_dir(&self) -> PathBuf { + self.data_dir().join("BO3 Mod Files") + } + + pub fn log_path(&self) -> PathBuf { + self.data_dir().join("PatchOpsIII.log") + } + + pub fn lock_operation(&self) -> Result, String> { + self.0 + .operation + .lock() + .map_err(|_| "another operation failed unexpectedly".to_string()) + } + + pub fn load_settings(&self) -> Settings { + fs::read_to_string(self.settings_path()) + .ok() + .and_then(|body| serde_json::from_str(&body).ok()) + .unwrap_or_default() + } + + pub fn save_settings(&self, settings: &Settings) -> Result<(), String> { + // Retain existing key order and avoid materializing absent/null defaults + // in Electron's shared settings document. + let mut document = fs::read(self.settings_path()) + .ok() + .and_then(|body| serde_json::from_slice::>(&body).ok()) + .unwrap_or_default(); + let updates = serde_json::to_value(settings).map_err(|error| error.to_string())?; + for (key, value) in updates.as_object().expect("Settings is an object") { + if document.contains_key(key) + || (!value.is_null() + && !(key == "enhanced_exe_hashes" + && value.as_object().is_some_and(Map::is_empty))) + { + document.insert(key.clone(), value.clone()); + } + } + let body = serde_json::to_vec_pretty(&document).map_err(|error| error.to_string())?; + atomic_write(&self.settings_path(), &body) + } + + pub fn log(&self, category: &str, message: impl Into) { + let message = message.into(); + let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let timestamp = format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + now.year(), + now.month() as u8, + now.day(), + now.hour(), + now.minute(), + now.second() + ); + let line = format!("{timestamp} - {category}: {message}"); + let entry = LogEntry { + category: category.to_owned(), + message, + line: line.clone(), + }; + + if let Ok(mut logs) = self.0.logs.lock() { + if logs.len() == 300 { + logs.pop_front(); + } + logs.push_back(entry.clone()); + } + if let Ok(mut file) = OpenOptions::new() + .create(true) + .append(true) + .open(self.log_path()) + { + let _ = writeln!(file, "{line}"); + } + self.progress("running", None, &entry.message); + if let Some(callback) = &self.0.on_log { + callback(entry); + } + } + + pub fn recent_logs(&self) -> Vec { + self.0 + .logs + .lock() + .map(|logs| logs.iter().rev().take(80).cloned().collect::>()) + .map(|mut logs| { + logs.reverse(); + logs + }) + .unwrap_or_default() + } + + pub fn clear_logs(&self) -> Result<(), String> { + atomic_write(&self.log_path(), b"")?; + self.0 + .logs + .lock() + .map_err(|_| "log buffer is unavailable".to_string())? + .clear(); + Ok(()) + } +} diff --git a/native/core/src/dxvk.rs b/native/core/src/dxvk.rs new file mode 100644 index 0000000..2cf5003 --- /dev/null +++ b/native/core/src/dxvk.rs @@ -0,0 +1,1498 @@ +use std::{ + collections::HashSet, + fs, + io::Read, + path::{Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + app::AppState, + fs_ops::{self, Snapshot, remove_file_if_present}, + models::{DxvkSettings, DxvkState}, +}; + +const RELEASE_API: &str = + "https://gitlab.com/api/v4/projects/Ph42oN%2Fdxvk-gplasync/releases/v3.0-1"; +const DXVK_FILES: [&str; 2] = ["dxgi.dll", "d3d11.dll"]; +const MANAGED_FILES: [&str; 3] = ["dxgi.dll", "d3d11.dll", "dxvk.conf"]; +const MANAGED_STATE_DIRECTORY: &str = "DXVK Managed"; +const MANAGED_MANIFEST_FILENAME: &str = "manifest.json"; +const MANAGED_BACKUP_DIRECTORY: &str = "originals"; +const MANAGED_STATE_VERSION: u32 = 1; +const MAX_RELEASE_JSON_BYTES: u64 = 4 * 1024 * 1024; +const MAX_ARCHIVE_BYTES: u64 = 2 * 1024 * 1024 * 1024; +const UNMANAGED_CONFIG_MESSAGE: &str = "DXVK is not managed by PatchOpsIII; install it before applying settings so existing files can be backed up safely."; +// Exact x64 member pairs from every official DXVK-GPLAsync release that the +// legacy application's dynamic "latest" installer could have installed since +// PatchOpsIII's first public beta. Pairing prevents mixed-release adoption. +const KNOWN_LEGACY_DXVK_PAIRS: [[&str; 2]; 8] = [ + // v2.5.2-1 + [ + "e534c7b9aadd90bf5744be7637abcccbeb66dd09ccf6b998e11b303bb14d1294", + "a3bd2c6fdec65f3984550cd8dc852e1ac2bff9a30430bcba897cac78a2d9bcb3", + ], + // v2.5.3-1 (latest when PatchOpsIII's first public beta shipped) + [ + "a5c3fa8b51559c77e9046a7c530b734eaf4a4d8a6f3d66edaa6375264bd3f4e9", + "c373241f40f465f762e15a3bf53c236585bfae755e6d8eaadad091c8bcd15c55", + ], + // v2.6-1 + [ + "d159fc62a0f8f47e8cfafd8b87a479c71ab907ba91fb597e640b997b46a0b237", + "e628c11af143ee75870c7758425dd2fe0039f898362176bef8cc23d6fee25be9", + ], + // v2.6.1-1 + [ + "7db4863ee96ee5358a64b70358c124c3c5228fa25f619c121dd9a38b48a2db71", + "8cd8cda2d5923c91c9cb095e22490f01cfd098254b1b261c1f8c5a916985dfd8", + ], + // v2.6.2-1 + [ + "d4811fca51d8e2517f63c680b724a9d566d163fad94c858d2029c6ab466d4163", + "225b611cd14b6291ddd46d598b05f0362263869b1ef2252c1b637d43565c3b9d", + ], + // v2.7-1 + [ + "5e291e1b41a2a5ec0727a629fab7b43b8ebc9a269f58439ea8eed937925084f9", + "e9b3a452bc623c39b025e06e248276a9b8bcd87eb7eee7de55e8d5c5bf822522", + ], + // v2.7.1-1 + [ + "8d5ae3c40962a4846e4dae9d373a8660227e85f051d12e5bb882ef92d136dfca", + "7f20637b7a9527fbed53985fa55c5fc56c7d8bd132589219e92bd49f488c2754", + ], + // v3.0-1 + [ + "177cea0f3d64ac7a2834e24637aecb4ab133e036c50c2568489079cefb8fd7ec", + "c9e9d1a7844077df38cd0e540be1b435db7c9fed878a60fc32d22c778345fc09", + ], +]; +static STAGE_COUNTER: AtomicU64 = AtomicU64::new(0); + +// App-owned trust pin for the public GitLab artifact produced by release v3.0-1. +// Provenance: GitLab job 15069375988, 18,365,240 bytes; the downloaded artifact's +// MD5 also matched GitLab's x-goog-hash/ETag value ae79003c73e0e36c7bd1ea985d77478f. +const RELEASE_PINS: [ReleasePin; 1] = [ReleasePin { + tag_name: "v3.0-1", + asset_name: "dxvk-gplasync-v3.0-1.zip", + url: "https://gitlab.com/Ph42oN/dxvk-gplasync/-/jobs/15069375988/artifacts/download?file_type=archive", + sha256: "83af6b77a080373ae00718239491e815f2bdc3145e074e8198ed7200cb296f7b", +}]; + +#[derive(Debug, Deserialize)] +struct Release { + #[serde(default)] + tag_name: String, + #[serde(default)] + name: String, + #[serde(default)] + assets: ReleaseAssets, +} + +#[derive(Debug, Default, Deserialize)] +struct ReleaseAssets { + #[serde(default)] + links: Vec, +} + +#[derive(Debug, Deserialize)] +struct ReleaseLink { + #[serde(default)] + name: String, + #[serde(default)] + url: String, +} + +#[derive(Clone, Copy, Debug)] +struct ReleasePin { + tag_name: &'static str, + asset_name: &'static str, + url: &'static str, + sha256: &'static str, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ArchiveKind { + Zip, + TarGz, +} + +#[derive(Debug, PartialEq, Eq)] +struct ReleaseAsset { + url: String, + filename: String, + kind: ArchiveKind, + sha256: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +struct ManagedManifest { + version: u32, + game_dir: PathBuf, + files: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +struct ManagedFile { + name: String, + installed_sha256: String, + original_sha256: Option, +} + +/// Read DXVK installation and configuration state. Invalid or stale game paths +/// are reported as an uninstalled default state. +pub fn status(game_dir: Option<&Path>) -> DxvkState { + let Some(game_dir) = game_dir.filter(|path| !path.as_os_str().is_empty() && path.is_dir()) + else { + return DxvkState::default(); + }; + + let conf = game_dir.join("dxvk.conf"); + let conf_exists = regular_file(&conf); + let settings = if conf_exists { + fs::read(&conf) + .map(|body| parse_conf(&String::from_utf8_lossy(&body))) + .unwrap_or_default() + } else { + DxvkSettings::default() + }; + + DxvkState { + installed: DXVK_FILES + .iter() + .all(|filename| regular_file(&game_dir.join(filename))), + conf_exists, + settings, + } +} + +/// Validate and atomically replace `dxvk.conf` using the renderer's existing +/// settings model. +pub fn configure(state: &AppState, game_dir: &Path, settings: &DxvkSettings) -> Result<(), String> { + validate_settings(settings)?; + let game_dir = validate_game_dir(game_dir)?; + configure_managed( + state.data_dir(), + &game_dir, + settings, + &state.mod_files_dir(), + )?; + state.log("Success", "Updated dxvk.conf from DXVK settings."); + Ok(()) +} + +fn configure_managed( + storage_dir: &Path, + game_dir: &Path, + settings: &DxvkSettings, + staging_root: &Path, +) -> Result<(), String> { + let conf = flat_target(game_dir, "dxvk.conf")?; + let mut manifest = managed_manifest_for_configure(storage_dir, game_dir)?; + verify_managed_targets(&manifest, game_dir, storage_dir)?; + let stage = create_private_dir(staging_root, "dxvk-configure")?; + let snapshot = Snapshot::capture(std::slice::from_ref(&conf), &stage.join("rollback"))?; + let result = (|| { + fs_ops::atomic_write(&conf, build_conf(settings, true).as_bytes())?; + update_installed_hashes(&mut manifest, game_dir)?; + save_managed_manifest(storage_dir, &manifest) + })(); + if let Err(error) = result { + return match snapshot.restore() { + Ok(()) => { + let _ = fs::remove_dir_all(&stage); + Err(format!("{error}; previous DXVK configuration was restored")) + } + Err(rollback) => Err(format!( + "{error}; rollback failed: {rollback}. Recovery files remain in {}", + stage.display() + )), + }; + } + let _ = fs::remove_dir_all(&stage); + Ok(()) +} + +/// Download the pinned DXVK-GPLAsync release, validate it in an isolated +/// staging directory, and commit the two required DLLs plus `dxvk.conf` with +/// rollback on failure. +pub fn install(state: &AppState, game_dir: &Path, settings: &DxvkSettings) -> Result<(), String> { + validate_settings(settings)?; + let game_dir = validate_game_dir(game_dir)?; + let stage = create_stage(state, "dxvk")?; + + let result = (|| { + state.log("Info", "Downloading DXVK-GPLAsync..."); + let release = fetch_pinned_release()?; + let asset = select_release_asset(&release)?; + let archive = stage.join(&asset.filename); + fs_ops::download( + state, + &asset.url, + &archive, + Some(&asset.sha256), + MAX_ARCHIVE_BYTES, + )?; + state.log("Success", "Downloaded DXVK-GPLAsync successfully."); + + let extracted = stage.join("extracted"); + match asset.kind { + ArchiveKind::Zip => fs_ops::extract_zip(&archive, &extracted)?, + ArchiveKind::TarGz => fs_ops::extract_tar_gz(&archive, &extracted)?, + } + let sources = find_dxvk_files(&extracted)?; + state.log( + "Success", + "Extracted and validated DXVK-GPLAsync successfully.", + ); + + apply_managed_install( + state.data_dir(), + &game_dir, + &sources, + settings, + supports_gpl_async_cache(&release), + &stage.join("rollback"), + )?; + + state.log("Success", "DXVK-GPLAsync installed successfully."); + Ok(()) + })(); + + if result.is_ok() { + let _ = fs::remove_dir_all(&stage); + } else { + state.log( + "Warning", + format!("DXVK staging files were retained at {}.", stage.display()), + ); + } + result +} + +/// Remove PatchOpsIII-managed DXVK files with rollback if any removal fails. +pub fn uninstall(state: &AppState, game_dir: &Path) -> Result<(), String> { + let game_dir = validate_game_dir(game_dir)?; + let known = known_legacy_pairs(); + adopt_legacy_install(state.data_dir(), &game_dir, &known)?; + match uninstall_managed_files(state.data_dir(), &game_dir) { + Ok(true) => { + state.log("Success", "DXVK-GPLAsync has been uninstalled."); + Ok(()) + } + Ok(false) => { + state.log("Info", "DXVK-GPLAsync was not installed."); + Ok(()) + } + Err(error) => { + state.log( + "Warning", + format!( + "DXVK recovery state was retained at {}.", + managed_state_dir(state.data_dir()).display() + ), + ); + Err(error) + } + } +} + +fn fetch_pinned_release() -> Result { + let response = reqwest::blocking::Client::builder() + .connect_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(30)) + .build() + .map_err(|error| error.to_string())? + .get(RELEASE_API) + .header(reqwest::header::USER_AGENT, "PatchOpsIII") + .send() + .and_then(reqwest::blocking::Response::error_for_status) + .map_err(|error| format!("failed to query DXVK releases: {error}"))?; + if response + .content_length() + .is_some_and(|length| length > MAX_RELEASE_JSON_BYTES) + { + return Err("DXVK release metadata is unexpectedly large".into()); + } + let mut body = Vec::new(); + response + .take(MAX_RELEASE_JSON_BYTES + 1) + .read_to_end(&mut body) + .map_err(|error| error.to_string())?; + if body.len() as u64 > MAX_RELEASE_JSON_BYTES { + return Err("DXVK release metadata is unexpectedly large".into()); + } + serde_json::from_slice::(&body) + .map_err(|error| format!("invalid DXVK release metadata: {error}")) +} + +fn select_release_asset(release: &Release) -> Result { + select_release_asset_with_pins(release, &RELEASE_PINS) +} + +fn select_release_asset_with_pins( + release: &Release, + pins: &[ReleasePin], +) -> Result { + let pin = pins + .iter() + .find(|pin| release.tag_name.trim() == pin.tag_name) + .ok_or_else(|| { + format!( + "DXVK-GPLAsync release {} is not supported by this PatchOpsIII build", + release.tag_name.trim() + ) + })?; + let sha256 = normalize_sha256(pin.sha256).ok_or_else(|| { + format!( + "DXVK-GPLAsync release {} does not have a valid app-owned SHA-256 pin", + pin.tag_name + ) + })?; + validate_download_url(pin.url)?; + let kind = archive_kind(pin.asset_name).ok_or_else(|| { + format!( + "DXVK-GPLAsync release {} has an unsupported pinned archive type", + pin.tag_name + ) + })?; + let link = release + .assets + .links + .iter() + .find(|link| link.name.trim() == pin.asset_name && link.url.trim() == pin.url) + .ok_or_else(|| { + format!( + "DXVK-GPLAsync release {} did not contain its exact pinned asset", + pin.tag_name + ) + })?; + Ok(ReleaseAsset { + url: link.url.trim().to_owned(), + filename: archive_filename(&link.name, &link.url, kind), + kind, + sha256, + }) +} + +fn archive_kind(value: &str) -> Option { + let value = value + .split(['?', '#']) + .next() + .unwrap_or(value) + .to_ascii_lowercase(); + if value.ends_with(".zip") { + Some(ArchiveKind::Zip) + } else if value.ends_with(".tar.gz") || value.ends_with(".tgz") { + Some(ArchiveKind::TarGz) + } else { + None + } +} + +fn archive_filename(name: &str, url: &str, kind: ArchiveKind) -> String { + let from_url = reqwest::Url::parse(url) + .ok() + .and_then(|url| url.path_segments()?.next_back().map(str::to_string)); + [Some(name.trim().to_string()), from_url] + .into_iter() + .flatten() + .find(|value| { + !value.is_empty() + && value.len() <= 200 + && !value.contains(['/', '\\']) + && archive_kind(value) == Some(kind) + }) + .unwrap_or_else(|| match kind { + ArchiveKind::Zip => "dxvk-gplasync.zip".into(), + ArchiveKind::TarGz => "dxvk-gplasync.tar.gz".into(), + }) +} + +fn validate_download_url(value: &str) -> Result<(), String> { + let url = reqwest::Url::parse(value).map_err(|_| "invalid DXVK download URL".to_string())?; + if url.scheme() != "https" + || url.host_str() != Some("gitlab.com") + || !url.username().is_empty() + || url.password().is_some() + || url.port_or_known_default() != Some(443) + || url.fragment().is_some() + || url.path() != "/Ph42oN/dxvk-gplasync/-/jobs/15069375988/artifacts/download" + || url.query() != Some("file_type=archive") + { + return Err("DXVK release metadata returned an unexpected download URL".into()); + } + Ok(()) +} + +fn normalize_sha256(value: &str) -> Option { + let value = value.trim(); + let value = value + .split_once(':') + .filter(|(prefix, _)| prefix.eq_ignore_ascii_case("sha256")) + .map_or(value, |(_, digest)| digest) + .trim() + .to_ascii_lowercase(); + (value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())).then_some(value) +} + +fn managed_state_dir(storage_dir: &Path) -> PathBuf { + storage_dir.join(MANAGED_STATE_DIRECTORY) +} + +fn managed_manifest_path(storage_dir: &Path) -> PathBuf { + managed_state_dir(storage_dir).join(MANAGED_MANIFEST_FILENAME) +} + +fn managed_backup_path(state_dir: &Path, name: &str) -> Result { + if !MANAGED_FILES.contains(&name) { + return Err("DXVK managed state contains an invalid filename".into()); + } + Ok(state_dir.join(MANAGED_BACKUP_DIRECTORY).join(name)) +} + +fn managed_targets(game_dir: &Path) -> Result, String> { + MANAGED_FILES + .iter() + .map(|name| flat_target(game_dir, name)) + .collect() +} + +fn load_managed_manifest( + storage_dir: &Path, + game_dir: &Path, +) -> Result, String> { + let state_dir = managed_state_dir(storage_dir); + match fs::symlink_metadata(&state_dir) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(error.to_string()), + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => { + return Err(format!( + "refusing unsafe DXVK managed state at {}", + state_dir.display() + )); + } + Ok(_) => {} + } + let path = managed_manifest_path(storage_dir); + if !regular_file(&path) { + return Err(format!( + "DXVK managed state is incomplete; recovery files were retained at {}", + state_dir.display() + )); + } + let body = fs::read(&path).map_err(|error| error.to_string())?; + if body.len() > 64 * 1024 { + return Err("DXVK managed manifest is unexpectedly large".into()); + } + let manifest: ManagedManifest = serde_json::from_slice(&body) + .map_err(|error| format!("invalid DXVK managed manifest: {error}"))?; + validate_managed_manifest(&manifest, game_dir, &state_dir)?; + Ok(Some(manifest)) +} + +fn managed_manifest_for_configure( + storage_dir: &Path, + game_dir: &Path, +) -> Result { + load_managed_manifest(storage_dir, game_dir)? + .ok_or_else(|| UNMANAGED_CONFIG_MESSAGE.to_string()) +} + +fn validate_managed_manifest( + manifest: &ManagedManifest, + game_dir: &Path, + state_dir: &Path, +) -> Result<(), String> { + if manifest.version != MANAGED_STATE_VERSION { + return Err("unsupported DXVK managed manifest version".into()); + } + if manifest.game_dir != game_dir { + return Err(format!( + "DXVK is managed for {}, not {}", + manifest.game_dir.display(), + game_dir.display() + )); + } + let mut names = HashSet::new(); + for file in &manifest.files { + if !MANAGED_FILES.contains(&file.name.as_str()) || !names.insert(file.name.as_str()) { + return Err("DXVK managed manifest contains invalid or duplicate files".into()); + } + if normalize_sha256(&file.installed_sha256).is_none() { + return Err("DXVK managed manifest contains an invalid installed checksum".into()); + } + let backup = managed_backup_path(state_dir, &file.name)?; + match &file.original_sha256 { + Some(expected) => { + if normalize_sha256(expected).is_none() + || !regular_file(&backup) + || !fs_ops::sha256_file(&backup)?.eq_ignore_ascii_case(expected) + { + return Err(format!( + "DXVK original backup {} failed validation", + backup.display() + )); + } + } + None if backup.exists() => { + return Err(format!( + "DXVK managed state contains an unexpected backup at {}", + backup.display() + )); + } + None => {} + } + } + if names.len() != MANAGED_FILES.len() { + return Err("DXVK managed manifest is incomplete".into()); + } + Ok(()) +} + +fn verify_managed_targets( + manifest: &ManagedManifest, + game_dir: &Path, + storage_dir: &Path, +) -> Result<(), String> { + validate_managed_manifest(manifest, game_dir, &managed_state_dir(storage_dir))?; + let mut changed = Vec::new(); + for file in &manifest.files { + let target = flat_target(game_dir, &file.name)?; + let matches = regular_file(&target) + && fs_ops::sha256_file(&target) + .map(|actual| actual.eq_ignore_ascii_case(&file.installed_sha256)) + .unwrap_or(false); + if !matches { + changed.push(file.name.clone()); + } + } + if changed.is_empty() { + Ok(()) + } else { + Err(format!( + "DXVK files changed after PatchOpsIII installed them ({}); refusing to overwrite them. Recovery state remains at {}", + changed.join(", "), + managed_state_dir(storage_dir).display() + )) + } +} + +fn known_legacy_pairs() -> Vec<[String; 2]> { + KNOWN_LEGACY_DXVK_PAIRS + .iter() + .map(|pair| [pair[0].to_owned(), pair[1].to_owned()]) + .collect() +} + +fn source_pair(sources: &[PathBuf; 2]) -> Result<[String; 2], String> { + Ok([ + fs_ops::sha256_file(&sources[0])?, + fs_ops::sha256_file(&sources[1])?, + ]) +} + +/// Adopt only an exact origin/main DXVK payload. The DLL pair proves the +/// legacy release; the user-editable config is backed up as an original so an +/// uninstall restores its current bytes instead of deleting them. +fn adopt_legacy_install( + storage_dir: &Path, + game_dir: &Path, + expected_pairs: &[[String; 2]], +) -> Result { + if load_managed_manifest(storage_dir, game_dir)?.is_some() { + return Ok(false); + } + let targets = managed_targets(game_dir)?; + let dlls = &targets[..DXVK_FILES.len()]; + if dlls.iter().all(|target| !target.exists()) { + return Ok(false); + } + if dlls.iter().any(|target| !regular_file(target)) { + return Err( + "The unmanaged DXVK DLL pair is incomplete or unsafe; no files were changed".into(), + ); + } + let actual = [ + fs_ops::sha256_file(&dlls[0])?, + fs_ops::sha256_file(&dlls[1])?, + ]; + if !expected_pairs.iter().any(|pair| pair == &actual) { + return Err("The unmanaged DXVK DLLs do not match the same verified release known to this build; no files were changed. Remove or restore the unknown installation manually before installing PatchOpsIII-managed DXVK.".into()); + } + let config = &targets[2]; + if !regular_file(config) { + return Err("The unmanaged DXVK config is missing or unsafe; no files were changed".into()); + } + + let state_dir = managed_state_dir(storage_dir); + fs::create_dir(&state_dir).map_err(|error| error.to_string())?; + let result = (|| { + let backup_dir = state_dir.join(MANAGED_BACKUP_DIRECTORY); + fs::create_dir(&backup_dir).map_err(|error| error.to_string())?; + let config_hash = fs_ops::sha256_file(config)?; + let config_backup = managed_backup_path(&state_dir, "dxvk.conf")?; + fs::copy(config, &config_backup).map_err(|error| error.to_string())?; + if !fs_ops::sha256_file(&config_backup)?.eq_ignore_ascii_case(&config_hash) { + return Err("DXVK legacy config backup failed verification".into()); + } + let manifest = ManagedManifest { + version: MANAGED_STATE_VERSION, + game_dir: game_dir.to_path_buf(), + files: vec![ + ManagedFile { + name: "dxgi.dll".into(), + installed_sha256: actual[0].clone(), + original_sha256: None, + }, + ManagedFile { + name: "d3d11.dll".into(), + installed_sha256: actual[1].clone(), + original_sha256: None, + }, + ManagedFile { + name: "dxvk.conf".into(), + installed_sha256: config_hash.clone(), + original_sha256: Some(config_hash), + }, + ], + }; + save_managed_manifest(storage_dir, &manifest) + })(); + if let Err(error) = result { + let _ = remove_new_managed_state(storage_dir); + return Err(error); + } + Ok(true) +} + +fn prepare_managed_install( + storage_dir: &Path, + game_dir: &Path, + targets: &[PathBuf], +) -> Result<(ManagedManifest, bool), String> { + if let Some(manifest) = load_managed_manifest(storage_dir, game_dir)? { + verify_managed_targets(&manifest, game_dir, storage_dir)?; + return Ok((manifest, false)); + } + if targets.len() != MANAGED_FILES.len() { + return Err("DXVK managed target list is incomplete".into()); + } + + let state_dir = managed_state_dir(storage_dir); + fs::create_dir(&state_dir).map_err(|error| error.to_string())?; + let result = (|| { + let backup_dir = state_dir.join(MANAGED_BACKUP_DIRECTORY); + fs::create_dir(&backup_dir).map_err(|error| error.to_string())?; + let mut files = Vec::with_capacity(MANAGED_FILES.len()); + for (name, target) in MANAGED_FILES.iter().zip(targets) { + let original_sha256 = if target.exists() { + if !regular_file(target) { + return Err(format!("refusing to back up {}", target.display())); + } + let expected = fs_ops::sha256_file(target)?; + let backup = managed_backup_path(&state_dir, name)?; + fs::copy(target, &backup).map_err(|error| error.to_string())?; + if fs_ops::sha256_file(&backup)? != expected { + return Err(format!( + "DXVK original backup {} failed verification", + backup.display() + )); + } + Some(expected) + } else { + None + }; + files.push(ManagedFile { + name: (*name).to_owned(), + installed_sha256: String::new(), + original_sha256, + }); + } + Ok(ManagedManifest { + version: MANAGED_STATE_VERSION, + game_dir: game_dir.to_path_buf(), + files, + }) + })(); + if result.is_err() { + let _ = fs::remove_dir_all(&state_dir); + } + result.map(|manifest| (manifest, true)) +} + +fn apply_managed_install( + storage_dir: &Path, + game_dir: &Path, + sources: &[PathBuf; 2], + settings: &DxvkSettings, + include_gpl_async_cache: bool, + rollback_directory: &Path, +) -> Result<(), String> { + let targets = managed_targets(game_dir)?; + let snapshot = Snapshot::capture(&targets, rollback_directory)?; + let mut expected_pairs = vec![source_pair(sources)?]; + expected_pairs.extend(known_legacy_pairs()); + let adopted = adopt_legacy_install(storage_dir, game_dir, &expected_pairs)?; + let (mut manifest, new_managed_state) = + match prepare_managed_install(storage_dir, game_dir, &targets) { + Ok(prepared) => prepared, + Err(error) => { + if adopted { + remove_new_managed_state(storage_dir).map_err(|cleanup| { + format!("{error}; legacy ownership migration cleanup failed: {cleanup}") + })?; + } + return Err(error); + } + }; + let commit = (|| { + for (source, target) in sources.iter().zip(targets.iter()) { + fs::copy(source, target).map_err(|error| { + format!( + "failed to install {} to {}: {error}", + source.display(), + target.display() + ) + })?; + } + fs_ops::atomic_write( + &targets[2], + build_conf(settings, include_gpl_async_cache).as_bytes(), + )?; + if !targets.iter().all(|path| regular_file(path)) { + return Err("DXVK install verification failed".to_string()); + } + update_installed_hashes(&mut manifest, game_dir)?; + save_managed_manifest(storage_dir, &manifest)?; + Ok(()) + })(); + + if let Err(error) = commit { + return match snapshot.restore() { + Ok(()) => { + if new_managed_state || adopted { + remove_new_managed_state(storage_dir).map_err(|cleanup| { + format!( + "{error}; previous DXVK files were restored, but recovery-state cleanup failed: {cleanup}" + ) + })?; + } + Err(format!("{error}; previous DXVK files were restored")) + } + Err(rollback) => Err(format!( + "{error}; rollback failed: {rollback}. Recovery files remain in {}", + rollback_directory.display() + )), + }; + } + Ok(()) +} + +fn update_installed_hashes(manifest: &mut ManagedManifest, game_dir: &Path) -> Result<(), String> { + for file in &mut manifest.files { + let target = flat_target(game_dir, &file.name)?; + if !regular_file(&target) { + return Err(format!( + "DXVK install verification failed for {}", + target.display() + )); + } + file.installed_sha256 = fs_ops::sha256_file(&target)?; + } + Ok(()) +} + +fn save_managed_manifest(storage_dir: &Path, manifest: &ManagedManifest) -> Result<(), String> { + validate_managed_manifest( + manifest, + &manifest.game_dir, + &managed_state_dir(storage_dir), + )?; + let body = serde_json::to_vec_pretty(manifest).map_err(|error| error.to_string())?; + fs_ops::atomic_write(&managed_manifest_path(storage_dir), &body) +} + +fn remove_new_managed_state(storage_dir: &Path) -> Result<(), String> { + let state_dir = managed_state_dir(storage_dir); + let metadata = fs::symlink_metadata(&state_dir).map_err(|error| error.to_string())?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(format!( + "refusing unsafe DXVK managed state at {}", + state_dir.display() + )); + } + fs::remove_dir_all(state_dir).map_err(|error| error.to_string()) +} + +fn uninstall_managed_files(storage_dir: &Path, game_dir: &Path) -> Result { + let targets = managed_targets(game_dir)?; + let Some(manifest) = load_managed_manifest(storage_dir, game_dir)? else { + if targets.iter().any(|target| target.exists()) { + return Err( + "No PatchOpsIII-managed DXVK record exists; refusing to remove unowned files" + .into(), + ); + } + return Ok(false); + }; + verify_managed_targets(&manifest, game_dir, storage_dir)?; + + let transaction = create_private_dir(storage_dir, "dxvk-uninstall")?; + let snapshot = match Snapshot::capture(&targets, &transaction.join("rollback")) { + Ok(snapshot) => snapshot, + Err(error) => { + let _ = fs::remove_dir_all(&transaction); + return Err(error); + } + }; + let state_dir = managed_state_dir(storage_dir); + let moved_state = transaction.join("managed-state"); + if let Err(error) = fs::rename(&state_dir, &moved_state) { + let _ = fs::remove_dir_all(&transaction); + return Err(format!("failed to secure DXVK recovery state: {error}")); + } + + let restore = (|| { + for file in &manifest.files { + let target = flat_target(game_dir, &file.name)?; + if let Some(expected) = &file.original_sha256 { + let backup = managed_backup_path(&moved_state, &file.name)?; + fs::copy(&backup, &target) + .map_err(|error| format!("failed to restore {}: {error}", target.display()))?; + if fs_ops::sha256_file(&target)? != *expected { + return Err(format!( + "restored DXVK file {} failed verification", + target.display() + )); + } + } else { + remove_file_if_present(&target)?; + if target.exists() { + return Err(format!("failed to remove {}", target.display())); + } + } + } + Ok(()) + })(); + + if let Err(error) = restore { + let rollback = snapshot.restore(); + let state_restore = fs::rename(&moved_state, &state_dir); + if rollback.is_ok() && state_restore.is_ok() { + let _ = fs::remove_dir_all(&transaction); + return Err(format!( + "{error}; the managed DXVK installation was restored" + )); + } + return Err(format!( + "{error}; recovery was incomplete. Files were retained at {}", + transaction.display() + )); + } + + fs::remove_dir_all(&transaction).map_err(|error| { + format!( + "DXVK files were restored, but recovery-state cleanup failed at {}: {error}", + transaction.display() + ) + })?; + Ok(true) +} + +fn supports_gpl_async_cache(release: &Release) -> bool { + let label = if release.tag_name.trim().is_empty() { + release.name.as_str() + } else { + release.tag_name.as_str() + }; + let Some(start) = label.find(|character: char| character.is_ascii_digit()) else { + return true; + }; + let version = &label[start..]; + let major_end = version + .find(|character: char| !character.is_ascii_digit()) + .unwrap_or(version.len()); + let Ok(major) = version[..major_end].parse::() else { + return true; + }; + let Some(minor_text) = version + .get(major_end..) + .and_then(|rest| rest.strip_prefix('.')) + else { + return true; + }; + let minor_end = minor_text + .find(|character: char| !character.is_ascii_digit()) + .unwrap_or(minor_text.len()); + let Ok(minor) = minor_text[..minor_end].parse::() else { + return true; + }; + (major, minor) < (2, 7) +} + +fn validate_settings(settings: &DxvkSettings) -> Result<(), String> { + if !(0..=64).contains(&settings.num_compiler_threads) { + return Err("DXVK compiler threads must be between 0 and 64".into()); + } + if !(0..=360).contains(&settings.max_frame_rate) { + return Err("DXVK frame rate must be between 0 and 360".into()); + } + if !(0..=16).contains(&settings.max_frame_latency) { + return Err("DXVK frame latency must be between 0 and 16".into()); + } + if !matches!(settings.tear_free.as_str(), "Auto" | "True" | "False") { + return Err("DXVK tear-free mode must be Auto, True, or False".into()); + } + Ok(()) +} + +fn build_conf(settings: &DxvkSettings, include_gpl_async_cache: bool) -> String { + let mut lines = vec![format!( + "dxvk.enableAsync={}", + if settings.enable_async { + "true" + } else { + "false" + } + )]; + if include_gpl_async_cache && settings.gpl_async_cache { + lines.push("dxvk.gplAsyncCache=true".into()); + } + lines.extend([ + format!("dxvk.numCompilerThreads={}", settings.num_compiler_threads), + format!("dxgi.maxFrameRate={}", settings.max_frame_rate), + format!("dxgi.maxFrameLatency={}", settings.max_frame_latency), + format!("dxvk.tearFree={}", settings.tear_free), + ]); + if settings.hud_enabled { + lines.push("dxvk.hud=fps,frametimes,gpuload".into()); + } + lines.join("\n") + "\n" +} + +fn parse_conf(contents: &str) -> DxvkSettings { + let mut settings = DxvkSettings::default(); + for line in contents.lines() { + if line.trim_start().starts_with('#') { + continue; + } + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let (key, value) = (key.trim(), value.trim()); + match key { + "dxvk.enableAsync" => settings.enable_async = value.eq_ignore_ascii_case("true"), + "dxvk.gplAsyncCache" => settings.gpl_async_cache = value.eq_ignore_ascii_case("true"), + "dxvk.numCompilerThreads" => { + if let Ok(value) = value.parse() { + settings.num_compiler_threads = value; + } + } + "dxgi.maxFrameRate" => { + if let Ok(value) = value.parse() { + settings.max_frame_rate = value; + } + } + "dxgi.maxFrameLatency" => { + if let Ok(value) = value.parse() { + settings.max_frame_latency = value; + } + } + "dxvk.tearFree" => settings.tear_free = value.to_string(), + "dxvk.hud" => settings.hud_enabled = !value.is_empty(), + _ => {} + } + } + settings +} + +fn find_dxvk_files(root: &Path) -> Result<[PathBuf; 2], String> { + let mut directories = vec![root.to_path_buf()]; + let mut index = 0; + while index < directories.len() { + let directory = directories[index].clone(); + index += 1; + for entry in fs::read_dir(&directory).map_err(|error| error.to_string())? { + let entry = entry.map_err(|error| error.to_string())?; + let metadata = fs::symlink_metadata(entry.path()).map_err(|error| error.to_string())?; + if metadata.file_type().is_symlink() { + return Err("DXVK archive contains a link".into()); + } + if metadata.is_dir() { + directories.push(entry.path()); + } + } + } + directories.sort_by_key(|path| { + let components = path + .components() + .filter_map(|component| component.as_os_str().to_str()) + .map(str::to_ascii_lowercase) + .collect::>(); + let architecture = if components.iter().any(|part| part == "x64") { + 0 + } else if components.iter().any(|part| part == "x32") { + 2 + } else { + 1 + }; + ( + architecture, + components.len(), + path.to_string_lossy().to_lowercase(), + ) + }); + + for directory in directories { + let components = directory + .components() + .filter_map(|component| component.as_os_str().to_str()) + .collect::>(); + if components + .iter() + .any(|part| part.eq_ignore_ascii_case("x32")) + && !components + .iter() + .any(|part| part.eq_ignore_ascii_case("x64")) + { + continue; + } + let files = fs::read_dir(&directory) + .map_err(|error| error.to_string())? + .filter_map(Result::ok) + .filter(|entry| regular_file(&entry.path())) + .map(|entry| { + ( + entry.file_name().to_string_lossy().to_ascii_lowercase(), + entry.path(), + ) + }) + .collect::>(); + let dxgi = files.iter().find(|(name, _)| name == "dxgi.dll"); + let d3d11 = files.iter().find(|(name, _)| name == "d3d11.dll"); + if let (Some((_, dxgi)), Some((_, d3d11))) = (dxgi, d3d11) { + return Ok([dxgi.clone(), d3d11.clone()]); + } + } + Err("Required 64-bit DXVK files were not found in the archive".into()) +} + +fn validate_game_dir(path: &Path) -> Result { + if path.as_os_str().is_empty() { + return Err("Game directory is not set".into()); + } + let path = path + .canonicalize() + .map_err(|error| format!("invalid game directory: {error}"))?; + if !path.is_dir() { + return Err("Game directory is not a directory".into()); + } + if !["BlackOpsIII.exe", "BlackOps3.exe"] + .iter() + .any(|name| regular_file(&path.join(name))) + { + return Err("Game directory does not contain a Black Ops III executable".into()); + } + Ok(path) +} + +fn flat_target(root: &Path, filename: &str) -> Result { + if filename.is_empty() || filename.contains(['/', '\\']) || filename == "." || filename == ".." + { + return Err("invalid game filename".into()); + } + let target = root.join(filename); + if let Ok(metadata) = fs::symlink_metadata(&target) { + if metadata.file_type().is_symlink() { + return Err(format!( + "refusing to replace linked path {}", + target.display() + )); + } + if !metadata.is_file() { + return Err(format!("expected a file at {}", target.display())); + } + } + Ok(target) +} + +fn regular_file(path: &Path) -> bool { + fs::symlink_metadata(path) + .map(|metadata| metadata.is_file() && !metadata.file_type().is_symlink()) + .unwrap_or(false) +} + +fn create_stage(state: &AppState, label: &str) -> Result { + let root = state.mod_files_dir(); + fs::create_dir_all(&root).map_err(|error| error.to_string())?; + create_private_dir(&root, label) +} + +fn create_private_dir(root: &Path, label: &str) -> Result { + fs::create_dir_all(root).map_err(|error| error.to_string())?; + for _ in 0..10 { + let id = STAGE_COUNTER.fetch_add(1, Ordering::Relaxed); + let stage = root.join(format!(".{label}-stage-{}-{id}", std::process::id())); + match fs::create_dir(&stage) { + Ok(()) => return Ok(stage), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => return Err(error.to_string()), + } + } + Err("could not create a unique staging directory".into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + static TEST_COUNTER: AtomicU64 = AtomicU64::new(0); + + fn temp_dir(label: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "patchops-dxvk-{label}-{}-{}", + std::process::id(), + TEST_COUNTER.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn writes_and_parses_the_existing_dxvk_format() { + let settings = DxvkSettings { + hud_enabled: true, + ..DxvkSettings::default() + }; + let conf = build_conf(&settings, true); + assert_eq!( + conf, + "dxvk.enableAsync=true\n\ +dxvk.gplAsyncCache=true\n\ +dxvk.numCompilerThreads=0\n\ +dxgi.maxFrameRate=0\n\ +dxgi.maxFrameLatency=1\n\ +dxvk.tearFree=True\n\ +dxvk.hud=fps,frametimes,gpuload\n" + ); + assert_eq!( + parse_conf(&format!("# preserved comment\n{conf}")), + settings + ); + assert!(!build_conf(&settings, false).contains("gplAsyncCache")); + } + + #[test] + fn release_selection_requires_the_exact_app_owned_pin() { + let release: Release = serde_json::from_str( + r#"{ + "tag_name":"v3.0-1", + "assets":{"links":[ + {"name":"dxvk-gplasync-v3.0-1.tar.gz","url":"https://gitlab.com/Ph42oN/dxvk-gplasync/-/raw/main/releases/dxvk-gplasync-v3.0-1.tar.gz"}, + {"name":"dxvk-gplasync-v3.0-1.zip","url":"https://gitlab.com/Ph42oN/dxvk-gplasync/-/jobs/15069375988/artifacts/download?file_type=archive"} + ]} + }"#, + ) + .unwrap(); + let selected = select_release_asset(&release).unwrap(); + assert_eq!(selected.kind, ArchiveKind::Zip); + assert_eq!(selected.sha256, RELEASE_PINS[0].sha256); + assert!(!supports_gpl_async_cache(&release)); + + let raw_main_only: Release = serde_json::from_str( + r#"{ + "tag_name":"v3.0-1", + "assets":{"links":[ + {"name":"dxvk-gplasync-v3.0-1.zip","url":"https://gitlab.com/Ph42oN/dxvk-gplasync/-/raw/main/releases/dxvk-gplasync-v3.0-1.zip"} + ]} + }"#, + ) + .unwrap(); + assert!(select_release_asset(&raw_main_only).is_err()); + } + + #[test] + fn release_selection_rejects_missing_or_invalid_pinned_digests() { + let release: Release = serde_json::from_str( + r#"{ + "tag_name":"v3.0-1", + "assets":{"links":[ + {"name":"dxvk-gplasync-v3.0-1.zip","url":"https://gitlab.com/Ph42oN/dxvk-gplasync/-/jobs/15069375988/artifacts/download?file_type=archive"} + ]} + }"#, + ) + .unwrap(); + for sha256 in ["", "sha256:not-a-digest"] { + let pins = [ReleasePin { + sha256, + ..RELEASE_PINS[0] + }]; + let error = select_release_asset_with_pins(&release, &pins).unwrap_err(); + assert!(error.contains("valid app-owned SHA-256")); + } + } + + #[test] + fn accepts_only_the_pinned_gitlab_release_url() { + assert!(validate_download_url(RELEASE_PINS[0].url).is_ok()); + for url in [ + "http://gitlab.com/Ph42oN/dxvk-gplasync/-/jobs/15069375988/artifacts/download?file_type=archive", + "https://gitlab.com.evil.invalid/Ph42oN/dxvk-gplasync/-/jobs/15069375988/artifacts/download?file_type=archive", + "https://gitlab.com/Ph42oN/dxvk-gplasync/-/raw/main/releases/dxvk-gplasync-v3.0-1.zip", + "https://gitlab.com/Ph42oN/dxvk-gplasync/-/jobs/999/artifacts/download?file_type=archive", + "https://gitlab.com:444/Ph42oN/dxvk-gplasync/-/jobs/15069375988/artifacts/download?file_type=archive", + ] { + assert!(validate_download_url(url).is_err(), "accepted {url}"); + } + } + + #[test] + fn chooses_x64_payload_and_snapshot_rolls_back() { + let root = temp_dir("source"); + for architecture in ["x32", "x64"] { + let dir = root.join(architecture); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("dxgi.dll"), architecture).unwrap(); + fs::write(dir.join("d3d11.dll"), architecture).unwrap(); + } + let selected = find_dxvk_files(&root).unwrap(); + assert!( + selected + .iter() + .all(|path| path.starts_with(root.join("x64"))) + ); + + let target = root.join("installed.dll"); + fs::write(&target, b"original").unwrap(); + let snapshot = + Snapshot::capture(std::slice::from_ref(&target), &root.join("rollback")).unwrap(); + fs::write(&target, b"partial").unwrap(); + snapshot.restore().unwrap(); + assert_eq!(fs::read(&target).unwrap(), b"original"); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn rejects_out_of_range_or_injected_settings() { + let settings = DxvkSettings { + max_frame_rate: 361, + ..DxvkSettings::default() + }; + assert!(validate_settings(&settings).is_err()); + let settings = DxvkSettings { + tear_free: "True\ndxvk.hud=full".into(), + ..DxvkSettings::default() + }; + assert!(validate_settings(&settings).is_err()); + } + + #[test] + fn managed_uninstall_restores_exact_originals_and_removes_created_files() { + let root = temp_dir("managed-restore"); + let storage = root.join("data"); + let game = root.join("game"); + fs::create_dir_all(&storage).unwrap(); + fs::create_dir_all(&game).unwrap(); + fs::write(game.join("dxgi.dll"), b"original-dxgi").unwrap(); + fs::write(game.join("dxvk.conf"), b"original-conf").unwrap(); + + let targets = managed_targets(&game).unwrap(); + let (mut manifest, new_state) = prepare_managed_install(&storage, &game, &targets).unwrap(); + assert!(new_state); + fs::write(game.join("dxgi.dll"), b"managed-dxgi").unwrap(); + fs::write(game.join("d3d11.dll"), b"managed-d3d11").unwrap(); + fs::write(game.join("dxvk.conf"), b"managed-conf").unwrap(); + update_installed_hashes(&mut manifest, &game).unwrap(); + save_managed_manifest(&storage, &manifest).unwrap(); + + assert!(uninstall_managed_files(&storage, &game).unwrap()); + assert_eq!(fs::read(game.join("dxgi.dll")).unwrap(), b"original-dxgi"); + assert!(!game.join("d3d11.dll").exists()); + assert_eq!(fs::read(game.join("dxvk.conf")).unwrap(), b"original-conf"); + assert!(!managed_state_dir(&storage).exists()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn managed_uninstall_refuses_changed_or_unowned_files() { + let root = temp_dir("managed-conflict"); + let storage = root.join("data"); + let game = root.join("game"); + fs::create_dir_all(&storage).unwrap(); + fs::create_dir_all(&game).unwrap(); + let targets = managed_targets(&game).unwrap(); + let (mut manifest, _) = prepare_managed_install(&storage, &game, &targets).unwrap(); + for (name, body) in [ + ("dxgi.dll", b"managed-dxgi".as_slice()), + ("d3d11.dll", b"managed-d3d11".as_slice()), + ("dxvk.conf", b"managed-conf".as_slice()), + ] { + fs::write(game.join(name), body).unwrap(); + } + update_installed_hashes(&mut manifest, &game).unwrap(); + save_managed_manifest(&storage, &manifest).unwrap(); + fs::write(game.join("dxgi.dll"), b"user-modified").unwrap(); + + let error = uninstall_managed_files(&storage, &game).unwrap_err(); + assert!(error.contains("changed after PatchOpsIII installed")); + assert_eq!(fs::read(game.join("dxgi.dll")).unwrap(), b"user-modified"); + assert_eq!(fs::read(game.join("d3d11.dll")).unwrap(), b"managed-d3d11"); + assert!(managed_state_dir(&storage).is_dir()); + + let unmanaged_storage = root.join("unmanaged-data"); + fs::create_dir_all(&unmanaged_storage).unwrap(); + let error = uninstall_managed_files(&unmanaged_storage, &game).unwrap_err(); + assert!(error.contains("refusing to remove unowned files")); + assert_eq!(fs::read(game.join("dxgi.dll")).unwrap(), b"user-modified"); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn managed_reinstall_and_reconfigure_preserve_the_first_originals() { + let root = temp_dir("managed-update"); + let storage = root.join("data"); + let game = root.join("game"); + fs::create_dir_all(&storage).unwrap(); + fs::create_dir_all(&game).unwrap(); + for (name, body) in [ + ("dxgi.dll", b"original-dxgi".as_slice()), + ("d3d11.dll", b"original-d3d11".as_slice()), + ("dxvk.conf", b"original-conf".as_slice()), + ] { + fs::write(game.join(name), body).unwrap(); + } + + let targets = managed_targets(&game).unwrap(); + let (mut first, _) = prepare_managed_install(&storage, &game, &targets).unwrap(); + for target in &targets { + fs::write(target, b"managed-v1").unwrap(); + } + update_installed_hashes(&mut first, &game).unwrap(); + save_managed_manifest(&storage, &first).unwrap(); + + let (mut second, new_state) = prepare_managed_install(&storage, &game, &targets).unwrap(); + assert!(!new_state); + for target in &targets { + fs::write(target, b"managed-v2").unwrap(); + } + update_installed_hashes(&mut second, &game).unwrap(); + save_managed_manifest(&storage, &second).unwrap(); + fs_ops::atomic_write(&game.join("dxvk.conf"), b"reconfigured").unwrap(); + update_installed_hashes(&mut second, &game).unwrap(); + save_managed_manifest(&storage, &second).unwrap(); + + assert!(uninstall_managed_files(&storage, &game).unwrap()); + assert_eq!(fs::read(game.join("dxgi.dll")).unwrap(), b"original-dxgi"); + assert_eq!(fs::read(game.join("d3d11.dll")).unwrap(), b"original-d3d11"); + assert_eq!(fs::read(game.join("dxvk.conf")).unwrap(), b"original-conf"); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn exact_legacy_pair_is_adopted_and_custom_config_is_restored() { + let root = temp_dir("legacy-adopt"); + let storage = root.join("data"); + let game = root.join("game"); + fs::create_dir_all(&storage).unwrap(); + fs::create_dir_all(&game).unwrap(); + fs::write(game.join("dxgi.dll"), b"legacy-dxgi").unwrap(); + fs::write(game.join("d3d11.dll"), b"legacy-d3d11").unwrap(); + fs::write(game.join("dxvk.conf"), b"custom legacy config\n").unwrap(); + let pair = [[ + fs_ops::sha256_file(&game.join("dxgi.dll")).unwrap(), + fs_ops::sha256_file(&game.join("d3d11.dll")).unwrap(), + ]]; + + assert!(adopt_legacy_install(&storage, &game, &pair).unwrap()); + let mut manifest = load_managed_manifest(&storage, &game).unwrap().unwrap(); + assert!( + manifest.files[..2] + .iter() + .all(|file| file.original_sha256.is_none()) + ); + assert!(manifest.files[2].original_sha256.is_some()); + + for target in managed_targets(&game).unwrap() { + fs::write(target, b"updated managed bytes").unwrap(); + } + update_installed_hashes(&mut manifest, &game).unwrap(); + save_managed_manifest(&storage, &manifest).unwrap(); + assert!(uninstall_managed_files(&storage, &game).unwrap()); + assert!(!game.join("dxgi.dll").exists()); + assert!(!game.join("d3d11.dll").exists()); + assert_eq!( + fs::read(game.join("dxvk.conf")).unwrap(), + b"custom legacy config\n" + ); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn historical_release_pairs_remain_exact_and_pair_bound() { + let pairs = known_legacy_pairs(); + assert_eq!(pairs.len(), KNOWN_LEGACY_DXVK_PAIRS.len()); + assert!(pairs.contains(&[ + "8d5ae3c40962a4846e4dae9d373a8660227e85f051d12e5bb882ef92d136dfca".into(), + "7f20637b7a9527fbed53985fa55c5fc56c7d8bd132589219e92bd49f488c2754".into(), + ])); + assert!(!pairs.contains(&[ + KNOWN_LEGACY_DXVK_PAIRS[6][0].into(), + KNOWN_LEGACY_DXVK_PAIRS[7][1].into(), + ])); + } + + #[test] + fn legacy_adoption_rejects_incomplete_or_mixed_dll_pairs_atomically() { + let root = temp_dir("legacy-reject"); + let storage = root.join("data"); + let game = root.join("game"); + fs::create_dir_all(&storage).unwrap(); + fs::create_dir_all(&game).unwrap(); + fs::write(game.join("dxgi.dll"), b"known-dxgi").unwrap(); + fs::write(game.join("dxvk.conf"), b"custom config").unwrap(); + let pair = [[ + fs_ops::sha256_file(&game.join("dxgi.dll")).unwrap(), + fs_ops::sha256_file(&game.join("dxvk.conf")).unwrap(), + ]]; + assert!(adopt_legacy_install(&storage, &game, &pair).is_err()); + assert!(!managed_state_dir(&storage).exists()); + assert_eq!(fs::read(game.join("dxgi.dll")).unwrap(), b"known-dxgi"); + + fs::write(game.join("d3d11.dll"), b"different-release").unwrap(); + assert!(adopt_legacy_install(&storage, &game, &pair).is_err()); + assert!(!managed_state_dir(&storage).exists()); + assert_eq!( + fs::read(game.join("d3d11.dll")).unwrap(), + b"different-release" + ); + assert_eq!(fs::read(game.join("dxvk.conf")).unwrap(), b"custom config"); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn configure_without_managed_state_cannot_overwrite_an_existing_config() { + let root = temp_dir("configure-unmanaged"); + let storage = root.join("data"); + let game = root.join("game"); + fs::create_dir_all(&storage).unwrap(); + fs::create_dir_all(&game).unwrap(); + let config = game.join("dxvk.conf"); + fs::write(&config, b"unmanaged sentinel").unwrap(); + + let staging = root.join("staging"); + let error = + configure_managed(&storage, &game, &DxvkSettings::default(), &staging).unwrap_err(); + assert_eq!(error, UNMANAGED_CONFIG_MESSAGE); + assert_eq!(fs::read(&config).unwrap(), b"unmanaged sentinel"); + assert!(!managed_state_dir(&storage).exists()); + assert!(!staging.exists()); + + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/native/core/src/engine.rs b/native/core/src/engine.rs new file mode 100644 index 0000000..15987ff --- /dev/null +++ b/native/core/src/engine.rs @@ -0,0 +1,787 @@ +use std::{ + cmp::Ordering, + io::Read, + path::{Path, PathBuf}, + time::Duration, +}; + +use serde::de::DeserializeOwned; +use serde_json::{Value, json}; + +use crate::{ + app::{self, AppState}, + dxvk, enhanced, exe, + models::{DxvkSettings, MaintenanceState, ModsState, PatchOpsState}, + steam, t7, +}; + +/// Synchronous operations facade. Callers are expected to own it on a worker +/// thread; no method requires a GUI or async runtime. +pub struct Engine { + state: AppState, +} + +impl Engine { + pub fn new(state: AppState) -> Self { + Self { state } + } + + pub fn state(&self) -> &AppState { + &self.state + } + + pub fn current_state(&self) -> Result { + current_state(&self.state) + } + + /// Dispatch the legacy local-HTTP paths without HTTP. The returned JSON + /// intentionally retains the Python API's wire shapes so existing UI code + /// can migrate independently from the backend. + pub fn dispatch(&self, path: &str, body: Option) -> Result { + let report = !matches!( + path, + "/api/status" | "/api/health" | "/api/logs/payload" | "/api/exe-swap/compatible-depot" + ); + if report { + self.state.begin_progress(path); + } + let requested_directory = body + .as_ref() + .and_then(|value| value["path"].as_str()) + .map(str::to_owned); + let result = self.dispatch_inner(path, body); + if report { + let error = result.as_ref().err().map(String::as_str).or_else(|| { + result + .as_ref() + .ok() + .filter(|value| value["ok"] == false || value["valid"] == false) + .and_then(|value| value["error"].as_str().or(value["message"].as_str())) + }); + if let Some(error) = error.filter(|error| { + // Python returns these input validation failures before its log/try block. + !(path == "/api/t7-config" + && (error.starts_with("Gamertag ") + || error.starts_with("t7patch.conf was not found."))) + }) { + let (category, message) = match path { + "/api/game-directory" => ( + "Error", + format!( + "Selected directory is not a Black Ops III install: {}", + requested_directory.as_deref().unwrap_or_default() + ), + ), + "/api/launch-options" if error == "Unsupported launch option." => { + ("Warning", error.to_owned()) + } + "/api/t7-config" => ( + "Error", + format!("Failed to update T7 Patch settings: {error}"), + ), + _ => ("Error", error.to_owned()), + }; + self.state.log(category, message); + } + self.state.end_progress(error); + } + result + .or_else(|error| { + if path == "/api/launch-options" && error == "Unsupported launch option." { + Ok(json!({"ok": false, "error": error, "state": self.current_state()?})) + } else if report { + Ok(json!({"ok": false, "error": error})) + } else { + Err(error) + } + }) + .map(normalize_wire_value) + } + + fn dispatch_inner(&self, path: &str, body: Option) -> Result { + let body = body.unwrap_or_else(|| json!({})); + match path { + "/api/health" => Ok(json!({"ok": true, "version": app::APP_VERSION})), + "/api/status" => serde_json::to_value(self.current_state()?).map_err(string_error), + "/api/game-directory" => { + let path = required_string(&body, "path")?; + self.mutate(|| app::set_game_directory(&self.state, path)) + } + "/api/launch" => self.mutate(|| steam::launch_game(&self.state)), + "/api/launch-options" => { + let options = required_string(&body, "options")?; + let preserve = optional_bool(&body, "preserve_fs_game").unwrap_or(false); + self.mutate(|| steam::apply_launch_options(&self.state, options, preserve)) + } + "/api/workshop-install" => { + let profile = required_string(&body, "profileId")?; + self.mutate(|| steam::install_workshop_profile(&self.state, profile)) + } + "/api/update-check" => self.update_check(), + "/api/release-channel" => { + let channel = required_string(&body, "channel")?; + self.mutate(|| app::set_release_channel(&self.state, channel)) + } + "/api/config" => { + let key = required_string(&body, "key")?; + let value = body + .get("value") + .cloned() + .ok_or_else(|| "Missing value.".to_string())?; + self.mutate(|| { + app::set_config_value( + &self.state, + &required_game_dir(&self.state)?, + key, + value, + body["comment"].as_str(), + ) + }) + } + "/api/dxvk-install" => { + let settings: DxvkSettings = decode(body)?; + self.mutate(|| { + dxvk::install(&self.state, &required_game_dir(&self.state)?, &settings) + }) + } + "/api/dxvk-uninstall" => { + self.mutate(|| dxvk::uninstall(&self.state, &required_game_dir(&self.state)?)) + } + "/api/dxvk-config" => { + let settings: DxvkSettings = decode(body)?; + self.mutate(|| { + dxvk::configure(&self.state, &required_game_dir(&self.state)?, &settings) + }) + } + "/api/t7-config" => { + let gamertag = optional_string(&body, "gamertag"); + let color = optional_string(&body, "colorCode").unwrap_or_default(); + let password = optional_string(&body, "networkPassword"); + let friends = optional_bool(&body, "friendsOnly"); + self.mutate(|| { + t7::configure( + &self.state, + &required_game_dir(&self.state)?, + gamertag.as_deref(), + &color, + password.as_deref(), + friends, + ) + }) + } + "/api/t7-install" => self.mutate(|| { + let game_dir = required_game_dir(&self.state)?; + let profile = exe::status(&self.state.load_settings(), Some(&game_dir)).profile; + t7::install(&self.state, &game_dir, &profile) + }), + "/api/t7-uninstall" => { + self.mutate(|| t7::uninstall(&self.state, &required_game_dir(&self.state)?)) + } + "/api/enhanced-validate" => { + let source = required_string(&body, "dumpSource")?.trim(); + if source.is_empty() { + return self.validation_response(false, "Select a dump source first."); + } + let valid = + enhanced::validate_and_remember_dump_source(&self.state, Path::new(source))?; + self.validation_response( + valid, + if valid { + "Source looks ready." + } else { + "Source is missing required dump files." + }, + ) + } + "/api/enhanced-install" => { + let source = required_string(&body, "dumpSource")?; + self.mutate(|| { + enhanced::install( + &self.state, + &required_game_dir(&self.state)?, + Path::new(source), + ) + }) + } + "/api/enhanced-uninstall" => { + self.mutate(|| enhanced::uninstall(&self.state, &required_game_dir(&self.state)?)) + } + "/api/exe-swap/compatible" => self.activate_compatible(), + "/api/exe-swap/compatible-depot" => Ok(json!({ + "ok": true, + "available": exe::compatible_depot_available(), + "state": self.current_state()?, + })), + "/api/exe-swap/current" => { + self.mutate(|| exe::activate_current(&self.state, &required_game_dir(&self.state)?)) + } + "/api/exe-swap/enhanced" => self + .mutate(|| exe::activate_enhanced(&self.state, &required_game_dir(&self.state)?)), + "/api/presets/apply" => { + let name = required_string(&body, "name")?; + self.mutate(|| { + app::apply_preset(&self.state, &required_game_dir(&self.state)?, name) + }) + } + "/api/intro-skip" => { + let enabled = required_bool(&body, "enabled")?; + self.mutate(|| { + app::set_intro_skip(&self.state, &required_game_dir(&self.state)?, enabled) + }) + } + "/api/d3dcompiler" => { + let enabled = required_bool(&body, "enabled")?; + self.mutate(|| { + app::set_d3dcompiler(&self.state, &required_game_dir(&self.state)?, enabled) + }) + } + "/api/all-intros-skip" => { + let enabled = required_bool(&body, "enabled")?; + self.mutate(|| { + app::set_all_intro_skip(&self.state, &required_game_dir(&self.state)?, enabled) + }) + } + "/api/config-readonly" => { + let enabled = required_bool(&body, "enabled")?; + self.mutate(|| { + app::set_config_readonly(&self.state, &required_game_dir(&self.state)?, enabled) + }) + } + "/api/vram-target" => { + let limited = required_bool(&body, "limited")?; + let target = required_i32(&body, "target")?; + self.mutate(|| { + app::set_vram_target( + &self.state, + &required_game_dir(&self.state)?, + limited, + target, + ) + }) + } + "/api/logs/payload" => Ok(json!({ + "ok": true, + "payload": app::log_payload(&self.state), + })), + "/api/logs/clear" => self.mutate(|| { + self.state.clear_logs()?; + self.state.log("Success", "Logs cleared."); + Ok(()) + }), + "/api/mod-files/clear" => self.mutate(|| app::clear_mod_files(&self.state)), + "/api/reset-stock" => self.mutate(|| reset_to_stock_inner(&self.state)), + _ => Err(format!("Unsupported backend path: {path}")), + } + } + + fn mutate(&self, operation: impl FnOnce() -> Result<(), String>) -> Result { + let _guard = self.state.lock_operation()?; + operation()?; + Ok(json!({"ok": true, "state": self.current_state()?})) + } + + fn validation_response(&self, valid: bool, message: &str) -> Result { + Ok(json!({ + "ok": true, + "valid": valid, + "message": message, + "state": self.current_state()?, + })) + } + + fn activate_compatible(&self) -> Result { + let _guard = self.state.lock_operation()?; + let game_dir = required_game_dir(&self.state)?; + match exe::activate_compatible(&self.state, &game_dir) { + Ok(()) => Ok(json!({"ok": true, "state": self.current_state()?})), + Err(error) if error == exe::COMPATIBLE_DEPOT_REQUIRED_MESSAGE => Ok(json!({ + "ok": false, + "error": error, + "depotRequired": true, + "depotCommand": exe::COMPATIBLE_DEPOT_COMMAND, + "state": self.current_state()?, + })), + Err(error) => Err(error), + } + } + + fn update_check(&self) -> Result { + let _guard = self.state.lock_operation()?; + let result = check_update(&self.state)?; + Ok(json!({ + "ok": true, + "update": result, + "state": self.current_state()?, + })) + } +} + +fn normalize_wire_value(mut value: Value) -> Value { + // Python omits `path` for built-in profiles but includes null for Workshop. + let state = if value.get("state").is_some() { + &mut value["state"] + } else { + &mut value + }; + if let Some(profiles) = state + .get_mut("launchProfiles") + .and_then(Value::as_array_mut) + { + for profile in profiles { + if matches!(profile["id"].as_str(), Some("default" | "offline")) + && let Some(profile) = profile.as_object_mut() + { + profile.remove("path"); + } + } + } + value +} + +fn current_state(state: &AppState) -> Result { + let settings = state.load_settings(); + let game_dir = steam::find_game_directory(settings.game_dir.as_deref()); + let current_launch_options = steam::current_launch_options(); + let launch_profiles = steam::launch_profiles(current_launch_options.as_deref()); + let active_launch_profile = launch_profiles + .iter() + .find(|profile| profile.active) + .map(|profile| profile.id.clone()) + .unwrap_or_else(|| "custom".into()); + let exe_swap = exe::status(&settings, game_dir.as_deref()); + let t7_state = t7::status(game_dir.as_deref(), Some(&exe_swap.profile)); + let dxvk_state = dxvk::status(game_dir.as_deref()); + let enhanced_state = enhanced::status( + state, + game_dir.as_deref(), + current_launch_options.as_deref(), + settings.enhanced_dump_source.clone().unwrap_or_default(), + ); + let config = game_dir + .as_deref() + .map(app::read_config) + .unwrap_or_default(); + let qol = app::qol_state(game_dir.as_deref()); + let mods = ModsState { + t7_patch: t7_state.installed, + dxvk: dxvk_state.installed, + enhanced: enhanced_state.installed, + }; + + Ok(PatchOpsState { + app_version: app::APP_VERSION.into(), + platform: app::platform_name().into(), + game_dir: game_dir + .as_ref() + .map(|path| path.to_string_lossy().into_owned()), + game_detected: game_dir.is_some(), + config_exists: !config.is_empty(), + steam_user_id: steam::user_id(), + log_path: state.log_path().to_string_lossy().into_owned(), + presets: app::preset_names(), + current_launch_options, + active_launch_profile, + release_channel: app::release_channel(&settings).into(), + launch_profiles, + enhanced: enhanced_state, + exe_swap, + t7: t7_state, + dxvk: dxvk_state, + qol, + graphics: app::graphics_state(&config), + advanced: app::advanced_state(game_dir.as_deref(), &config), + maintenance: MaintenanceState { + mod_files_dir: state.mod_files_dir().to_string_lossy().into_owned(), + log_payload: app::log_payload(state), + }, + mods, + logs: state.recent_logs(), + }) +} + +fn detected_game_dir(state: &AppState) -> Option { + steam::find_game_directory(state.load_settings().game_dir.as_deref()) +} + +fn required_game_dir(state: &AppState) -> Result { + detected_game_dir(state).ok_or_else(|| "Game directory is not set.".into()) +} + +fn decode(value: Value) -> Result { + serde_json::from_value(value).map_err(|error| format!("Invalid request payload: {error}")) +} + +fn required_string<'a>(body: &'a Value, key: &str) -> Result<&'a str, String> { + body.get(key) + .and_then(Value::as_str) + .ok_or_else(|| format!("Missing or invalid {key}.")) +} + +fn optional_string(body: &Value, key: &str) -> Option { + body.get(key).and_then(Value::as_str).map(str::to_owned) +} + +fn required_bool(body: &Value, key: &str) -> Result { + body.get(key) + .and_then(Value::as_bool) + .ok_or_else(|| format!("Missing or invalid {key}.")) +} + +fn optional_bool(body: &Value, key: &str) -> Option { + body.get(key).and_then(Value::as_bool) +} + +fn required_i32(body: &Value, key: &str) -> Result { + body.get(key) + .and_then(Value::as_i64) + .and_then(|value| i32::try_from(value).ok()) + .ok_or_else(|| format!("Missing or invalid {key}.")) +} + +fn string_error(error: impl std::fmt::Display) -> String { + error.to_string() +} + +fn version_key(value: &str) -> (Vec, bool, u64) { + let value = value.trim().trim_start_matches(['v', 'V']); + let base_end = value + .find(|character: char| !character.is_ascii_digit() && character != '.') + .unwrap_or(value.len()); + let mut base = value[..base_end] + .split('.') + .filter_map(|part| part.parse().ok()) + .collect::>(); + base.resize(3, 0); + let suffix = &value[base_end..]; + let prerelease_number = suffix + .split(|character: char| !character.is_ascii_digit()) + .filter_map(|part| part.parse().ok()) + .next_back() + .unwrap_or(0); + (base, suffix.is_empty(), prerelease_number) +} + +fn compare_versions(left: &str, right: &str) -> Ordering { + version_key(left).cmp(&version_key(right)) +} + +fn check_update(state: &AppState) -> Result { + const MAX_BODY: u64 = 4 * 1024 * 1024; + let settings = state.load_settings(); + let channel = app::release_channel(&settings); + let endpoint = if channel == "beta" { + "https://api.github.com/repos/boggedbrush/PatchOpsIII/releases?per_page=20" + } else { + "https://api.github.com/repos/boggedbrush/PatchOpsIII/releases/latest" + }; + let response = reqwest::blocking::Client::builder() + .connect_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(30)) + .build() + .map_err(string_error)? + .get(endpoint) + .header(reqwest::header::USER_AGENT, "PatchOpsIII") + .send() + .and_then(reqwest::blocking::Response::error_for_status) + .map_err(|error| format!("Update check failed: {error}"))?; + if response + .content_length() + .is_some_and(|length| length > MAX_BODY) + { + return Err("Update metadata is unexpectedly large.".into()); + } + let mut body = Vec::new(); + response + .take(MAX_BODY + 1) + .read_to_end(&mut body) + .map_err(string_error)?; + if body.len() as u64 > MAX_BODY { + return Err("Update metadata is unexpectedly large.".into()); + } + let root: Value = serde_json::from_slice(&body).map_err(string_error)?; + let release = if channel == "beta" { + root.as_array() + .and_then(|releases| { + releases.iter().find(|release| { + release.get("draft").and_then(Value::as_bool) != Some(true) + && release.get("prerelease").and_then(Value::as_bool) == Some(true) + && format!( + "{} {}", + release + .get("tag_name") + .and_then(Value::as_str) + .unwrap_or_default(), + release + .get("name") + .and_then(Value::as_str) + .unwrap_or_default() + ) + .to_ascii_lowercase() + .contains("beta") + }) + }) + .ok_or_else(|| "No beta release was found.".to_string())? + } else { + &root + }; + let result = update_metadata(release, channel, std::env::consts::OS); + let available = result["available"].as_bool() == Some(true); + state.log( + if available { "Success" } else { "Info" }, + if available { + format!( + "{} update available: {}", + title_case(channel), + result["latestVersion"].as_str().unwrap_or_default() + ) + } else { + format!("No {channel} updates available.") + }, + ); + Ok(result) +} + +// Preserve Python's metadata shape but choose the native client artifact. +fn update_metadata(release: &Value, channel: &str, platform: &str) -> Value { + let expected_name = match platform { + "windows" => "PatchOpsIII-native-windows-x64.zip", + "linux" => "PatchOpsIII-native-linux-x64.zip", + _ => "", + }; + let asset = release["assets"].as_array().and_then(|assets| { + assets.iter().find_map(|asset| { + let name = asset["name"].as_str()?; + if expected_name.is_empty() || !name.eq_ignore_ascii_case(expected_name) { + return None; + } + let url = asset["browser_download_url"] + .as_str() + .filter(|url| !url.is_empty())?; + Some(json!({ + "name": name, "url": url, + "size": asset["size"].as_u64().unwrap_or(0), + "contentType": asset["content_type"].as_str().unwrap_or("application/octet-stream"), + })) + }) + }); + let latest = release["tag_name"] + .as_str() + .filter(|tag| !tag.is_empty()) + .or(release["name"].as_str()) + .unwrap_or("0.0.0"); + let available = asset.is_some() + && release["draft"] != true + && (channel == "beta" || release["prerelease"] != true) + && compare_versions(latest, app::APP_VERSION).is_gt(); + json!({ + "available": available, "channel": channel, "currentVersion": app::APP_VERSION, + "latestVersion": latest, "name": release["name"].as_str().unwrap_or("PatchOpsIII"), + "body": release["body"].as_str().unwrap_or(""), + "pageUrl": release["html_url"].as_str().unwrap_or("https://github.com/boggedbrush/PatchOpsIII/releases"), + "asset": asset, + }) +} + +fn title_case(value: &str) -> String { + let mut chars = value.chars(); + chars + .next() + .map(|first| first.to_uppercase().collect::() + chars.as_str()) + .unwrap_or_default() +} + +fn reset_to_stock_inner(state: &AppState) -> Result<(), String> { + let game_dir = required_game_dir(state)?; + let mut errors = Vec::new(); + if (enhanced::detect_install(&game_dir) || enhanced::has_owned_install(state, &game_dir)) + && let Err(error) = enhanced::uninstall(state, &game_dir) + { + errors.push(format!("Enhanced: {error}")); + } + if exe::status(&state.load_settings(), Some(&game_dir)).profile != exe::CURRENT_EXE_ID + && let Err(error) = exe::activate_current(state, &game_dir) + { + errors.push(format!("EXE: {error}")); + } + if let Err(error) = t7::uninstall(state, &game_dir) { + errors.push(format!("T7 Patch: {error}")); + } + if let Err(error) = dxvk::uninstall(state, &game_dir) { + errors.push(format!("DXVK: {error}")); + } + let qol = app::qol_state(Some(&game_dir)); + if qol.d3dcompiler + && let Err(error) = app::set_d3dcompiler(state, &game_dir, false) + { + errors.push(format!("d3dcompiler: {error}")); + } + if qol.all_intros { + if let Err(error) = app::set_all_intro_skip(state, &game_dir, false) { + errors.push(format!("intros: {error}")); + } + } else if qol.intro + && let Err(error) = app::set_intro_skip(state, &game_dir, false) + { + errors.push(format!("intro: {error}")); + } + if app::config_path(&game_dir).is_file() { + let updates = vec![ + ("SmoothFramerate".into(), "0".into(), "0 or 1".into()), + ("VideoMemory".into(), "1".into(), "0.75 to 1".into()), + ("StreamMinResident".into(), "0".into(), "0 or 1".into()), + ("MaxFrameLatency".into(), "1".into(), "0 to 4".into()), + ("SerializeRender".into(), "0".into(), "0 to 2".into()), + ( + "RestrictGraphicsOptions".into(), + "1".into(), + "0 or 1".into(), + ), + ]; + if let Err(error) = app::write_config_values(&game_dir, &updates) { + errors.push(format!("config: {error}")); + } + } + if let Err(error) = steam::apply_launch_options(state, "", false) { + errors.push(format!("launch options: {error}")); + } + if errors.is_empty() { + state.log("Success", "Reset to stock complete."); + Ok(()) + } else { + let message = format!("Reset completed with errors: {}", errors.join("; ")); + state.log("Error", &message); + Err(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs, sync::Arc}; + + fn test_engine(name: &str) -> (Engine, PathBuf) { + let root = std::env::temp_dir().join(format!( + "patchops-core-engine-{name}-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).unwrap(); + ( + Engine::new(AppState::new(root.join("data"), None, None).unwrap()), + root, + ) + } + + #[test] + fn status_preserves_legacy_json_shape() { + let (engine, root) = test_engine("status"); + let status = engine.dispatch("/api/status", None).unwrap(); + assert!(status["appVersion"].is_string()); + assert!(status["gameDetected"].is_boolean()); + assert!(status["exeSwap"]["currentBuildId"].is_string()); + assert!(status["launchProfiles"].is_array()); + assert_eq!(status["platform"], app::platform_name()); + assert!(status["maintenance"]["logPayload"].is_string()); + assert!(status["launchProfiles"][0].get("path").is_none()); + assert!(status["launchProfiles"][2].get("path").is_some()); + assert!( + engine + .dispatch("/api/health", None) + .unwrap() + .get("launchProfiles") + .is_none() + ); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn log_callback_is_toolkit_independent() { + let root = + std::env::temp_dir().join(format!("patchops-core-events-{}", std::process::id())); + let _ = fs::remove_dir_all(&root); + let seen = Arc::new(std::sync::Mutex::new(Vec::new())); + let capture = seen.clone(); + let state = AppState::new( + root.join("data"), + None, + Some(Arc::new(move |entry| capture.lock().unwrap().push(entry))), + ) + .unwrap(); + state.log("Info", "hello"); + assert_eq!(seen.lock().unwrap()[0].message, "hello"); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn updates_select_native_archives_and_preserve_python_metadata() { + let release = json!({"tag_name": "v9.0.0", "name": "New release", "body": "Notes", "html_url": "https://example.invalid/release", "assets": [ + {"name": "PatchOpsIII.AppImage", "browser_download_url": "https://example.invalid/legacy"}, + {"name": "PatchOpsIII-native-linux-x64.zip", "browser_download_url": "https://example.invalid/linux", "size": 42, "content_type": "application/zip"}, + {"name": "PatchOpsIII-native-windows-x64.zip", "browser_download_url": "https://example.invalid/windows"} + ]}); + let linux = update_metadata(&release, "stable", "linux"); + assert_eq!(linux["asset"]["url"], "https://example.invalid/linux"); + assert_eq!(linux["asset"]["size"], 42); + assert_eq!(linux["body"], "Notes"); + assert_eq!(linux["available"], true); + assert_eq!( + update_metadata(&release, "stable", "windows")["asset"]["url"], + "https://example.invalid/windows" + ); + assert_eq!( + update_metadata(&release, "stable", "macos")["available"], + false + ); + let mut beta = release.clone(); + beta["prerelease"] = json!(true); + assert_eq!( + update_metadata(&beta, "stable", "linux")["available"], + false + ); + assert_eq!(update_metadata(&beta, "beta", "linux")["available"], true); + beta["draft"] = json!(true); + assert_eq!(update_metadata(&beta, "beta", "linux")["available"], false); + beta["assets"] = json!([]); + assert!(update_metadata(&beta, "stable", "linux")["asset"].is_null()); + } + + #[test] + fn progress_reports_failure_and_stays_quiet_during_status_polls() { + let (engine, root) = test_engine("progress"); + let seen = Arc::new(std::sync::Mutex::new(Vec::new())); + let capture = seen.clone(); + engine + .state() + .set_progress_callback(Arc::new(move |event| capture.lock().unwrap().push(event))); + engine.dispatch("/api/status", None).unwrap(); + assert!(seen.lock().unwrap().is_empty()); + assert_eq!( + engine + .dispatch("/api/config", Some(json!({"key": "FOV", "value": 80}))) + .unwrap()["ok"], + false + ); + let events = seen.lock().unwrap(); + assert_eq!(events.first().unwrap().stage, "started"); + assert_eq!(events.last().unwrap().stage, "failed"); + assert_eq!(events.last().unwrap().op, "/api/config"); + assert!(events.last().unwrap().fraction.is_none()); + drop(events); + engine + .dispatch("/api/release-channel", Some(json!({"channel": "beta"}))) + .unwrap(); + let events = seen.lock().unwrap(); + assert_eq!(events.last().unwrap().stage, "completed"); + assert_eq!(events.last().unwrap().op, "/api/release-channel"); + assert_eq!(events.last().unwrap().fraction, Some(1.0)); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn version_order_matches_stable_and_beta_channels() { + assert!(compare_versions("v1.3.1", "1.3.0-beta3").is_gt()); + assert!(compare_versions("1.3.0", "1.3.0-beta3").is_gt()); + assert!(compare_versions("1.3.0-beta4", "1.3.0-beta3").is_gt()); + assert_eq!(compare_versions("v1.3", "1.3.0"), Ordering::Equal); + } +} diff --git a/native/core/src/enhanced.rs b/native/core/src/enhanced.rs new file mode 100644 index 0000000..8a26817 --- /dev/null +++ b/native/core/src/enhanced.rs @@ -0,0 +1,2340 @@ +use std::{ + collections::{BTreeMap, BTreeSet, HashSet}, + fs::{self, File}, + io::{self, Read}, + path::{Component, Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use sha2::{Digest, Sha256}; +use time::OffsetDateTime; + +use crate::{app::AppState, exe, fs_ops, models::EnhancedState, steam}; + +pub const GITHUB_LATEST_ENHANCED_API: &str = + "https://api.github.com/repos/shiversoftdev/BO3Enhanced/releases/latest"; +pub const STATE_FILENAME: &str = "bo3_enhanced_state.json"; +pub const ENHANCED_ARCHIVE_NAME: &str = "BO3Enhanced_latest.zip"; +pub const CHECKSUMS_FILENAME: &str = "bo3_enhanced_checksums.json"; + +const EXPECTED_ENHANCED_FILES: [&str; 4] = [ + "T7WSBootstrapper.dll", + "T7InternalWS.dll", + "steam_api65.dll", + "WindowsCodecs.dll", +]; +const EXPECTED_DUMP_FILES: [&str; 2] = ["BlackOps3.exe", "MicrosoftGame.config"]; +const UWP_DUMP_WHITELIST: [&str; 8] = [ + "BlackOps3.exe", + "MicrosoftGame.config", + "GameChat2.dll", + "Party.dll", + "PartyXboxLive.dll", + "PlayFabMultiplayerGDK.dll", + "libScePad.dll", + "XCurl.dll", +]; +const MAX_ARCHIVE_BYTES: u64 = 16 * 1024 * 1024 * 1024; +const MAX_ARCHIVE_ENTRIES: usize = 100_000; +const MAX_DOWNLOAD_BYTES: u64 = 2 * 1024 * 1024 * 1024; +const MAX_RELEASE_METADATA_BYTES: u64 = 4 * 1024 * 1024; +const ENHANCED_RELEASE_PATH_PREFIX: &str = "/shiversoftdev/BO3Enhanced/releases/download/"; + +#[derive(Clone, Default, Serialize, Deserialize)] +struct PersistedState { + #[serde(default)] + game_directory: Option, + #[serde(default)] + installed: bool, + #[serde(default)] + detected_at: Option, + #[serde(default)] + acknowledged_at: Option, + #[serde(default)] + installed_files: Vec, + #[serde(default)] + dump_only_files: Vec, + #[serde(default)] + created_files: Vec, + #[serde(default)] + owned_backups: BTreeMap, + #[serde(default)] + installed_hashes: BTreeMap, + #[serde(default)] + original_backup_hashes: BTreeMap, + #[serde(flatten)] + extra: Map, +} + +enum MemberSource { + File(PathBuf), + Zip(usize), +} + +struct PayloadMember { + relative: PathBuf, + source: MemberSource, +} + +struct AppliedFile { + target: PathBuf, + rollback_copy: Option, + new_permanent_backup: Option, +} + +#[derive(Clone)] +enum UninstallAction { + Restore(PathBuf), + Remove, +} + +#[derive(Clone)] +struct PlannedUninstall { + target: PathBuf, + action: UninstallAction, +} + +struct AppliedUninstall { + plan: PlannedUninstall, + displaced: Option, +} + +struct EnhancedUninstallTransaction { + applied: Vec, + transaction: Option, + storage_dir: PathBuf, + original_state: PersistedState, +} + +#[derive(Debug)] +struct EnhancedRelease { + version: String, + asset_url: String, + asset_sha256: String, +} + +fn state_path(storage_dir: &Path) -> PathBuf { + storage_dir.join(STATE_FILENAME) +} + +fn checksums_path(storage_dir: &Path) -> PathBuf { + storage_dir.join(CHECKSUMS_FILENAME) +} + +fn load_state(storage_dir: &Path) -> PersistedState { + fs::read(state_path(storage_dir)) + .ok() + .and_then(|body| serde_json::from_slice(&body).ok()) + .unwrap_or_default() +} + +fn save_state(storage_dir: &Path, state: &PersistedState) -> Result<(), String> { + let body = serde_json::to_vec_pretty(state).map_err(|error| error.to_string())?; + fs_ops::atomic_write(&state_path(storage_dir), &body) +} + +fn canonical_game_key(game_dir: &Path) -> Result { + let path = fs::canonicalize(game_dir).map_err(|error| error.to_string())?; + let key = path.to_string_lossy().into_owned(); + #[cfg(windows)] + return Ok(key.to_ascii_lowercase()); + #[cfg(not(windows))] + Ok(key) +} + +fn state_belongs_to_game(state: &PersistedState, game_dir: &Path) -> bool { + canonical_game_key(game_dir) + .ok() + .is_some_and(|key| state.game_directory.as_deref() == Some(key.as_str())) +} + +fn state_has_ownership(state: &PersistedState) -> bool { + state.installed + || !state.installed_files.is_empty() + || !state.dump_only_files.is_empty() + || !state.created_files.is_empty() + || !state.owned_backups.is_empty() + || !state.installed_hashes.is_empty() + || !state.original_backup_hashes.is_empty() +} + +fn state_has_owned_install(state: &PersistedState, game_dir: &Path) -> bool { + state_belongs_to_game(state, game_dir) && state_has_ownership(state) +} + +pub fn has_owned_install(state: &AppState, game_dir: &Path) -> bool { + state_has_owned_install(&load_state(state.data_dir()), game_dir) +} + +fn load_checksums(storage_dir: &Path) -> BTreeMap { + fs::read(checksums_path(storage_dir)) + .ok() + .and_then(|body| serde_json::from_slice(&body).ok()) + .unwrap_or_default() +} + +fn save_checksums(storage_dir: &Path, checksums: &BTreeMap) -> Result<(), String> { + let body = serde_json::to_vec_pretty(checksums).map_err(|error| error.to_string())?; + fs_ops::atomic_write(&checksums_path(storage_dir), &body) +} + +fn utc_timestamp() -> String { + let now = OffsetDateTime::now_utc(); + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + now.year(), + now.month() as u8, + now.day(), + now.hour(), + now.minute(), + now.second() + ) +} + +pub fn detect_install(game_dir: &Path) -> bool { + EXPECTED_ENHANCED_FILES + .iter() + .all(|name| game_dir.join(name).is_file()) +} + +fn status_at( + storage_dir: &Path, + game_dir: Option<&Path>, + current_launch_options: Option<&str>, + dump_source: String, +) -> (EnhancedState, Option) { + let stored = load_state(storage_dir); + let mut persisted = match game_dir.filter(|game_dir| state_belongs_to_game(&stored, game_dir)) { + Some(_) => stored, + None => PersistedState::default(), + }; + let installed_files = persisted.installed_files.len(); + let detected = game_dir.is_some_and(detect_install); + let stale_state_error = + if !detected && persisted.installed_files.is_empty() && persisted.installed { + persisted.installed = false; + save_state(storage_dir, &persisted).err() + } else { + None + }; + let installed = game_dir.is_some() && (detected || persisted.installed); + ( + EnhancedState { + installed, + detected_at: persisted.detected_at, + acknowledged_at: persisted.acknowledged_at, + launch_options_active: current_launch_options + .unwrap_or_default() + .to_ascii_lowercase() + .contains("windowscodecs=n,b"), + dump_source, + files_installed: installed_files, + backup_status: if installed && installed_files > 0 { + "Created".into() + } else { + "Not created".into() + }, + }, + stale_state_error, + ) +} + +pub fn status( + state: &AppState, + game_dir: Option<&Path>, + current_launch_options: Option<&str>, + dump_source: String, +) -> EnhancedState { + let (result, stale_state_error) = status_at( + state.data_dir(), + game_dir, + current_launch_options, + dump_source, + ); + if let Some(error) = stale_state_error { + state.log( + "Warning", + format!("Failed to clear stale BO3 Enhanced state: {error}"), + ); + } + result +} + +fn safe_relative(path: &Path) -> bool { + path.components() + .all(|component| matches!(component, Component::Normal(_) | Component::CurDir)) +} + +fn contained_target(root: &Path, relative: &Path) -> Result { + if !safe_relative(relative) { + return Err(format!( + "Refusing unsafe payload path: {}", + relative.display() + )); + } + let canonical_root = fs::canonicalize(root).map_err(|error| error.to_string())?; + let target = root.join(relative); + let mut existing_parent = target.parent().unwrap_or(root); + while !existing_parent.exists() { + existing_parent = existing_parent + .parent() + .ok_or_else(|| "Payload target has no contained parent.".to_string())?; + } + let canonical_parent = fs::canonicalize(existing_parent).map_err(|error| error.to_string())?; + if !canonical_parent.starts_with(&canonical_root) { + return Err(format!( + "Payload target escapes the game directory: {}", + target.display() + )); + } + Ok(target) +} + +fn archive_relative(name: &str) -> Result { + if name.contains('\0') { + return Err("Archive contains an unsafe path.".into()); + } + let normalized = name.replace('\\', "/"); + let relative = PathBuf::from(normalized); + if !safe_relative(&relative) { + return Err(format!("Archive contains an unsafe path: {name}")); + } + Ok(relative) +} + +fn strip_dump_prefix(path: &Path) -> PathBuf { + let mut components = path.components(); + if components + .next() + .is_some_and(|component| component.as_os_str() == "DUMP") + { + components.as_path().to_owned() + } else { + path.to_owned() + } +} + +fn should_copy_dump_member(relative: &Path) -> bool { + relative + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| UWP_DUMP_WHITELIST.contains(&name)) +} + +fn validate_required_dump_files(members: &[PayloadMember]) -> Result<(), String> { + let found: HashSet<_> = members + .iter() + .map(|member| member.relative.to_string_lossy().replace('\\', "/")) + .collect(); + let missing: Vec<_> = EXPECTED_DUMP_FILES + .iter() + .filter(|name| !found.contains(**name)) + .copied() + .collect(); + if missing.is_empty() { + Ok(()) + } else { + Err(format!( + "Dump source missing required files: {}", + missing.join(", ") + )) + } +} + +fn collect_directory_members( + root: &Path, + directory: &Path, + members: &mut Vec, + seen: &mut HashSet, + total: &mut u64, + count: &mut usize, + depth: usize, +) -> Result<(), String> { + if depth > 64 { + return Err("Dump source directory nesting is too deep.".into()); + } + for entry in fs::read_dir(directory).map_err(|error| error.to_string())? { + let entry = entry.map_err(|error| error.to_string())?; + let file_type = entry.file_type().map_err(|error| error.to_string())?; + if file_type.is_symlink() { + return Err(format!( + "Dump source contains a symbolic link: {}", + entry.path().display() + )); + } + if file_type.is_dir() { + collect_directory_members(root, &entry.path(), members, seen, total, count, depth + 1)?; + continue; + } + if !file_type.is_file() { + return Err(format!( + "Dump source contains an unsupported file type: {}", + entry.path().display() + )); + } + *count += 1; + if *count > MAX_ARCHIVE_ENTRIES { + return Err("Dump source contains too many files.".into()); + } + *total = total.saturating_add(entry.metadata().map_err(|error| error.to_string())?.len()); + if *total > MAX_ARCHIVE_BYTES { + return Err("Dump source is larger than the allowed size.".into()); + } + let relative = entry + .path() + .strip_prefix(root) + .map_err(|error| error.to_string())? + .to_owned(); + let relative = strip_dump_prefix(&relative); + if relative.as_os_str().is_empty() || !should_copy_dump_member(&relative) { + continue; + } + let key = relative_string(&relative).to_ascii_lowercase(); + if !safe_relative(&relative) || !seen.insert(key) { + return Err(format!( + "Dump source contains an unsafe or duplicate path: {}", + relative.display() + )); + } + members.push(PayloadMember { + relative, + source: MemberSource::File(entry.path()), + }); + } + Ok(()) +} + +fn zip_entry_is_link(entry: &zip::read::ZipFile<'_>) -> bool { + entry + .unix_mode() + .is_some_and(|mode| mode & 0o170000 == 0o120000) +} + +fn dump_members(source: &Path) -> Result, String> { + let mut members = Vec::new(); + let mut seen = HashSet::new(); + let mut total = 0_u64; + if source.is_dir() { + let mut count = 0; + collect_directory_members( + source, + source, + &mut members, + &mut seen, + &mut total, + &mut count, + 0, + )?; + } else { + let mut archive = zip::ZipArchive::new( + File::open(source).map_err(|error| format!("{}: {error}", source.display()))?, + ) + .map_err(|_| "Dump source is not a valid zip file or directory.".to_string())?; + if archive.len() > MAX_ARCHIVE_ENTRIES { + return Err("Dump archive contains too many entries.".into()); + } + for index in 0..archive.len() { + let entry = archive.by_index(index).map_err(|error| error.to_string())?; + let relative = archive_relative(entry.name())?; + if zip_entry_is_link(&entry) { + return Err(format!("Dump archive contains a link: {}", entry.name())); + } + total = total.saturating_add(entry.size()); + if total > MAX_ARCHIVE_BYTES { + return Err("Dump archive expands beyond the allowed size.".into()); + } + if entry.is_dir() { + continue; + } + let relative = strip_dump_prefix(&relative); + if relative.as_os_str().is_empty() || !should_copy_dump_member(&relative) { + continue; + } + let key = relative_string(&relative).to_ascii_lowercase(); + if !safe_relative(&relative) || !seen.insert(key) { + return Err(format!( + "Dump archive contains an unsafe or duplicate path: {}", + relative.display() + )); + } + members.push(PayloadMember { + relative, + source: MemberSource::Zip(index), + }); + } + } + validate_required_dump_files(&members)?; + Ok(members) +} + +fn enhanced_members(archive_path: &Path) -> Result, String> { + let mut archive = zip::ZipArchive::new( + File::open(archive_path).map_err(|error| format!("{}: {error}", archive_path.display()))?, + ) + .map_err(|_| "Enhanced archive is not a valid zip.".to_string())?; + if archive.len() > MAX_ARCHIVE_ENTRIES { + return Err("Enhanced archive contains too many entries.".into()); + } + + let mut members = Vec::new(); + let mut found = HashSet::new(); + let mut total = 0_u64; + for index in 0..archive.len() { + let entry = archive.by_index(index).map_err(|error| error.to_string())?; + let relative = archive_relative(entry.name())?; + if zip_entry_is_link(&entry) { + return Err(format!( + "Enhanced archive contains a link: {}", + entry.name() + )); + } + total = total.saturating_add(entry.size()); + if total > MAX_ARCHIVE_BYTES { + return Err("Enhanced archive expands beyond the allowed size.".into()); + } + if entry.is_dir() { + continue; + } + let Some(name) = relative.file_name().and_then(|name| name.to_str()) else { + continue; + }; + if !EXPECTED_ENHANCED_FILES.contains(&name) { + continue; + } + if !found.insert(name.to_owned()) { + return Err(format!( + "Enhanced archive contains duplicate expected file: {name}" + )); + } + members.push(PayloadMember { + relative: PathBuf::from(name), + source: MemberSource::Zip(index), + }); + } + let missing: Vec<_> = EXPECTED_ENHANCED_FILES + .iter() + .filter(|name| !found.contains(**name)) + .copied() + .collect(); + if missing.is_empty() { + Ok(members) + } else { + Err(format!( + "Enhanced archive missing expected files: {}", + missing.join(", ") + )) + } +} + +pub fn validate_dump_source(source: &Path) -> Result { + if source.as_os_str().is_empty() || !source.exists() { + return Ok(false); + } + Ok(dump_members(source).is_ok()) +} + +pub fn validate_enhanced_archive(archive: &Path) -> Result { + if !archive.is_file() { + return Ok(false); + } + Ok(enhanced_members(archive).is_ok()) +} + +pub fn validate_and_remember_dump_source(state: &AppState, source: &Path) -> Result { + let mut settings = state.load_settings(); + settings.enhanced_dump_source = Some(source.display().to_string()); + state.save_settings(&settings)?; + validate_dump_source(source) +} + +fn valid_enhanced_asset_url(value: &str) -> bool { + reqwest::Url::parse(value).is_ok_and(|url| { + url.scheme() == "https" + && url.host_str() == Some("github.com") + && url.port().is_none() + && url.username().is_empty() + && url.password().is_none() + && url.path().starts_with(ENHANCED_RELEASE_PATH_PREFIX) + && url.path().to_ascii_lowercase().ends_with(".zip") + && url.query().is_none() + && url.fragment().is_none() + }) +} + +fn fetch_latest_release() -> Result { + let response = reqwest::blocking::Client::builder() + .connect_timeout(std::time::Duration::from_secs(15)) + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|error| error.to_string())? + .get(GITHUB_LATEST_ENHANCED_API) + .header(reqwest::header::USER_AGENT, "PatchOpsIII") + .header(reqwest::header::ACCEPT, "application/vnd.github+json") + .send() + .and_then(reqwest::blocking::Response::error_for_status) + .map_err(|error| format!("Failed to fetch BO3 Enhanced release metadata: {error}"))?; + if response + .content_length() + .is_some_and(|length| length > MAX_RELEASE_METADATA_BYTES) + { + return Err("BO3 Enhanced release metadata was unexpectedly large.".into()); + } + let mut body = Vec::new(); + response + .take(MAX_RELEASE_METADATA_BYTES + 1) + .read_to_end(&mut body) + .map_err(|error| error.to_string())?; + if body.len() as u64 > MAX_RELEASE_METADATA_BYTES { + return Err("BO3 Enhanced release metadata was unexpectedly large.".into()); + } + let data: Value = serde_json::from_slice(&body) + .map_err(|error| format!("Invalid BO3 Enhanced release metadata: {error}"))?; + + let assets = data + .get("assets") + .and_then(Value::as_array) + .ok_or_else(|| { + "No downloadable asset found for BO3 Enhanced latest release.".to_string() + })?; + let selected = assets + .iter() + .find(|asset| { + asset + .get("name") + .and_then(Value::as_str) + .is_some_and(|name| name.to_ascii_lowercase().ends_with(".zip")) + }) + .ok_or_else(|| { + "No downloadable asset found for BO3 Enhanced latest release.".to_string() + })?; + let digest = selected + .get("digest") + .and_then(Value::as_str) + .unwrap_or_default() + .trim() + .to_ascii_lowercase(); + let checksum = digest.strip_prefix("sha256:").unwrap_or_default().trim(); + if checksum.len() != 64 || !checksum.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err("Latest BO3 Enhanced release metadata did not include a SHA-256 digest; refusing unverified download.".into()); + } + let asset_url = selected + .get("browser_download_url") + .and_then(Value::as_str) + .filter(|url| valid_enhanced_asset_url(url)) + .ok_or_else(|| "BO3 Enhanced release asset has an invalid download URL.".to_string())?; + Ok(EnhancedRelease { + version: data + .get("tag_name") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .or_else(|| data.get("name").and_then(Value::as_str)) + .unwrap_or("0.0.0") + .to_owned(), + asset_url: asset_url.to_owned(), + asset_sha256: checksum.to_owned(), + }) +} + +pub fn download_latest(state: &AppState) -> Result { + let release = fetch_latest_release()?; + fs::create_dir_all(state.mod_files_dir()).map_err(|error| error.to_string())?; + let destination = state.mod_files_dir().join(ENHANCED_ARCHIVE_NAME); + let checksum = fs_ops::download( + state, + &release.asset_url, + &destination, + Some(&release.asset_sha256), + MAX_DOWNLOAD_BYTES, + )?; + if !validate_enhanced_archive(&destination)? { + let _ = fs::remove_file(&destination); + return Err("Enhanced archive failed validation.".into()); + } + let mut checksums = load_checksums(state.data_dir()); + checksums.insert(ENHANCED_ARCHIVE_NAME.into(), checksum); + save_checksums(state.data_dir(), &checksums)?; + state.log( + "Success", + format!( + "Downloaded BO3 Enhanced {} to {}", + release.version, + destination.display() + ), + ); + Ok(destination) +} + +fn verify_cached_checksum(storage_dir: &Path, archive: &Path) -> Result<(), String> { + let Some(filename) = archive.file_name().and_then(|name| name.to_str()) else { + return Err("Invalid Enhanced archive filename.".into()); + }; + let checksums = load_checksums(storage_dir); + let Some(expected) = checksums.get(filename) else { + return Ok(()); + }; + let actual = fs_ops::sha256_file(archive)?; + if actual.eq_ignore_ascii_case(expected) { + Ok(()) + } else { + Err("Enhanced archive failed cached SHA-256 verification.".into()) + } +} + +fn sha256_reader(reader: &mut impl Read) -> Result { + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 1024 * 1024]; + loop { + let read = reader + .read(&mut buffer) + .map_err(|error| error.to_string())?; + if read == 0 { + break; + } + digest.update(&buffer[..read]); + } + Ok(format!("{:x}", digest.finalize())) +} + +fn member_hashes( + source_archive: Option<&Path>, + members: &[PayloadMember], +) -> Result, String> { + let mut archive = source_archive + .map(|path| { + zip::ZipArchive::new( + File::open(path).map_err(|error| format!("{}: {error}", path.display()))?, + ) + .map_err(|error| error.to_string()) + }) + .transpose()?; + let mut hashes = BTreeMap::new(); + for member in members { + if !safe_relative(&member.relative) || member.relative.as_os_str().is_empty() { + return Err(format!( + "Refusing unsafe legacy provenance path: {}", + member.relative.display() + )); + } + let hash = match &member.source { + MemberSource::File(source) => { + if !regular_file_without_links(source) { + return Err(format!( + "Legacy provenance source is missing or unsafe: {}", + source.display() + )); + } + let hash = fs_ops::sha256_file(source)?; + if !regular_file_without_links(source) { + return Err(format!( + "Legacy provenance source changed while it was read: {}", + source.display() + )); + } + hash + } + MemberSource::Zip(index) => { + let mut entry = archive + .as_mut() + .ok_or_else(|| "Missing legacy provenance archive.".to_string())? + .by_index(*index) + .map_err(|error| error.to_string())?; + if entry.is_dir() || zip_entry_is_link(&entry) { + return Err(format!( + "Legacy provenance archive member is unsafe: {}", + entry.name() + )); + } + sha256_reader(&mut entry)? + } + }; + let relative = relative_string(&member.relative); + if hashes.insert(relative.clone(), hash).is_some() { + return Err(format!( + "Legacy provenance contains a duplicate path: {relative}" + )); + } + } + Ok(hashes) +} + +fn verified_cached_enhanced_hashes( + storage_dir: &Path, + archive: &Path, +) -> Result, String> { + if !regular_file_without_links(archive) { + return Err( + "Legacy Enhanced ownership cannot be verified because the cached archive is missing or unsafe." + .into(), + ); + } + let filename = archive + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| "Invalid cached Enhanced archive filename.".to_string())?; + let checksums = load_checksums(storage_dir); + let expected = checksums.get(filename).ok_or_else(|| { + "Legacy Enhanced ownership cannot be verified because the cached archive has no recorded SHA-256 checksum." + .to_string() + })?; + if expected.len() != 64 || !expected.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err( + "Legacy Enhanced ownership cannot be verified because the cached archive checksum is invalid." + .into(), + ); + } + let before = fs_ops::sha256_file(archive)?; + if !before.eq_ignore_ascii_case(expected) { + return Err( + "Legacy Enhanced ownership cannot be verified because the cached archive failed SHA-256 verification." + .into(), + ); + } + let members = enhanced_members(archive)?; + let hashes = member_hashes(Some(archive), &members)?; + let after = fs_ops::sha256_file(archive)?; + if !after.eq_ignore_ascii_case(expected) || !after.eq_ignore_ascii_case(&before) { + return Err( + "Legacy Enhanced ownership cannot be verified because the cached archive changed while it was inspected." + .into(), + ); + } + Ok(hashes) +} + +fn verified_dump_member_hashes(source: &Path) -> Result, String> { + let metadata = fs::symlink_metadata(source).map_err(|_| { + "Legacy Enhanced ownership cannot be verified because the original dump source is unavailable." + .to_string() + })?; + if metadata.file_type().is_symlink() || (!metadata.is_file() && !metadata.is_dir()) { + return Err( + "Legacy Enhanced ownership cannot be verified because the original dump source is unsafe." + .into(), + ); + } + let archive_hash = metadata + .is_file() + .then(|| fs_ops::sha256_file(source)) + .transpose()?; + let members = dump_members(source)?; + let hashes = member_hashes(metadata.is_file().then_some(source), &members)?; + if let Some(before) = archive_hash { + let after = fs_ops::sha256_file(source)?; + if !after.eq_ignore_ascii_case(&before) { + return Err( + "Legacy Enhanced ownership cannot be verified because the dump archive changed while it was inspected." + .into(), + ); + } + } + Ok(hashes) +} + +fn normalized_legacy_path(value: &str) -> Result<(PathBuf, String), String> { + let normalized = value.replace('\\', "/"); + let relative = PathBuf::from(&normalized); + if normalized.is_empty() || !safe_relative(&relative) { + return Err(format!( + "Legacy Enhanced state contains an unsafe path: {value}." + )); + } + Ok((relative, normalized)) +} + +fn legacy_backup_for_target( + game_dir: &Path, + target: &Path, + relative_text: &str, +) -> Result, String> { + for candidate in [ + fs_ops::backup_path(target), + fs_ops::legacy_backup_path(target), + ] { + let relative = candidate.strip_prefix(game_dir).map_err(|_| { + format!("Legacy Enhanced backup escaped the game directory for {relative_text}.") + })?; + if !safe_relative(relative) || relative.as_os_str().is_empty() { + return Err(format!( + "Legacy Enhanced backup path is unsafe for {relative_text}." + )); + } + let candidate = contained_target(game_dir, relative)?; + match fs::symlink_metadata(&candidate) { + Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => { + let hash = fs_ops::sha256_file(&candidate)?; + return Ok(Some((relative_string(relative), hash))); + } + Ok(_) => { + return Err(format!( + "Legacy Enhanced backup is not a regular file for {relative_text}: {}.", + candidate.display() + )); + } + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => return Err(error.to_string()), + } + } + Ok(None) +} + +fn adopt_legacy_state( + game_dir: &Path, + enhanced_archive: &Path, + dump_source: Option<&Path>, + storage_dir: &Path, +) -> Result { + let mut persisted = load_state(storage_dir); + if persisted.game_directory.is_some() || !state_has_ownership(&persisted) { + return Ok(false); + } + if !game_dir.is_dir() { + return Err("Invalid game directory for legacy Enhanced ownership adoption.".into()); + } + if !persisted.created_files.is_empty() + || !persisted.owned_backups.is_empty() + || !persisted.installed_hashes.is_empty() + || !persisted.original_backup_hashes.is_empty() + { + return Err( + "Enhanced ownership state is unbound but already contains partial ownership metadata; refusing unsafe adoption." + .into(), + ); + } + + let mut tracked = BTreeSet::new(); + for value in persisted + .installed_files + .iter() + .chain(persisted.dump_only_files.iter()) + { + let (_, normalized) = normalized_legacy_path(value)?; + tracked.insert(normalized); + } + if tracked.is_empty() { + return Err( + "Legacy Enhanced ownership cannot be verified because no managed files were recorded." + .into(), + ); + } + + let mut needs_enhanced = false; + let mut needs_dump = false; + for relative_text in &tracked { + let (relative, _) = normalized_legacy_path(relative_text)?; + if EXPECTED_ENHANCED_FILES.contains(&relative_text.as_str()) { + needs_enhanced = true; + } else if relative + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| UWP_DUMP_WHITELIST.contains(&name)) + { + needs_dump = true; + } else { + return Err(format!( + "Legacy Enhanced state contains a path outside the managed payload: {relative_text}." + )); + } + } + + let enhanced_hashes = needs_enhanced + .then(|| verified_cached_enhanced_hashes(storage_dir, enhanced_archive)) + .transpose()? + .unwrap_or_default(); + let dump_hashes = if needs_dump { + let source = dump_source.ok_or_else(|| { + "Legacy Enhanced ownership cannot be verified because the original dump source is unavailable." + .to_string() + })?; + verified_dump_member_hashes(source)? + } else { + BTreeMap::new() + }; + + let mut created = BTreeSet::new(); + let mut owned_backups = BTreeMap::new(); + let mut installed_hashes = BTreeMap::new(); + let mut original_backup_hashes = BTreeMap::new(); + let mut canonical_targets = HashSet::new(); + for relative_text in &tracked { + let (relative, _) = normalized_legacy_path(relative_text)?; + let expected = enhanced_hashes + .get(relative_text) + .or_else(|| dump_hashes.get(relative_text)) + .ok_or_else(|| { + format!( + "Legacy Enhanced ownership cannot be verified because {relative_text} is absent from its provenance source." + ) + })?; + let target = contained_target(game_dir, &relative)?; + let metadata = fs::symlink_metadata(&target).map_err(|_| { + format!( + "Legacy Enhanced ownership cannot be verified because the managed target is missing: {}.", + target.display() + ) + })?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(format!( + "Legacy Enhanced ownership cannot be verified because the managed target is unsafe: {}.", + target.display() + )); + } + let canonical_target = canonical_game_key(&target)?; + if !canonical_targets.insert(canonical_target) { + return Err(format!( + "Legacy Enhanced state refers to the same target more than once: {relative_text}." + )); + } + let actual = fs_ops::sha256_file(&target)?; + if !actual.eq_ignore_ascii_case(expected) { + return Err(format!( + "Legacy Enhanced target does not match its verified payload source: {}.", + target.display() + )); + } + installed_hashes.insert(relative_text.clone(), actual); + + match legacy_backup_for_target(game_dir, &target, relative_text)? { + Some((backup, hash)) => { + owned_backups.insert(relative_text.clone(), backup); + original_backup_hashes.insert(relative_text.clone(), hash); + } + None if relative + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case("BlackOps3.exe")) => + { + return Err( + "Legacy Enhanced ownership cannot be adopted because BlackOps3.exe has no original backup." + .into(), + ); + } + None => { + created.insert(relative_text.clone()); + } + } + } + + let mut dump_only = BTreeSet::new(); + for value in &persisted.dump_only_files { + let (_, normalized) = normalized_legacy_path(value)?; + dump_only.insert(normalized); + } + persisted.game_directory = Some(canonical_game_key(game_dir)?); + persisted.installed_files = tracked.into_iter().collect(); + persisted.dump_only_files = dump_only.into_iter().collect(); + persisted.created_files = created.into_iter().collect(); + persisted.owned_backups = owned_backups; + persisted.installed_hashes = installed_hashes; + persisted.original_backup_hashes = original_backup_hashes; + save_state(storage_dir, &persisted)?; + Ok(true) +} + +fn private_dir(parent: &Path, label: &str) -> Result { + for index in 1..1000 { + let candidate = parent.join(format!(".patchops-{label}-{}-{index}", std::process::id())); + match fs::create_dir(&candidate) { + Ok(()) => return Ok(candidate), + Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue, + Err(error) => return Err(error.to_string()), + } + } + Err(format!("Could not allocate a private {label} directory.")) +} + +fn regular_file_without_links(path: &Path) -> bool { + fs::symlink_metadata(path) + .is_ok_and(|metadata| metadata.is_file() && !metadata.file_type().is_symlink()) +} + +fn allocate_owned_backup(game_dir: &Path, target: &Path) -> Result<(PathBuf, String), String> { + let name = target + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| "Invalid Enhanced backup filename.".to_string())?; + for index in 1..1000 { + let backup = target.with_file_name(format!( + "{name}.patchops.original-{}-{index}.bak", + std::process::id() + )); + match fs::symlink_metadata(&backup) { + Ok(_) => continue, + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => return Err(error.to_string()), + } + let relative = backup + .strip_prefix(game_dir) + .map_err(|_| "Enhanced backup escaped the game directory.".to_string())? + .to_path_buf(); + if !safe_relative(&relative) { + return Err("Enhanced backup path was unsafe.".into()); + } + return Ok((backup, relative_string(&relative))); + } + Err(format!( + "Could not allocate a unique backup for {}.", + target.display() + )) +} + +fn exact_owned_backup( + game_dir: &Path, + state: &PersistedState, + relative_text: &str, +) -> Result, String> { + let Some(stored) = state.owned_backups.get(relative_text) else { + return Ok(None); + }; + let relative = PathBuf::from(stored); + if relative.as_os_str().is_empty() || !safe_relative(&relative) || stored == relative_text { + return Err(format!( + "Enhanced backup manifest is invalid for {relative_text}." + )); + } + let backup = contained_target(game_dir, &relative)?; + if !regular_file_without_links(&backup) { + return Err(format!( + "The exact PatchOpsIII backup for {relative_text} is missing or unsafe." + )); + } + let expected = state + .original_backup_hashes + .get(relative_text) + .ok_or_else(|| { + format!( + "No original-backup checksum is recorded for {relative_text}; refusing to restore it." + ) + })?; + let actual = fs_ops::sha256_file(&backup)?; + if !actual.eq_ignore_ascii_case(expected) { + return Err(format!( + "The exact PatchOpsIII backup for {relative_text} was modified or corrupted; refusing to restore it." + )); + } + Ok(Some(backup)) +} + +fn stage_members( + source_archive: Option<&Path>, + members: &[PayloadMember], + destination: &Path, +) -> Result<(), String> { + let mut archive = source_archive + .map(|path| { + zip::ZipArchive::new(File::open(path).map_err(|error| error.to_string())?) + .map_err(|error| error.to_string()) + }) + .transpose()?; + for member in members { + if !safe_relative(&member.relative) { + return Err(format!( + "Refusing unsafe payload path: {}", + member.relative.display() + )); + } + let output = destination.join(&member.relative); + if let Some(parent) = output.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + match &member.source { + MemberSource::File(source) => { + fs::copy(source, &output).map_err(|error| error.to_string())?; + } + MemberSource::Zip(index) => { + let mut entry = archive + .as_mut() + .ok_or_else(|| "Missing source archive.".to_string())? + .by_index(*index) + .map_err(|error| error.to_string())?; + let mut output = File::create(&output).map_err(|error| error.to_string())?; + io::copy(&mut entry, &mut output).map_err(|error| error.to_string())?; + } + } + } + Ok(()) +} + +fn relative_string(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + +fn rollback_install(applied: &mut Vec) -> Result<(), String> { + let mut errors = Vec::new(); + for file in applied.drain(..).rev() { + if file.target.exists() + && let Err(error) = fs::remove_file(&file.target) + { + errors.push(format!("{}: {error}", file.target.display())); + continue; + } + let restore = file + .rollback_copy + .as_ref() + .filter(|path| path.exists()) + .or_else(|| { + file.new_permanent_backup + .as_ref() + .filter(|path| path.exists()) + }); + if let Some(restore) = restore + && let Err(error) = fs::rename(restore, &file.target) + { + errors.push(format!("{}: {error}", file.target.display())); + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join("; ")) + } +} + +fn install_files( + game_dir: &Path, + enhanced_archive: &Path, + dump_source: &Path, + storage_dir: &Path, +) -> Result<(), String> { + if !game_dir.is_dir() { + return Err("Invalid game directory for Enhanced install.".into()); + } + let game_key = canonical_game_key(game_dir)?; + verify_cached_checksum(storage_dir, enhanced_archive)?; + let enhanced_archive_hash = fs_ops::sha256_file(enhanced_archive)?; + let dump = dump_members(dump_source)?; + let enhanced = enhanced_members(enhanced_archive)?; + let stage = private_dir(game_dir, "enhanced-stage")?; + let rollback = match private_dir(game_dir, "enhanced-rollback") { + Ok(path) => path, + Err(error) => { + let _ = fs::remove_dir_all(&stage); + return Err(error); + } + }; + + let mut applied = Vec::new(); + let result = (|| { + stage_members( + (!dump_source.is_dir()).then_some(dump_source), + &dump, + &stage, + )?; + stage_members(Some(enhanced_archive), &enhanced, &stage)?; + if !fs_ops::sha256_file(enhanced_archive)?.eq_ignore_ascii_case(&enhanced_archive_hash) { + return Err("Enhanced archive changed while it was being staged.".into()); + } + + let loaded = load_state(storage_dir); + let mut persisted = if loaded.game_directory.as_deref() == Some(game_key.as_str()) { + loaded + } else if state_has_ownership(&loaded) { + return Err( + "Enhanced ownership state belongs to a different or unbound game directory; refusing to reuse it." + .into(), + ); + } else { + PersistedState::default() + }; + persisted.game_directory = Some(game_key.clone()); + let prior_tracked: HashSet = persisted.installed_files.iter().cloned().collect(); + let mut installed: BTreeSet = persisted.installed_files.drain(..).collect(); + let mut dump_files: BTreeSet = persisted.dump_only_files.drain(..).collect(); + let mut created: BTreeSet = persisted.created_files.drain(..).collect(); + let mut payload = Vec::new(); + for member in &dump { + let path = relative_string(&member.relative); + dump_files.insert(path.clone()); + payload.push((member.relative.clone(), path)); + } + for member in &enhanced { + let path = relative_string(&member.relative); + payload.push((member.relative.clone(), path)); + } + + for (relative, relative_text) in &payload { + let target = contained_target(game_dir, relative)?; + if prior_tracked.contains(relative_text) + && persisted.owned_backups.contains_key(relative_text) + { + exact_owned_backup(game_dir, &persisted, relative_text)?; + } + if prior_tracked.contains(relative_text) + && !persisted.owned_backups.contains_key(relative_text) + && !created.contains(relative_text) + { + return Err(format!( + "Enhanced ownership data is incomplete for {}; refusing to overwrite it.", + target.display() + )); + } + match fs::symlink_metadata(&target) { + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => { + return Err(format!( + "Refusing to replace an unsafe Enhanced target: {}", + target.display() + )); + } + Ok(_) if prior_tracked.contains(relative_text) => { + if let Some(expected) = persisted.installed_hashes.get(relative_text) { + let actual = fs_ops::sha256_file(&target)?; + if !actual.eq_ignore_ascii_case(expected) { + return Err(format!( + "Enhanced target was modified since installation; refusing to overwrite {}.", + target.display() + )); + } + } else { + return Err(format!( + "Enhanced ownership data is incomplete for {}; refusing to overwrite it.", + target.display() + )); + } + } + Ok(_) => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => return Err(error.to_string()), + } + } + + for (relative, relative_text) in payload { + if !safe_relative(&relative) { + return Err(format!("Refusing unsafe payload path: {relative_text}")); + } + let target = contained_target(game_dir, &relative)?; + if let Some(parent) = target.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + let existed_before = regular_file_without_links(&target); + let mut new_permanent_backup = None; + if existed_before + && !persisted.owned_backups.contains_key(&relative_text) + && !created.contains(&relative_text) + { + let (backup, backup_relative) = allocate_owned_backup(game_dir, &target)?; + fs::rename(&target, &backup).map_err(|error| error.to_string())?; + persisted + .owned_backups + .insert(relative_text.clone(), backup_relative); + new_permanent_backup = Some(backup); + } else if !prior_tracked.contains(&relative_text) && !existed_before { + created.insert(relative_text.clone()); + } + + applied.push(AppliedFile { + target: target.clone(), + rollback_copy: None, + new_permanent_backup, + }); + if let Some(backup) = applied + .last() + .and_then(|file| file.new_permanent_backup.as_ref()) + { + persisted + .original_backup_hashes + .insert(relative_text.clone(), fs_ops::sha256_file(backup)?); + } + if target.is_file() { + let path = rollback.join(&relative); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + fs::rename(&target, &path).map_err(|error| error.to_string())?; + applied.last_mut().expect("applied entry").rollback_copy = Some(path); + } + fs::rename(stage.join(&relative), &target).map_err(|error| error.to_string())?; + persisted + .installed_hashes + .insert(relative_text.clone(), fs_ops::sha256_file(&target)?); + installed.insert(relative_text); + } + + persisted.installed = !installed.is_empty(); + persisted.detected_at.get_or_insert_with(utc_timestamp); + persisted.installed_files = installed.into_iter().collect(); + persisted.dump_only_files = dump_files.into_iter().collect(); + persisted.created_files = created.into_iter().collect(); + save_state(storage_dir, &persisted)?; + Ok(()) + })(); + + let mut remove_rollback = true; + let result = match result { + Ok(()) => Ok(()), + Err(error) => match rollback_install(&mut applied) { + Ok(()) => Err(error), + Err(rollback_error) => { + remove_rollback = false; + Err(format!( + "{error} Rollback also failed: {rollback_error}. Recovery files were preserved at {}", + rollback.display() + )) + } + }, + }; + let _ = fs::remove_dir_all(&stage); + if remove_rollback { + let _ = fs::remove_dir_all(&rollback); + } + result +} + +fn apply_uninstall_plan( + plan: PlannedUninstall, + transaction: &Path, + index: usize, +) -> Result { + let displaced = if plan.target.exists() { + let path = transaction.join(index.to_string()); + fs::rename(&plan.target, &path).map_err(|error| error.to_string())?; + Some(path) + } else { + None + }; + + if let UninstallAction::Restore(backup) = &plan.action + && let Err(error) = fs::rename(backup, &plan.target) + { + return if let Some(displaced) = displaced { + match fs::rename(&displaced, &plan.target) { + Ok(()) => Err(error.to_string()), + Err(rollback) => Err(format!( + "{error}; restoring the managed file also failed: {rollback}" + )), + } + } else { + Err(error.to_string()) + }; + } + + Ok(AppliedUninstall { plan, displaced }) +} + +fn rollback_uninstall(applied: &mut Vec) -> Result<(), String> { + let mut errors = Vec::new(); + for item in applied.drain(..).rev() { + if let UninstallAction::Restore(backup) = &item.plan.action + && item.plan.target.exists() + && let Err(error) = fs::rename(&item.plan.target, backup) + { + errors.push(format!("{}: {error}", backup.display())); + continue; + } + if let Some(displaced) = item.displaced + && let Err(error) = fs::rename(displaced, &item.plan.target) + { + errors.push(format!("{}: {error}", item.plan.target.display())); + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join("; ")) + } +} + +impl EnhancedUninstallTransaction { + fn rollback(mut self) -> Result<(), String> { + let mut errors = Vec::new(); + if let Err(error) = rollback_uninstall(&mut self.applied) { + errors.push(error); + } + if let Err(error) = save_state(&self.storage_dir, &self.original_state) { + errors.push(format!("ownership-state rollback failed: {error}")); + } + if errors.is_empty() { + if let Some(transaction) = self.transaction { + let _ = fs::remove_dir_all(transaction); + } + Ok(()) + } else { + Err(errors.join("; ")) + } + } + + fn commit(self) -> Result<(), String> { + match self.transaction { + Some(transaction) => fs::remove_dir_all(&transaction) + .map_err(|error| format!("{}: {error}", transaction.display())), + None => Ok(()), + } + } +} + +fn begin_uninstall_files( + game_dir: &Path, + storage_dir: &Path, +) -> Result { + if !game_dir.is_dir() { + return Err("Invalid game directory for Enhanced uninstall.".into()); + } + let game_key = canonical_game_key(game_dir)?; + let original_state = load_state(storage_dir); + match original_state.game_directory.as_deref() { + Some(bound) if bound == game_key => {} + Some(_) => { + return Err( + "Enhanced ownership state belongs to a different game directory; refusing cleanup." + .into(), + ); + } + None if state_has_ownership(&original_state) => { + return Err( + "Enhanced ownership state is not bound to a game directory; refusing cleanup." + .into(), + ); + } + None => {} + } + let mut persisted = original_state.clone(); + let tracked: BTreeSet = persisted.installed_files.iter().cloned().collect(); + if tracked.is_empty() { + if persisted.installed || detect_install(game_dir) { + return Err( + "Enhanced files were detected without an ownership manifest; refusing unsafe cleanup." + .into(), + ); + } + return Ok(EnhancedUninstallTransaction { + applied: Vec::new(), + transaction: None, + storage_dir: storage_dir.to_path_buf(), + original_state, + }); + } + let created: HashSet = persisted.created_files.iter().cloned().collect(); + let mut plans = Vec::new(); + + for relative_text in tracked { + let relative = PathBuf::from(&relative_text); + if relative.as_os_str().is_empty() || !safe_relative(&relative) { + return Err(format!( + "Enhanced ownership manifest contains an unsafe path: {relative_text}." + )); + } + let target = contained_target(game_dir, &relative)?; + let backup = exact_owned_backup(game_dir, &persisted, &relative_text)?; + let target_exists = match fs::symlink_metadata(&target) { + Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => true, + Ok(_) => { + return Err(format!( + "Enhanced target is no longer a regular file; refusing cleanup: {}.", + target.display() + )); + } + Err(error) if error.kind() == io::ErrorKind::NotFound => false, + Err(error) => return Err(error.to_string()), + }; + if target_exists { + let expected = persisted + .installed_hashes + .get(&relative_text) + .ok_or_else(|| { + format!( + "No managed checksum is recorded for {}; refusing to overwrite or remove it.", + target.display() + ) + })?; + let actual = fs_ops::sha256_file(&target)?; + if !actual.eq_ignore_ascii_case(expected) { + return Err(format!( + "Enhanced target was modified after installation; leaving it and its backup untouched: {}.", + target.display() + )); + } + } + + match backup { + Some(backup) => plans.push(PlannedUninstall { + target, + action: UninstallAction::Restore(backup), + }), + None if created.contains(&relative_text) => { + if target_exists { + plans.push(PlannedUninstall { + target, + action: UninstallAction::Remove, + }); + } + } + None if !target_exists => { + return Err(format!( + "The original file and exact PatchOpsIII backup are both missing for {relative_text}." + )); + } + None => { + return Err(format!( + "No exact PatchOpsIII-owned backup is recorded for {}; refusing unsafe cleanup.", + target.display() + )); + } + } + } + + let transaction = private_dir(game_dir, "enhanced-uninstall")?; + let mut applied = Vec::new(); + let result = (|| { + for (index, plan) in plans.into_iter().enumerate() { + applied.push(apply_uninstall_plan(plan, &transaction, index)?); + } + persisted.installed = false; + persisted.installed_files.clear(); + persisted.dump_only_files.clear(); + persisted.created_files.clear(); + persisted.owned_backups.clear(); + persisted.installed_hashes.clear(); + persisted.original_backup_hashes.clear(); + save_state(storage_dir, &persisted) + })(); + + match result { + Ok(()) => Ok(EnhancedUninstallTransaction { + applied, + transaction: Some(transaction), + storage_dir: storage_dir.to_path_buf(), + original_state, + }), + Err(error) => match rollback_uninstall(&mut applied) { + Ok(()) => { + let state_error = save_state(storage_dir, &original_state).err(); + let _ = fs::remove_dir_all(&transaction); + Err(match state_error { + Some(rollback) => { + format!("{error} Ownership-state rollback also failed: {rollback}") + } + None => error, + }) + } + Err(rollback) => Err(format!( + "{error} Rollback also failed: {rollback}. Recovery files were preserved at {}", + transaction.display() + )), + }, + } +} + +fn uninstall_files(game_dir: &Path, storage_dir: &Path) -> Result<(), String> { + begin_uninstall_files(game_dir, storage_dir)?.commit() +} + +fn expand_home(path: &Path) -> PathBuf { + let Some(text) = path.to_str() else { + return path.to_owned(); + }; + if (text == "~" || text.starts_with("~/") || text.starts_with("~\\")) + && let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) + { + return PathBuf::from(home) + .join(text.trim_start_matches('~').trim_start_matches(['/', '\\'])); + } + path.to_owned() +} + +#[cfg(target_os = "linux")] +fn configure_linux(state: &AppState) -> Result<(), String> { + steam::configure_bo3_enhanced_linux(state, None) +} + +#[cfg(not(target_os = "linux"))] +fn configure_linux(_: &AppState) -> Result<(), String> { + Ok(()) +} + +#[cfg(target_os = "linux")] +fn cleanup_linux(state: &AppState) -> Result<(), String> { + steam::cleanup_bo3_enhanced_linux(state) +} + +#[cfg(not(target_os = "linux"))] +fn cleanup_linux(_: &AppState) -> Result<(), String> { + Ok(()) +} + +pub fn install(state: &AppState, game_dir: &Path, dump_source: &Path) -> Result<(), String> { + let dump_source = expand_home(dump_source); + if dump_source.as_os_str().is_empty() || !dump_source.exists() { + return Err("The selected dump source does not exist.".into()); + } + if !validate_dump_source(&dump_source)? { + return Err("The dump source is missing required files.".into()); + } + let mut settings = state.load_settings(); + settings.enhanced_dump_source = Some(dump_source.display().to_string()); + state.save_settings(&settings)?; + + let cached_archive = state.mod_files_dir().join(ENHANCED_ARCHIVE_NAME); + if adopt_legacy_state( + game_dir, + &cached_archive, + Some(&dump_source), + state.data_dir(), + )? { + state.log( + "Info", + "Adopted verified legacy BO3 Enhanced ownership before updating the cached release.", + ); + } + + state.log("Info", "Fetching latest BO3 Enhanced release..."); + let archive = download_latest(state)?; + state.log("Info", "Installing BO3 Enhanced files..."); + install_files(game_dir, &archive, &dump_source, state.data_dir())?; + if let Err(error) = configure_linux(state) { + let rollback = uninstall_files(game_dir, state.data_dir()).err(); + return Err(match rollback { + Some(rollback) => format!( + "Linux compatibility setup failed: {error} File rollback also failed: {rollback}" + ), + None => format!("Linux compatibility setup failed: {error}"), + }); + } + + if let Some(executable) = exe::find_executable(game_dir) { + let hash = fs_ops::sha256_file(&executable)?; + exe::record_enhanced_hash(state, game_dir, &hash)?; + } + if let Err(error) = exe::write_variant(game_dir, exe::ENHANCED_EXE_ID) { + state.log( + "Warning", + format!( + "Enhanced was installed, but its EXE variant marker could not be saved: {error}" + ), + ); + } + state.log("Success", "Installed BO3 Enhanced successfully."); + Ok(()) +} + +pub fn uninstall(state: &AppState, game_dir: &Path) -> Result<(), String> { + let dump_source = state + .load_settings() + .enhanced_dump_source + .map(|source| expand_home(Path::new(&source))); + let cached_archive = state.mod_files_dir().join(ENHANCED_ARCHIVE_NAME); + if adopt_legacy_state( + game_dir, + &cached_archive, + dump_source.as_deref(), + state.data_dir(), + )? { + state.log("Info", "Adopted verified legacy BO3 Enhanced ownership."); + } + let transaction = begin_uninstall_files(game_dir, state.data_dir())?; + if let Err(error) = cleanup_linux(state) { + return Err(match transaction.rollback() { + Ok(()) => error, + Err(rollback) => format!( + "Linux compatibility cleanup failed: {error} Enhanced file rollback also failed: {rollback}" + ), + }); + } + transaction.commit()?; + if let Err(error) = exe::write_variant(game_dir, "default") { + state.log( + "Warning", + format!("Enhanced was removed, but its EXE variant marker could not be saved: {error}"), + ); + } + state.log("Success", "Uninstalled BO3 Enhanced successfully."); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + io::Write, + sync::atomic::{AtomicUsize, Ordering}, + }; + use zip::write::SimpleFileOptions; + + static NEXT: AtomicUsize = AtomicUsize::new(0); + + fn temp_dir(label: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "patchops-enhanced-{label}-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + path + } + + fn write_zip(path: &Path, files: &[(&str, &[u8])]) { + let file = File::create(path).unwrap(); + let mut archive = zip::ZipWriter::new(file); + for (name, body) in files { + archive + .start_file(*name, SimpleFileOptions::default()) + .unwrap(); + archive.write_all(body).unwrap(); + } + archive.finish().unwrap(); + } + + fn enhanced_zip(path: &Path) { + write_zip( + path, + &EXPECTED_ENHANCED_FILES + .iter() + .map(|name| (*name, b"enhanced".as_slice())) + .collect::>(), + ); + } + + fn enhanced_zip_with_body(path: &Path, body: &'static [u8]) { + write_zip( + path, + &EXPECTED_ENHANCED_FILES + .iter() + .map(|name| (*name, body)) + .collect::>(), + ); + } + + fn record_cached_checksum(storage: &Path, archive: &Path) { + let mut checksums = BTreeMap::new(); + checksums.insert( + archive.file_name().unwrap().to_string_lossy().into_owned(), + fs_ops::sha256_file(archive).unwrap(), + ); + save_checksums(storage, &checksums).unwrap(); + } + + #[test] + fn adopts_verified_legacy_state_before_upgrade_and_restores_originals() { + let root = temp_dir("legacy-adoption-roundtrip"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip_with_body(&enhanced, b"legacy enhanced"); + record_cached_checksum(&storage, &enhanced); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"legacy dump exe"), + ("DUMP/MicrosoftGame.config", b"legacy dump config"), + ("DUMP/GameChat2.dll", b"legacy dump dll"), + ], + ); + + for name in EXPECTED_ENHANCED_FILES { + fs::write(game.join(name), b"legacy enhanced").unwrap(); + } + fs::write(game.join("BlackOps3.exe"), b"legacy dump exe").unwrap(); + fs::write(game.join("BlackOps3.exe.patchops.bak"), b"original exe").unwrap(); + fs::write(game.join("MicrosoftGame.config"), b"legacy dump config").unwrap(); + fs::write(game.join("MicrosoftGame.config.bak"), b"original config").unwrap(); + fs::write(game.join("GameChat2.dll"), b"legacy dump dll").unwrap(); + + let mut legacy_files = EXPECTED_ENHANCED_FILES + .iter() + .map(|name| Value::String((*name).to_owned())) + .collect::>(); + legacy_files.extend([ + Value::String("BlackOps3.exe".into()), + Value::String("MicrosoftGame.config".into()), + Value::String("GameChat2.dll".into()), + ]); + let legacy = serde_json::json!({ + "installed": true, + "detected_at": "2025-01-02T03:04:05Z", + "installed_files": legacy_files, + "dump_only_files": ["GameChat2.dll"], + "legacy_custom": {"keep": true} + }); + fs::write( + state_path(&storage), + serde_json::to_vec_pretty(&legacy).unwrap(), + ) + .unwrap(); + + assert!(adopt_legacy_state(&game, &enhanced, Some(&dump), &storage).unwrap()); + let adopted = load_state(&storage); + assert!(state_belongs_to_game(&adopted, &game)); + assert_eq!( + adopted.extra.get("legacy_custom"), + Some(&serde_json::json!({"keep": true})) + ); + assert!( + adopted + .created_files + .contains(&"T7WSBootstrapper.dll".into()) + ); + assert!(adopted.created_files.contains(&"GameChat2.dll".into())); + assert!(!adopted.created_files.contains(&"BlackOps3.exe".into())); + assert_eq!( + adopted.owned_backups.get("BlackOps3.exe"), + Some(&"BlackOps3.exe.patchops.bak".into()) + ); + assert_eq!( + adopted.owned_backups.get("MicrosoftGame.config"), + Some(&"MicrosoftGame.config.bak".into()) + ); + assert_eq!(adopted.installed_hashes.len(), 7); + assert_eq!(adopted.original_backup_hashes.len(), 2); + + enhanced_zip_with_body(&enhanced, b"current enhanced"); + record_cached_checksum(&storage, &enhanced); + install_files(&game, &enhanced, &dump, &storage).unwrap(); + let upgraded = load_state(&storage); + for name in EXPECTED_ENHANCED_FILES { + assert!(upgraded.created_files.contains(&name.to_owned())); + assert!(!upgraded.owned_backups.contains_key(name)); + assert_eq!(fs::read(game.join(name)).unwrap(), b"current enhanced"); + } + + uninstall_files(&game, &storage).unwrap(); + assert_eq!( + fs::read(game.join("BlackOps3.exe")).unwrap(), + b"original exe" + ); + assert_eq!( + fs::read(game.join("MicrosoftGame.config")).unwrap(), + b"original config" + ); + assert!(!game.join("GameChat2.dll").exists()); + assert!( + EXPECTED_ENHANCED_FILES + .iter() + .all(|name| !game.join(name).exists()) + ); + assert_eq!( + load_state(&storage).extra.get("legacy_custom"), + Some(&serde_json::json!({"keep": true})) + ); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn legacy_adoption_requires_a_verified_cached_archive_without_mutating_state() { + let root = temp_dir("legacy-cache-provenance"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip(&enhanced); + for name in EXPECTED_ENHANCED_FILES { + fs::write(game.join(name), b"enhanced").unwrap(); + } + let legacy = serde_json::json!({ + "installed": true, + "installed_files": EXPECTED_ENHANCED_FILES, + "legacy_custom": "unchanged" + }); + let original = serde_json::to_vec_pretty(&legacy).unwrap(); + fs::write(state_path(&storage), &original).unwrap(); + + let error = adopt_legacy_state(&game, &enhanced, None, &storage).unwrap_err(); + assert!(error.contains("no recorded SHA-256 checksum")); + assert_eq!(fs::read(state_path(&storage)).unwrap(), original); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn legacy_marker_without_tracked_files_remains_unowned() { + let root = temp_dir("legacy-marker-only"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + for name in EXPECTED_ENHANCED_FILES { + fs::write(game.join(name), b"enhanced").unwrap(); + } + let legacy = serde_json::json!({ + "installed": true, + "legacy_custom": "unchanged" + }); + let original = serde_json::to_vec_pretty(&legacy).unwrap(); + fs::write(state_path(&storage), &original).unwrap(); + + let error = adopt_legacy_state(&game, &root.join(ENHANCED_ARCHIVE_NAME), None, &storage) + .unwrap_err(); + assert!(error.contains("no managed files were recorded")); + assert_eq!(fs::read(state_path(&storage)).unwrap(), original); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn legacy_adoption_rejects_dump_mismatch_without_mutating_state() { + let root = temp_dir("legacy-dump-mismatch"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"modified dump exe").unwrap(); + fs::write(game.join("BlackOps3.exe.patchops.bak"), b"original exe").unwrap(); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"verified dump exe"), + ("DUMP/MicrosoftGame.config", b"verified config"), + ], + ); + let legacy = serde_json::json!({ + "installed": false, + "dump_only_files": ["BlackOps3.exe"], + "legacy_custom": 42 + }); + let original = serde_json::to_vec_pretty(&legacy).unwrap(); + fs::write(state_path(&storage), &original).unwrap(); + + let error = adopt_legacy_state( + &game, + &root.join(ENHANCED_ARCHIVE_NAME), + Some(&dump), + &storage, + ) + .unwrap_err(); + assert!(error.contains("does not match its verified payload source")); + assert_eq!(fs::read(state_path(&storage)).unwrap(), original); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn legacy_adoption_rejects_missing_executable_backup_and_unsafe_backup() { + let root = temp_dir("legacy-backup-preflight"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"dump exe").unwrap(); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"dump exe"), + ("DUMP/MicrosoftGame.config", b"config"), + ], + ); + let legacy = serde_json::json!({ + "installed": false, + "dump_only_files": ["BlackOps3.exe"] + }); + let original = serde_json::to_vec_pretty(&legacy).unwrap(); + fs::write(state_path(&storage), &original).unwrap(); + + let error = adopt_legacy_state( + &game, + &root.join(ENHANCED_ARCHIVE_NAME), + Some(&dump), + &storage, + ) + .unwrap_err(); + assert!(error.contains("BlackOps3.exe has no original backup")); + assert_eq!(fs::read(state_path(&storage)).unwrap(), original); + + fs::create_dir(game.join("BlackOps3.exe.patchops.bak")).unwrap(); + let error = adopt_legacy_state( + &game, + &root.join(ENHANCED_ARCHIVE_NAME), + Some(&dump), + &storage, + ) + .unwrap_err(); + assert!(error.contains("backup is not a regular file")); + assert_eq!(fs::read(state_path(&storage)).unwrap(), original); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn accepts_only_expected_enhanced_release_urls() { + assert!(valid_enhanced_asset_url( + "https://github.com/shiversoftdev/BO3Enhanced/releases/download/v1.15/BO3Enhanced.zip" + )); + assert!(!valid_enhanced_asset_url( + "http://github.com/shiversoftdev/BO3Enhanced/releases/download/v1.15/BO3Enhanced.zip" + )); + assert!(!valid_enhanced_asset_url( + "https://github.com.evil.invalid/shiversoftdev/BO3Enhanced/releases/download/v1.15/BO3Enhanced.zip" + )); + assert!(!valid_enhanced_asset_url( + "https://github.com/other/BO3Enhanced/releases/download/v1.15/BO3Enhanced.zip" + )); + assert!(!valid_enhanced_asset_url( + "https://github.com/shiversoftdev/BO3Enhanced/releases/download/v1.15/BO3Enhanced.7z" + )); + } + + #[test] + fn rejects_archive_path_traversal() { + let root = temp_dir("traversal"); + let archive = root.join("dump.zip"); + write_zip( + &archive, + &[ + ("DUMP/BlackOps3.exe", b"exe"), + ("DUMP/MicrosoftGame.config", b"config"), + ("../GameChat2.dll", b"escape"), + ], + ); + assert!(dump_members(&archive).is_err()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn combined_uninstall_restores_every_dump_file() { + let root = temp_dir("roundtrip"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"original exe").unwrap(); + fs::write(game.join("MicrosoftGame.config"), b"original config").unwrap(); + + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip(&enhanced); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"dump exe"), + ("DUMP/MicrosoftGame.config", b"dump config"), + ("DUMP/GameChat2.dll", b"dump dll"), + ], + ); + + install_files(&game, &enhanced, &dump, &storage).unwrap(); + assert_eq!(fs::read(game.join("BlackOps3.exe")).unwrap(), b"dump exe"); + assert!(game.join("GameChat2.dll").is_file()); + uninstall_files(&game, &storage).unwrap(); + assert_eq!( + fs::read(game.join("BlackOps3.exe")).unwrap(), + b"original exe" + ); + assert_eq!( + fs::read(game.join("MicrosoftGame.config")).unwrap(), + b"original config" + ); + assert!(!game.join("GameChat2.dll").exists()); + assert!( + EXPECTED_ENHANCED_FILES + .iter() + .all(|name| !game.join(name).exists()) + ); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn uninstall_restores_exact_owned_backup_and_leaves_stale_backup_untouched() { + let root = temp_dir("owned-backup"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"exact original").unwrap(); + fs::write(game.join("MicrosoftGame.config"), b"original config").unwrap(); + let stale = game.join("BlackOps3.exe.patchops.bak"); + fs::write(&stale, b"stale backup").unwrap(); + + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip(&enhanced); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"dump exe"), + ("DUMP/MicrosoftGame.config", b"dump config"), + ], + ); + + install_files(&game, &enhanced, &dump, &storage).unwrap(); + let state = load_state(&storage); + let owned = state.owned_backups.get("BlackOps3.exe").unwrap(); + assert_ne!(owned, "BlackOps3.exe.patchops.bak"); + assert_eq!(fs::read(&stale).unwrap(), b"stale backup"); + + uninstall_files(&game, &storage).unwrap(); + assert_eq!( + fs::read(game.join("BlackOps3.exe")).unwrap(), + b"exact original" + ); + assert_eq!(fs::read(&stale).unwrap(), b"stale backup"); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn uninstall_refuses_modified_managed_file_without_consuming_backup() { + let root = temp_dir("modified-conflict"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"exact original").unwrap(); + fs::write(game.join("MicrosoftGame.config"), b"original config").unwrap(); + + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip(&enhanced); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"dump exe"), + ("DUMP/MicrosoftGame.config", b"dump config"), + ], + ); + + install_files(&game, &enhanced, &dump, &storage).unwrap(); + let state = load_state(&storage); + let backup = game.join(state.owned_backups.get("BlackOps3.exe").unwrap()); + fs::write(game.join("BlackOps3.exe"), b"user modified").unwrap(); + + let error = uninstall_files(&game, &storage).unwrap_err(); + assert!(error.contains("modified after installation")); + assert_eq!( + fs::read(game.join("BlackOps3.exe")).unwrap(), + b"user modified" + ); + assert_eq!(fs::read(&backup).unwrap(), b"exact original"); + assert!(load_state(&storage).installed); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn deferred_uninstall_can_restore_files_and_ownership_state() { + let root = temp_dir("deferred-uninstall"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"original exe").unwrap(); + fs::write(game.join("MicrosoftGame.config"), b"original config").unwrap(); + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip(&enhanced); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"dump exe"), + ("DUMP/MicrosoftGame.config", b"dump config"), + ], + ); + + install_files(&game, &enhanced, &dump, &storage).unwrap(); + let transaction = begin_uninstall_files(&game, &storage).unwrap(); + assert_eq!( + fs::read(game.join("BlackOps3.exe")).unwrap(), + b"original exe" + ); + assert!(!load_state(&storage).installed); + + transaction.rollback().unwrap(); + assert_eq!(fs::read(game.join("BlackOps3.exe")).unwrap(), b"dump exe"); + let state = load_state(&storage); + assert!(state.installed); + assert!(state.owned_backups.contains_key("BlackOps3.exe")); + uninstall_files(&game, &storage).unwrap(); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn ownership_state_is_bound_to_one_canonical_game_directory() { + let root = temp_dir("game-binding"); + let game_a = root.join("game-a"); + let game_b = root.join("game-b"); + let storage = root.join("storage"); + for game in [&game_a, &game_b] { + fs::create_dir_all(game).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"original exe").unwrap(); + fs::write(game.join("MicrosoftGame.config"), b"original config").unwrap(); + } + fs::create_dir_all(&storage).unwrap(); + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip(&enhanced); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"dump exe"), + ("DUMP/MicrosoftGame.config", b"dump config"), + ], + ); + + install_files(&game_a, &enhanced, &dump, &storage).unwrap(); + let state = load_state(&storage); + assert!(state_belongs_to_game(&state, &game_a)); + assert!(!state_belongs_to_game(&state, &game_b)); + + assert!( + install_files(&game_b, &enhanced, &dump, &storage) + .unwrap_err() + .contains("different or unbound game directory") + ); + let error = match begin_uninstall_files(&game_b, &storage) { + Err(error) => error, + Ok(_) => panic!("uninstall unexpectedly accepted another game directory"), + }; + assert!(error.contains("different game directory")); + assert_eq!(fs::read(game_a.join("BlackOps3.exe")).unwrap(), b"dump exe"); + assert_eq!( + fs::read(game_b.join("BlackOps3.exe")).unwrap(), + b"original exe" + ); + uninstall_files(&game_a, &storage).unwrap(); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn partial_owned_install_remains_eligible_for_cleanup() { + let root = temp_dir("partial-owned-install"); + let game_a = root.join("game-a"); + let game_b = root.join("game-b"); + fs::create_dir_all(&game_a).unwrap(); + fs::create_dir_all(&game_b).unwrap(); + + let state = PersistedState { + game_directory: Some(canonical_game_key(&game_a).unwrap()), + installed: false, + installed_files: vec!["T7WSBootstrapper.dll".into()], + ..PersistedState::default() + }; + assert!(state_has_owned_install(&state, &game_a)); + assert!(!state_has_owned_install(&state, &game_b)); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn uninstall_refuses_a_changed_exact_original_backup() { + let root = temp_dir("changed-original-backup"); + let game = root.join("game"); + let storage = root.join("storage"); + fs::create_dir_all(&game).unwrap(); + fs::create_dir_all(&storage).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"original exe").unwrap(); + fs::write(game.join("MicrosoftGame.config"), b"original config").unwrap(); + let enhanced = root.join(ENHANCED_ARCHIVE_NAME); + enhanced_zip(&enhanced); + let dump = root.join("dump.zip"); + write_zip( + &dump, + &[ + ("DUMP/BlackOps3.exe", b"dump exe"), + ("DUMP/MicrosoftGame.config", b"dump config"), + ], + ); + + install_files(&game, &enhanced, &dump, &storage).unwrap(); + let state = load_state(&storage); + assert_eq!( + state.original_backup_hashes.len(), + state.owned_backups.len() + ); + let backup = game.join(state.owned_backups.get("BlackOps3.exe").unwrap()); + fs::write(&backup, b"corrupt backup").unwrap(); + + let error = uninstall_files(&game, &storage).unwrap_err(); + assert!(error.contains("modified or corrupted")); + assert_eq!(fs::read(game.join("BlackOps3.exe")).unwrap(), b"dump exe"); + assert_eq!(fs::read(backup).unwrap(), b"corrupt backup"); + assert!(load_state(&storage).installed); + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/native/core/src/exe.rs b/native/core/src/exe.rs new file mode 100644 index 0000000..03211ea --- /dev/null +++ b/native/core/src/exe.rs @@ -0,0 +1,863 @@ +use std::{ + collections::{HashMap, HashSet}, + fs, + path::{Path, PathBuf}, + sync::{Mutex, OnceLock}, + time::SystemTime, +}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{ + app::{AppState, Settings}, + fs_ops, + models::ExeSwapState, + steam, +}; + +pub const CURRENT_EXE_ID: &str = "current"; +pub const COMPATIBLE_EXE_ID: &str = "compatible"; +pub const ENHANCED_EXE_ID: &str = "enhanced"; +pub const DEFAULT_STEAM_EXE_SHA256: &str = + "51ca63bbc660e0826943c60da67606f6bcb4b3b519528b5e0548c68c9423a323"; +pub const COMPATIBLE_BUILD_SHA256: &[&str] = + &["66b95eb4667bd5b3b3d230e7bed1d29ccd261d48ca2699f01216c863be24ff44"]; +pub const COMPATIBLE_DEPOT_MANIFEST_ID: &str = "9084453472036406216"; +pub const COMPATIBLE_DEPOT_COMMAND: &str = "download_depot 311210 311211 9084453472036406216"; +pub const COMPATIBLE_DEPOT_REQUIRED_MESSAGE: &str = "Compatible Steam depot was not found. Copy the depot command, open Steam Console, run it, then continue once the download finishes."; +pub const CURRENT_STEAM_BUILD_ID: &str = "24784313"; +pub const CURRENT_STEAM_BUILD_DATE: &str = "Sep 10, 2026"; +pub const COMPATIBLE_STEAM_BUILD_ID: &str = "10650222"; +pub const COMPATIBLE_STEAM_BUILD_DATE: &str = "Mar 3, 2023"; + +const GAME_EXECUTABLE_NAMES: [&str; 2] = ["BlackOpsIII.exe", "BlackOps3.exe"]; +const VARIANT_STATE_FILENAME: &str = ".patchops_exe_variant.json"; + +#[derive(Clone)] +struct HashCache { + length: u64, + modified: Option, + hash: String, +} + +static ACTIVE_EXE_HASH: OnceLock>> = OnceLock::new(); + +fn active_exe_hash(path: &Path) -> Result { + let metadata = path.metadata().map_err(|error| error.to_string())?; + let modified = metadata.modified().ok(); + let cache = ACTIVE_EXE_HASH.get_or_init(|| Mutex::new(HashMap::new())); + if let Ok(cached) = cache.lock() + && let Some(cached) = cached.get(path) + && modified.is_some() + && cached.length == metadata.len() + && cached.modified == modified + { + return Ok(cached.hash.clone()); + } + let hash = fs_ops::sha256_file(path)?; + if let Ok(mut cached) = cache.lock() { + if cached.len() >= 32 { + cached.clear(); + } + cached.insert( + path.to_owned(), + HashCache { + length: metadata.len(), + modified, + hash: hash.clone(), + }, + ); + } + Ok(hash) +} + +// Status probes may reuse metadata-identified hashes. Mutations always reread +// bytes, including the source, staging file and activated target. +fn hash_for_probe(path: &Path, use_cache: bool) -> Result { + if use_cache { + active_exe_hash(path) + } else { + fs_ops::sha256_file(path) + } +} + +#[derive(Clone, Debug)] +struct Integrity { + trusted: bool, + status: &'static str, + profile: &'static str, + message: String, +} + +#[derive(Serialize, Deserialize)] +struct VariantState { + variant: String, +} + +pub fn find_executable(game_dir: &Path) -> Option { + GAME_EXECUTABLE_NAMES + .iter() + .map(|name| game_dir.join(name)) + .find(|candidate| candidate.is_file()) +} + +pub fn read_variant(game_dir: &Path) -> Option { + let body = fs::read(game_dir.join(VARIANT_STATE_FILENAME)).ok()?; + serde_json::from_slice::(&body) + .ok() + .map(|state| state.variant) +} + +pub fn write_variant(game_dir: &Path, variant: &str) -> Result<(), String> { + let body = serde_json::to_vec_pretty(&VariantState { + variant: variant.to_owned(), + }) + .map_err(|error| error.to_string())?; + fs_ops::atomic_write(&game_dir.join(VARIANT_STATE_FILENAME), &body) +} + +fn known_enhanced_hashes(settings: &Settings, game_dir: Option<&Path>) -> HashSet { + let mut known = HashSet::new(); + if let Some(game_dir) = game_dir + && let Some(value) = settings + .enhanced_exe_hashes + .get(&game_dir.display().to_string()) + { + match value { + Value::String(hash) if !hash.is_empty() => { + known.insert(hash.to_ascii_lowercase()); + } + Value::Array(hashes) => { + known.extend( + hashes + .iter() + .filter_map(Value::as_str) + .filter(|hash| !hash.is_empty()) + .map(str::to_ascii_lowercase), + ); + } + _ => {} + } + } + if let Some(hash) = settings + .enhanced_exe_hash + .as_deref() + .filter(|hash| !hash.is_empty()) + { + known.insert(hash.to_ascii_lowercase()); + } + known +} + +pub fn record_enhanced_hash( + state: &AppState, + game_dir: &Path, + exe_hash: &str, +) -> Result<(), String> { + let normalized = exe_hash.to_ascii_lowercase(); + if normalized.is_empty() { + return Ok(()); + } + + let mut settings = state.load_settings(); + let key = game_dir.display().to_string(); + let mut values = match settings.enhanced_exe_hashes.remove(&key) { + Some(Value::String(value)) => vec![Value::String(value)], + Some(Value::Array(values)) => values, + _ => Vec::new(), + }; + if !values + .iter() + .filter_map(Value::as_str) + .any(|value| value.eq_ignore_ascii_case(&normalized)) + { + values.push(Value::String(normalized)); + } + if values.len() > 5 { + values.drain(..values.len() - 5); + } + settings + .enhanced_exe_hashes + .insert(key, Value::Array(values)); + state.save_settings(&settings) +} + +fn integrity_for_hash( + exe_hash: &str, + executable_exists: bool, + enhanced_active: bool, + enhanced_hash_known: bool, +) -> Integrity { + let normalized = exe_hash.to_ascii_lowercase(); + if !executable_exists { + return Integrity { + trusted: false, + status: "missing", + profile: "", + message: "BlackOps3.exe or BlackOpsIII.exe was not found.".into(), + }; + } + if normalized == DEFAULT_STEAM_EXE_SHA256 { + return Integrity { + trusted: true, + status: "trusted", + profile: CURRENT_EXE_ID, + message: format!("Executable matches latest Steam BuildID {CURRENT_STEAM_BUILD_ID}."), + }; + } + if COMPATIBLE_BUILD_SHA256.contains(&normalized.as_str()) { + return Integrity { + trusted: true, + status: "trusted", + profile: COMPATIBLE_EXE_ID, + message: format!("Executable matches compatible BuildID {COMPATIBLE_STEAM_BUILD_ID}."), + }; + } + if enhanced_active || enhanced_hash_known { + return Integrity { + trusted: true, + status: "enhanced", + profile: ENHANCED_EXE_ID, + message: "Executable is managed by BO3 Enhanced.".into(), + }; + } + let short_hash = if normalized.is_empty() { + "unreadable" + } else { + &normalized[..normalized.len().min(12)] + }; + Integrity { + trusted: false, + status: "unverified", + profile: "", + message: format!( + "Active executable hash {short_hash} does not match latest BuildID {CURRENT_STEAM_BUILD_ID}, compatible BuildID {COMPATIBLE_STEAM_BUILD_ID}, or a PatchOpsIII-preserved Enhanced EXE." + ), + } +} + +fn active_integrity(state: &AppState, game_dir: &Path, target: &Path) -> Integrity { + let hash = fs_ops::sha256_file(target) + .unwrap_or_default() + .to_ascii_lowercase(); + let settings = state.load_settings(); + integrity_for_hash( + &hash, + target.is_file(), + crate::enhanced::detect_install(game_dir), + known_enhanced_hashes(&settings, Some(game_dir)).contains(&hash), + ) +} + +fn inactive_build_path(target: &Path, build_id: &str) -> PathBuf { + target.with_file_name(format!( + "{}.{}.bak", + target + .file_stem() + .and_then(|name| name.to_str()) + .unwrap_or("BlackOps3"), + build_id + )) +} + +fn enhanced_backup_path(target: &Path) -> PathBuf { + target.with_file_name(format!( + "{}.enhanced.bak", + target + .file_stem() + .and_then(|name| name.to_str()) + .unwrap_or("BlackOps3") + )) +} + +fn sorted_matching_files(parent: &Path, prefix: &str, suffix: &str) -> Vec { + let mut matches: Vec<_> = fs::read_dir(parent) + .into_iter() + .flatten() + .flatten() + .filter_map(|entry| { + let path = entry.path(); + let name = path.file_name()?.to_str()?; + (path.is_file() && name.starts_with(prefix) && name.ends_with(suffix)).then_some(path) + }) + .collect(); + matches.sort_by_key(|path| { + std::cmp::Reverse( + path.metadata() + .and_then(|metadata| metadata.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH), + ) + }); + matches +} + +fn build_backup_candidates(target: &Path, build_id: &str, role: &str) -> Vec { + let stem = target + .file_stem() + .and_then(|name| name.to_str()) + .unwrap_or("BlackOps3"); + let extension = target + .extension() + .and_then(|name| name.to_str()) + .unwrap_or(""); + let exact = inactive_build_path(target, build_id); + let old_exact = target.with_file_name(format!("{stem}.{role}-{build_id}.{extension}")); + let mut candidates = Vec::new(); + if exact.is_file() { + candidates.push(exact); + } + if old_exact.is_file() { + candidates.push(old_exact); + } + candidates.extend(sorted_matching_files( + target.parent().unwrap_or_else(|| Path::new(".")), + &format!("{stem}.{role}-{build_id}-"), + &format!(".{extension}"), + )); + candidates +} + +fn first_matching_hash( + candidates: Vec, + hashes: &[&str], + use_cache: bool, +) -> Option { + candidates.into_iter().find(|candidate| { + hash_for_probe(candidate, use_cache) + .map(|hash| { + hashes + .iter() + .any(|expected| hash.eq_ignore_ascii_case(expected)) + }) + .unwrap_or(false) + }) +} + +fn validated_preserved_compatible(target: &Path, use_cache: bool) -> Option { + first_matching_hash( + build_backup_candidates(target, COMPATIBLE_STEAM_BUILD_ID, "compatible"), + COMPATIBLE_BUILD_SHA256, + use_cache, + ) +} + +fn validated_latest_backup(target: &Path, use_cache: bool) -> Option { + first_matching_hash( + build_backup_candidates(target, CURRENT_STEAM_BUILD_ID, "current"), + &[DEFAULT_STEAM_EXE_SHA256], + use_cache, + ) + .or_else(|| { + fs_ops::existing_backup(target).filter(|candidate| { + hash_for_probe(candidate, use_cache) + .map(|hash| hash.eq_ignore_ascii_case(DEFAULT_STEAM_EXE_SHA256)) + .unwrap_or(false) + }) + }) +} + +fn validated_enhanced_backup( + settings: &Settings, + game_dir: Option<&Path>, + target: &Path, + use_cache: bool, +) -> Option { + let known = known_enhanced_hashes(settings, game_dir); + if known.is_empty() { + return None; + } + let stem = target + .file_stem() + .and_then(|name| name.to_str()) + .unwrap_or("BlackOps3"); + let exact = enhanced_backup_path(target); + let mut candidates = if exact.is_file() { + vec![exact] + } else { + Vec::new() + }; + candidates.extend(sorted_matching_files( + target.parent().unwrap_or_else(|| Path::new(".")), + &format!("{stem}.enhanced-"), + ".bak", + )); + candidates.into_iter().find(|candidate| { + hash_for_probe(candidate, use_cache) + .map(|hash| known.contains(&hash.to_ascii_lowercase())) + .unwrap_or(false) + }) +} + +pub fn status(settings: &Settings, game_dir: Option<&Path>) -> ExeSwapState { + let executable = game_dir.and_then(find_executable); + let exe_hash = executable + .as_deref() + .and_then(|path| active_exe_hash(path).ok()) + .unwrap_or_default() + .to_ascii_lowercase(); + let enhanced_active = game_dir.is_some_and(crate::enhanced::detect_install); + let integrity = integrity_for_hash( + &exe_hash, + executable.is_some(), + enhanced_active, + known_enhanced_hashes(settings, game_dir).contains(&exe_hash), + ); + let mut profile = if integrity.profile.is_empty() { + game_dir.and_then(read_variant).unwrap_or_default() + } else { + integrity.profile.to_owned() + }; + let (active_build_id, active_build_date) = if executable.is_none() { + ("Unknown", "") + } else if profile == ENHANCED_EXE_ID { + ("Enhanced", "") + } else if profile == COMPATIBLE_EXE_ID { + (COMPATIBLE_STEAM_BUILD_ID, COMPATIBLE_STEAM_BUILD_DATE) + } else if profile == CURRENT_EXE_ID || profile == "default" { + profile = CURRENT_EXE_ID.into(); + (CURRENT_STEAM_BUILD_ID, CURRENT_STEAM_BUILD_DATE) + } else { + ("Unverified", "") + }; + + let latest_available = executable + .as_deref() + .is_some_and(|path| validated_latest_backup(path, true).is_some()); + let compatible_available = executable + .as_deref() + .is_some_and(|path| validated_preserved_compatible(path, true).is_some()); + let enhanced_available = executable + .as_deref() + .is_some_and(|path| validated_enhanced_backup(settings, game_dir, path, true).is_some()); + let patch_label = if profile == COMPATIBLE_EXE_ID { + "T7 Patch 2.04" + } else { + "T7 Patch Scroptss/T7Patch" + }; + let mode_label = match profile.as_str() { + COMPATIBLE_EXE_ID => "Compatible EXE", + CURRENT_EXE_ID => "Current EXE", + ENHANCED_EXE_ID => "Enhanced", + _ => "Unknown", + }; + + ExeSwapState { + profile: profile.clone(), + mode_label: mode_label.into(), + patch_label: patch_label.into(), + display_label: format!("{} T7 installs will use {patch_label}.", integrity.message), + state: integrity.status.into(), + active_build_id: active_build_id.into(), + active_build_date: active_build_date.into(), + current_build_id: CURRENT_STEAM_BUILD_ID.into(), + current_build_date: CURRENT_STEAM_BUILD_DATE.into(), + compatible_build_id: COMPATIBLE_STEAM_BUILD_ID.into(), + compatible_build_date: COMPATIBLE_STEAM_BUILD_DATE.into(), + enhanced_build_id: "Enhanced".into(), + enhanced_build_date: String::new(), + executable: executable + .as_deref() + .map(|path| path.display().to_string()) + .unwrap_or_default(), + executable_name: executable + .as_deref() + .and_then(Path::file_name) + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_default(), + executable_hash: exe_hash, + trusted_executable: integrity.trusted, + integrity_status: integrity.status.into(), + integrity_message: integrity.message, + backup_available: latest_available, + latest_available, + compatible_available, + enhanced_available: enhanced_available || profile == ENHANCED_EXE_ID, + compatible_active: profile == COMPATIBLE_EXE_ID, + enhanced_exe_active: profile == ENHANCED_EXE_ID, + enhanced_active, + } +} + +fn available_sibling_path(path: &Path) -> Result { + if !path.exists() { + return Ok(path.to_owned()); + } + let stem = path + .file_stem() + .and_then(|name| name.to_str()) + .ok_or_else(|| "Invalid executable backup filename.".to_string())?; + let extension = path + .extension() + .and_then(|name| name.to_str()) + .unwrap_or(""); + for index in 2..1000 { + let candidate = path.with_file_name(if extension.is_empty() { + format!("{stem}-{index}") + } else { + format!("{stem}-{index}.{extension}") + }); + if !candidate.exists() { + return Ok(candidate); + } + } + Err(format!( + "No available filename found near {}", + path.display() + )) +} + +fn unique_stage_path(target: &Path) -> Result { + let name = target + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| "Invalid executable filename.".to_string())?; + for index in 1..1000 { + let candidate = target.with_file_name(format!( + ".{name}.patchops-stage-{}-{index}", + std::process::id() + )); + if !candidate.exists() { + return Ok(candidate); + } + } + Err("Could not allocate an executable staging path.".into()) +} + +fn activate_source( + target: &Path, + source: &Path, + expected_hashes: &[&str], + preserved_active: &Path, + consume_source: bool, +) -> Result<(), String> { + let source_hash = fs_ops::sha256_file(source)?; + if !expected_hashes + .iter() + .any(|expected| source_hash.eq_ignore_ascii_case(expected)) + { + return Err(format!( + "Refusing executable with untrusted SHA-256: {}", + source.display() + )); + } + + let stage = unique_stage_path(target)?; + fs::copy(source, &stage) + .map_err(|error| format!("Failed to stage {}: {error}", source.display()))?; + let staged_hash = fs_ops::sha256_file(&stage); + if !staged_hash.is_ok_and(|hash| hash.eq_ignore_ascii_case(&source_hash)) { + let _ = fs::remove_file(&stage); + return Err("Staged executable failed SHA-256 verification.".into()); + } + + let mut active_preserved = false; + let result = (|| { + if target.exists() { + fs::rename(target, preserved_active) + .map_err(|error| format!("Failed to preserve active executable: {error}"))?; + active_preserved = true; + } + fs::rename(&stage, target) + .map_err(|error| format!("Failed to activate staged executable: {error}"))?; + let activated_hash = fs_ops::sha256_file(target)?; + if !activated_hash.eq_ignore_ascii_case(&source_hash) + || !expected_hashes + .iter() + .any(|expected| activated_hash.eq_ignore_ascii_case(expected)) + { + return Err("Activated executable failed SHA-256 verification.".into()); + } + Ok(()) + })(); + + if let Err(error) = result { + let _ = fs::remove_file(&stage); + let _ = fs::remove_file(target); + let rollback = if active_preserved { + fs::rename(preserved_active, target).map_err(|rollback| rollback.to_string()) + } else { + Ok(()) + }; + return match rollback { + Ok(()) => Err(error), + Err(rollback) => Err(format!("{error} Rollback also failed: {rollback}")), + }; + } + + if consume_source { + let _ = fs::remove_file(source); + } + Ok(()) +} + +fn preserved_path_for_profile(target: &Path, profile: &str) -> Result { + let preferred = match profile { + CURRENT_EXE_ID => inactive_build_path(target, CURRENT_STEAM_BUILD_ID), + COMPATIBLE_EXE_ID => inactive_build_path(target, COMPATIBLE_STEAM_BUILD_ID), + ENHANCED_EXE_ID => enhanced_backup_path(target), + _ => return Err("Active executable is not a recognized PatchOpsIII build.".into()), + }; + available_sibling_path(&preferred) +} + +fn prepare_activation( + state: &AppState, + game_dir: &Path, + target: &Path, + desired_profile: &str, +) -> Result, String> { + if !target.is_file() { + return Ok(Some(available_sibling_path( + &target.with_file_name(".patchops-missing-exe.rollback"), + )?)); + } + let integrity = active_integrity(state, game_dir, target); + if integrity.profile == desired_profile { + return Ok(None); + } + if !integrity.trusted || integrity.profile.is_empty() { + return Err(integrity.message); + } + if integrity.profile == ENHANCED_EXE_ID { + let hash = fs_ops::sha256_file(target)?; + record_enhanced_hash(state, game_dir, &hash)?; + } + preserved_path_for_profile(target, integrity.profile).map(Some) +} + +fn copy_depot_installscript(state: &AppState, depot: &Path, game_dir: &Path) { + let source = depot.join("installscript_311210.vdf"); + if !source.is_file() { + return; + } + let target = game_dir.join("installscript_311210.vdf"); + let backup = fs_ops::backup_path(&target); + let result = (|| { + if target.is_file() && !backup.exists() { + fs::copy(&target, &backup).map_err(|error| error.to_string())?; + } + let body = fs::read(&source).map_err(|error| error.to_string())?; + fs_ops::atomic_write(&target, &body) + })(); + if let Err(error) = result { + state.log("Warning", format!("Compatible executable is active, but installscript_311210.vdf could not be updated: {error}")); + } +} + +fn restore_installscript_backup(state: &AppState, game_dir: &Path) { + let target = game_dir.join("installscript_311210.vdf"); + let backup = fs_ops::backup_path(&target); + if !backup.is_file() { + return; + } + let result = fs::read(&backup) + .and_then(|body| fs_ops::atomic_write(&target, &body).map_err(std::io::Error::other)); + if let Err(error) = result { + state.log("Warning", format!("Current executable is active, but installscript_311210.vdf could not be restored: {error}")); + } +} + +fn remember_variant(state: &AppState, game_dir: &Path, variant: &str) { + if let Err(error) = write_variant(game_dir, variant) { + state.log( + "Warning", + format!("Executable was activated, but its variant marker could not be saved: {error}"), + ); + } +} + +fn activate_compatible_from_depot( + state: &AppState, + game_dir: &Path, + depot: Option, +) -> Result<(), String> { + let target = find_executable(game_dir) + .ok_or_else(|| "BlackOps3.exe or BlackOpsIII.exe was not found.".to_string())?; + let Some(preserved_active) = prepare_activation(state, game_dir, &target, COMPATIBLE_EXE_ID)? + else { + state.log("Info", "Compatible executable is already active."); + return Ok(()); + }; + + if let Some(source) = validated_preserved_compatible(&target, false) { + activate_source( + &target, + &source, + COMPATIBLE_BUILD_SHA256, + &preserved_active, + true, + )?; + remember_variant(state, game_dir, COMPATIBLE_EXE_ID); + state.log( + "Success", + format!( + "Reused preserved compatible executable from {}.", + source.file_name().unwrap_or_default().to_string_lossy() + ), + ); + return Ok(()); + } + + let depot = depot.ok_or_else(|| COMPATIBLE_DEPOT_REQUIRED_MESSAGE.to_string())?; + activate_source( + &target, + &depot.join("BlackOps3.exe"), + COMPATIBLE_BUILD_SHA256, + &preserved_active, + false, + )?; + copy_depot_installscript(state, &depot, game_dir); + remember_variant(state, game_dir, COMPATIBLE_EXE_ID); + state.log( + "Success", + format!("Compatible executable installed from Steam depot {COMPATIBLE_DEPOT_MANIFEST_ID}."), + ); + Ok(()) +} + +pub fn activate_current(state: &AppState, game_dir: &Path) -> Result<(), String> { + if !game_dir.is_dir() { + return Err("Game directory is not set.".into()); + } + let target = find_executable(game_dir).unwrap_or_else(|| game_dir.join("BlackOps3.exe")); + let Some(preserved_active) = prepare_activation(state, game_dir, &target, CURRENT_EXE_ID)? + else { + state.log("Info", "Latest executable is already active."); + return Ok(()); + }; + let source = validated_latest_backup(&target, false) + .ok_or_else(|| "No backup executable found. Cannot restore the current EXE.".to_string())?; + activate_source( + &target, + &source, + &[DEFAULT_STEAM_EXE_SHA256], + &preserved_active, + true, + )?; + restore_installscript_backup(state, game_dir); + remember_variant(state, game_dir, "default"); + state.log("Success", "Current executable restored from backup."); + Ok(()) +} + +pub fn activate_enhanced(state: &AppState, game_dir: &Path) -> Result<(), String> { + if !game_dir.is_dir() { + return Err("Game directory is not set.".into()); + } + let target = find_executable(game_dir).unwrap_or_else(|| game_dir.join("BlackOps3.exe")); + let Some(preserved_active) = prepare_activation(state, game_dir, &target, ENHANCED_EXE_ID)? + else { + state.log("Info", "Enhanced executable is already active."); + return Ok(()); + }; + let settings = state.load_settings(); + let source = validated_enhanced_backup(&settings, Some(game_dir), &target, false) + .ok_or_else(|| "No PatchOpsIII-preserved Enhanced executable was found.".to_string())?; + let hash = fs_ops::sha256_file(&source)?; + let expected = [hash.as_str()]; + activate_source(&target, &source, &expected, &preserved_active, true)?; + record_enhanced_hash(state, game_dir, &hash)?; + remember_variant(state, game_dir, ENHANCED_EXE_ID); + state.log( + "Success", + "Enhanced executable restored from PatchOpsIII backup.", + ); + Ok(()) +} + +pub fn compatible_depot_available() -> bool { + steam::compatible_depot_download_exists() +} + +pub fn activate_compatible(state: &AppState, game_dir: &Path) -> Result<(), String> { + activate_compatible_from_depot(state, game_dir, steam::find_valid_compatible_depot()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static NEXT: AtomicUsize = AtomicUsize::new(0); + + fn temp_dir(label: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "patchops-exe-{label}-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + path + } + + #[test] + fn classifies_exact_supported_hashes() { + let current = integrity_for_hash(DEFAULT_STEAM_EXE_SHA256, true, false, false); + assert!(current.trusted); + assert_eq!(current.profile, CURRENT_EXE_ID); + + let compatible = integrity_for_hash(COMPATIBLE_BUILD_SHA256[0], true, false, false); + assert!(compatible.trusted); + assert_eq!(compatible.profile, COMPATIBLE_EXE_ID); + + let unknown = integrity_for_hash(&"a".repeat(64), true, false, false); + assert!(!unknown.trusted); + assert_eq!(unknown.status, "unverified"); + } + + #[test] + fn recognizes_september_2026_steam_build() { + assert_eq!( + DEFAULT_STEAM_EXE_SHA256, + "51ca63bbc660e0826943c60da67606f6bcb4b3b519528b5e0548c68c9423a323" + ); + assert_eq!(CURRENT_STEAM_BUILD_ID, "24784313"); + assert_eq!(CURRENT_STEAM_BUILD_DATE, "Sep 10, 2026"); + } + + #[test] + fn active_executable_hash_cache_invalidates_when_file_changes() { + let root = temp_dir("hash-cache"); + let executable = root.join("BlackOps3.exe"); + fs::write(&executable, b"one").unwrap(); + let first = active_exe_hash(&executable).unwrap(); + assert_eq!(active_exe_hash(&executable).unwrap(), first); + fs::write(&executable, b"a longer executable").unwrap(); + assert_ne!(active_exe_hash(&executable).unwrap(), first); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn staged_activation_verifies_before_touching_active_exe() { + let root = temp_dir("staged"); + let target = root.join("BlackOps3.exe"); + let source = root.join("source.exe"); + let preserved = root.join("BlackOps3.21201493.bak"); + fs::write(&target, b"current").unwrap(); + fs::write(&source, b"compatible").unwrap(); + let expected = fs_ops::sha256_file(&source).unwrap(); + + activate_source(&target, &source, &[&expected], &preserved, false).unwrap(); + assert_eq!(fs::read(&target).unwrap(), b"compatible"); + assert_eq!(fs::read(&preserved).unwrap(), b"current"); + + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn hash_mismatch_leaves_active_exe_untouched() { + let root = temp_dir("mismatch"); + let target = root.join("BlackOps3.exe"); + let source = root.join("source.exe"); + let preserved = root.join("BlackOps3.21201493.bak"); + fs::write(&target, b"current").unwrap(); + fs::write(&source, b"tampered").unwrap(); + + assert!(activate_source(&target, &source, &[&"0".repeat(64)], &preserved, false).is_err()); + assert_eq!(fs::read(&target).unwrap(), b"current"); + assert!(!preserved.exists()); + + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/native/core/src/fs_ops.rs b/native/core/src/fs_ops.rs new file mode 100644 index 0000000..95fe9b3 --- /dev/null +++ b/native/core/src/fs_ops.rs @@ -0,0 +1,655 @@ +use std::{ + collections::HashSet, + fs::{self, File, OpenOptions}, + io::{Read, Write}, + path::{Component, Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; + +use flate2::read::GzDecoder; +use sha2::{Digest, Sha256}; + +pub const PATCHOPS_BACKUP_SUFFIX: &str = ".patchops.bak"; +pub const LEGACY_BACKUP_SUFFIX: &str = ".bak"; +const MAX_ARCHIVE_BYTES: u64 = 16 * 1024 * 1024 * 1024; +const MAX_ARCHIVE_ENTRIES: usize = 100_000; +static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); + +pub(crate) struct Snapshot { + entries: Vec<(PathBuf, Option)>, +} + +pub(crate) fn remove_file_if_present(path: &Path) -> Result<(), String> { + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => Err(format!( + "refusing to remove non-file path {}", + path.display() + )), + Ok(_) => fs::remove_file(path).map_err(|error| error.to_string()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error.to_string()), + } +} + +impl Snapshot { + pub(crate) fn capture(targets: &[PathBuf], directory: &Path) -> Result { + fs::create_dir_all(directory).map_err(|error| error.to_string())?; + let mut seen = HashSet::new(); + let mut entries = Vec::new(); + for target in targets { + if !seen.insert(target.clone()) { + continue; + } + let backup = match fs::symlink_metadata(target) { + Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => { + let backup = directory.join(entries.len().to_string()); + fs::copy(target, &backup).map_err(|error| error.to_string())?; + Some(backup) + } + Ok(_) => return Err(format!("refusing to replace {}", target.display())), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(error.to_string()), + }; + entries.push((target.clone(), backup)); + } + Ok(Self { entries }) + } + + pub(crate) fn restore(&self) -> Result<(), String> { + let mut errors = Vec::new(); + for (target, backup) in self.entries.iter().rev() { + if let Err(error) = remove_file_if_present(target) { + errors.push(error); + continue; + } + if let Some(backup) = backup { + if let Some(parent) = target.parent() + && let Err(error) = fs::create_dir_all(parent) + { + errors.push(format!("failed to recreate {}: {error}", parent.display())); + continue; + } + if let Err(error) = fs::copy(backup, target) { + errors.push(format!("failed to restore {}: {error}", target.display())); + } + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join("; ")) + } + } +} + +pub fn sha256_file(path: &Path) -> Result { + let mut file = File::open(path).map_err(|error| format!("{}: {error}", path.display()))?; + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 1024 * 1024]; + loop { + let read = file.read(&mut buffer).map_err(|error| error.to_string())?; + if read == 0 { + break; + } + digest.update(&buffer[..read]); + } + Ok(format!("{:x}", digest.finalize())) +} + +pub fn atomic_write(path: &Path, contents: &[u8]) -> Result<(), String> { + let parent = path + .parent() + .ok_or_else(|| format!("{} has no parent directory", path.display()))?; + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + let name = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| "invalid output filename".to_string())?; + let (temporary, mut file) = (0..100) + .find_map(|_| { + let id = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); + let temporary = + parent.join(format!(".{name}.patchops-{}-{id}.tmp", std::process::id())); + match OpenOptions::new() + .create_new(true) + .write(true) + .open(&temporary) + { + Ok(file) => Some(Ok((temporary, file))), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => None, + Err(error) => Some(Err(error.to_string())), + } + }) + .unwrap_or_else(|| Err("could not allocate a unique temporary file".into()))?; + let permissions = path.metadata().ok().map(|metadata| metadata.permissions()); + let write_result = permissions + .map_or(Ok(()), |permissions| file.set_permissions(permissions)) + .and_then(|_| file.write_all(contents)) + .and_then(|_| file.sync_all()) + .map_err(|error| error.to_string()); + drop(file); + if let Err(error) = write_result { + let _ = fs::remove_file(&temporary); + return Err(error); + } + if let Err(error) = replace_path(&temporary, path) { + let _ = fs::remove_file(&temporary); + return Err(error); + } + Ok(()) +} + +#[cfg(not(windows))] +fn replace_path(source: &Path, destination: &Path) -> Result<(), String> { + fs::rename(source, destination).map_err(|error| error.to_string()) +} + +#[cfg(windows)] +fn replace_path(source: &Path, destination: &Path) -> Result<(), String> { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Storage::FileSystem::{ + MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW, + }; + + let source: Vec = source.as_os_str().encode_wide().chain(Some(0)).collect(); + let destination: Vec = destination + .as_os_str() + .encode_wide() + .chain(Some(0)) + .collect(); + let ok = unsafe { + MoveFileExW( + source.as_ptr(), + destination.as_ptr(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, + ) + }; + if ok == 0 { + Err(std::io::Error::last_os_error().to_string()) + } else { + Ok(()) + } +} + +pub fn backup_path(path: &Path) -> PathBuf { + PathBuf::from(format!("{}{}", path.display(), PATCHOPS_BACKUP_SUFFIX)) +} + +pub fn legacy_backup_path(path: &Path) -> PathBuf { + PathBuf::from(format!("{}{}", path.display(), LEGACY_BACKUP_SUFFIX)) +} + +pub fn existing_backup(path: &Path) -> Option { + [backup_path(path), legacy_backup_path(path)] + .into_iter() + .find(|candidate| candidate.is_file()) +} + +pub fn download( + state: &crate::AppState, + url: &str, + destination: &Path, + expected_sha256: Option<&str>, + max_bytes: u64, +) -> Result { + state.progress("download", None, "Connecting to download server"); + let response = reqwest::blocking::Client::builder() + .connect_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(180)) + .build() + .map_err(|error| error.to_string())? + .get(url) + .header(reqwest::header::USER_AGENT, "PatchOpsIII") + .send() + .and_then(reqwest::blocking::Response::error_for_status) + .map_err(|error| error.to_string())?; + if response + .content_length() + .is_some_and(|length| length > max_bytes) + { + return Err("download is larger than the allowed size".into()); + } + + let parent = destination + .parent() + .ok_or_else(|| "download destination has no parent".to_string())?; + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + let part = PathBuf::from(format!("{}.part", destination.display())); + let mut output = File::create(&part).map_err(|error| error.to_string())?; + let length = response.content_length().filter(|length| *length > 0); + let mut input = response; + let mut last_progress = std::time::Instant::now(); + let mut digest = Sha256::new(); + let mut total = 0_u64; + let mut buffer = [0_u8; 128 * 1024]; + let result = (|| { + loop { + let read = input.read(&mut buffer).map_err(|error| error.to_string())?; + if read == 0 { + break; + } + total += read as u64; + if last_progress.elapsed() >= Duration::from_millis(100) { + state.progress( + "download", + length.map(|length| (total as f32 / length as f32).min(1.0)), + &format!("Downloaded {total} bytes"), + ); + last_progress = std::time::Instant::now(); + } + if total > max_bytes { + return Err("download is larger than the allowed size".into()); + } + digest.update(&buffer[..read]); + output + .write_all(&buffer[..read]) + .map_err(|error| error.to_string())?; + } + output.sync_all().map_err(|error| error.to_string())?; + state.progress("verify", None, "Verifying download SHA-256"); + let actual = format!("{:x}", digest.finalize()); + if expected_sha256.is_some_and(|expected| !actual.eq_ignore_ascii_case(expected)) { + return Err("download failed SHA-256 verification".into()); + } + replace_path(&part, destination)?; + state.progress("download", Some(1.0), "Download verified"); + Ok(actual) + })(); + if result.is_err() { + let _ = fs::remove_file(part); + } + result +} + +fn safe_relative(path: &Path) -> bool { + path.components() + .all(|component| matches!(component, Component::Normal(_) | Component::CurDir)) +} + +fn resolved_link_target(base: &Path, target: &Path) -> Option { + let mut parts = base + .components() + .filter_map(|component| match component { + Component::Normal(part) => Some(part.to_owned()), + Component::CurDir => None, + _ => None, + }) + .collect::>(); + for component in target.components() { + match component { + Component::CurDir => {} + Component::Normal(part) => parts.push(part.to_owned()), + Component::ParentDir => { + parts.pop()?; + } + Component::RootDir | Component::Prefix(_) => return None, + } + } + Some(parts.into_iter().collect()) +} + +fn prepare_empty_directory(destination: &Path) -> Result<(), String> { + match fs::symlink_metadata(destination) { + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => { + return Err("archive destination is not a regular directory".into()); + } + Ok(_) => { + if fs::read_dir(destination) + .map_err(|error| error.to_string())? + .next() + .is_some() + { + return Err("archive destination must be empty".into()); + } + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + fs::create_dir_all(destination).map_err(|error| error.to_string())?; + } + Err(error) => return Err(error.to_string()), + } + Ok(()) +} + +pub fn extract_zip(archive: &Path, destination: &Path) -> Result<(), String> { + prepare_empty_directory(destination)?; + let file = File::open(archive).map_err(|error| error.to_string())?; + let mut zip = zip::ZipArchive::new(file).map_err(|error| error.to_string())?; + if zip.len() > MAX_ARCHIVE_ENTRIES { + return Err("archive contains too many entries".into()); + } + let mut total = 0_u64; + let mut seen = HashSet::new(); + for index in 0..zip.len() { + let mut entry = zip.by_index(index).map_err(|error| error.to_string())?; + let relative = entry + .enclosed_name() + .ok_or_else(|| "archive contains an unsafe path".to_string())? + .to_path_buf(); + if !safe_relative(&relative) || !seen.insert(relative.clone()) { + return Err("archive contains an unsafe or duplicate path".into()); + } + if entry + .unix_mode() + .is_some_and(|mode| mode & 0o170000 == 0o120000) + { + return Err("archive links are not allowed".into()); + } + total = total.saturating_add(entry.size()); + if total > MAX_ARCHIVE_BYTES { + return Err("archive expands beyond the allowed size".into()); + } + let output = destination.join(relative); + if entry.is_dir() { + fs::create_dir_all(&output).map_err(|error| error.to_string())?; + continue; + } + if let Some(parent) = output.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + let mut file = File::create(output).map_err(|error| error.to_string())?; + std::io::copy(&mut entry, &mut file).map_err(|error| error.to_string())?; + } + Ok(()) +} + +pub fn extract_tar_gz(archive: &Path, destination: &Path) -> Result<(), String> { + prepare_empty_directory(destination)?; + let decoder = GzDecoder::new(File::open(archive).map_err(|error| error.to_string())?); + let mut tar = tar::Archive::new(decoder); + let mut count = 0_usize; + let mut total = 0_u64; + let mut seen = HashSet::new(); + let mut hard_links = Vec::new(); + for item in tar.entries().map_err(|error| error.to_string())? { + let mut entry = item.map_err(|error| error.to_string())?; + count += 1; + if count > MAX_ARCHIVE_ENTRIES { + return Err("archive contains too many entries".into()); + } + let relative = entry + .path() + .map_err(|error| error.to_string())? + .into_owned(); + if !safe_relative(&relative) || !seen.insert(relative.clone()) { + return Err("archive contains an unsafe or duplicate path".into()); + } + let kind = entry.header().entry_type(); + total = total.saturating_add(entry.size()); + if total > MAX_ARCHIVE_BYTES { + return Err("archive expands beyond the allowed size".into()); + } + let output = destination.join(&relative); + if kind.is_file() || kind.is_dir() { + entry.unpack(&output).map_err(|error| error.to_string())?; + continue; + } + if !kind.is_symlink() && !kind.is_hard_link() { + return Err("archive contains a special file".into()); + } + let target = entry + .link_name() + .map_err(|error| error.to_string())? + .ok_or_else(|| "archive link has no target".to_string())? + .into_owned(); + let base = if kind.is_symlink() { + relative.parent().unwrap_or_else(|| Path::new("")) + } else { + Path::new("") + }; + let resolved = resolved_link_target(base, &target) + .ok_or_else(|| "archive link escapes the destination".to_string())?; + if let Some(parent) = output.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + if kind.is_hard_link() { + hard_links.push((destination.join(resolved), output)); + continue; + } + #[cfg(unix)] + std::os::unix::fs::symlink(&target, &output).map_err(|error| error.to_string())?; + #[cfg(not(unix))] + return Err("archive symbolic links are not supported on this platform".into()); + } + for (target, output) in hard_links { + if !target.is_file() { + return Err("archive hard link target is missing or is not a file".into()); + } + fs::hard_link(target, output).map_err(|error| error.to_string())?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions}; + + struct TestDirectory(PathBuf); + + impl TestDirectory { + fn new(label: &str) -> Self { + let path = std::env::temp_dir().join(format!( + "patchops-{label}-{}-{}", + std::process::id(), + TEMP_COUNTER.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } + + fn path(&self) -> &Path { + &self.0 + } + } + + impl Drop for TestDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + fn write_zip(path: &Path, method: CompressionMethod, entries: &[(&str, &[u8])]) { + let mut writer = ZipWriter::new(File::create(path).unwrap()); + let options = SimpleFileOptions::default().compression_method(method); + for (name, contents) in entries { + writer.start_file(*name, options).unwrap(); + writer.write_all(contents).unwrap(); + } + writer.finish().unwrap(); + } + + #[test] + fn snapshot_restores_deleted_parents_and_removes_new_files() { + let root = TestDirectory::new("snapshot"); + let parent = root.path().join("game"); + fs::create_dir_all(&parent).unwrap(); + let original = parent.join("original.dll"); + let created = root.path().join("new.dll"); + fs::write(&original, b"original").unwrap(); + let snapshot = Snapshot::capture( + &[original.clone(), created.clone(), original.clone()], + &root.path().join("rollback"), + ) + .unwrap(); + assert_eq!(snapshot.entries.len(), 2); + fs::remove_dir_all(&parent).unwrap(); + fs::write(&created, b"new").unwrap(); + snapshot.restore().unwrap(); + assert_eq!(fs::read(&original).unwrap(), b"original"); + assert!(!created.exists()); + } + + #[cfg(unix)] + #[test] + fn atomic_write_preserves_existing_permissions() { + use std::os::unix::fs::PermissionsExt; + let root = + std::env::temp_dir().join(format!("patchops-permissions-{}", std::process::id())); + fs::create_dir_all(&root).unwrap(); + let path = root.join("settings.json"); + fs::write(&path, b"old").unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap(); + atomic_write(&path, b"new").unwrap(); + assert_eq!(fs::read(&path).unwrap(), b"new"); + assert_eq!(path.metadata().unwrap().permissions().mode() & 0o777, 0o600); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn hashes_known_bytes() { + let path = std::env::temp_dir().join(format!("patchops-hash-{}", std::process::id())); + fs::write(&path, b"patchops").unwrap(); + assert_eq!( + sha256_file(&path).unwrap(), + "3e0cec93c1876296af254daff5dba161649c003683320e5fefbaf1ad21e67f98" + ); + fs::remove_file(path).unwrap(); + } + + #[test] + fn rejects_parent_paths() { + assert!(!safe_relative(Path::new("../outside"))); + assert!(!safe_relative(Path::new("/outside"))); + assert!(safe_relative(Path::new("inside/file"))); + } + + #[test] + fn link_targets_must_resolve_inside_the_archive_root() { + assert_eq!( + resolved_link_target(Path::new("bin"), Path::new("../lib/tool")), + Some(PathBuf::from("lib/tool")) + ); + assert_eq!( + resolved_link_target(Path::new("bin"), Path::new("../../outside")), + None + ); + assert_eq!( + resolved_link_target(Path::new(""), Path::new("/outside")), + None + ); + } + + #[test] + fn extracts_each_enabled_zip_compression_method() { + let root = TestDirectory::new("zip-compression-methods"); + let payload = b"PatchOpsIII archive fixture\n".repeat(128); + for (label, method) in [ + ("stored", CompressionMethod::Stored), + ("deflate", CompressionMethod::Deflated), + ("bzip2", CompressionMethod::Bzip2), + ("zstd", CompressionMethod::Zstd), + ] { + let archive = root.path().join(format!("{label}.zip")); + let destination = root.path().join(format!("{label}-output")); + write_zip( + &archive, + method, + &[("nested/payload.txt", payload.as_slice())], + ); + { + let mut zip = zip::ZipArchive::new(File::open(&archive).unwrap()).unwrap(); + assert_eq!(zip.by_index(0).unwrap().compression(), method); + } + + extract_zip(&archive, &destination).unwrap(); + + assert_eq!( + fs::read(destination.join("nested/payload.txt")).unwrap(), + payload + ); + } + } + + #[test] + fn rejects_malformed_zip_without_writing_files() { + let root = TestDirectory::new("malformed-zip"); + let archive = root.path().join("malformed.zip"); + let destination = root.path().join("output"); + fs::write(&archive, b"this is not a ZIP archive").unwrap(); + + assert!(extract_zip(&archive, &destination).is_err()); + assert_eq!(fs::read_dir(destination).unwrap().count(), 0); + } + + #[test] + fn rejects_zip_path_traversal_and_allows_caller_cleanup() { + let root = TestDirectory::new("zip-traversal"); + let archive = root.path().join("traversal.zip"); + let destination = root.path().join("output"); + let entries: &[(&str, &[u8])] = &[ + ("safe.txt", b"written before the invalid entry"), + ("../outside.txt", b"must not escape"), + ]; + write_zip(&archive, CompressionMethod::Stored, entries); + + let error = extract_zip(&archive, &destination).unwrap_err(); + + assert!(error.contains("unsafe path"), "{error}"); + assert_eq!( + fs::read(destination.join("safe.txt")).unwrap(), + entries[0].1 + ); + assert!(!root.path().join("outside.txt").exists()); + fs::remove_dir_all(&destination).unwrap(); + assert!(!destination.exists()); + } + + #[test] + fn rejects_zip_symlinks() { + let root = TestDirectory::new("zip-symlink"); + let archive = root.path().join("symlink.zip"); + let destination = root.path().join("output"); + let mut writer = ZipWriter::new(File::create(&archive).unwrap()); + writer + .add_symlink("link", "../outside", SimpleFileOptions::default()) + .unwrap(); + writer.finish().unwrap(); + + let error = extract_zip(&archive, &destination).unwrap_err(); + + assert!(error.contains("links are not allowed"), "{error}"); + assert_eq!(fs::read_dir(destination).unwrap().count(), 0); + } + + #[test] + fn rejects_equivalent_duplicate_zip_paths_without_overwriting_partial_output() { + let root = TestDirectory::new("zip-duplicate"); + let archive = root.path().join("duplicate.zip"); + let destination = root.path().join("output"); + let entries: &[(&str, &[u8])] = &[ + ("nested/duplicate.txt", b"first entry"), + ("nested/./duplicate.txt", b"second entry"), + ]; + write_zip(&archive, CompressionMethod::Stored, entries); + + let error = extract_zip(&archive, &destination).unwrap_err(); + + assert!(error.contains("duplicate path"), "{error}"); + assert_eq!( + fs::read(destination.join("nested/duplicate.txt")).unwrap(), + b"first entry" + ); + } + + #[cfg(unix)] + #[test] + fn atomic_write_does_not_follow_a_predictable_temporary_symlink() { + let root = std::env::temp_dir().join(format!( + "patchops-atomic-{}-{}", + std::process::id(), + TEMP_COUNTER.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&root).unwrap(); + let target = root.join("settings.json"); + let victim = root.join("victim"); + fs::write(&victim, b"untouched").unwrap(); + std::os::unix::fs::symlink(&victim, root.join(".settings.json.patchops.tmp")).unwrap(); + + atomic_write(&target, b"new settings").unwrap(); + + assert_eq!(fs::read(&target).unwrap(), b"new settings"); + assert_eq!(fs::read(&victim).unwrap(), b"untouched"); + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/native/core/src/lib.rs b/native/core/src/lib.rs new file mode 100644 index 0000000..180eea0 --- /dev/null +++ b/native/core/src/lib.rs @@ -0,0 +1,14 @@ +//! Toolkit-independent PatchOpsIII operations and JSON-compatible dispatcher. + +mod app; +mod dxvk; +mod engine; +mod enhanced; +mod exe; +mod fs_ops; +pub mod models; +mod steam; +mod t7; + +pub use app::{AppState, EventCallback, ProgressCallback}; +pub use engine::Engine; diff --git a/native/core/src/models.rs b/native/core/src/models.rs new file mode 100644 index 0000000..e7f7685 --- /dev/null +++ b/native/core/src/models.rs @@ -0,0 +1,220 @@ +use serde::{Deserialize, Serialize}; + +/// Worker-thread progress. Fractions are stage-local; None means indeterminate. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct OperationProgress { + /// Legacy request path, e.g. `/api/t7-install`. + pub op: String, + /// `started`, `running`, `download`, `verify`, `completed`, or `failed`. + pub stage: String, + pub fraction: Option, + pub message: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LogEntry { + pub category: String, + pub message: String, + pub line: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LaunchProfile { + pub id: String, + pub label: String, + pub option: String, + pub active: bool, + pub installed: bool, + pub subscribed: bool, + pub state: String, + pub path: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct EnhancedState { + pub installed: bool, + pub detected_at: Option, + pub acknowledged_at: Option, + pub launch_options_active: bool, + pub dump_source: String, + pub files_installed: usize, + pub backup_status: String, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ExeSwapState { + pub profile: String, + pub mode_label: String, + pub patch_label: String, + pub display_label: String, + pub state: String, + pub active_build_id: String, + pub active_build_date: String, + pub current_build_id: String, + pub current_build_date: String, + pub compatible_build_id: String, + pub compatible_build_date: String, + pub enhanced_build_id: String, + pub enhanced_build_date: String, + pub executable: String, + pub executable_name: String, + pub executable_hash: String, + pub trusted_executable: bool, + pub integrity_status: String, + pub integrity_message: String, + pub backup_available: bool, + pub latest_available: bool, + pub compatible_available: bool, + pub enhanced_available: bool, + pub compatible_active: bool, + pub enhanced_exe_active: bool, + pub enhanced_active: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct T7State { + pub installed: bool, + pub conf_exists: bool, + pub gamertag: String, + pub plain_name: String, + pub color_code: String, + pub network_password: String, + pub friends_only: bool, + pub mode: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DxvkSettings { + pub enable_async: bool, + pub gpl_async_cache: bool, + pub num_compiler_threads: i32, + pub max_frame_rate: i32, + pub max_frame_latency: i32, + pub tear_free: String, + pub hud_enabled: bool, +} + +impl Default for DxvkSettings { + fn default() -> Self { + Self { + enable_async: true, + gpl_async_cache: true, + num_compiler_threads: 0, + max_frame_rate: 0, + max_frame_latency: 1, + tear_free: "True".into(), + hud_enabled: false, + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DxvkState { + pub installed: bool, + pub conf_exists: bool, + pub settings: DxvkSettings, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct QolState { + pub d3dcompiler: bool, + pub intro: bool, + pub all_intros: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct GraphicsState { + pub max_fps: i32, + pub fov: i32, + pub display_mode: i32, + pub resolution: String, + pub refresh_rate: f64, + pub render_resolution: i32, + pub vsync: bool, + pub draw_fps: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AdvancedState { + pub smooth_framerate: bool, + pub unlock_options: bool, + pub reduce_cpu: bool, + pub max_frame_latency: i32, + pub vram_limited: bool, + pub vram_target: i32, + pub config_readonly: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct MaintenanceState { + pub mod_files_dir: String, + pub log_payload: String, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ModsState { + pub t7_patch: bool, + pub dxvk: bool, + pub enhanced: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PatchOpsState { + pub app_version: String, + pub platform: String, + pub game_dir: Option, + pub game_detected: bool, + pub config_exists: bool, + pub steam_user_id: Option, + pub log_path: String, + pub presets: Vec, + pub current_launch_options: Option, + pub active_launch_profile: String, + pub release_channel: String, + pub launch_profiles: Vec, + pub enhanced: EnhancedState, + pub exe_swap: ExeSwapState, + pub t7: T7State, + pub dxvk: DxvkState, + pub qol: QolState, + pub graphics: GraphicsState, + pub advanced: AdvancedState, + pub maintenance: MaintenanceState, + pub mods: ModsState, + pub logs: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompatibleExeResult { + pub state: PatchOpsState, + pub depot_command: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DepotStatus { + pub available: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EnhancedValidation { + pub valid: bool, + pub message: String, + pub state: PatchOpsState, +} diff --git a/native/core/src/steam.rs b/native/core/src/steam.rs new file mode 100644 index 0000000..c55bd2f --- /dev/null +++ b/native/core/src/steam.rs @@ -0,0 +1,4315 @@ +use std::{ + cmp::Ordering, + collections::HashSet, + env, fs, + path::{Path, PathBuf}, + process::{Command, Stdio}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use crate::{app::AppState, fs_ops, models::LaunchProfile}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use serde_json::{Map as JsonMap, Value as JsonValue, json}; + +pub const APP_ID: &str = "311210"; +pub const COMPATIBLE_DEPOT_APP_ID: &str = APP_ID; +pub const COMPATIBLE_DEPOT_ID: &str = "311211"; +pub const ENHANCED_TOOL_NAME: &str = "BO3 Enhanced"; +pub const ENHANCED_LAUNCH_OPTIONS: &str = "WINEDLLOVERRIDES=\"WindowsCodecs=n,b\" %command%"; + +const GAME_DIRECTORY: &str = "Call of Duty Black Ops III"; +const GAME_EXECUTABLES: [&str; 2] = ["BlackOpsIII.exe", "BlackOps3.exe"]; +const ENHANCED_PROTON_TAG: &str = "release10-32"; +const ENHANCED_PROTON_ASSET: &str = "GDK-Proton10-32.tar.gz"; +const ENHANCED_PROTON_DOWNLOAD: &str = "https://github.com/Weather-OS/GDK-Proton/releases/download/release10-32/GDK-Proton10-32.tar.gz"; +const ENHANCED_PROTON_SHA256: &str = + "1e80f4e714f877f42101d5775bd38ca0a15a38d304e24af1f15c6deec4ebac2d"; +const MAX_PROTON_ARCHIVE_BYTES: u64 = 4 * 1024 * 1024 * 1024; +const COMPATIBLE_BUILD_SHA256: &str = + "66b95eb4667bd5b3b3d230e7bed1d29ccd261d48ca2699f01216c863be24ff44"; +const STEAM_ID64_OFFSET: u64 = 76_561_197_960_265_728; +const TOOL_OWNERSHIP_VERSION: u8 = 1; +const TOOL_MARKER_FILENAME: &str = ".patchops-owner.json"; + +#[derive(Clone, Copy)] +struct WorkshopProfile { + id: &'static str, + label: &'static str, + workshop_id: &'static str, + option: &'static str, +} + +const WORKSHOP_PROFILES: [WorkshopProfile; 3] = [ + WorkshopProfile { + id: "all_around", + label: "All-around Enhancement Lite", + workshop_id: "2994481309", + option: "+set fs_game 2994481309", + }, + WorkshopProfile { + id: "ultimate", + label: "Ultimate Experience Mod", + workshop_id: "2942053577", + option: "+set fs_game 2942053577", + }, + WorkshopProfile { + id: "reforged", + label: "BO3 Reforged", + workshop_id: "3667377161", + option: "+set fs_game 3667377161", + }, +]; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WorkshopState { + pub state: String, + pub installed: bool, + pub subscribed: bool, + pub path: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum VdfValue { + String(String), + Object(VdfObject), +} + +type VdfObject = Vec; + +#[derive(Clone, Debug, PartialEq, Eq)] +struct VdfEntry { + key: String, + value: VdfValue, + condition: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum Token { + Text(String), + Open, + Close, + Condition(String), +} + +fn lex_vdf(input: &str) -> Result, String> { + let chars: Vec = input.trim_start_matches('\u{feff}').chars().collect(); + let mut tokens = Vec::new(); + let mut index = 0; + while index < chars.len() { + if chars[index].is_whitespace() { + index += 1; + continue; + } + if chars[index] == '/' && chars.get(index + 1) == Some(&'/') { + index += 2; + while index < chars.len() && chars[index] != '\n' { + index += 1; + } + continue; + } + match chars[index] { + '{' => { + tokens.push(Token::Open); + index += 1; + } + '}' => { + tokens.push(Token::Close); + index += 1; + } + '[' => { + index += 1; + let start = index; + while index < chars.len() && chars[index] != ']' { + index += 1; + } + if index == chars.len() { + return Err("unterminated VDF condition".into()); + } + tokens.push(Token::Condition(chars[start..index].iter().collect())); + index += 1; + } + '"' => { + index += 1; + let mut value = String::new(); + let mut closed = false; + while index < chars.len() { + match chars[index] { + '"' => { + index += 1; + closed = true; + break; + } + '\\' if index + 1 < chars.len() => { + let escaped = chars[index + 1]; + match escaped { + '"' => value.push('"'), + '\\' => value.push('\\'), + 'n' => value.push('\n'), + 'r' => value.push('\r'), + 't' => value.push('\t'), + other => { + value.push('\\'); + value.push(other); + } + } + index += 2; + } + character => { + value.push(character); + index += 1; + } + } + } + if !closed { + return Err("unterminated quoted VDF value".into()); + } + tokens.push(Token::Text(value)); + } + _ => { + let start = index; + while index < chars.len() + && !chars[index].is_whitespace() + && !matches!(chars[index], '{' | '}' | '[' | '"') + && !(chars[index] == '/' && chars.get(index + 1) == Some(&'/')) + { + index += 1; + } + if start == index { + return Err("invalid VDF token".into()); + } + tokens.push(Token::Text(chars[start..index].iter().collect())); + } + } + } + Ok(tokens) +} + +fn parse_vdf(input: &str) -> Result { + fn entries(tokens: &[Token], index: &mut usize, nested: bool) -> Result { + let mut result = Vec::new(); + while *index < tokens.len() { + if tokens[*index] == Token::Close { + if !nested { + return Err("unexpected closing VDF brace".into()); + } + *index += 1; + return Ok(result); + } + let key = match tokens.get(*index) { + Some(Token::Text(value)) => value.clone(), + Some(_) => return Err("expected a VDF key".into()), + None => break, + }; + *index += 1; + let value = match tokens.get(*index) { + Some(Token::Text(value)) => { + *index += 1; + VdfValue::String(value.clone()) + } + Some(Token::Open) => { + *index += 1; + VdfValue::Object(entries(tokens, index, true)?) + } + _ => return Err(format!("missing VDF value for '{key}'")), + }; + let condition = match tokens.get(*index) { + Some(Token::Condition(value)) => { + *index += 1; + Some(value.clone()) + } + _ => None, + }; + result.push(VdfEntry { + key, + value, + condition, + }); + } + if nested { + Err("unterminated VDF object".into()) + } else { + Ok(result) + } + } + + let tokens = lex_vdf(input)?; + let mut index = 0; + let object = entries(&tokens, &mut index, false)?; + if index == tokens.len() { + Ok(object) + } else { + Err("trailing VDF tokens".into()) + } +} + +fn quote_vdf(value: &str) -> String { + let mut quoted = String::with_capacity(value.len() + 2); + quoted.push('"'); + for character in value.chars() { + match character { + '"' => quoted.push_str("\\\""), + '\\' => quoted.push_str("\\\\"), + '\n' => quoted.push_str("\\n"), + '\r' => quoted.push_str("\\r"), + '\t' => quoted.push_str("\\t"), + other => quoted.push(other), + } + } + quoted.push('"'); + quoted +} + +fn serialize_vdf(object: &VdfObject) -> String { + fn write_entries(object: &VdfObject, depth: usize, output: &mut String) { + let indent = "\t".repeat(depth); + for entry in object { + output.push_str(&indent); + output.push_str("e_vdf(&entry.key)); + match &entry.value { + VdfValue::String(value) => { + output.push('\t'); + output.push_str("e_vdf(value)); + if let Some(condition) = &entry.condition { + output.push_str("\t["); + output.push_str(condition); + output.push(']'); + } + output.push('\n'); + } + VdfValue::Object(children) => { + output.push('\n'); + output.push_str(&indent); + output.push_str("{\n"); + write_entries(children, depth + 1, output); + output.push_str(&indent); + output.push('}'); + if let Some(condition) = &entry.condition { + output.push_str("\t["); + output.push_str(condition); + output.push(']'); + } + output.push('\n'); + } + } + } + } + + let mut output = String::new(); + write_entries(object, 0, &mut output); + output +} + +fn entry_index(object: &VdfObject, key: &str) -> Option { + object + .iter() + .rposition(|entry| entry.key.eq_ignore_ascii_case(key)) +} + +fn value_at<'a>(object: &'a VdfObject, path: &[&str]) -> Option<&'a VdfValue> { + let (key, remaining) = path.split_first()?; + let value = &object.get(entry_index(object, key)?)?.value; + if remaining.is_empty() { + return Some(value); + } + match value { + VdfValue::Object(children) => value_at(children, remaining), + VdfValue::String(_) => None, + } +} + +fn ensure_object<'a>(object: &'a mut VdfObject, key: &str) -> Result<&'a mut VdfObject, String> { + let index = match entry_index(object, key) { + Some(index) => index, + None => { + object.push(VdfEntry { + key: key.into(), + value: VdfValue::Object(Vec::new()), + condition: None, + }); + object.len() - 1 + } + }; + match &mut object[index].value { + VdfValue::Object(children) => Ok(children), + VdfValue::String(_) => Err(format!("VDF key '{key}' is not an object")), + } +} + +fn ensure_path<'a>(object: &'a mut VdfObject, path: &[&str]) -> Result<&'a mut VdfObject, String> { + match path.split_first() { + Some((key, remaining)) => ensure_path(ensure_object(object, key)?, remaining), + None => Ok(object), + } +} + +fn set_string(object: &mut VdfObject, key: &str, value: String) -> Result<(), String> { + if let Some(index) = entry_index(object, key) { + match &mut object[index].value { + VdfValue::String(existing) => *existing = value, + VdfValue::Object(_) => return Err(format!("VDF key '{key}' is not a string")), + } + } else { + object.push(VdfEntry { + key: key.into(), + value: VdfValue::String(value), + condition: None, + }); + } + Ok(()) +} + +fn set_value(object: &mut VdfObject, key: &str, value: VdfValue) { + if let Some(index) = entry_index(object, key) { + object[index].value = value; + } else { + object.push(VdfEntry { + key: key.into(), + value, + condition: None, + }); + } +} + +fn read_vdf(path: &Path) -> Result { + let bytes = fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?; + let text = String::from_utf8(bytes) + .map_err(|error| format!("{} is not valid UTF-8: {error}", path.display()))?; + parse_vdf(&text).map_err(|error| format!("{}: {error}", path.display())) +} + +fn atomic_temp_path(path: &Path) -> Option { + Some( + path.parent()? + .join(format!(".{}.patchops.tmp", path.file_name()?.to_str()?)), + ) +} + +fn preserve_original( + path: &Path, + original: &[u8], + legacy_backup: Option<&Path>, +) -> Result<(), String> { + let backup = fs_ops::backup_path(path); + match fs::metadata(&backup) { + Ok(metadata) if metadata.is_file() => {} + Ok(_) => return Err(format!("{} is not a file", backup.display())), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + fs_ops::atomic_write(&backup, original)?; + } + Err(error) => return Err(format!("{}: {error}", backup.display())), + } + if let Some(legacy_backup) = legacy_backup { + fs_ops::atomic_write(legacy_backup, original)?; + } + Ok(()) +} + +fn update_vdf( + path: &Path, + legacy_backup: Option<&Path>, + change: impl FnOnce(&mut VdfObject) -> Result<(), String>, +) -> Result<(), String> { + let original = fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?; + let text = String::from_utf8(original.clone()) + .map_err(|error| format!("{} is not valid UTF-8: {error}", path.display()))?; + let mut document = parse_vdf(&text).map_err(|error| format!("{}: {error}", path.display()))?; + change(&mut document)?; + let updated = serialize_vdf(&document); + parse_vdf(&updated).map_err(|error| format!("refusing to write invalid VDF: {error}"))?; + if updated.as_bytes() == original { + return Ok(()); + } + + preserve_original(path, &original, legacy_backup)?; + if let Err(write_error) = fs_ops::atomic_write(path, updated.as_bytes()) { + if let Some(temporary) = atomic_temp_path(path) { + let _ = fs::remove_file(temporary); + } + return match fs_ops::atomic_write(path, &original) { + Ok(()) => Err(format!( + "failed to update {}: {write_error}; original restored", + path.display() + )), + Err(rollback_error) => Err(format!( + "failed to update {}: {write_error}; rollback also failed: {rollback_error}", + path.display() + )), + }; + } + Ok(()) +} + +fn write_new_or_update_vdf( + path: &Path, + change: impl FnOnce(&mut VdfObject) -> Result<(), String>, +) -> Result<(), String> { + if path.exists() { + return update_vdf(path, None, change); + } + let mut document = Vec::new(); + change(&mut document)?; + let updated = serialize_vdf(&document); + parse_vdf(&updated).map_err(|error| format!("refusing to write invalid VDF: {error}"))?; + fs_ops::atomic_write(path, updated.as_bytes()) +} + +fn home_dir() -> Option { + #[cfg(windows)] + let variable = "USERPROFILE"; + #[cfg(not(windows))] + let variable = "HOME"; + env::var_os(variable) + .filter(|value| !value.is_empty()) + .map(PathBuf::from) +} + +#[cfg(windows)] +fn windows_steam_roots() -> Vec { + use winreg::{RegKey, enums::*}; + + let mut candidates = Vec::new(); + for (hive, subkey, value_name) in [ + (HKEY_CURRENT_USER, r"Software\Valve\Steam", "SteamPath"), + ( + HKEY_LOCAL_MACHINE, + r"SOFTWARE\WOW6432Node\Valve\Steam", + "InstallPath", + ), + (HKEY_LOCAL_MACHINE, r"SOFTWARE\Valve\Steam", "InstallPath"), + ] { + if let Ok(key) = RegKey::predef(hive).open_subkey(subkey) { + if let Ok(path) = key.get_value::(value_name) { + candidates.push(PathBuf::from(path)); + } + } + } + for variable in ["PROGRAMFILES(X86)", "PROGRAMFILES"] { + if let Some(path) = env::var_os(variable) { + candidates.push(PathBuf::from(path).join("Steam")); + } + } + candidates.push(PathBuf::from(r"C:\Program Files (x86)\Steam")); + candidates.push(PathBuf::from(r"C:\Program Files\Steam")); + candidates +} + +fn candidate_steam_roots() -> Vec { + #[cfg(windows)] + let candidates = windows_steam_roots(); + + #[cfg(target_os = "macos")] + let candidates = home_dir() + .map(|home| vec![home.join("Library/Application Support/Steam")]) + .unwrap_or_default(); + + #[cfg(all(unix, not(target_os = "macos")))] + let candidates = home_dir() + .map(|home| vec![home.join(".steam/steam"), home.join(".local/share/Steam")]) + .unwrap_or_default(); + + dedupe_existing_dirs(candidates) +} + +fn normalized_path_key(path: &Path) -> String { + let resolved = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + #[cfg(windows)] + return resolved.to_string_lossy().to_ascii_lowercase(); + #[cfg(not(windows))] + resolved.to_string_lossy().into_owned() +} + +fn dedupe_existing_dirs(paths: impl IntoIterator) -> Vec { + let mut seen = HashSet::new(); + paths + .into_iter() + .filter(|path| path.is_dir()) + .filter(|path| seen.insert(normalized_path_key(path))) + .collect() +} + +pub(crate) fn library_paths_from_roots(roots: Vec) -> Vec { + let mut paths = roots.clone(); + for root in roots { + let library_file = root.join("steamapps/libraryfolders.vdf"); + let Ok(document) = read_vdf(&library_file) else { + continue; + }; + let Some(VdfValue::Object(folders)) = value_at(&document, &["libraryfolders"]) else { + continue; + }; + for entry in folders { + if entry.key.eq_ignore_ascii_case("contentstatsid") { + continue; + } + match &entry.value { + VdfValue::String(path) => paths.push(PathBuf::from(path)), + VdfValue::Object(folder) => { + if let Some(VdfValue::String(path)) = value_at(folder, &["path"]) { + paths.push(PathBuf::from(path)); + } + } + } + } + } + dedupe_existing_dirs(paths) +} + +pub fn steam_root() -> Option { + candidate_steam_roots().into_iter().next() +} + +pub fn library_paths() -> Vec { + library_paths_from_roots(candidate_steam_roots()) +} + +fn has_game_executable(directory: &Path) -> bool { + directory.is_dir() + && GAME_EXECUTABLES + .iter() + .any(|executable| directory.join(executable).is_file()) +} + +pub fn find_game_directory(saved: Option<&str>) -> Option { + if let Some(saved) = saved + .map(PathBuf::from) + .filter(|path| has_game_executable(path)) + { + return Some(saved); + } + for library in library_paths() { + let candidate = library.join("steamapps/common").join(GAME_DIRECTORY); + if has_game_executable(&candidate) { + return Some(candidate); + } + } + + #[cfg(windows)] + let candidates = windows_steam_roots() + .into_iter() + .map(|root| root.join("steamapps/common").join(GAME_DIRECTORY)) + .collect::>(); + #[cfg(target_os = "macos")] + let candidates = vec![ + home_dir()? + .join("Library/Application Support/Steam/steamapps/common") + .join(GAME_DIRECTORY), + ]; + #[cfg(all(unix, not(target_os = "macos")))] + let candidates = { + let home = home_dir()?; + vec![ + home.join(".steam/steam/steamapps/common") + .join(GAME_DIRECTORY), + home.join(".local/share/Steam/steamapps/common") + .join(GAME_DIRECTORY), + ] + }; + candidates + .into_iter() + .find(|path| has_game_executable(path)) +} + +fn numeric_id_cmp(left: &str, right: &str) -> Ordering { + let left = left.trim_start_matches('0'); + let right = right.trim_start_matches('0'); + left.len().cmp(&right.len()).then_with(|| left.cmp(right)) +} + +fn numeric_user_ids(userdata: &Path) -> Vec { + let mut ids = fs::read_dir(userdata) + .ok() + .into_iter() + .flatten() + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .filter_map(|entry| entry.file_name().into_string().ok()) + .filter(|name| !name.is_empty() && name.bytes().all(|byte| byte.is_ascii_digit())) + .collect::>(); + ids.sort_by(|left, right| numeric_id_cmp(left, right).then_with(|| left.cmp(right))); + ids +} + +fn user_id_in(steam_root: &Path) -> Option { + let ids = numeric_user_ids(&steam_root.join("userdata")); + let login_users = read_vdf(&steam_root.join("config/loginusers.vdf")).ok(); + let recent = login_users + .as_ref() + .and_then(|document| value_at(document, &["users"])) + .and_then(|value| match value { + VdfValue::Object(users) => Some(users), + VdfValue::String(_) => None, + }) + .into_iter() + .flatten() + .filter(|entry| { + matches!( + &entry.value, + VdfValue::Object(user) + if matches!(value_at(user, &["MostRecent"]), Some(VdfValue::String(value)) if value == "1") + ) + }) + .filter_map(|entry| { + if ids.iter().any(|id| id == &entry.key) { + return Some(entry.key.clone()); + } + entry + .key + .parse::() + .ok()? + .checked_sub(STEAM_ID64_OFFSET) + .map(|id| id.to_string()) + .filter(|id| ids.iter().any(|candidate| candidate == id)) + }) + .min_by(|left, right| numeric_id_cmp(left, right).then_with(|| left.cmp(right))); + recent.or_else(|| ids.into_iter().next()) +} + +pub fn user_id() -> Option { + user_id_in(&steam_root()?) +} + +fn local_config_path_in(steam_root: &Path, user: &str) -> Option { + if user.is_empty() || !user.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + Some( + steam_root + .join("userdata") + .join(user) + .join("config/localconfig.vdf"), + ) +} + +fn local_config_path(user: &str) -> Option { + local_config_path_in(&steam_root()?, user) +} + +fn read_launch_options_at(path: &Path, game_app_id: &str) -> Result { + let document = read_vdf(path)?; + Ok( + match value_at( + &document, + &[ + "UserLocalConfigStore", + "Software", + "Valve", + "Steam", + "apps", + game_app_id, + "LaunchOptions", + ], + ) { + Some(VdfValue::String(options)) => options.clone(), + _ => String::new(), + }, + ) +} + +pub fn current_launch_options() -> Option { + let user = user_id()?; + read_launch_options_at(&local_config_path(&user)?, APP_ID).ok() +} + +fn normalized_options(options: &str) -> String { + options.split_whitespace().collect::>().join(" ") +} + +fn without_token(options: &str, token: &str) -> String { + options + .split_whitespace() + .filter(|part| *part != token) + .collect::>() + .join(" ") +} + +fn merged_launch_options(current: &str, requested: &str, preserve_fs_game: bool) -> String { + let fs_game = Regex::new(r"\+set\s+fs_game\s+\S+").expect("fixed regular expression"); + let existing_fs = fs_game + .find_iter(current) + .map(|matched| matched.as_str().to_owned()) + .collect::>(); + let requested_fs = fs_game + .find_iter(requested) + .map(|matched| matched.as_str().to_owned()) + .collect::>(); + let selected_fs = if requested_fs.is_empty() && preserve_fs_game { + existing_fs + } else { + requested_fs + }; + + let wine_override = "WINEDLLOVERRIDES=\"dsound=n,b\""; + let command_marker = "%command%"; + let current_without_fs = fs_game.replace_all(current, ""); + let requested_without_fs = fs_game.replace_all(requested, ""); + let has_wine = current_without_fs + .split_whitespace() + .any(|part| part == wine_override) + || requested_without_fs + .split_whitespace() + .any(|part| part == wine_override); + let mut has_command = current_without_fs + .split_whitespace() + .any(|part| part == command_marker) + || requested_without_fs + .split_whitespace() + .any(|part| part == command_marker); + if has_wine { + has_command = true; + } + + let cleaned_current = without_token( + &without_token(¤t_without_fs, wine_override), + command_marker, + ); + let cleaned_requested = without_token( + &without_token(&requested_without_fs, wine_override), + command_marker, + ); + let mut segments = Vec::new(); + if has_wine { + segments.push(wine_override.to_owned()); + } + if has_command { + segments.push(command_marker.to_owned()); + } + if !cleaned_current.is_empty() { + segments.push(normalized_options(&cleaned_current)); + } + if !cleaned_requested.is_empty() { + segments.push(normalized_options(&cleaned_requested)); + } + let mut unique_fs = Vec::new(); + for entry in selected_fs { + if !unique_fs.contains(&entry) { + unique_fs.push(entry); + } + } + if !unique_fs.is_empty() { + segments.push(normalized_options(&unique_fs.join(" "))); + } + normalized_options(&segments.join(" ")) +} + +fn set_launch_options_at( + config: &Path, + legacy_backup: &Path, + game_app_id: &str, + requested: &str, + preserve_fs_game: bool, + exact: bool, +) -> Result { + let current = read_launch_options_at(config, game_app_id)?; + let final_options = if exact { + requested.to_owned() + } else { + merged_launch_options(¤t, requested, preserve_fs_game) + }; + update_vdf(config, Some(legacy_backup), |document| { + let app = ensure_path( + document, + &[ + "UserLocalConfigStore", + "Software", + "Valve", + "Steam", + "apps", + game_app_id, + ], + )?; + set_string(app, "LaunchOptions", final_options.clone()) + })?; + Ok(final_options) +} + +fn workshop_profile(profile_id: &str) -> Option { + WORKSHOP_PROFILES + .iter() + .copied() + .find(|profile| profile.id == profile_id) +} + +fn profile_option(profile_id: &str) -> Option<&'static str> { + match profile_id { + "default" => Some(""), + "offline" => Some("+set fs_game offlinemp"), + _ => workshop_profile(profile_id).map(|profile| profile.option), + } +} + +fn supported_option(option: &str) -> bool { + option.is_empty() + || option == "+set fs_game offlinemp" + || WORKSHOP_PROFILES + .iter() + .any(|profile| profile.option == option) +} + +fn vdf_contains(value: &VdfValue, needle: &str) -> bool { + match value { + VdfValue::String(value) => value == needle, + VdfValue::Object(object) => object + .iter() + .any(|entry| entry.key == needle || vdf_contains(&entry.value, needle)), + } +} + +fn workshop_item_state_in( + libraries: &[PathBuf], + game_app_id: &str, + workshop_id: &str, +) -> WorkshopState { + let mut subscribed = false; + for library in libraries { + let install_dir = library + .join("steamapps/workshop/content") + .join(game_app_id) + .join(workshop_id); + if install_dir.is_dir() + && fs::read_dir(&install_dir) + .ok() + .and_then(|mut entries| entries.next()) + .is_some() + { + return WorkshopState { + state: "Installed".into(), + installed: true, + subscribed: true, + path: Some(install_dir), + }; + } + + let manifest = library + .join("steamapps/workshop") + .join(format!("appworkshop_{game_app_id}.acf")); + if let Ok(document) = read_vdf(&manifest) + && let Some(value) = value_at(&document, &["AppWorkshop"]) + { + subscribed |= vdf_contains(value, workshop_id); + } + } + if subscribed { + WorkshopState { + state: "Subscribed (not installed yet)".into(), + installed: false, + subscribed: true, + path: None, + } + } else { + WorkshopState { + state: "Not Subscribed".into(), + installed: false, + subscribed: false, + path: None, + } + } +} + +pub fn workshop_item_state(game_app_id: &str, workshop_id: &str) -> WorkshopState { + workshop_item_state_in(&library_paths(), game_app_id, workshop_id) +} + +pub fn launch_profiles(current: Option<&str>) -> Vec { + let current = current.unwrap_or_default(); + let mut profiles = vec![ + LaunchProfile { + id: "default".into(), + label: "Default (None)".into(), + option: String::new(), + active: current.trim().is_empty(), + installed: true, + subscribed: true, + state: "Ready".into(), + path: None, + }, + LaunchProfile { + id: "offline".into(), + label: "Play Offline".into(), + option: "+set fs_game offlinemp".into(), + active: current.contains("+set fs_game offlinemp"), + installed: true, + subscribed: true, + state: "Ready".into(), + path: None, + }, + ]; + for profile in WORKSHOP_PROFILES { + let workshop = workshop_item_state(APP_ID, profile.workshop_id); + profiles.push(LaunchProfile { + id: profile.id.into(), + label: profile.label.into(), + option: profile.option.into(), + active: current.contains(profile.option), + installed: workshop.installed, + subscribed: workshop.subscribed, + state: workshop.state, + path: workshop + .path + .map(|path| path.to_string_lossy().into_owned()), + }); + } + profiles +} + +fn find_on_path(command: &str) -> Option { + env::var_os("PATH") + .into_iter() + .flat_map(|paths| env::split_paths(&paths).collect::>()) + .map(|directory| directory.join(command)) + .find(|path| path.is_file()) +} + +#[cfg(windows)] +fn steam_executable() -> Option { + candidate_steam_roots() + .into_iter() + .map(|root| root.join("steam.exe")) + .find(|path| path.is_file()) +} + +#[cfg(all(unix, not(target_os = "macos")))] +fn steam_executable() -> Option { + find_on_path("steam").or_else(|| { + steam_root() + .map(|root| root.join("steam.sh")) + .filter(|path| path.is_file()) + }) +} + +fn wait_for_steam_exit_with( + checks: usize, + mut is_running: impl FnMut() -> Result, + mut pause: impl FnMut(), +) -> Result<(), String> { + for check in 0..checks { + if !is_running()? { + return Ok(()); + } + if check + 1 < checks { + pause(); + } + } + Err("Steam did not exit before the configuration timeout.".into()) +} + +#[cfg(target_os = "linux")] +fn linux_steam_running() -> Result { + for entry in fs::read_dir("/proc").map_err(|error| format!("/proc: {error}"))? { + let Ok(entry) = entry else { + continue; + }; + if !entry + .file_name() + .to_string_lossy() + .bytes() + .all(|byte| byte.is_ascii_digit()) + { + continue; + } + if fs::read_to_string(entry.path().join("comm")).is_ok_and(|name| name.trim() == "steam") { + return Ok(true); + } + } + Ok(false) +} + +fn close_steam(app: &AppState) -> Result<(), String> { + #[cfg(windows)] + let result = Command::new("taskkill") + .args(["/F", "/IM", "steam.exe"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + #[cfg(target_os = "linux")] + let result = Command::new("pkill") + .args(["-x", "steam"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + #[cfg(target_os = "macos")] + let result = Command::new("pkill") + .args(["-x", "steam"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + #[cfg(windows)] + match result { + Ok(status) if status.success() => { + app.log("Info", "Closed Steam before updating its configuration.") + } + Ok(_) => app.log("Info", "Steam was not running."), + Err(error) => return Err(format!("Could not close Steam: {error}")), + } + + #[cfg(unix)] + match result { + Ok(status) if status.success() => { + app.log("Info", "Closed Steam before updating its configuration.") + } + Ok(status) if status.code() == Some(1) => app.log("Info", "Steam was not running."), + Ok(status) => { + return Err(format!( + "Could not close Steam (exit code {}).", + status + .code() + .map_or_else(|| "unknown".into(), |code| code.to_string()) + )); + } + Err(error) => return Err(format!("Could not close Steam: {error}")), + } + + #[cfg(target_os = "linux")] + wait_for_steam_exit_with(50, linux_steam_running, || { + std::thread::sleep(Duration::from_millis(100)) + })?; + + Ok(()) +} + +fn open_steam(app: &AppState) -> Result<(), String> { + #[cfg(windows)] + let child = steam_executable() + .ok_or_else(|| "Steam executable was not found.".to_string()) + .and_then(|executable| { + Command::new(executable) + .arg("-silent") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + }); + + #[cfg(all(unix, not(target_os = "macos")))] + let child = steam_executable() + .ok_or_else(|| "Steam executable was not found.".to_string()) + .and_then(|executable| { + Command::new(executable) + .arg("-silent") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + }); + + #[cfg(target_os = "macos")] + let child = Command::new("open") + .args(["-a", "Steam"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()); + + child.map(|_| app.log("Success", "Opened Steam.")) +} + +pub fn apply_launch_options( + app: &AppState, + options: &str, + preserve_fs_game: bool, +) -> Result<(), String> { + if !supported_option(options) { + return Err("Unsupported launch option.".into()); + } + let user = user_id().ok_or_else(|| "Steam user ID not found.".to_string())?; + let config = + local_config_path(&user).ok_or_else(|| "Steam userdata path not found.".to_string())?; + let legacy_backup = app.data_dir().join("backups/localconfig_backup.vdf"); + close_steam(app)?; + let result = set_launch_options_at( + &config, + &legacy_backup, + APP_ID, + options, + preserve_fs_game, + false, + ); + if let Err(error) = open_steam(app) { + app.log( + "Warning", + format!("Launch options were processed, but Steam could not be reopened: {error}"), + ); + } + result.map(|final_options| { + app.log( + "Success", + format!("Set Steam launch options to: {final_options}"), + ); + }) +} + +pub fn apply_launch_profile(app: &AppState, profile_id: &str) -> Result<(), String> { + let option = + profile_option(profile_id).ok_or_else(|| "Unsupported launch profile.".to_string())?; + apply_launch_options(app, option, false) +} + +fn open_workshop_page(workshop_id: &str) -> Result<(), String> { + let uri = format!( + "steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id={workshop_id}" + ); + #[cfg(windows)] + let result = steam_executable() + .ok_or_else(|| "Steam executable was not found.".to_string()) + .and_then(|executable| { + Command::new(executable) + .arg(&uri) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + }); + #[cfg(all(unix, not(target_os = "macos")))] + let result = if let Some(executable) = steam_executable() { + Command::new(executable) + .arg(&uri) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + } else if let Some(xdg_open) = find_on_path("xdg-open") { + Command::new(xdg_open) + .arg(&uri) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + } else { + Err("No Steam URI launcher was found.".into()) + }; + #[cfg(target_os = "macos")] + let result = Command::new("open") + .arg(&uri) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()); + result.map(|_| ()) +} + +pub fn install_workshop_profile(app: &AppState, profile_id: &str) -> Result<(), String> { + let profile = workshop_profile(profile_id) + .ok_or_else(|| "Select an installable Workshop mod.".to_string())?; + apply_launch_profile(app, profile_id)?; + open_workshop_page(profile.workshop_id)?; + app.log( + "Info", + format!("Opened {} Workshop page in Steam.", profile.label), + ); + Ok(()) +} + +pub fn launch_game(app: &AppState) -> Result<(), String> { + #[cfg(windows)] + let result = steam_executable() + .ok_or_else(|| "Steam executable was not found.".to_string()) + .and_then(|executable| { + Command::new(executable) + .args(["-applaunch", APP_ID]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + }); + #[cfg(all(unix, not(target_os = "macos")))] + let result = if let Some(executable) = steam_executable() { + Command::new(executable) + .args(["-applaunch", APP_ID]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + } else if let Some(xdg_open) = find_on_path("xdg-open") { + Command::new(xdg_open) + .arg(format!("steam://rungameid/{APP_ID}")) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()) + } else { + Err("No Steam launcher was found.".into()) + }; + #[cfg(target_os = "macos")] + let result = Command::new("open") + .arg(format!("steam://rungameid/{APP_ID}")) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|error| error.to_string()); + + result.map(|_| { + app.log( + "Success", + format!("Launched Black Ops III via Steam (AppID: {APP_ID})."), + ); + }) +} + +pub fn compatible_depot_candidates() -> Vec { + let mut roots = library_paths(); + if let Some(root) = steam_root() { + roots.push(root); + } + #[cfg(windows)] + roots.extend(windows_steam_roots()); + let mut seen = HashSet::new(); + roots + .into_iter() + .map(|root| { + root.join("steamapps/content") + .join(format!("app_{COMPATIBLE_DEPOT_APP_ID}")) + .join(format!("depot_{COMPATIBLE_DEPOT_ID}")) + }) + .filter(|path| seen.insert(normalized_path_key(path))) + .collect() +} + +pub fn compatible_depot_download_exists() -> bool { + compatible_depot_candidates().into_iter().any(|directory| { + let executable = directory.join("BlackOps3.exe"); + executable + .metadata() + .is_ok_and(|metadata| metadata.is_file() && metadata.len() > 0) + && directory.join("installscript_311210.vdf").is_file() + }) +} + +pub fn find_valid_compatible_depot() -> Option { + compatible_depot_candidates() + .into_iter() + .filter(|directory| { + fs_ops::sha256_file(&directory.join("BlackOps3.exe")) + .is_ok_and(|hash| hash.eq_ignore_ascii_case(COMPATIBLE_BUILD_SHA256)) + }) + .max_by_key(|directory| { + directory + .join("BlackOps3.exe") + .metadata() + .and_then(|metadata| metadata.modified()) + .unwrap_or(UNIX_EPOCH) + }) +} + +fn vdf_to_json(value: &VdfValue) -> JsonValue { + match value { + VdfValue::String(value) => JsonValue::String(value.clone()), + VdfValue::Object(object) => { + let mut values = JsonMap::new(); + for entry in object { + values.insert(entry.key.clone(), vdf_to_json(&entry.value)); + } + JsonValue::Object(values) + } + } +} + +fn json_to_vdf(value: &JsonValue) -> Option { + match value { + JsonValue::String(value) => Some(VdfValue::String(value.clone())), + JsonValue::Object(object) => Some(VdfValue::Object( + object + .iter() + .filter_map(|(key, value)| { + Some(VdfEntry { + key: key.clone(), + value: json_to_vdf(value)?, + condition: None, + }) + }) + .collect(), + )), + JsonValue::Bool(value) => Some(VdfValue::String(value.to_string())), + JsonValue::Number(value) => Some(VdfValue::String(value.to_string())), + _ => None, + } +} + +fn atomic_json(path: &Path, value: &JsonValue) -> Result<(), String> { + let bytes = serde_json::to_vec_pretty(value).map_err(|error| error.to_string())?; + fs_ops::atomic_write(path, &bytes) +} + +fn mapping_snapshot_path(data_dir: &Path) -> PathBuf { + data_dir.join(format!("backups/compat_mapping_{APP_ID}.json")) +} + +fn launch_snapshot_path(data_dir: &Path) -> PathBuf { + data_dir.join(format!("backups/launch_options_{APP_ID}.json")) +} + +fn tool_ownership_path(data_dir: &Path) -> PathBuf { + data_dir.join(format!("backups/compat_tool_{APP_ID}.json")) +} + +fn required_json(path: &Path) -> Result { + let metadata = + fs::symlink_metadata(path).map_err(|error| format!("{}: {error}", path.display()))?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(format!("{} is not a regular file", path.display())); + } + let body = fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?; + serde_json::from_slice(&body) + .map_err(|error| format!("{} is not valid ownership data: {error}", path.display())) +} + +fn enhanced_mapping_value() -> VdfValue { + VdfValue::Object(vec![ + VdfEntry { + key: "name".into(), + value: VdfValue::String(ENHANCED_TOOL_NAME.into()), + condition: None, + }, + VdfEntry { + key: "config".into(), + value: VdfValue::String(String::new()), + condition: None, + }, + VdfEntry { + key: "priority".into(), + value: VdfValue::String("250".into()), + condition: None, + }, + ]) +} + +fn mapping_restore_value(snapshot: &Path) -> Result<(bool, Option), String> { + let saved = required_json(snapshot)?; + let object = saved + .as_object() + .ok_or_else(|| format!("{} has invalid mapping ownership data", snapshot.display()))?; + let had_entry = object + .get("had_entry") + .and_then(JsonValue::as_bool) + .ok_or_else(|| format!("{} has invalid mapping ownership data", snapshot.display()))?; + let previous = object.get("entry").and_then(json_to_vdf); + if had_entry && previous.is_none() { + return Err(format!( + "{} is missing the original compatibility mapping", + snapshot.display() + )); + } + Ok((had_entry, previous)) +} + +fn set_compatibility_mapping_at( + config: &Path, + legacy_backup: &Path, + snapshot: &Path, +) -> Result<(), String> { + let map_path = [ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + ]; + match fs::symlink_metadata(snapshot) { + Ok(_) => { + mapping_restore_value(snapshot)?; + let document = read_vdf(config)?; + let mut managed_path = map_path.to_vec(); + managed_path.push(APP_ID); + if value_at(&document, &managed_path) != Some(&enhanced_mapping_value()) { + return Err( + "Steam compatibility mapping changed after PatchOpsIII configured it; refusing to overwrite it." + .into(), + ); + } + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let document = read_vdf(config)?; + let previous = match value_at(&document, &map_path) { + Some(VdfValue::Object(map)) => { + entry_index(map, APP_ID).map(|index| map[index].value.clone()) + } + Some(VdfValue::String(_)) => { + return Err("Steam CompatToolMapping is not an object".into()); + } + None => None, + }; + atomic_json( + snapshot, + &json!({ + "had_entry": previous.is_some(), + "entry": previous.as_ref().map(vdf_to_json).unwrap_or(JsonValue::Null), + }), + )?; + } + Err(error) => return Err(format!("{}: {error}", snapshot.display())), + } + + update_vdf(config, Some(legacy_backup), |document| { + let map = ensure_path( + document, + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + ], + )?; + set_value(map, APP_ID, enhanced_mapping_value()); + Ok(()) + }) +} + +fn clear_compatibility_mapping_at( + config: &Path, + legacy_backup: &Path, + snapshot: &Path, +) -> Result<(), String> { + let (had_entry, previous) = mapping_restore_value(snapshot)?; + let document = read_vdf(config)?; + let map_path = [ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + ]; + let mut managed_path = map_path.to_vec(); + managed_path.push(APP_ID); + if value_at(&document, &managed_path) != Some(&enhanced_mapping_value()) { + return Err( + "Steam compatibility mapping changed after PatchOpsIII configured it; leaving it untouched." + .into(), + ); + } + + update_vdf(config, Some(legacy_backup), |document| { + let map = ensure_path( + document, + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + ], + )?; + if let (true, Some(previous)) = (had_entry, previous) { + set_value(map, APP_ID, previous); + } else { + map.retain(|entry| !entry.key.eq_ignore_ascii_case(APP_ID)); + } + Ok(()) + })?; + if snapshot.exists() { + fs::remove_file(snapshot).map_err(|error| format!("{}: {error}", snapshot.display()))?; + } + Ok(()) +} + +fn update_compatibility_manifest(path: &Path) -> Result<(), String> { + write_new_or_update_vdf(path, |document| { + let tools = ensure_path(document, &["compatibilitytools", "compat_tools"])?; + let index = entry_index(tools, ENHANCED_TOOL_NAME).or_else(|| { + tools + .iter() + .position(|entry| matches!(entry.value, VdfValue::Object(_))) + }); + let index = match index { + Some(index) => { + tools[index].key = ENHANCED_TOOL_NAME.into(); + index + } + None => { + tools.push(VdfEntry { + key: ENHANCED_TOOL_NAME.into(), + value: VdfValue::Object(Vec::new()), + condition: None, + }); + tools.len() - 1 + } + }; + let VdfValue::Object(tool) = &mut tools[index].value else { + return Err("compatibility tool manifest entry is not an object".into()); + }; + for (key, default) in [ + ("install_path", "."), + ("display_name", ENHANCED_TOOL_NAME), + ("from_oslist", "windows"), + ("to_oslist", "linux"), + ] { + let value = if matches!(key, "install_path" | "from_oslist" | "to_oslist") { + match value_at(tool, &[key]) { + Some(VdfValue::String(value)) => value.clone(), + _ => default.into(), + } + } else { + default.into() + }; + set_string(tool, key, value)?; + } + Ok(()) + }) +} + +fn copy_directory(source: &Path, destination: &Path) -> Result<(), String> { + let metadata = fs::symlink_metadata(source).map_err(|error| error.to_string())?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(format!( + "Compatibility tool source is not a safe directory: {}", + source.display() + )); + } + let root = source.canonicalize().map_err(|error| error.to_string())?; + copy_directory_inside(&root, &root, destination, &mut HashSet::new()) +} + +fn copy_directory_inside( + root: &Path, + source: &Path, + destination: &Path, + active: &mut HashSet, +) -> Result<(), String> { + let resolved = source.canonicalize().map_err(|error| error.to_string())?; + if !resolved.starts_with(root) { + return Err(format!( + "Compatibility tool link escapes its source: {}", + source.display() + )); + } + if !active.insert(resolved.clone()) { + return Err(format!( + "Compatibility tool contains a directory-link cycle: {}", + source.display() + )); + } + fs::create_dir_all(destination).map_err(|error| error.to_string())?; + let result = (|| { + for entry in + fs::read_dir(&resolved).map_err(|error| format!("{}: {error}", source.display()))? + { + let entry = entry.map_err(|error| error.to_string())?; + let source_path = entry.path(); + let destination_path = destination.join(entry.file_name()); + let metadata = fs::symlink_metadata(&source_path).map_err(|error| error.to_string())?; + let resolved_path = if metadata.file_type().is_symlink() { + let target = fs::read_link(&source_path).map_err(|error| error.to_string())?; + if target.is_absolute() { + return Err(format!( + "Compatibility tool contains an absolute link: {}", + source_path.display() + )); + } + source_path + .canonicalize() + .map_err(|error| format!("{}: {error}", source_path.display()))? + } else { + source_path.clone() + }; + if !resolved_path.starts_with(root) { + return Err(format!( + "Compatibility tool link escapes its source: {}", + source_path.display() + )); + } + let resolved_metadata = + fs::metadata(&resolved_path).map_err(|error| error.to_string())?; + if resolved_metadata.is_dir() { + copy_directory_inside(root, &resolved_path, &destination_path, active)?; + } else if resolved_metadata.is_file() { + fs::copy(&resolved_path, &destination_path).map_err(|error| error.to_string())?; + } else { + return Err(format!( + "unsupported compatibility tool file: {}", + source_path.display() + )); + } + } + Ok(()) + })(); + active.remove(&resolved); + result +} + +fn unique_suffix() -> String { + format!( + "{}-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(), + std::process::id() + ) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct ToolOwnership { + version: u8, + transaction_id: String, + original_backup: Option, +} + +#[derive(Debug)] +struct ToolInstallTransaction { + destination: PathBuf, + displaced_managed: Option, + original_backup: Option, + ownership_path: PathBuf, + transaction_id: String, + new_ownership: bool, +} + +#[derive(Debug)] +struct ToolRemovalTransaction { + destination: PathBuf, + displaced_managed: Option, + restored_backup: Option, + ownership_path: PathBuf, + transaction_id: String, +} + +#[derive(Debug)] +struct LegacyToolAdoption { + destination: PathBuf, + ownership_path: PathBuf, + transaction_id: String, +} + +fn tool_directory(path: &Path) -> Result { + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(true), + Ok(_) => Err(format!("{} is not a safe tool directory", path.display())), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(format!("{}: {error}", path.display())), + } +} + +fn unique_tool_path(directory: &Path, prefix: &str) -> PathBuf { + for index in 0..1000_u16 { + let candidate = directory.join(format!("{prefix}-{}-{index}", unique_suffix())); + if fs::symlink_metadata(&candidate).is_err() { + return candidate; + } + } + directory.join(format!("{prefix}-{}", unique_suffix())) +} + +fn load_tool_ownership(path: &Path) -> Result, String> { + match fs::symlink_metadata(path) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(format!("{}: {error}", path.display())), + Ok(_) => {} + } + let value = required_json(path)?; + let ownership: ToolOwnership = serde_json::from_value(value).map_err(|error| { + format!( + "{} has invalid tool ownership data: {error}", + path.display() + ) + })?; + if ownership.version != TOOL_OWNERSHIP_VERSION + || ownership.transaction_id.is_empty() + || !ownership + .transaction_id + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + { + return Err(format!( + "{} has invalid tool ownership data", + path.display() + )); + } + if let Some(name) = &ownership.original_backup { + let path = Path::new(name); + if path.parent() != Some(Path::new("")) + || !name.starts_with(&format!("{ENHANCED_TOOL_NAME}.patchops.original-")) + { + return Err("Compatibility tool ownership contains an unsafe backup path.".into()); + } + } + Ok(Some(ownership)) +} + +fn compare_tool_trees( + expected: &Path, + actual: &Path, + allow_actual_links: bool, + ignore_source_marker: bool, +) -> Result<(), String> { + let actual_metadata = + fs::symlink_metadata(actual).map_err(|error| format!("{}: {error}", actual.display()))?; + if actual_metadata.file_type().is_symlink() || !actual_metadata.is_dir() { + return Err(format!("{} is not a safe tool directory", actual.display())); + } + let actual_root = actual.canonicalize().map_err(|error| error.to_string())?; + compare_tool_trees_inside( + expected, + actual, + Path::new(""), + &actual_root, + allow_actual_links, + ignore_source_marker, + &mut HashSet::new(), + ) +} + +#[allow(clippy::too_many_arguments)] +fn compare_tool_trees_inside( + expected: &Path, + actual: &Path, + relative: &Path, + actual_root: &Path, + allow_actual_links: bool, + ignore_source_marker: bool, + active: &mut HashSet, +) -> Result<(), String> { + let expected_metadata = fs::symlink_metadata(expected) + .map_err(|error| format!("{}: {error}", expected.display()))?; + if expected_metadata.file_type().is_symlink() { + return Err(format!( + "Trusted compatibility tool contains an unexpected link at {}.", + relative.display() + )); + } + let metadata = + fs::symlink_metadata(actual).map_err(|error| format!("{}: {error}", actual.display()))?; + let actual_resolved = if metadata.file_type().is_symlink() { + if !allow_actual_links { + return Err(format!( + "Legacy compatibility tool contains an unsafe link at {}.", + relative.display() + )); + } + let target = fs::read_link(actual).map_err(|error| error.to_string())?; + if target.is_absolute() { + return Err(format!( + "Compatibility tool contains an absolute link at {}.", + relative.display() + )); + } + actual + .canonicalize() + .map_err(|error| format!("{}: {error}", actual.display()))? + } else { + actual.to_path_buf() + }; + if !actual_resolved + .canonicalize() + .is_ok_and(|path| path.starts_with(actual_root)) + { + return Err(format!( + "Compatibility tool link escapes its source at {}.", + relative.display() + )); + } + let actual_metadata = fs::metadata(&actual_resolved).map_err(|error| error.to_string())?; + if expected_metadata.is_dir() && actual_metadata.is_dir() { + let resolved_directory = actual_resolved + .canonicalize() + .map_err(|error| error.to_string())?; + if !active.insert(resolved_directory.clone()) { + return Err(format!( + "Compatibility tool contains a directory-link cycle at {}.", + relative.display() + )); + } + let names = |directory: &Path, ignore_marker: bool| -> Result, String> { + let mut names = fs::read_dir(directory) + .map_err(|error| format!("{}: {error}", directory.display()))? + .map(|entry| { + entry + .map(|entry| entry.file_name()) + .map_err(|error| error.to_string()) + }) + .collect::, _>>()?; + if ignore_marker { + names.retain(|name| name != ".patchops-source.json"); + } + names.sort(); + Ok(names) + }; + let expected_names = names(expected, false)?; + if expected_names + != names( + &actual_resolved, + ignore_source_marker && relative.as_os_str().is_empty(), + )? + { + active.remove(&resolved_directory); + return Err(format!( + "Legacy compatibility tool contents differ at {}.", + relative.display() + )); + } + for name in expected_names { + compare_tool_trees_inside( + &expected.join(&name), + &actual.join(&name), + &relative.join(name), + actual_root, + allow_actual_links, + ignore_source_marker, + active, + )?; + } + active.remove(&resolved_directory); + return Ok(()); + } + if expected_metadata.is_file() && actual_metadata.is_file() { + if relative == Path::new("compatibilitytool.vdf") { + if read_vdf(expected)? != read_vdf(actual)? { + return Err("Legacy compatibility-tool manifest differs from release10-32.".into()); + } + } else if fs_ops::sha256_file(expected)? != fs_ops::sha256_file(actual)? { + return Err(format!( + "Legacy compatibility tool bytes differ at {}.", + relative.display() + )); + } + return Ok(()); + } + Err(format!( + "Legacy compatibility tool has an unexpected file type at {}.", + relative.display() + )) +} + +fn require_legacy_compatibility_state( + steam_config: &Path, + mapping_snapshot: &Path, + local_config: &Path, + launch_snapshot: &Path, +) -> Result<(), String> { + mapping_restore_value(mapping_snapshot)?; + launch_restore_value(launch_snapshot)?; + let document = read_vdf(steam_config)?; + if value_at( + &document, + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + APP_ID, + ], + ) != Some(&enhanced_mapping_value()) + { + return Err("Legacy Steam compatibility mapping is not the exact managed value.".into()); + } + if read_launch_options_at(local_config, APP_ID)? != ENHANCED_LAUNCH_OPTIONS { + return Err("Legacy Steam launch options are not the exact managed value.".into()); + } + Ok(()) +} + +fn legacy_tool_backup(directory: &Path) -> Result, String> { + let entries = match fs::read_dir(directory) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(format!("{}: {error}", directory.display())), + }; + let prefix = format!("{ENHANCED_TOOL_NAME}.patchops.bak-"); + for entry in entries { + let entry = entry.map_err(|error| error.to_string())?; + if entry + .file_name() + .to_str() + .is_some_and(|name| name.starts_with(&prefix)) + { + return Ok(Some(entry.path())); + } + } + Ok(None) +} + +fn compatibility_is_fully_clean( + steam_root: &Path, + ownership_path: &Path, + steam_config: &Path, + mapping_snapshot: &Path, + local_config: &Path, + launch_snapshot: &Path, +) -> Result { + if load_tool_ownership(ownership_path)?.is_some() { + return Ok(false); + } + let destination = steam_root + .join("compatibilitytools.d") + .join(ENHANCED_TOOL_NAME); + if tool_directory(&destination)? { + return Ok(false); + } + if legacy_tool_backup(&steam_root.join("compatibilitytools.d"))?.is_some() { + return Ok(false); + } + for snapshot in [mapping_snapshot, launch_snapshot] { + match fs::symlink_metadata(snapshot) { + Ok(_) => return Ok(false), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(format!("{}: {error}", snapshot.display())), + } + } + let document = read_vdf(steam_config)?; + let managed_mapping = value_at( + &document, + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + APP_ID, + ], + ) == Some(&enhanced_mapping_value()); + let managed_launch = read_launch_options_at(local_config, APP_ID)? == ENHANCED_LAUNCH_OPTIONS; + Ok(!managed_mapping && !managed_launch) +} + +fn adopt_legacy_compatibility_tool_at( + steam_root: &Path, + trusted_source: &Path, + ownership_path: &Path, + steam_config: &Path, + mapping_snapshot: &Path, + local_config: &Path, + launch_snapshot: &Path, +) -> Result { + if load_tool_ownership(ownership_path)?.is_some() { + return Err( + "Compatibility-tool ownership already exists; legacy adoption is invalid.".into(), + ); + } + let directory = steam_root.join("compatibilitytools.d"); + let destination = directory.join(ENHANCED_TOOL_NAME); + if !tool_directory(&destination)? { + return Err("Legacy BO3 Enhanced compatibility tool is missing.".into()); + } + match fs::symlink_metadata(destination.join(TOOL_MARKER_FILENAME)) { + Ok(_) => { + return Err( + "Untracked compatibility tool already contains an ownership marker.".into(), + ); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(error.to_string()), + } + if let Some(backup) = legacy_tool_backup(&directory)? { + return Err(format!( + "Legacy compatibility-tool backup is ambiguous: {}.", + backup.display() + )); + } + require_legacy_compatibility_state( + steam_config, + mapping_snapshot, + local_config, + launch_snapshot, + )?; + compare_tool_trees(trusted_source, &destination, false, false)?; + + let transaction_id = unique_suffix(); + atomic_json( + &destination.join(TOOL_MARKER_FILENAME), + &json!({ "transaction_id": transaction_id }), + )?; + if let Err(error) = save_tool_ownership( + ownership_path, + &ToolOwnership { + version: TOOL_OWNERSHIP_VERSION, + transaction_id: transaction_id.clone(), + original_backup: None, + }, + ) { + let rollback = fs::remove_file(destination.join(TOOL_MARKER_FILENAME)); + return Err(match rollback { + Ok(()) => error, + Err(rollback) => format!("{error}; removing the adoption marker failed: {rollback}"), + }); + } + Ok(LegacyToolAdoption { + destination, + ownership_path: ownership_path.to_path_buf(), + transaction_id, + }) +} + +impl LegacyToolAdoption { + fn rollback(self) -> Result<(), String> { + require_managed_tool(&self.destination, &self.transaction_id)?; + fs::remove_file(self.destination.join(TOOL_MARKER_FILENAME)) + .map_err(|error| error.to_string())?; + fs::remove_file(&self.ownership_path) + .map_err(|error| format!("{}: {error}", self.ownership_path.display())) + } +} + +fn finish_legacy_adoption( + adoption: Option, + result: Result, +) -> Result { + match (adoption, result) { + (_, Ok(value)) => Ok(value), + (None, Err(error)) => Err(error), + (Some(adoption), Err(error)) => match adoption.rollback() { + Ok(()) => Err(error), + Err(rollback) => Err(format!( + "{error}; legacy adoption rollback failed: {rollback}" + )), + }, + } +} + +fn save_tool_ownership(path: &Path, ownership: &ToolOwnership) -> Result<(), String> { + let value = serde_json::to_value(ownership).map_err(|error| error.to_string())?; + atomic_json(path, &value) +} + +fn tool_marker_id(path: &Path) -> Result { + required_json(&path.join(TOOL_MARKER_FILENAME))? + .get("transaction_id") + .and_then(JsonValue::as_str) + .filter(|id| !id.is_empty()) + .map(str::to_owned) + .ok_or_else(|| format!("{} has invalid PatchOpsIII ownership", path.display())) +} + +fn require_managed_tool(path: &Path, transaction_id: &str) -> Result<(), String> { + if tool_marker_id(path)? != transaction_id { + return Err(format!( + "{} is no longer the compatibility tool installed by PatchOpsIII; leaving it untouched", + path.display() + )); + } + Ok(()) +} + +fn exact_tool_backup( + directory: &Path, + ownership: &ToolOwnership, +) -> Result, String> { + let Some(name) = &ownership.original_backup else { + return Ok(None); + }; + let backup = directory.join(name); + if !tool_directory(&backup)? { + return Err(format!( + "The exact PatchOpsIII-owned tool backup is missing: {}", + backup.display() + )); + } + Ok(Some(backup)) +} + +fn remove_managed_tool(path: &Path, transaction_id: &str) -> Result<(), String> { + require_managed_tool(path, transaction_id)?; + fs::remove_dir_all(path).map_err(|error| format!("{}: {error}", path.display())) +} + +impl ToolInstallTransaction { + fn rollback(self) -> Result<(), String> { + remove_managed_tool(&self.destination, &self.transaction_id)?; + let restore = if self.new_ownership { + self.original_backup.as_ref() + } else { + self.displaced_managed.as_ref() + }; + if let Some(restore) = restore { + fs::rename(restore, &self.destination).map_err(|error| { + format!( + "failed to restore compatibility tool {}: {error}", + self.destination.display() + ) + })?; + } + if self.new_ownership { + fs::remove_file(&self.ownership_path) + .map_err(|error| format!("{}: {error}", self.ownership_path.display()))?; + } + Ok(()) + } + + fn commit(mut self) -> Result<(), Box<(String, Self)>> { + if let Some(displaced) = &self.displaced_managed + && let Err(error) = require_managed_tool(displaced, &self.transaction_id) + { + return Err(Box::new((error, self))); + } + if let Some(displaced) = self.displaced_managed.take() { + // Best-effort reaping keeps an unlink failure from becoming an + // unrollbackable partial commit. Add persisted cleanup if this ever leaks. + let _ = fs::remove_dir_all(displaced); + } + Ok(()) + } +} + +impl ToolRemovalTransaction { + fn rollback(self) -> Result<(), String> { + if let Some(backup) = self.restored_backup { + if tool_directory(&backup)? { + return Err(format!( + "Cannot roll back because {} already exists", + backup.display() + )); + } + fs::rename(&self.destination, &backup).map_err(|error| { + format!( + "failed to return the original compatibility tool to {}: {error}", + backup.display() + ) + })?; + } + if let Some(displaced) = self.displaced_managed { + fs::rename(&displaced, &self.destination).map_err(|error| { + format!("failed to restore the managed compatibility tool: {error}") + })?; + } + Ok(()) + } + + fn commit(mut self) -> Result<(), Box<(String, Self)>> { + if let Some(displaced) = &self.displaced_managed + && let Err(error) = require_managed_tool(displaced, &self.transaction_id) + { + return Err(Box::new((error, self))); + } + if let Err(error) = fs::remove_file(&self.ownership_path) { + return Err(Box::new(( + format!("{}: {error}", self.ownership_path.display()), + self, + ))); + } + if let Some(displaced) = self.displaced_managed.take() { + // Best-effort reaping keeps the ownership commit rollback-safe. + // Add persisted cleanup if managed leftovers become measurable. + let _ = fs::remove_dir_all(displaced); + } + Ok(()) + } +} + +fn install_compatibility_tool_at( + steam_root: &Path, + source: &Path, + ownership_path: &Path, +) -> Result { + if !source.is_dir() { + return Err(format!( + "Compatibility tool source not found: {}", + source.display() + )); + } + let directory = steam_root.join("compatibilitytools.d"); + fs::create_dir_all(&directory).map_err(|error| error.to_string())?; + let destination = directory.join(ENHANCED_TOOL_NAME); + let existing_ownership = load_tool_ownership(ownership_path)?; + let new_ownership = existing_ownership.is_none(); + let transaction_id = existing_ownership + .as_ref() + .map(|ownership| ownership.transaction_id.clone()) + .unwrap_or_else(unique_suffix); + let original_backup = match &existing_ownership { + Some(ownership) => exact_tool_backup(&directory, ownership)?, + None if tool_directory(&destination)? => Some(unique_tool_path( + &directory, + &format!("{ENHANCED_TOOL_NAME}.patchops.original"), + )), + None => None, + }; + if existing_ownership.is_some() && tool_directory(&destination)? { + require_managed_tool(&destination, &transaction_id)?; + } + + let staging = unique_tool_path(&directory, &format!("{ENHANCED_TOOL_NAME}.patchops.tmp")); + if let Err(error) = copy_directory(source, &staging) { + let _ = fs::remove_dir_all(&staging); + return Err(error); + } + if let Err(error) = update_compatibility_manifest(&staging.join("compatibilitytool.vdf")) { + let _ = fs::remove_dir_all(&staging); + return Err(error); + } + if let Err(error) = atomic_json( + &staging.join(TOOL_MARKER_FILENAME), + &json!({ "transaction_id": transaction_id }), + ) { + let _ = fs::remove_dir_all(&staging); + return Err(error); + } + + if new_ownership { + let original_backup_name = original_backup + .as_ref() + .and_then(|path| path.file_name()) + .and_then(|name| name.to_str()) + .map(str::to_owned); + if let Err(error) = save_tool_ownership( + ownership_path, + &ToolOwnership { + version: TOOL_OWNERSHIP_VERSION, + transaction_id: transaction_id.clone(), + original_backup: original_backup_name, + }, + ) { + let _ = fs::remove_dir_all(&staging); + return Err(error); + } + } + + let displaced_managed = if !new_ownership && tool_directory(&destination)? { + let displaced = unique_tool_path( + &directory, + &format!("{ENHANCED_TOOL_NAME}.patchops.rollback"), + ); + if let Err(error) = fs::rename(&destination, &displaced) { + let _ = fs::remove_dir_all(&staging); + return Err(error.to_string()); + } + Some(displaced) + } else { + None + }; + if new_ownership + && let Some(backup) = &original_backup + && let Err(error) = fs::rename(&destination, backup) + { + let _ = fs::remove_dir_all(&staging); + let _ = fs::remove_file(ownership_path); + return Err(error.to_string()); + } + if let Err(error) = fs::rename(&staging, &destination) { + let _ = fs::remove_dir_all(&staging); + let restore = if new_ownership { + original_backup.as_ref() + } else { + displaced_managed.as_ref() + }; + if let Some(restore) = restore + && let Err(rollback_error) = fs::rename(restore, &destination) + { + return Err(format!( + "{error}; restoring the previous compatibility tool also failed: {rollback_error}" + )); + } + if new_ownership { + let _ = fs::remove_file(ownership_path); + } + return Err(error.to_string()); + } + Ok(ToolInstallTransaction { + destination, + displaced_managed, + original_backup, + ownership_path: ownership_path.to_path_buf(), + transaction_id, + new_ownership, + }) +} + +fn remove_compatibility_tool_at( + steam_root: &Path, + ownership_path: &Path, +) -> Result { + let directory = steam_root.join("compatibilitytools.d"); + let ownership = load_tool_ownership(ownership_path)?.ok_or_else(|| { + "No PatchOpsIII compatibility-tool ownership record exists; refusing unsafe cleanup." + .to_string() + })?; + let destination = directory.join(ENHANCED_TOOL_NAME); + let original_backup = exact_tool_backup(&directory, &ownership)?; + let displaced_managed = if tool_directory(&destination)? { + require_managed_tool(&destination, &ownership.transaction_id)?; + let displaced = + unique_tool_path(&directory, &format!("{ENHANCED_TOOL_NAME}.patchops.remove")); + fs::rename(&destination, &displaced).map_err(|error| error.to_string())?; + Some(displaced) + } else { + None + }; + if let Some(backup) = &original_backup + && let Err(error) = fs::rename(backup, &destination) + { + if let Some(displaced) = &displaced_managed + && let Err(rollback) = fs::rename(displaced, &destination) + { + return Err(format!( + "{error}; restoring the managed compatibility tool also failed: {rollback}" + )); + } + return Err(error.to_string()); + } + Ok(ToolRemovalTransaction { + destination, + displaced_managed, + restored_backup: original_backup, + ownership_path: ownership_path.to_path_buf(), + transaction_id: ownership.transaction_id, + }) +} + +#[derive(Clone)] +struct FileSnapshot { + path: PathBuf, + contents: Option>, +} + +fn capture_files(paths: &[PathBuf]) -> Result, String> { + paths + .iter() + .map(|path| { + let contents = match fs::symlink_metadata(path) { + Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => { + Some(fs::read(path).map_err(|error| { + format!("failed to snapshot {}: {error}", path.display()) + })?) + } + Ok(_) => { + return Err(format!( + "Refusing to snapshot unsafe transaction path: {}", + path.display() + )); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(format!("{}: {error}", path.display())), + }; + Ok(FileSnapshot { + path: path.clone(), + contents, + }) + }) + .collect() +} + +fn restore_files(snapshots: &[FileSnapshot]) -> Result<(), String> { + let mut errors = Vec::new(); + for snapshot in snapshots.iter().rev() { + let result = match &snapshot.contents { + Some(contents) => fs_ops::atomic_write(&snapshot.path, contents), + None => match fs::symlink_metadata(&snapshot.path) { + Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => { + fs::remove_file(&snapshot.path).map_err(|error| error.to_string()) + } + Ok(_) => Err("transaction created an unsafe path".into()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error.to_string()), + }, + }; + if let Err(error) = result { + errors.push(format!("{}: {error}", snapshot.path.display())); + } + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors.join("; ")) + } +} + +fn launch_restore_value(snapshot: &Path) -> Result { + required_json(snapshot)? + .get("launch_options") + .and_then(JsonValue::as_str) + .map(str::to_owned) + .ok_or_else(|| { + format!( + "{} has invalid launch-option ownership data", + snapshot.display() + ) + }) +} + +fn set_enhanced_launch_options_at( + config: &Path, + legacy_backup: &Path, + snapshot: &Path, +) -> Result<(), String> { + match fs::symlink_metadata(snapshot) { + Ok(_) => { + launch_restore_value(snapshot)?; + if read_launch_options_at(config, APP_ID)? != ENHANCED_LAUNCH_OPTIONS { + return Err( + "Steam launch options changed after PatchOpsIII configured them; refusing to overwrite them." + .into(), + ); + } + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let previous = read_launch_options_at(config, APP_ID)?; + atomic_json(snapshot, &json!({ "launch_options": previous }))?; + } + Err(error) => return Err(format!("{}: {error}", snapshot.display())), + } + set_launch_options_at( + config, + legacy_backup, + APP_ID, + ENHANCED_LAUNCH_OPTIONS, + false, + true, + ) + .map(|_| ()) +} + +fn restore_enhanced_launch_options_at( + config: &Path, + legacy_backup: &Path, + snapshot: &Path, +) -> Result<(), String> { + let previous = launch_restore_value(snapshot)?; + if read_launch_options_at(config, APP_ID)? != ENHANCED_LAUNCH_OPTIONS { + return Err( + "Steam launch options changed after PatchOpsIII configured them; leaving them untouched." + .into(), + ); + } + set_launch_options_at(config, legacy_backup, APP_ID, &previous, false, true)?; + fs::remove_file(snapshot).map_err(|error| format!("{}: {error}", snapshot.display())) +} + +fn steam_transaction_files( + steam_config: &Path, + steam_backup: &Path, + mapping_snapshot: &Path, + local_config: &Path, + local_backup: &Path, + launch_snapshot: &Path, +) -> Vec { + vec![ + steam_config.to_path_buf(), + fs_ops::backup_path(steam_config), + steam_backup.to_path_buf(), + mapping_snapshot.to_path_buf(), + local_config.to_path_buf(), + fs_ops::backup_path(local_config), + local_backup.to_path_buf(), + launch_snapshot.to_path_buf(), + ] +} + +fn rollback_error(error: String, file_error: Option, tool_error: Option) -> String { + let mut rollbacks = Vec::new(); + if let Some(error) = file_error { + rollbacks.push(format!("Steam file rollback failed: {error}")); + } + if let Some(error) = tool_error { + rollbacks.push(format!("compatibility tool rollback failed: {error}")); + } + if rollbacks.is_empty() { + error + } else { + format!("{error}; {}", rollbacks.join("; ")) + } +} + +fn finish_tool_install( + tool: ToolInstallTransaction, + snapshots: Vec, + destination: PathBuf, + result: Result<(), String>, +) -> Result { + match result { + Ok(()) => match tool.commit() { + Ok(()) => Ok(destination), + Err(failure) => { + let (error, tool) = *failure; + let file_error = restore_files(&snapshots).err(); + let tool_error = tool.rollback().err(); + Err(rollback_error(error, file_error, tool_error)) + } + }, + Err(error) => { + let file_error = restore_files(&snapshots).err(); + let tool_error = tool.rollback().err(); + Err(rollback_error(error, file_error, tool_error)) + } + } +} + +fn finish_tool_removal( + tool: ToolRemovalTransaction, + snapshots: Vec, + result: Result<(), String>, +) -> Result<(), String> { + match result { + Ok(()) => match tool.commit() { + Ok(()) => Ok(()), + Err(failure) => { + let (error, tool) = *failure; + let file_error = restore_files(&snapshots).err(); + let tool_error = tool.rollback().err(); + Err(rollback_error(error, file_error, tool_error)) + } + }, + Err(error) => { + let file_error = restore_files(&snapshots).err(); + let tool_error = tool.rollback().err(); + Err(rollback_error(error, file_error, tool_error)) + } + } +} + +#[allow(clippy::too_many_arguments)] +fn configure_compatibility_at( + steam_root: &Path, + source: &Path, + trusted_legacy_source: Option<&Path>, + ownership_path: &Path, + steam_config: &Path, + steam_backup: &Path, + mapping_snapshot: &Path, + local_config: &Path, + local_backup: &Path, + launch_snapshot: &Path, +) -> Result { + let snapshots = capture_files(&steam_transaction_files( + steam_config, + steam_backup, + mapping_snapshot, + local_config, + local_backup, + launch_snapshot, + ))?; + let existing_ownership = load_tool_ownership(ownership_path)?; + if existing_ownership.is_some() { + mapping_restore_value(mapping_snapshot)?; + launch_restore_value(launch_snapshot)?; + } + let destination = steam_root + .join("compatibilitytools.d") + .join(ENHANCED_TOOL_NAME); + let destination_exists = match fs::symlink_metadata(&destination) { + Ok(_) => true, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => false, + Err(error) => return Err(format!("{}: {error}", destination.display())), + }; + let adoption = if existing_ownership.is_none() && destination_exists { + Some(adopt_legacy_compatibility_tool_at( + steam_root, + trusted_legacy_source.ok_or_else(|| { + "The exact release10-32 source is required to adopt a legacy compatibility tool." + .to_string() + })?, + ownership_path, + steam_config, + mapping_snapshot, + local_config, + launch_snapshot, + )?) + } else { + None + }; + let result = (|| { + let tool = install_compatibility_tool_at(steam_root, source, ownership_path)?; + let destination = tool.destination.clone(); + let result = (|| { + set_compatibility_mapping_at(steam_config, steam_backup, mapping_snapshot)?; + set_enhanced_launch_options_at(local_config, local_backup, launch_snapshot) + })(); + finish_tool_install(tool, snapshots, destination, result) + })(); + finish_legacy_adoption(adoption, result) +} + +#[allow(clippy::too_many_arguments)] +fn cleanup_compatibility_at( + steam_root: &Path, + trusted_legacy_source: Option<&Path>, + ownership_path: &Path, + steam_config: &Path, + steam_backup: &Path, + mapping_snapshot: &Path, + local_config: &Path, + local_backup: &Path, + launch_snapshot: &Path, +) -> Result<(), String> { + if compatibility_is_fully_clean( + steam_root, + ownership_path, + steam_config, + mapping_snapshot, + local_config, + launch_snapshot, + )? { + return Ok(()); + } + let snapshots = capture_files(&steam_transaction_files( + steam_config, + steam_backup, + mapping_snapshot, + local_config, + local_backup, + launch_snapshot, + ))?; + let adoption = if load_tool_ownership(ownership_path)?.is_none() { + Some(adopt_legacy_compatibility_tool_at( + steam_root, + trusted_legacy_source.ok_or_else(|| { + "The exact release10-32 source is required to adopt a legacy compatibility tool." + .to_string() + })?, + ownership_path, + steam_config, + mapping_snapshot, + local_config, + launch_snapshot, + )?) + } else { + None + }; + let result = (|| { + let tool = remove_compatibility_tool_at(steam_root, ownership_path)?; + let result = (|| { + clear_compatibility_mapping_at(steam_config, steam_backup, mapping_snapshot)?; + restore_enhanced_launch_options_at(local_config, local_backup, launch_snapshot) + })(); + finish_tool_removal(tool, snapshots, result) + })(); + finish_legacy_adoption(adoption, result) +} + +#[cfg(target_os = "linux")] +fn valid_sha256(value: &str) -> bool { + value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +#[cfg(target_os = "linux")] +fn valid_proton_layout(path: &Path) -> bool { + let Ok(metadata) = fs::symlink_metadata(path) else { + return false; + }; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return false; + } + let Ok(root) = path.canonicalize() else { + return false; + }; + [("proton", false), ("files", true)] + .into_iter() + .all(|(name, directory)| { + let entry = path.join(name); + let Ok(resolved) = entry.canonicalize() else { + return false; + }; + resolved.starts_with(&root) + && fs::metadata(resolved).is_ok_and(|metadata| { + if directory { + metadata.is_dir() + } else { + metadata.is_file() + } + }) + }) +} + +#[cfg(target_os = "linux")] +fn valid_cached_proton(path: &Path) -> bool { + if !valid_proton_layout(path) { + return false; + } + let Ok(manifest) = read_vdf(&path.join("compatibilitytool.vdf")) else { + return false; + }; + if !matches!( + value_at( + &manifest, + &["compatibilitytools", "compat_tools", ENHANCED_TOOL_NAME] + ), + Some(VdfValue::Object(_)) + ) { + return false; + } + let Ok(marker) = required_json(&path.join(".patchops-source.json")) else { + return false; + }; + marker.get("tag").and_then(JsonValue::as_str) == Some(ENHANCED_PROTON_TAG) + && marker.get("asset").and_then(JsonValue::as_str) == Some(ENHANCED_PROTON_ASSET) + && marker + .get("sha256") + .and_then(JsonValue::as_str) + .is_some_and(|hash| hash.eq_ignore_ascii_case(ENHANCED_PROTON_SHA256)) +} + +#[cfg(target_os = "linux")] +#[derive(Debug)] +struct TrustedProtonSource { + temporary_root: PathBuf, + source: PathBuf, +} + +#[cfg(target_os = "linux")] +impl Drop for TrustedProtonSource { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.temporary_root); + } +} + +#[cfg(target_os = "linux")] +fn trusted_proton_source_at( + cache_root: &Path, + expected_digest: &str, +) -> Result { + if !valid_sha256(expected_digest) { + return Err("Pinned GDK-Proton archive digest is invalid.".into()); + } + let archive = cache_root.join(format!("GDK-Proton-{ENHANCED_PROTON_TAG}.tar.gz")); + let metadata = fs::symlink_metadata(&archive) + .map_err(|error| format!("Verified GDK-Proton archive is unavailable: {error}"))?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err("Verified GDK-Proton archive is not a regular file.".into()); + } + if !fs_ops::sha256_file(&archive)?.eq_ignore_ascii_case(expected_digest) { + return Err("Cached GDK-Proton archive failed release10-32 verification.".into()); + } + + let temporary_root = cache_root + .parent() + .unwrap_or_else(|| Path::new(".")) + .join(format!(".patchops-trusted-proton-{}", unique_suffix())); + fs::create_dir(&temporary_root).map_err(|error| error.to_string())?; + let result = (|| { + let extraction = temporary_root.join("extracted"); + fs_ops::extract_tar_gz(&archive, &extraction)?; + if !fs_ops::sha256_file(&archive)?.eq_ignore_ascii_case(expected_digest) { + return Err("Cached GDK-Proton archive changed during verification.".into()); + } + let mut candidates = Vec::new(); + if valid_proton_layout(&extraction) { + candidates.push(extraction.clone()); + } + for entry in fs::read_dir(&extraction) + .map_err(|error| format!("{}: {error}", extraction.display()))? + { + let path = entry.map_err(|error| error.to_string())?.path(); + if valid_proton_layout(&path) { + candidates.push(path); + } + } + if candidates.len() != 1 { + return Err( + "Verified GDK-Proton archive did not contain one unambiguous tool directory." + .into(), + ); + } + let source = temporary_root.join("normalized"); + copy_directory(&candidates[0], &source)?; + update_compatibility_manifest(&source.join("compatibilitytool.vdf"))?; + fs::remove_dir_all(&extraction).map_err(|error| error.to_string())?; + Ok(source) + })(); + match result { + Ok(source) => Ok(TrustedProtonSource { + temporary_root, + source, + }), + Err(error) => { + let _ = fs::remove_dir_all(&temporary_root); + Err(error) + } + } +} + +#[cfg(target_os = "linux")] +fn prepare_enhanced_proton_cache(app: &AppState) -> Result { + let cache_root = app.data_dir().join("bo3-enhanced-proton-cache"); + let target = cache_root.join(ENHANCED_TOOL_NAME); + if valid_cached_proton(&target) + && let Ok(trusted) = trusted_proton_source_at(&cache_root, ENHANCED_PROTON_SHA256) + && compare_tool_trees(&trusted.source, &target, true, true).is_ok() + { + return Ok(target); + } + fs::create_dir_all(&cache_root).map_err(|error| error.to_string())?; + let digest = ENHANCED_PROTON_SHA256.to_owned(); + let archive = cache_root.join(format!("GDK-Proton-{ENHANCED_PROTON_TAG}.tar.gz")); + let archive_valid = fs::symlink_metadata(&archive).is_ok_and(|metadata| { + !metadata.file_type().is_symlink() + && metadata.is_file() + && fs_ops::sha256_file(&archive) + .is_ok_and(|actual| actual.eq_ignore_ascii_case(&digest)) + }); + if !archive_valid { + match fs::symlink_metadata(&archive) { + Ok(_) => fs::remove_file(&archive).map_err(|error| error.to_string())?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(error.to_string()), + } + app.log( + "Info", + format!("Downloading BO3 Enhanced Proton ({ENHANCED_PROTON_TAG})..."), + ); + fs_ops::download( + app, + ENHANCED_PROTON_DOWNLOAD, + &archive, + Some(&digest), + MAX_PROTON_ARCHIVE_BYTES, + )?; + } + + let suffix = unique_suffix(); + let extraction = cache_root.join(format!("extract-{ENHANCED_PROTON_TAG}-{suffix}")); + let staging = cache_root.join(format!("{ENHANCED_TOOL_NAME}.tmp-{suffix}")); + let backup = cache_root.join(format!("{ENHANCED_TOOL_NAME}.patchops.bak-{suffix}")); + let result = (|| { + fs_ops::extract_tar_gz(&archive, &extraction)?; + let source = if valid_proton_layout(&extraction) { + extraction.clone() + } else { + fs::read_dir(&extraction) + .map_err(|error| error.to_string())? + .filter_map(Result::ok) + .map(|entry| entry.path()) + .find(|path| valid_proton_layout(path)) + .ok_or_else(|| { + "Verified GDK-Proton archive did not contain a valid tool directory." + .to_string() + })? + }; + fs::rename(&source, &staging).map_err(|error| error.to_string())?; + update_compatibility_manifest(&staging.join("compatibilitytool.vdf"))?; + atomic_json( + &staging.join(".patchops-source.json"), + &json!({ + "tag": ENHANCED_PROTON_TAG, + "asset": ENHANCED_PROTON_ASSET, + "sha256": digest, + }), + )?; + if !valid_cached_proton(&staging) { + return Err("Prepared GDK-Proton cache failed validation.".into()); + } + + let moved_existing = if tool_directory(&target)? { + fs::rename(&target, &backup).map_err(|error| error.to_string())?; + true + } else { + false + }; + if let Err(error) = fs::rename(&staging, &target) { + if moved_existing && let Err(rollback_error) = fs::rename(&backup, &target) { + return Err(format!( + "{error}; restoring the previous Proton cache also failed: {rollback_error}" + )); + } + return Err(error.to_string()); + } + if moved_existing { + let _ = fs::remove_dir_all(&backup); + } + Ok(target.clone()) + })(); + let _ = fs::remove_dir_all(&extraction); + if result.is_err() { + let _ = fs::remove_dir_all(&staging); + } + if result.is_ok() { + app.log( + "Success", + format!( + "Prepared verified BO3 Enhanced Proton cache at {}.", + target.display() + ), + ); + } + result +} + +#[cfg(target_os = "linux")] +fn resolve_enhanced_tool_source( + app: &AppState, + preferred: Option<&Path>, +) -> Result { + if let Some(preferred) = preferred { + if valid_proton_layout(preferred) { + return Ok(preferred.to_path_buf()); + } + return Err(format!( + "Compatibility tool source not found: {}", + preferred.display() + )); + } + if let Some(resources) = app.resource_dir() { + let bundled = resources.join("bo3-enhanced-proton/BO3 Enhanced"); + if valid_proton_layout(&bundled) { + return Ok(bundled); + } + } + prepare_enhanced_proton_cache(app) +} + +pub fn configure_bo3_enhanced_linux( + app: &AppState, + tool_source: Option<&Path>, +) -> Result<(), String> { + #[cfg(not(target_os = "linux"))] + { + let _ = (app, tool_source); + return Ok(()); + } + #[cfg(target_os = "linux")] + { + let root = + steam_root().ok_or_else(|| "Steam root path could not be resolved.".to_string())?; + let user = user_id_in(&root).ok_or_else(|| "Steam user ID not found.".to_string())?; + let local_config = local_config_path_in(&root, &user) + .ok_or_else(|| "Steam userdata path not found.".to_string())?; + let steam_config = root.join("config/config.vdf"); + let steam_backup = app.data_dir().join("backups/steam_config_backup.vdf"); + let local_backup = app.data_dir().join("backups/localconfig_backup.vdf"); + let mapping_snapshot = mapping_snapshot_path(app.data_dir()); + let launch_snapshot = launch_snapshot_path(app.data_dir()); + let ownership = tool_ownership_path(app.data_dir()); + let destination = root.join("compatibilitytools.d").join(ENHANCED_TOOL_NAME); + let destination_exists = match fs::symlink_metadata(&destination) { + Ok(_) => true, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => false, + Err(error) => return Err(format!("{}: {error}", destination.display())), + }; + let trusted = if load_tool_ownership(&ownership)?.is_none() && destination_exists { + Some(trusted_proton_source_at( + &app.data_dir().join("bo3-enhanced-proton-cache"), + ENHANCED_PROTON_SHA256, + )?) + } else { + None + }; + let source = match &trusted { + Some(trusted) => trusted.source.clone(), + None => resolve_enhanced_tool_source(app, tool_source)?, + }; + + close_steam(app)?; + let result = configure_compatibility_at( + &root, + &source, + trusted.as_ref().map(|trusted| trusted.source.as_path()), + &ownership, + &steam_config, + &steam_backup, + &mapping_snapshot, + &local_config, + &local_backup, + &launch_snapshot, + ) + .map(|destination| { + app.log( + "Success", + format!( + "Installed '{ENHANCED_TOOL_NAME}' to {}.", + destination.display() + ), + ); + }); + if let Err(error) = open_steam(app) { + app.log("Warning", format!("Steam could not be reopened: {error}")); + } + if result.is_ok() { + app.log( + "Success", + "Linux BO3 Enhanced compatibility tool, mapping, and launch options configured.", + ); + } + result + } +} + +pub fn cleanup_bo3_enhanced_linux(app: &AppState) -> Result<(), String> { + #[cfg(not(target_os = "linux"))] + { + let _ = app; + return Ok(()); + } + #[cfg(target_os = "linux")] + { + let root = + steam_root().ok_or_else(|| "Steam root path could not be resolved.".to_string())?; + let user = user_id_in(&root).ok_or_else(|| "Steam user ID not found.".to_string())?; + let local_config = local_config_path_in(&root, &user) + .ok_or_else(|| "Steam userdata path not found.".to_string())?; + let steam_config = root.join("config/config.vdf"); + let steam_backup = app.data_dir().join("backups/steam_config_backup.vdf"); + let local_backup = app.data_dir().join("backups/localconfig_backup.vdf"); + let mapping_snapshot = mapping_snapshot_path(app.data_dir()); + let launch_snapshot = launch_snapshot_path(app.data_dir()); + let ownership = tool_ownership_path(app.data_dir()); + if compatibility_is_fully_clean( + &root, + &ownership, + &steam_config, + &mapping_snapshot, + &local_config, + &launch_snapshot, + )? { + app.log( + "Info", + "Linux BO3 Enhanced compatibility state was already clean.", + ); + return Ok(()); + } + let trusted = if load_tool_ownership(&ownership)?.is_none() { + Some(trusted_proton_source_at( + &app.data_dir().join("bo3-enhanced-proton-cache"), + ENHANCED_PROTON_SHA256, + )?) + } else { + None + }; + close_steam(app)?; + + let result = cleanup_compatibility_at( + &root, + trusted.as_ref().map(|trusted| trusted.source.as_path()), + &ownership, + &steam_config, + &steam_backup, + &mapping_snapshot, + &local_config, + &local_backup, + &launch_snapshot, + ); + if let Err(error) = open_steam(app) { + app.log("Warning", format!("Steam could not be reopened: {error}")); + } + if result.is_ok() { + app.log( + "Success", + "Linux BO3 Enhanced compatibility mapping, launch options, and tool removed.", + ); + } + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture_dir(name: &str) -> PathBuf { + let path = env::temp_dir().join(format!( + "patchops-steam-{name}-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir_all(&path).unwrap(); + path + } + + fn string_value<'a>(document: &'a VdfObject, path: &[&str]) -> &'a str { + match value_at(document, path).unwrap() { + VdfValue::String(value) => value, + VdfValue::Object(_) => panic!("expected string"), + } + } + + fn tool_source(root: &Path) -> PathBuf { + let source = root.join("source"); + fs::create_dir_all(source.join("files")).unwrap(); + fs::write(source.join("proton"), b"managed proton").unwrap(); + source + } + + fn normalized_tool_source(root: &Path, name: &str, proton: &[u8]) -> PathBuf { + let source = root.join(name); + fs::create_dir_all(source.join("files")).unwrap(); + fs::write(source.join("proton"), proton).unwrap(); + fs::write(source.join("files/payload"), b"release payload").unwrap(); + update_compatibility_manifest(&source.join("compatibilitytool.vdf")).unwrap(); + source + } + + fn steam_config(options: &str) -> String { + format!( + r#""UserLocalConfigStore" {{ + "Software" {{ "Valve" {{ "Steam" {{ "apps" {{ + "{APP_ID}" {{ "LaunchOptions" {} }} + }} }} }} }} +}}"#, + quote_vdf(options) + ) + } + + fn compatibility_config() -> &'static str { + r#""InstallConfigStore" { + "Unrelated" "preserve" + "Software" { "Valve" { "Steam" { "CompatToolMapping" { + "311210" { "name" "GE-Proton" "priority" "75" "custom" "yes" } + } } } } +}"# + } + + fn enhanced_compatibility_config() -> &'static str { + r#""InstallConfigStore" +{ + "Software" + { + "Valve" + { + "Steam" + { + "CompatToolMapping" + { + "311210" { "name" "BO3 Enhanced" "config" "" "priority" "250" } + } + } + } + } +}"# + } + + struct LegacyCompatibilityFixture { + root: PathBuf, + steam: PathBuf, + trusted: PathBuf, + destination: PathBuf, + steam_vdf: PathBuf, + local_vdf: PathBuf, + steam_backup: PathBuf, + local_backup: PathBuf, + mapping_snapshot: PathBuf, + launch_snapshot: PathBuf, + ownership: PathBuf, + } + + fn legacy_compatibility_fixture(label: &str) -> LegacyCompatibilityFixture { + let root = fixture_dir(label); + let steam = root.join("steam"); + let trusted = normalized_tool_source(&root, "trusted", b"legacy proton"); + let destination = steam.join("compatibilitytools.d").join(ENHANCED_TOOL_NAME); + copy_directory(&trusted, &destination).unwrap(); + let steam_vdf = steam.join("config/config.vdf"); + let local_vdf = steam.join("userdata/1/config/localconfig.vdf"); + fs::create_dir_all(steam_vdf.parent().unwrap()).unwrap(); + fs::create_dir_all(local_vdf.parent().unwrap()).unwrap(); + fs::write(&steam_vdf, enhanced_compatibility_config()).unwrap(); + fs::write(&local_vdf, steam_config(ENHANCED_LAUNCH_OPTIONS)).unwrap(); + let backups = root.join("data/backups"); + let mapping_snapshot = backups.join("compat_mapping_311210.json"); + let launch_snapshot = backups.join("launch_options_311210.json"); + let ownership = backups.join("compat_tool_311210.json"); + atomic_json( + &mapping_snapshot, + &json!({ + "had_entry": true, + "entry": { "name": "GE-Proton", "priority": "75", "custom": "yes" } + }), + ) + .unwrap(); + atomic_json(&launch_snapshot, &json!({ "launch_options": "-novid" })).unwrap(); + LegacyCompatibilityFixture { + root, + steam, + trusted, + destination, + steam_vdf, + local_vdf, + steam_backup: backups.join("steam_config_backup.vdf"), + local_backup: backups.join("localconfig_backup.vdf"), + mapping_snapshot, + launch_snapshot, + ownership, + } + } + + fn assert_legacy_unowned(fixture: &LegacyCompatibilityFixture) { + assert!(!fixture.ownership.exists()); + assert!(!fixture.destination.join(TOOL_MARKER_FILENAME).exists()); + assert_eq!( + fs::read(fixture.destination.join("proton")).unwrap(), + b"legacy proton" + ); + assert_eq!( + read_launch_options_at(&fixture.local_vdf, APP_ID).unwrap(), + ENHANCED_LAUNCH_OPTIONS + ); + } + + fn adopt_fixture(fixture: &LegacyCompatibilityFixture) -> Result { + adopt_legacy_compatibility_tool_at( + &fixture.steam, + &fixture.trusted, + &fixture.ownership, + &fixture.steam_vdf, + &fixture.mapping_snapshot, + &fixture.local_vdf, + &fixture.launch_snapshot, + ) + } + + #[cfg(target_os = "linux")] + fn proton_archive_fixture(cache_root: &Path) -> (PathBuf, String) { + use flate2::{Compression, write::GzEncoder}; + use tar::{Builder, EntryType, Header}; + + fs::create_dir_all(cache_root).unwrap(); + let archive = cache_root.join(format!("GDK-Proton-{ENHANCED_PROTON_TAG}.tar.gz")); + let encoder = GzEncoder::new(fs::File::create(&archive).unwrap(), Compression::default()); + let mut builder = Builder::new(encoder); + for path in [ + "GDK-Proton10-32/", + "GDK-Proton10-32/files/", + "GDK-Proton10-32/protonfixes/", + "GDK-Proton10-32/protonfixes/gamefixes-steam/", + "GDK-Proton10-32/protonfixes/gamefixes-umu/", + ] { + let mut header = Header::new_gnu(); + header.set_entry_type(EntryType::Directory); + header.set_size(0); + header.set_mode(0o755); + header.set_path(path).unwrap(); + header.set_cksum(); + builder.append(&header, std::io::empty()).unwrap(); + } + { + let mut append = |path: &str, contents: &[u8]| { + let mut header = Header::new_gnu(); + header.set_entry_type(EntryType::Regular); + header.set_size(contents.len() as u64); + header.set_mode(0o755); + header.set_path(path).unwrap(); + header.set_cksum(); + builder.append(&header, contents).unwrap(); + }; + append("GDK-Proton10-32/proton", b"proton"); + append("GDK-Proton10-32/files/payload", b"payload"); + append( + "GDK-Proton10-32/compatibilitytool.vdf", + br#""compatibilitytools" { "compat_tools" { "GDK-Proton10-32" { "install_path" "." "display_name" "GDK-Proton10-32" "from_oslist" "windows" "to_oslist" "linux" } } }"#, + ); + append( + "GDK-Proton10-32/protonfixes/gamefixes-steam/271590.py", + b"fix", + ); + } + let mut link = Header::new_gnu(); + link.set_entry_type(EntryType::Symlink); + link.set_size(0); + link.set_mode(0o777); + link.set_path("GDK-Proton10-32/protonfixes/gamefixes-umu/umu-271590.py") + .unwrap(); + link.set_link_name("../gamefixes-steam/271590.py").unwrap(); + link.set_cksum(); + builder.append(&link, std::io::empty()).unwrap(); + builder.into_inner().unwrap().finish().unwrap(); + let digest = fs_ops::sha256_file(&archive).unwrap(); + (archive, digest) + } + + #[test] + fn vdf_launch_update_preserves_unknown_fields_and_original_backup() { + let root = fixture_dir("vdf"); + let config = root.join("localconfig.vdf"); + let legacy = root.join("backups/localconfig_backup.vdf"); + let fixture = r#" +"UserLocalConfigStore" +{ + "UnknownTop" "keep me" + "Software" + { + "Valve" + { + "Steam" + { + "apps" + { + "311210" + { + "LaunchOptions" "-novid +set fs_game old" + "Cloud" { "enabled" "1" } + } + } + } + } + } +} +"OtherRoot" "still here" [$LINUX] +"#; + fs::write(&config, fixture).unwrap(); + + let options = set_launch_options_at( + &config, + &legacy, + APP_ID, + "+set fs_game 2994481309", + false, + false, + ) + .unwrap(); + assert_eq!(options, "-novid +set fs_game 2994481309"); + let updated = read_vdf(&config).unwrap(); + assert_eq!( + string_value(&updated, &["UserLocalConfigStore", "UnknownTop"]), + "keep me" + ); + assert_eq!( + string_value( + &updated, + &[ + "UserLocalConfigStore", + "Software", + "Valve", + "Steam", + "apps", + APP_ID, + "Cloud", + "enabled", + ], + ), + "1" + ); + assert_eq!(string_value(&updated, &["OtherRoot"]), "still here"); + assert_eq!(fs::read(&legacy).unwrap(), fixture.as_bytes()); + assert_eq!( + fs::read(fs_ops::backup_path(&config)).unwrap(), + fixture.as_bytes() + ); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn discovers_modern_and_legacy_library_folders_and_game() { + let root = fixture_dir("libraries"); + let primary = root.join("primary"); + let legacy = root.join("legacy"); + let modern = root.join("modern"); + for directory in [&primary, &legacy, &modern] { + fs::create_dir_all(directory.join("steamapps")).unwrap(); + } + fs::write( + primary.join("steamapps/libraryfolders.vdf"), + format!( + "\"libraryfolders\"\n{{\n\"0\" \"{}\"\n\"1\" {{ \"path\" \"{}\" \"apps\" {{ \"311210\" \"1\" }} }}\n}}", + legacy.display(), + modern.display() + ), + ) + .unwrap(); + let game = modern.join("steamapps/common").join(GAME_DIRECTORY); + fs::create_dir_all(&game).unwrap(); + fs::write(game.join("BlackOps3.exe"), b"exe").unwrap(); + + let libraries = library_paths_from_roots(vec![primary.clone()]); + assert_eq!(libraries, vec![primary, legacy, modern.clone()]); + assert!(has_game_executable( + &modern.join("steamapps/common").join(GAME_DIRECTORY) + )); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn selects_only_numeric_steam_users_deterministically() { + let root = fixture_dir("users"); + for name in ["config", "200", "00010", "9x", "9"] { + fs::create_dir_all(root.join("userdata").join(name)).unwrap(); + } + assert_eq!(user_id_in(&root).as_deref(), Some("9")); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn selects_most_recent_login_user_when_present() { + let root = fixture_dir("recent-user"); + fs::create_dir_all(root.join("userdata/10")).unwrap(); + fs::create_dir_all(root.join("userdata/20")).unwrap(); + fs::create_dir_all(root.join("config")).unwrap(); + fs::write( + root.join("config/loginusers.vdf"), + format!( + r#""users" {{ + "{}" {{ "MostRecent" "0" }} + "{}" {{ "MostRecent" "1" }} +}}"#, + STEAM_ID64_OFFSET + 10, + STEAM_ID64_OFFSET + 20, + ), + ) + .unwrap(); + + assert_eq!(user_id_in(&root).as_deref(), Some("20")); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn steam_exit_wait_is_bounded_and_needs_no_test_sleep() { + let mut checks = 0; + let mut pauses = 0; + wait_for_steam_exit_with( + 5, + || { + checks += 1; + Ok(checks < 3) + }, + || pauses += 1, + ) + .unwrap(); + assert_eq!(checks, 3); + assert_eq!(pauses, 2); + + let mut timeout_checks = 0; + let error = wait_for_steam_exit_with( + 3, + || { + timeout_checks += 1; + Ok(true) + }, + || {}, + ) + .unwrap_err(); + assert_eq!(timeout_checks, 3); + assert!(error.contains("did not exit")); + } + + #[test] + fn reports_subscribed_then_installed_workshop_state() { + let root = fixture_dir("workshop"); + let workshop = root.join("steamapps/workshop"); + fs::create_dir_all(&workshop).unwrap(); + fs::write( + workshop.join("appworkshop_311210.acf"), + r#""AppWorkshop" { "WorkshopItemsInstalled" { "2994481309" { "size" "4" } } }"#, + ) + .unwrap(); + let libraries = vec![root.clone()]; + let state = workshop_item_state_in(&libraries, APP_ID, "2994481309"); + assert_eq!(state.state, "Subscribed (not installed yet)"); + assert!(state.subscribed); + assert!(!state.installed); + + let content = workshop.join("content/311210/2994481309"); + fs::create_dir_all(&content).unwrap(); + fs::write(content.join("mod.ff"), b"data").unwrap(); + let state = workshop_item_state_in(&libraries, APP_ID, "2994481309"); + assert_eq!(state.state, "Installed"); + assert!(state.installed); + assert_eq!(state.path.as_deref(), Some(content.as_path())); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn mapping_snapshot_restores_prior_entry_and_unknown_fields() { + let root = fixture_dir("mapping"); + let config = root.join("config.vdf"); + let legacy = root.join("backups/steam_config_backup.vdf"); + let snapshot = root.join("backups/compat_mapping_311210.json"); + fs::write( + &config, + r#" +"InstallConfigStore" { + "Unrelated" "preserve" + "Software" { "Valve" { "Steam" { "CompatToolMapping" { + "311210" { "name" "GE-Proton" "priority" "75" "custom" "yes" } + "other" { "name" "Other Tool" } + } } } } +} +"#, + ) + .unwrap(); + + set_compatibility_mapping_at(&config, &legacy, &snapshot).unwrap(); + let mapped = read_vdf(&config).unwrap(); + assert_eq!( + string_value( + &mapped, + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + APP_ID, + "name", + ], + ), + ENHANCED_TOOL_NAME + ); + assert_eq!( + string_value(&mapped, &["InstallConfigStore", "Unrelated"]), + "preserve" + ); + + clear_compatibility_mapping_at(&config, &legacy, &snapshot).unwrap(); + let restored = read_vdf(&config).unwrap(); + assert_eq!( + string_value( + &restored, + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + APP_ID, + "name", + ], + ), + "GE-Proton" + ); + assert_eq!( + string_value( + &restored, + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + APP_ID, + "custom", + ], + ), + "yes" + ); + assert!(!snapshot.exists()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn exact_legacy_tool_is_adopted_and_cleaned_up() { + let fixture = legacy_compatibility_fixture("legacy-cleanup"); + + cleanup_compatibility_at( + &fixture.steam, + Some(&fixture.trusted), + &fixture.ownership, + &fixture.steam_vdf, + &fixture.steam_backup, + &fixture.mapping_snapshot, + &fixture.local_vdf, + &fixture.local_backup, + &fixture.launch_snapshot, + ) + .unwrap(); + + assert!(!fixture.destination.exists()); + assert!(!fixture.ownership.exists()); + assert!(!fixture.mapping_snapshot.exists()); + assert!(!fixture.launch_snapshot.exists()); + assert_eq!( + string_value( + &read_vdf(&fixture.steam_vdf).unwrap(), + &[ + "InstallConfigStore", + "Software", + "Valve", + "Steam", + "CompatToolMapping", + APP_ID, + "name", + ], + ), + "GE-Proton" + ); + assert_eq!( + read_launch_options_at(&fixture.local_vdf, APP_ID).unwrap(), + "-novid" + ); + fs::remove_dir_all(fixture.root).unwrap(); + } + + #[test] + fn exact_legacy_tool_can_be_upgraded_then_cleaned_up() { + let fixture = legacy_compatibility_fixture("legacy-upgrade"); + let upgrade = normalized_tool_source(&fixture.root, "upgrade", b"upgraded proton"); + + configure_compatibility_at( + &fixture.steam, + &upgrade, + Some(&fixture.trusted), + &fixture.ownership, + &fixture.steam_vdf, + &fixture.steam_backup, + &fixture.mapping_snapshot, + &fixture.local_vdf, + &fixture.local_backup, + &fixture.launch_snapshot, + ) + .unwrap(); + assert_eq!( + fs::read(fixture.destination.join("proton")).unwrap(), + b"upgraded proton" + ); + assert_eq!( + load_tool_ownership(&fixture.ownership) + .unwrap() + .unwrap() + .original_backup, + None + ); + + cleanup_compatibility_at( + &fixture.steam, + None, + &fixture.ownership, + &fixture.steam_vdf, + &fixture.steam_backup, + &fixture.mapping_snapshot, + &fixture.local_vdf, + &fixture.local_backup, + &fixture.launch_snapshot, + ) + .unwrap(); + assert!(!fixture.destination.exists()); + assert_eq!( + read_launch_options_at(&fixture.local_vdf, APP_ID).unwrap(), + "-novid" + ); + fs::remove_dir_all(fixture.root).unwrap(); + } + + #[test] + fn compatibility_cleanup_is_idempotent_only_for_the_fully_clean_tuple() { + let clean = legacy_compatibility_fixture("compat-clean-retry"); + fs::remove_dir_all(&clean.destination).unwrap(); + fs::remove_file(&clean.mapping_snapshot).unwrap(); + fs::remove_file(&clean.launch_snapshot).unwrap(); + fs::write(&clean.steam_vdf, compatibility_config()).unwrap(); + fs::write(&clean.local_vdf, steam_config("-novid")).unwrap(); + let steam_before = fs::read(&clean.steam_vdf).unwrap(); + let local_before = fs::read(&clean.local_vdf).unwrap(); + + cleanup_compatibility_at( + &clean.steam, + None, + &clean.ownership, + &clean.steam_vdf, + &clean.steam_backup, + &clean.mapping_snapshot, + &clean.local_vdf, + &clean.local_backup, + &clean.launch_snapshot, + ) + .unwrap(); + + assert_eq!(fs::read(&clean.steam_vdf).unwrap(), steam_before); + assert_eq!(fs::read(&clean.local_vdf).unwrap(), local_before); + assert!(!clean.ownership.exists()); + assert!(!clean.destination.exists()); + fs::remove_dir_all(clean.root).unwrap(); + } + + #[test] + fn compatibility_cleanup_rejects_partial_unowned_state_without_mutation() { + let stale_snapshot = legacy_compatibility_fixture("compat-partial-snapshot"); + fs::remove_dir_all(&stale_snapshot.destination).unwrap(); + fs::remove_file(&stale_snapshot.launch_snapshot).unwrap(); + fs::write(&stale_snapshot.steam_vdf, compatibility_config()).unwrap(); + fs::write(&stale_snapshot.local_vdf, steam_config("-novid")).unwrap(); + let mapping_before = fs::read(&stale_snapshot.mapping_snapshot).unwrap(); + let error = cleanup_compatibility_at( + &stale_snapshot.steam, + Some(&stale_snapshot.trusted), + &stale_snapshot.ownership, + &stale_snapshot.steam_vdf, + &stale_snapshot.steam_backup, + &stale_snapshot.mapping_snapshot, + &stale_snapshot.local_vdf, + &stale_snapshot.local_backup, + &stale_snapshot.launch_snapshot, + ) + .unwrap_err(); + assert!(error.contains("missing"), "{error}"); + assert_eq!( + fs::read(&stale_snapshot.mapping_snapshot).unwrap(), + mapping_before + ); + assert!(!stale_snapshot.ownership.exists()); + assert!(!stale_snapshot.destination.exists()); + fs::remove_dir_all(stale_snapshot.root).unwrap(); + + let managed_values = legacy_compatibility_fixture("compat-partial-values"); + fs::remove_dir_all(&managed_values.destination).unwrap(); + fs::remove_file(&managed_values.mapping_snapshot).unwrap(); + fs::remove_file(&managed_values.launch_snapshot).unwrap(); + let steam_before = fs::read(&managed_values.steam_vdf).unwrap(); + let local_before = fs::read(&managed_values.local_vdf).unwrap(); + let error = cleanup_compatibility_at( + &managed_values.steam, + Some(&managed_values.trusted), + &managed_values.ownership, + &managed_values.steam_vdf, + &managed_values.steam_backup, + &managed_values.mapping_snapshot, + &managed_values.local_vdf, + &managed_values.local_backup, + &managed_values.launch_snapshot, + ) + .unwrap_err(); + assert!(error.contains("missing"), "{error}"); + assert_eq!(fs::read(&managed_values.steam_vdf).unwrap(), steam_before); + assert_eq!(fs::read(&managed_values.local_vdf).unwrap(), local_before); + assert!(!managed_values.ownership.exists()); + fs::remove_dir_all(managed_values.root).unwrap(); + } + + #[test] + fn owned_reconfigure_requires_both_restore_snapshots_before_mutation() { + for (label, remove_mapping) in [ + ("owned-missing-mapping", true), + ("owned-missing-launch", false), + ] { + let fixture = legacy_compatibility_fixture(label); + let installed = normalized_tool_source(&fixture.root, "installed", b"installed"); + configure_compatibility_at( + &fixture.steam, + &installed, + Some(&fixture.trusted), + &fixture.ownership, + &fixture.steam_vdf, + &fixture.steam_backup, + &fixture.mapping_snapshot, + &fixture.local_vdf, + &fixture.local_backup, + &fixture.launch_snapshot, + ) + .unwrap(); + let missing = if remove_mapping { + &fixture.mapping_snapshot + } else { + &fixture.launch_snapshot + }; + fs::remove_file(missing).unwrap(); + let ownership_before = fs::read(&fixture.ownership).unwrap(); + let marker_before = fs::read(fixture.destination.join(TOOL_MARKER_FILENAME)).unwrap(); + let steam_before = fs::read(&fixture.steam_vdf).unwrap(); + let local_before = fs::read(&fixture.local_vdf).unwrap(); + let replacement = normalized_tool_source(&fixture.root, "replacement", b"replacement"); + + let error = configure_compatibility_at( + &fixture.steam, + &replacement, + None, + &fixture.ownership, + &fixture.steam_vdf, + &fixture.steam_backup, + &fixture.mapping_snapshot, + &fixture.local_vdf, + &fixture.local_backup, + &fixture.launch_snapshot, + ) + .unwrap_err(); + assert!(error.contains(&missing.display().to_string()), "{error}"); + assert!(!missing.exists()); + assert_eq!(fs::read(&fixture.ownership).unwrap(), ownership_before); + assert_eq!( + fs::read(fixture.destination.join(TOOL_MARKER_FILENAME)).unwrap(), + marker_before + ); + assert_eq!( + fs::read(fixture.destination.join("proton")).unwrap(), + b"installed" + ); + assert_eq!(fs::read(&fixture.steam_vdf).unwrap(), steam_before); + assert_eq!(fs::read(&fixture.local_vdf).unwrap(), local_before); + fs::remove_dir_all(fixture.root).unwrap(); + } + } + + #[test] + fn legacy_adoption_rejects_missing_or_mismatched_state_without_mutation() { + let missing = legacy_compatibility_fixture("legacy-missing-snapshot"); + fs::remove_file(&missing.launch_snapshot).unwrap(); + assert!( + adopt_fixture(&missing) + .unwrap_err() + .contains("launch_options") + ); + assert_legacy_unowned(&missing); + fs::remove_dir_all(missing.root).unwrap(); + + let mapping = legacy_compatibility_fixture("legacy-mapping-mismatch"); + fs::write(&mapping.steam_vdf, compatibility_config()).unwrap(); + assert!(adopt_fixture(&mapping).unwrap_err().contains("mapping")); + assert_legacy_unowned(&mapping); + fs::remove_dir_all(mapping.root).unwrap(); + + let launch = legacy_compatibility_fixture("legacy-launch-mismatch"); + fs::write(&launch.local_vdf, steam_config("-novid")).unwrap(); + assert!( + adopt_fixture(&launch) + .unwrap_err() + .contains("launch options") + ); + assert!(!launch.ownership.exists()); + assert!(!launch.destination.join(TOOL_MARKER_FILENAME).exists()); + fs::remove_dir_all(launch.root).unwrap(); + } + + #[test] + fn legacy_adoption_rejects_payload_mismatch_and_backup_sibling_without_mutation() { + let mismatch = legacy_compatibility_fixture("legacy-payload-mismatch"); + fs::write(mismatch.destination.join("proton"), b"modified").unwrap(); + assert!( + adopt_fixture(&mismatch) + .unwrap_err() + .contains("bytes differ") + ); + assert!(!mismatch.ownership.exists()); + assert!(!mismatch.destination.join(TOOL_MARKER_FILENAME).exists()); + assert_eq!( + fs::read(mismatch.destination.join("proton")).unwrap(), + b"modified" + ); + fs::remove_dir_all(mismatch.root).unwrap(); + + let backup = legacy_compatibility_fixture("legacy-backup"); + let ambiguous = backup + .steam + .join("compatibilitytools.d/BO3 Enhanced.patchops.bak-20240101-000000"); + fs::create_dir_all(&ambiguous).unwrap(); + assert!(adopt_fixture(&backup).unwrap_err().contains("ambiguous")); + assert_legacy_unowned(&backup); + assert!(ambiguous.exists()); + fs::remove_dir_all(backup.root).unwrap(); + } + + #[cfg(unix)] + #[test] + fn legacy_adoption_rejects_a_symlinked_payload_without_mutation() { + use std::os::unix::fs::symlink; + + let fixture = legacy_compatibility_fixture("legacy-symlink"); + fs::remove_file(fixture.destination.join("files/payload")).unwrap(); + symlink( + fixture.trusted.join("files/payload"), + fixture.destination.join("files/payload"), + ) + .unwrap(); + + assert!(adopt_fixture(&fixture).unwrap_err().contains("unsafe link")); + assert!(!fixture.ownership.exists()); + assert!(!fixture.destination.join(TOOL_MARKER_FILENAME).exists()); + assert!( + fs::symlink_metadata(fixture.destination.join("files/payload")) + .unwrap() + .file_type() + .is_symlink() + ); + fs::remove_dir_all(fixture.root).unwrap(); + } + + #[cfg(unix)] + #[test] + fn safe_relative_source_links_are_materialized_and_escaping_links_are_rejected() { + use std::os::unix::fs::symlink; + + let root = fixture_dir("source-links"); + let source = normalized_tool_source(&root, "linked-source", b"proton"); + fs::create_dir_all(source.join("protonfixes/gamefixes-steam")).unwrap(); + fs::create_dir_all(source.join("protonfixes/gamefixes-umu")).unwrap(); + fs::write( + source.join("protonfixes/gamefixes-steam/271590.py"), + b"trusted fix", + ) + .unwrap(); + symlink( + "../gamefixes-steam/271590.py", + source.join("protonfixes/gamefixes-umu/umu-271590.py"), + ) + .unwrap(); + let copied = root.join("copied"); + copy_directory(&source, &copied).unwrap(); + assert_eq!( + fs::read(copied.join("protonfixes/gamefixes-umu/umu-271590.py")).unwrap(), + b"trusted fix" + ); + assert!( + fs::symlink_metadata(copied.join("protonfixes/gamefixes-umu/umu-271590.py")) + .unwrap() + .is_file() + ); + + let outside = root.join("outside"); + fs::write(&outside, b"outside").unwrap(); + symlink( + "../../../outside", + source.join("protonfixes/gamefixes-umu/escape"), + ) + .unwrap(); + let error = copy_directory(&source, &root.join("rejected")).unwrap_err(); + assert!(error.contains("escapes its source"), "{error}"); + assert_eq!(fs::read(outside).unwrap(), b"outside"); + fs::remove_dir_all(root).unwrap(); + } + + #[cfg(all(target_os = "linux", unix))] + #[test] + fn trusted_archive_requires_exact_regular_bytes_and_materializes_safe_links() { + use std::os::unix::fs::symlink; + + let root = fixture_dir("trusted-archive"); + let cache = root.join("cache"); + let (archive, digest) = proton_archive_fixture(&cache); + { + let trusted = trusted_proton_source_at(&cache, &digest).unwrap(); + assert_eq!(fs::read(trusted.source.join("proton")).unwrap(), b"proton"); + let link = trusted + .source + .join("protonfixes/gamefixes-umu/umu-271590.py"); + assert_eq!(fs::read(&link).unwrap(), b"fix"); + assert!(!fs::symlink_metadata(link).unwrap().file_type().is_symlink()); + } + assert!(archive.is_file()); + + fs::remove_file(&archive).unwrap(); + assert!( + trusted_proton_source_at(&cache, &digest) + .unwrap_err() + .contains("unavailable") + ); + let outside = root.join("outside.tar.gz"); + fs::write(&outside, b"not trusted").unwrap(); + symlink(&outside, &archive).unwrap(); + assert!( + trusted_proton_source_at(&cache, &digest) + .unwrap_err() + .contains("regular file") + ); + assert!( + fs::symlink_metadata(&archive) + .unwrap() + .file_type() + .is_symlink() + ); + assert_eq!(fs::read(outside).unwrap(), b"not trusted"); + fs::remove_dir_all(root).unwrap(); + } + + #[cfg(unix)] + #[test] + fn failed_configure_rolls_legacy_adoption_back_to_unowned() { + use std::os::unix::fs::symlink; + + let fixture = legacy_compatibility_fixture("legacy-rollback"); + let invalid_source = normalized_tool_source(&fixture.root, "invalid-upgrade", b"new"); + fs::write(fixture.root.join("outside"), b"outside").unwrap(); + symlink("../../outside", invalid_source.join("files/escape")).unwrap(); + + let error = configure_compatibility_at( + &fixture.steam, + &invalid_source, + Some(&fixture.trusted), + &fixture.ownership, + &fixture.steam_vdf, + &fixture.steam_backup, + &fixture.mapping_snapshot, + &fixture.local_vdf, + &fixture.local_backup, + &fixture.launch_snapshot, + ) + .unwrap_err(); + assert!(error.contains("escapes its source"), "{error}"); + assert_legacy_unowned(&fixture); + assert!(fixture.mapping_snapshot.exists()); + assert!(fixture.launch_snapshot.exists()); + fs::remove_dir_all(fixture.root).unwrap(); + } + + #[test] + fn tool_cleanup_restores_only_its_exact_backup() { + let root = fixture_dir("tool-ownership"); + let steam = root.join("steam"); + let directory = steam.join("compatibilitytools.d"); + let destination = directory.join(ENHANCED_TOOL_NAME); + let stale = directory.join(format!("{ENHANCED_TOOL_NAME}.patchops.bak-stale")); + let ownership = root.join("data/backups/compat_tool_311210.json"); + let source = tool_source(&root); + fs::create_dir_all(&destination).unwrap(); + fs::write(destination.join("original"), b"original tool").unwrap(); + fs::create_dir_all(&stale).unwrap(); + fs::write(stale.join("keep"), b"unowned").unwrap(); + + install_compatibility_tool_at(&steam, &source, &ownership) + .unwrap() + .commit() + .unwrap(); + let state = load_tool_ownership(&ownership).unwrap().unwrap(); + let exact_backup = directory.join(state.original_backup.as_ref().unwrap()); + assert_eq!( + fs::read(exact_backup.join("original")).unwrap(), + b"original tool" + ); + + remove_compatibility_tool_at(&steam, &ownership) + .unwrap() + .commit() + .unwrap(); + assert_eq!( + fs::read(destination.join("original")).unwrap(), + b"original tool" + ); + assert_eq!(fs::read(stale.join("keep")).unwrap(), b"unowned"); + assert!(!ownership.exists()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn tool_cleanup_refuses_a_replaced_destination() { + let root = fixture_dir("tool-conflict"); + let steam = root.join("steam"); + let directory = steam.join("compatibilitytools.d"); + let destination = directory.join(ENHANCED_TOOL_NAME); + let ownership = root.join("data/backups/compat_tool_311210.json"); + let source = tool_source(&root); + fs::create_dir_all(&destination).unwrap(); + fs::write(destination.join("original"), b"original tool").unwrap(); + + install_compatibility_tool_at(&steam, &source, &ownership) + .unwrap() + .commit() + .unwrap(); + let state = load_tool_ownership(&ownership).unwrap().unwrap(); + let exact_backup = directory.join(state.original_backup.as_ref().unwrap()); + atomic_json( + &destination.join(TOOL_MARKER_FILENAME), + &json!({ "transaction_id": "someone-else" }), + ) + .unwrap(); + + let error = match remove_compatibility_tool_at(&steam, &ownership) { + Ok(_) => panic!("replaced tool must not be removed"), + Err(error) => error, + }; + assert!(error.contains("leaving it untouched")); + assert!(destination.join("proton").is_file()); + assert_eq!( + fs::read(exact_backup.join("original")).unwrap(), + b"original tool" + ); + assert!(ownership.is_file()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn configure_failure_restores_tool_mapping_launch_and_backups() { + let root = fixture_dir("configure-rollback"); + let steam = root.join("steam"); + let destination = steam.join("compatibilitytools.d").join(ENHANCED_TOOL_NAME); + let steam_vdf = steam.join("config/config.vdf"); + let local_vdf = steam.join("userdata/1/config/localconfig.vdf"); + let data = root.join("data/backups"); + let steam_backup = data.join("steam_config_backup.vdf"); + let local_backup = data.join("localconfig_backup.vdf"); + let mapping_snapshot = data.join("compat_mapping_311210.json"); + let launch_snapshot = data.join("launch_options_311210.json"); + let ownership = data.join("compat_tool_311210.json"); + let source = tool_source(&root); + fs::create_dir_all(steam_vdf.parent().unwrap()).unwrap(); + fs::create_dir_all(local_vdf.parent().unwrap()).unwrap(); + fs::write(&steam_vdf, enhanced_compatibility_config()).unwrap(); + fs::write(&local_vdf, b"invalid-vdf").unwrap(); + install_compatibility_tool_at(&steam, &source, &ownership) + .unwrap() + .commit() + .unwrap(); + atomic_json( + &mapping_snapshot, + &json!({ "had_entry": false, "entry": null }), + ) + .unwrap(); + atomic_json(&launch_snapshot, &json!({ "launch_options": "-novid" })).unwrap(); + let original_steam = fs::read(&steam_vdf).unwrap(); + + let error = configure_compatibility_at( + &steam, + &source, + None, + &ownership, + &steam_vdf, + &steam_backup, + &mapping_snapshot, + &local_vdf, + &local_backup, + &launch_snapshot, + ) + .unwrap_err(); + assert!(error.contains("localconfig.vdf")); + assert_eq!(fs::read(&steam_vdf).unwrap(), original_steam); + assert_eq!(fs::read(&local_vdf).unwrap(), b"invalid-vdf"); + assert_eq!( + fs::read(destination.join("proton")).unwrap(), + b"managed proton" + ); + assert!(mapping_snapshot.is_file()); + assert!(launch_snapshot.is_file()); + for path in [ + steam_backup, + local_backup, + fs_ops::backup_path(&steam_vdf), + fs_ops::backup_path(&local_vdf), + ] { + assert!(!path.exists(), "rollback left {}", path.display()); + } + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn cleanup_conflict_rolls_back_tool_and_mapping() { + let root = fixture_dir("cleanup-rollback"); + let steam = root.join("steam"); + let destination = steam.join("compatibilitytools.d").join(ENHANCED_TOOL_NAME); + let steam_vdf = steam.join("config/config.vdf"); + let local_vdf = steam.join("userdata/1/config/localconfig.vdf"); + let data = root.join("data/backups"); + let steam_backup = data.join("steam_config_backup.vdf"); + let local_backup = data.join("localconfig_backup.vdf"); + let mapping_snapshot = data.join("compat_mapping_311210.json"); + let launch_snapshot = data.join("launch_options_311210.json"); + let ownership = data.join("compat_tool_311210.json"); + let source = tool_source(&root); + fs::create_dir_all(&destination).unwrap(); + fs::create_dir_all(steam_vdf.parent().unwrap()).unwrap(); + fs::create_dir_all(local_vdf.parent().unwrap()).unwrap(); + fs::write(destination.join("original"), b"original tool").unwrap(); + fs::write(&steam_vdf, compatibility_config()).unwrap(); + fs::write(&local_vdf, steam_config("-novid")).unwrap(); + install_compatibility_tool_at(&steam, &source, &ownership) + .unwrap() + .commit() + .unwrap(); + atomic_json( + &mapping_snapshot, + &json!({ + "had_entry": true, + "entry": { "name": "GE-Proton", "priority": "75", "custom": "yes" } + }), + ) + .unwrap(); + atomic_json(&launch_snapshot, &json!({ "launch_options": "-novid" })).unwrap(); + fs::write(&steam_vdf, enhanced_compatibility_config()).unwrap(); + fs::write(&local_vdf, steam_config(ENHANCED_LAUNCH_OPTIONS)).unwrap(); + fs::write(&local_vdf, steam_config("user-modified")).unwrap(); + let mapping_before = fs::read(&steam_vdf).unwrap(); + + let error = cleanup_compatibility_at( + &steam, + None, + &ownership, + &steam_vdf, + &steam_backup, + &mapping_snapshot, + &local_vdf, + &local_backup, + &launch_snapshot, + ) + .unwrap_err(); + assert!(error.contains("launch options changed")); + assert_eq!(fs::read(&steam_vdf).unwrap(), mapping_before); + assert_eq!( + read_launch_options_at(&local_vdf, APP_ID).unwrap(), + "user-modified" + ); + assert_eq!( + fs::read(destination.join("proton")).unwrap(), + b"managed proton" + ); + let state = load_tool_ownership(&ownership).unwrap().unwrap(); + assert!( + steam + .join("compatibilitytools.d") + .join(state.original_backup.unwrap()) + .join("original") + .is_file() + ); + assert!(mapping_snapshot.is_file()); + assert!(launch_snapshot.is_file()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn configure_commit_failure_restores_tool_and_file_snapshots() { + let root = fixture_dir("configure-commit-rollback"); + let steam = root.join("steam"); + let directory = steam.join("compatibilitytools.d"); + let destination = directory.join(ENHANCED_TOOL_NAME); + let ownership = root.join("data/backups/compat_tool_311210.json"); + let config = root.join("config.vdf"); + let source = tool_source(&root); + fs::create_dir_all(&destination).unwrap(); + fs::write(destination.join("original"), b"original tool").unwrap(); + fs::write(&config, b"before").unwrap(); + install_compatibility_tool_at(&steam, &source, &ownership) + .unwrap() + .commit() + .unwrap(); + + fs::write(source.join("proton"), b"replacement proton").unwrap(); + let snapshots = capture_files(std::slice::from_ref(&config)).unwrap(); + let tool = install_compatibility_tool_at(&steam, &source, &ownership).unwrap(); + let displaced = tool.displaced_managed.as_ref().unwrap(); + atomic_json( + &displaced.join(TOOL_MARKER_FILENAME), + &json!({ "transaction_id": "commit-conflict" }), + ) + .unwrap(); + fs::write(&config, b"after").unwrap(); + + let error = finish_tool_install(tool, snapshots, destination.clone(), Ok(())).unwrap_err(); + assert!(error.contains("leaving it untouched")); + assert_eq!(fs::read(&config).unwrap(), b"before"); + assert_eq!( + fs::read(destination.join("proton")).unwrap(), + b"managed proton" + ); + assert!(ownership.is_file()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn cleanup_commit_failure_restores_tool_and_file_snapshots() { + let root = fixture_dir("cleanup-commit-rollback"); + let steam = root.join("steam"); + let directory = steam.join("compatibilitytools.d"); + let destination = directory.join(ENHANCED_TOOL_NAME); + let ownership = root.join("data/backups/compat_tool_311210.json"); + let config = root.join("config.vdf"); + let source = tool_source(&root); + fs::create_dir_all(&destination).unwrap(); + fs::write(destination.join("original"), b"original tool").unwrap(); + fs::write(&config, b"before").unwrap(); + install_compatibility_tool_at(&steam, &source, &ownership) + .unwrap() + .commit() + .unwrap(); + let state = load_tool_ownership(&ownership).unwrap().unwrap(); + let exact_backup = directory.join(state.original_backup.unwrap()); + + let snapshots = capture_files(std::slice::from_ref(&config)).unwrap(); + let tool = remove_compatibility_tool_at(&steam, &ownership).unwrap(); + let displaced = tool.displaced_managed.as_ref().unwrap(); + atomic_json( + &displaced.join(TOOL_MARKER_FILENAME), + &json!({ "transaction_id": "commit-conflict" }), + ) + .unwrap(); + fs::write(&config, b"after").unwrap(); + + let error = finish_tool_removal(tool, snapshots, Ok(())).unwrap_err(); + assert!(error.contains("leaving it untouched")); + assert_eq!(fs::read(&config).unwrap(), b"before"); + assert_eq!( + fs::read(destination.join("proton")).unwrap(), + b"managed proton" + ); + assert_eq!( + fs::read(exact_backup.join("original")).unwrap(), + b"original tool" + ); + assert!(ownership.is_file()); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn merge_matches_legacy_launch_option_rules() { + assert_eq!( + merged_launch_options( + "WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game old", + "+set fs_game new", + false, + ), + "WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game new" + ); + assert_eq!( + merged_launch_options("-novid +set fs_game old", "", true), + "-novid +set fs_game old" + ); + } +} diff --git a/native/core/src/t7.rs b/native/core/src/t7.rs new file mode 100644 index 0000000..8608ed7 --- /dev/null +++ b/native/core/src/t7.rs @@ -0,0 +1,2814 @@ +use std::{ + collections::{HashMap, HashSet}, + fs, + io::Read, + path::{Component, Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + app::AppState, + fs_ops::{self, Snapshot, remove_file_if_present}, + models::T7State, +}; + +const CURRENT_RELEASE_API: &str = "https://api.github.com/repos/Scroptss/T7Patch/releases/latest"; +const CURRENT_ARCHIVE_URL: &str = "https://github.com/Scroptss/T7Patch/releases/latest/download/Linux.Steamdeck.and.Manual.Windows.Install.zip"; +const LEGACY_RELEASE_API: &str = + "https://api.github.com/repos/shiversoftdev/t7patch/releases/tags/Current"; +const COMPATIBLE_ARCHIVE_URL: &str = "https://github.com/shiversoftdev/t7patch/releases/download/Current/Linux.Steamdeck.and.Manual.Windows.Install.zip"; +const LPC_ARCHIVE_URL: &str = + "https://github.com/shiversoftdev/t7patch/releases/download/Current/LPC.1.zip"; +const PATCH_ARCHIVE_NAME: &str = "Linux.Steamdeck.and.Manual.Windows.Install.zip"; +const LPC_ARCHIVE_NAME: &str = "LPC.1.zip"; +const COMPATIBLE_ARCHIVE_SHA256: &str = + "388491c01643b0abd51f13290d0c36dec9737fcfbb0ed5e2f5ef6804e1b73dcb"; +const LPC_ARCHIVE_SHA256: &str = "c94855841a233c9dcdea2799c12693fed8554d0e59fe68257ae66ffbdf2fa58b"; +const T7_MARKERS: [&str; 2] = ["t7patchloader.dll", "t7patch.dll"]; +const T7_REQUIRED_FILES: [&str; 2] = ["t7patch.dll", "t7patchloader.dll"]; +const T7_GAME_FILES: [&str; 6] = [ + "t7patch.dll", + "t7patch.conf", + "discord_game_sdk.dll", + "dsound.dll", + "t7patchloader.dll", + "zbr2.dll", +]; +const COMPATIBLE_ONLY_FILES: [&str; 2] = ["discord_game_sdk.dll", "zbr2.dll"]; +const MAX_RELEASE_JSON_BYTES: u64 = 4 * 1024 * 1024; +const MAX_ARCHIVE_BYTES: u64 = 512 * 1024 * 1024; +const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; +const OWNERSHIP_DIRECTORY: &str = ".patchopsiii"; +const OWNERSHIP_BACKUP_DIRECTORY: &str = "t7-backups"; +const OWNERSHIP_MANIFEST: &str = "t7-ownership.json"; +const OWNERSHIP_VERSION: u32 = 1; +const UNTRACKED_UNINSTALL_ERROR: &str = "T7 ownership manifest was not found; refusing to remove files from a legacy or untracked install. PatchOpsIII cannot safely infer ownership from .bak or .patchops.bak filenames"; +// Exact member hashes from the verified release archives shipped by +// origin/main at migration time: current archive SHA-256 +// 57e5cb5abba203382a0549e46c431ed34c4ce6c7aa3f4cdd8003ef0325d3dce5, +// plus the pinned compatible/LPC archives declared above. They are used only +// to recognize legacy installs; new downloads still use their archive digest. +const KNOWN_CURRENT_FILES: [(&str, &str); 3] = [ + ( + "dsound.dll", + "fe05893af78173bdae36bd8202f8e69739d7c1d16c562941eef25a0b5e7c57d2", + ), + ( + "t7patch.dll", + "bc4c9e4edb895f237477a9074552a8186e69b7d36db4886d983dc13c143645c6", + ), + ( + "t7patchloader.dll", + "17614526688ee9d8c85fe89ade78700687ef19d04885dfc133e7ba587fdda811", + ), +]; +// Historical Scroptss archives that this application's legacy dynamic +// `releases/latest` installer could have installed before v3.04 became +// current. The release API supplied the archive digests and these member +// hashes were derived from those exact official archives. +// v3.03 archive SHA-256: 609546e72dd4f9c5649b8197cfdf71dcee47318b438754035e947b5586ee9493. +const KNOWN_SCROPTSS_V3_03_FILES: [(&str, &str); 3] = [ + ( + "dsound.dll", + "fe05893af78173bdae36bd8202f8e69739d7c1d16c562941eef25a0b5e7c57d2", + ), + ( + "t7patch.dll", + "935cb98a4cc7d33ad023444d403be25f6da3865f028975b65c69c7a8884d4078", + ), + ( + "t7patchloader.dll", + "17614526688ee9d8c85fe89ade78700687ef19d04885dfc133e7ba587fdda811", + ), +]; +// v3.02 archive SHA-256: e34411e70d3c99773445ab758851304d7f6a80867a987ec7f4a1a1df72b11bb1. +const KNOWN_SCROPTSS_V3_02_FILES: [(&str, &str); 3] = [ + ( + "dsound.dll", + "fe05893af78173bdae36bd8202f8e69739d7c1d16c562941eef25a0b5e7c57d2", + ), + ( + "t7patch.dll", + "4c582171dfa4409440f31141b4b0fa48f297fcca98b3e36917960bb55d3a1542", + ), + ( + "t7patchloader.dll", + "1ef71784bd654afb45ec58dde5f7e454ea91351faf8c4977d65affa236e1b7ba", + ), +]; +// v2.03 archive SHA-256: 5e6159c264a1d6f2c4425fafdaf2ca382ef49abf4b1915d7c7d183bcb5513440. +const KNOWN_SHIVERSOFT_V2_03_FILES: [(&str, &str); 5] = [ + ( + "discord_game_sdk.dll", + "527768710ddb0953fce5eb1700c2566b6451135d76f1d0610b63907cd5ba94c5", + ), + ( + "dsound.dll", + "fe05893af78173bdae36bd8202f8e69739d7c1d16c562941eef25a0b5e7c57d2", + ), + ( + "t7patch.dll", + "21dd4bac65d467b3f890ec4f7d77d73944646803e0c95fca8c1ee5b6ba71f92d", + ), + ( + "t7patchloader.dll", + "a140a38d7cb451c2ccf44e693921b5305cb0fbdccfde05913efcb40e5b83509c", + ), + ( + "zbr2.dll", + "065fe09942b8005099319feee7734404872d5d0e9a2a1ee6de4877de4c030ede", + ), +]; +const KNOWN_COMPATIBLE_FILES: [(&str, &str); 5] = [ + ( + "discord_game_sdk.dll", + "527768710ddb0953fce5eb1700c2566b6451135d76f1d0610b63907cd5ba94c5", + ), + ( + "dsound.dll", + "fe05893af78173bdae36bd8202f8e69739d7c1d16c562941eef25a0b5e7c57d2", + ), + ( + "t7patch.dll", + "aaaaaaea2e889db7c4453fa355c86f17bebf74b6822ea7a3701d420dae481d3f", + ), + ( + "t7patchloader.dll", + "4df88503030a671d0804012ea585e6f6b01b70bec74cf3ddc42b9f4c73fe73cd", + ), + ( + "zbr2.dll", + "065fe09942b8005099319feee7734404872d5d0e9a2a1ee6de4877de4c030ede", + ), +]; +const KNOWN_LPC_FILES: [(&str, &str); 13] = [ + ( + "bp_core_ffotd_tu32_593.ff", + "729ff5a20e81085be6884a131cf68300b56f141dc0e10a1be56d1b27e96f2d25", + ), + ( + "core_ffotd_tu32_593.ff", + "beba007a3474abc3d59bc9048da222bdd9757401f59657683e5f99cf05b6befb", + ), + ( + "ea_core_ffotd_tu32_593.ff", + "b2e03032eefee5b20706009b0b76bc3dd187e200a65de37144746799621229bb", + ), + ( + "en_core_ffotd_tu32_593.ff", + "218d1ae315a222ef13483d43b4f92f2ec1f974556d8483746f9c2046883f68b2", + ), + ( + "es_core_ffotd_tu32_593.ff", + "ed6db64b5444fd4990d70a642d4b088ac3cee09d09c690c1a45d6258a035ba2f", + ), + ( + "fr_core_ffotd_tu32_593.ff", + "36403f40c2e81eb4e98a9ff49591c9afecb8f7390e35d55d6779a213be1aba93", + ), + ( + "ge_core_ffotd_tu32_593.ff", + "a132a000248646b50a8919a1f2f10455d51102efba3e1068d9f884a81d4af54a", + ), + ( + "it_core_ffotd_tu32_593.ff", + "e3cf42fbdef69b2ebe996495bafecc367cc0c9b434b2b7e08cf473b337c60c80", + ), + ( + "ja_core_ffotd_tu32_593.ff", + "f1d7acb16e7634bea8cac749c7f115b7605300d5c9d9fd63256a91ce719a963d", + ), + ( + "po_core_ffotd_tu32_593.ff", + "cd189d4ad235aa75cc3f32228e2b7782f6ee2decd7efa37c41f5d580c708932d", + ), + ( + "ru_core_ffotd_tu32_593.ff", + "ce96501ac3e5a5cbd50c493ecc3110ffad8a8208fe33c5c256bb56b30ed65fb7", + ), + ( + "sc_core_ffotd_tu32_593.ff", + "f04645efe12219d6fef3f706e8cd01afcbf000de95bfb0107e385362a5dc5ae6", + ), + ( + "tc_core_ffotd_tu32_593.ff", + "031066009975c5c6c1d1d68a54f18855077287eadee556482a8a45e060d08b2f", + ), +]; +static STAGE_COUNTER: AtomicU64 = AtomicU64::new(0); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PatchProfile { + Current, + Compatible, +} + +#[derive(Clone, Copy)] +struct AssetSpec { + name: &'static str, + url: &'static str, + pinned_sha256: Option<&'static str>, +} + +const CURRENT_ARCHIVE: AssetSpec = AssetSpec { + name: PATCH_ARCHIVE_NAME, + url: CURRENT_ARCHIVE_URL, + pinned_sha256: None, +}; +const COMPATIBLE_ARCHIVE: AssetSpec = AssetSpec { + name: PATCH_ARCHIVE_NAME, + url: COMPATIBLE_ARCHIVE_URL, + pinned_sha256: Some(COMPATIBLE_ARCHIVE_SHA256), +}; +const LPC_ARCHIVE: AssetSpec = AssetSpec { + name: LPC_ARCHIVE_NAME, + url: LPC_ARCHIVE_URL, + pinned_sha256: Some(LPC_ARCHIVE_SHA256), +}; + +#[derive(Debug, Deserialize)] +struct GitHubRelease { + #[serde(default)] + assets: Vec, +} + +#[derive(Debug, Deserialize)] +struct GitHubAsset { + #[serde(default)] + name: String, + #[serde(default)] + digest: String, + #[serde(default)] + browser_download_url: String, +} + +#[derive(Clone, Debug)] +struct ReleaseAsset { + name: String, + digest: String, + download_url: String, +} + +#[derive(Clone, Debug)] +struct InstallOperation { + source: PathBuf, + relative: PathBuf, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct OwnershipManifest { + version: u32, + entries: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct ManagedFile { + path: PathBuf, + managed_sha256: String, + original: OriginalFile, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +enum OriginalFile { + Created, + Overwritten { + backup_path: PathBuf, + sha256: String, + #[serde(default)] + active_was_missing: bool, + }, +} + +/// Read T7 Patch markers and `t7patch.conf`. `executable_profile` is the +/// already-validated EXE profile (`current`, `compatible`, or `enhanced`) and +/// avoids hashing the large game executable again during ordinary state reads. +pub fn status(game_dir: Option<&Path>, executable_profile: Option<&str>) -> T7State { + let Some(game_dir) = game_dir.filter(|path| !path.as_os_str().is_empty() && path.is_dir()) + else { + return empty_state("Unknown"); + }; + + let conf = game_dir.join("t7patch.conf"); + let conf_exists = regular_file(&conf); + let mut result = if conf_exists { + fs::read(&conf) + .map(|body| parse_conf(&String::from_utf8_lossy(&body))) + .unwrap_or_else(|_| empty_state("Unknown")) + } else { + empty_state("Unknown") + }; + result.installed = T7_MARKERS + .iter() + .any(|name| regular_file(&game_dir.join(name))); + result.conf_exists = conf_exists; + result.mode = if crate::enhanced::detect_install(game_dir) { + "Enhanced" + } else if executable_profile == Some("enhanced") { + "Custom" + } else { + mode_for_profile(game_dir, executable_profile) + } + .into(); + result +} + +/// Update one or more T7 config values while preserving all unrelated lines. +pub fn configure( + state: &AppState, + game_dir: &Path, + gamertag: Option<&str>, + color_code: &str, + network_password: Option<&str>, + friends_only: Option, +) -> Result<(), String> { + let game_dir = validate_game_dir(game_dir)?; + let conf = safe_target(&game_dir, Path::new("t7patch.conf"))?; + if !regular_file(&conf) { + return Err( + "t7patch.conf was not found. Install T7 Patch before updating the gamertag.".into(), + ); + } + + let name = gamertag + .map(|value| validate_gamertag(value, color_code)) + .transpose()?; + let password = network_password.map(validate_config_value).transpose()?; + let contents = fs::read_to_string(&conf).map_err(|error| error.to_string())?; + let updated = update_conf( + &contents, + name.as_deref(), + password.as_deref(), + friends_only, + ); + let cache_existed = state.mod_files_dir().exists(); + let stage = create_stage(state, "t7-config")?; + let result = + write_config_with_ownership(&game_dir, updated.as_bytes(), &stage.join("rollback")); + if result.is_ok() { + let _ = fs::remove_dir_all(&stage); + if !cache_existed { + let _ = fs::remove_dir(state.mod_files_dir()); + } + } else { + state.log( + "Warning", + format!( + "T7 configuration recovery files were retained at {}.", + stage.display() + ), + ); + } + result?; + + if let Some(name) = name { + state.log( + "Success", + format!("Updated 'playername' in t7patch.conf to '{name}'."), + ); + } + if let Some(password) = password { + state.log( + "Success", + if password.is_empty() { + "Cleared network password in t7patch.conf." + } else { + "Updated network password in t7patch.conf." + }, + ); + } + if let Some(enabled) = friends_only { + state.log( + "Success", + format!( + "Updated 'isfriendsonly' in t7patch.conf to {}.", + if enabled { "On" } else { "Off" } + ), + ); + } + Ok(()) +} + +/// Install the T7 archive matching the active executable profile together with +/// the pinned legacy LPC payload. Everything is staged and validated before +/// the game directory is changed, then restored from a snapshot on failure. +pub fn install(state: &AppState, game_dir: &Path, executable_profile: &str) -> Result<(), String> { + let profile = patch_profile(executable_profile)?; + let game_dir = validate_game_dir(game_dir)?; + let stage = create_stage(state, "t7")?; + + let result = (|| { + let legacy_release = fetch_release_digests(LEGACY_RELEASE_API); + let current_release = if profile == PatchProfile::Current { + Some(fetch_release_digests(CURRENT_RELEASE_API)) + } else { + None + }; + let patch_spec = match profile { + PatchProfile::Current => CURRENT_ARCHIVE, + PatchProfile::Compatible => COMPATIBLE_ARCHIVE, + }; + let patch_release = current_release.as_ref().unwrap_or(&legacy_release); + let (patch_url, patch_digest) = trusted_asset( + patch_spec, + patch_release, + profile == PatchProfile::Current, + state, + )?; + let (lpc_url, lpc_digest) = trusted_asset(LPC_ARCHIVE, &legacy_release, false, state)?; + + state.log( + "Info", + format!( + "Detected {}. Installing {}...", + if profile == PatchProfile::Compatible { + "Compatible EXE" + } else { + "Current EXE" + }, + if profile == PatchProfile::Compatible { + "T7 Patch 2.04" + } else { + "T7 Patch Scroptss/T7Patch" + } + ), + ); + + let patch_archive = stage.join("T7Patch.zip"); + fs_ops::download( + state, + &patch_url, + &patch_archive, + Some(&patch_digest), + MAX_ARCHIVE_BYTES, + )?; + let patch_extract = stage.join("patch"); + fs_ops::extract_zip(&patch_archive, &patch_extract)?; + let patch_source = find_patch_source(&patch_extract)?; + let mut patch_files = collect_files(&patch_source)?; + patch_files.retain(|(_, relative)| { + relative.components().count() == 1 + && relative + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| { + T7_GAME_FILES + .iter() + .any(|allowed| name.eq_ignore_ascii_case(allowed)) + }) + }); + if profile == PatchProfile::Current { + patch_files.retain(|(_, relative)| { + relative + .file_name() + .and_then(|name| name.to_str()) + .is_none_or(|name| { + !COMPATIBLE_ONLY_FILES + .iter() + .any(|blocked| name.eq_ignore_ascii_case(blocked)) + }) + }); + } + + let lpc_archive = stage.join("LPC.zip"); + fs_ops::download( + state, + &lpc_url, + &lpc_archive, + Some(&lpc_digest), + MAX_ARCHIVE_BYTES, + )?; + let lpc_extract = stage.join("lpc"); + fs_ops::extract_zip(&lpc_archive, &lpc_extract)?; + let lpc_files = find_lpc_files(&lpc_extract)?; + state.log( + "Success", + "Downloaded and validated T7 Patch and LPC archives.", + ); + + validate_lpc_dir(&game_dir)?; + let mut operations = Vec::new(); + let mut retained = Vec::new(); + for (source, relative) in patch_files { + let target = safe_target(&game_dir, &relative)?; + if relative + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case("t7patch.conf")) + && regular_file(&target) + { + retained.push(relative); + continue; + } + operations.push(InstallOperation { source, relative }); + } + for source in lpc_files { + let filename = source + .file_name() + .ok_or_else(|| "invalid LPC filename".to_string())? + .to_owned(); + operations.push(InstallOperation { + source, + relative: Path::new("LPC").join(filename), + }); + } + + let staged_hashes = expected_install_hashes(&operations)?; + let legacy_hashes = match known_legacy_hashes(&game_dir) { + Ok(Some(expected)) => expected, + Ok(None) | Err(_) => staged_hashes, + }; + let adopted = adopt_legacy_install(&game_dir, &legacy_hashes)?; + let install_result = reject_unsafe_profile_transition(&game_dir, profile).and_then(|()| { + apply_managed_install(&game_dir, &operations, &retained, &stage.join("rollback")) + }); + if let Err(error) = install_result { + if adopted { + discard_adopted_manifest(&game_dir).map_err(|cleanup| { + format!("{error}; legacy ownership migration cleanup failed: {cleanup}") + })?; + } + return Err(error); + } + + state.log( + "Success", + if profile == PatchProfile::Compatible { + "Installed T7 Patch 2.04 successfully." + } else { + "Installed T7 Patch Scroptss/T7Patch successfully." + }, + ); + Ok(()) + })(); + + if result.is_ok() { + let _ = fs::remove_dir_all(&stage); + } else { + state.log( + "Warning", + format!("T7 staging files were retained at {}.", stage.display()), + ); + } + result +} + +/// Remove only files recorded in the T7 ownership manifest. Managed bytes must +/// still match their recorded hashes, and overwritten files are restored only +/// from the exact PatchOps-owned backups recorded at install time. +pub fn uninstall(state: &AppState, game_dir: &Path) -> Result<(), String> { + let game_dir = validate_game_dir(game_dir)?; + if load_manifest(&game_dir)?.is_none() { + let Some(expected) = known_legacy_hashes(&game_dir)? else { + state.log("Info", "T7 Patch was not installed."); + return Ok(()); + }; + adopt_legacy_install(&game_dir, &expected)?; + } + let stage = create_stage(state, "t7-uninstall")?; + let result = apply_managed_uninstall(&game_dir, &stage.join("rollback")); + + if result.is_ok() { + let _ = fs::remove_dir_all(&stage); + clear_legacy_cache(state); + state.log("Success", "T7 Patch has been completely uninstalled."); + } else { + state.log( + "Warning", + format!("T7 recovery files were retained at {}.", stage.display()), + ); + } + result +} + +fn patch_profile(value: &str) -> Result { + match value.trim().to_ascii_lowercase().as_str() { + "current" | "default" | "enhanced" => Ok(PatchProfile::Current), + "compatible" => Ok(PatchProfile::Compatible), + _ => Err("Cannot install T7 Patch for an unverified executable profile".into()), + } +} + +fn mode_for_profile(game_dir: &Path, profile: Option<&str>) -> &'static str { + match profile.map(str::trim) { + Some(value) if value.eq_ignore_ascii_case("enhanced") => "Enhanced", + Some(value) + if value.eq_ignore_ascii_case("current") || value.eq_ignore_ascii_case("default") => + { + "Default" + } + Some(value) if value.eq_ignore_ascii_case("compatible") => "Compatible", + Some(_) => "Custom", + None if ["BlackOpsIII.exe", "BlackOps3.exe"] + .iter() + .any(|name| regular_file(&game_dir.join(name))) => + { + "Custom" + } + None => "Unknown", + } +} + +fn empty_state(mode: &str) -> T7State { + T7State { + installed: false, + conf_exists: false, + gamertag: String::new(), + plain_name: String::new(), + color_code: String::new(), + network_password: String::new(), + friends_only: false, + mode: mode.into(), + } +} + +fn parse_conf(contents: &str) -> T7State { + let mut state = empty_state("Unknown"); + for line in contents.lines() { + if let Some(value) = line.strip_prefix("playername=") { + state.gamertag = value.trim().into(); + if state.gamertag.starts_with('^') && state.gamertag.chars().count() > 2 { + state.color_code = state.gamertag.chars().take(2).collect(); + state.plain_name = state.gamertag.chars().skip(2).collect(); + } else { + state.plain_name = state.gamertag.clone(); + } + } else if let Some(value) = line.strip_prefix("networkpassword=") { + state.network_password = value.trim().into(); + } else if let Some(value) = line.strip_prefix("isfriendsonly=") { + state.friends_only = value.trim() == "1"; + } + } + state +} + +fn validate_gamertag(value: &str, color_code: &str) -> Result { + let value = value.trim(); + validate_config_value(value)?; + if value.is_empty() { + return Err("Gamertag cannot be empty.".into()); + } + if value.chars().count() > 20 { + return Err("Gamertag cannot exceed 20 characters.".into()); + } + if !matches!( + color_code, + "" | "^0" | "^1" | "^2" | "^3" | "^4" | "^5" | "^6" | "^8" | "^9" + ) { + return Err("Invalid gamertag color code".into()); + } + Ok(format!("{color_code}{value}")) +} + +fn validate_config_value(value: &str) -> Result { + let value = value.trim(); + if value.contains(['\r', '\n', '\0']) { + return Err("T7 Patch settings cannot contain line breaks or NUL bytes".into()); + } + Ok(value.into()) +} + +fn update_conf( + contents: &str, + name: Option<&str>, + password: Option<&str>, + friends_only: Option, +) -> String { + let mut name_found = false; + let mut password_found = false; + let mut friends_found = false; + let mut lines = contents + .split_inclusive('\n') + .map(|line| { + if let Some(value) = name.filter(|_| line.starts_with("playername=")) { + name_found = true; + format!("playername={value}\n") + } else if let Some(value) = password.filter(|_| line.starts_with("networkpassword=")) { + password_found = true; + format!("networkpassword={value}\n") + } else if let Some(value) = friends_only.filter(|_| line.starts_with("isfriendsonly=")) + { + friends_found = true; + format!("isfriendsonly={}\n", i32::from(value)) + } else { + line.to_string() + } + }) + .collect::>(); + if contents.is_empty() { + lines.clear(); + } + if let Some(value) = name.filter(|_| !name_found) { + lines.insert(0, format!("playername={value}\n")); + } + if let Some(value) = password.filter(|_| !password_found) { + lines.insert(0, format!("networkpassword={value}\n")); + } + if let Some(value) = friends_only.filter(|_| !friends_found) { + lines.insert(0, format!("isfriendsonly={}\n", i32::from(value))); + } + lines.concat() +} + +fn fetch_release_digests(api: &str) -> Result, String> { + let response = reqwest::blocking::Client::builder() + .connect_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(30)) + .build() + .map_err(|error| error.to_string())? + .get(api) + .header(reqwest::header::USER_AGENT, "PatchOpsIII") + .send() + .and_then(reqwest::blocking::Response::error_for_status) + .map_err(|error| format!("failed to query T7 Patch release metadata: {error}"))?; + if response + .content_length() + .is_some_and(|length| length > MAX_RELEASE_JSON_BYTES) + { + return Err("T7 Patch release metadata is unexpectedly large".into()); + } + let mut body = Vec::new(); + response + .take(MAX_RELEASE_JSON_BYTES + 1) + .read_to_end(&mut body) + .map_err(|error| error.to_string())?; + if body.len() as u64 > MAX_RELEASE_JSON_BYTES { + return Err("T7 Patch release metadata is unexpectedly large".into()); + } + release_digests_from_json(&body) +} + +fn release_digests_from_json(body: &[u8]) -> Result, String> { + let release: GitHubRelease = serde_json::from_slice(body) + .map_err(|error| format!("invalid T7 Patch release metadata: {error}"))?; + Ok(release + .assets + .into_iter() + .filter_map(|asset| { + let digest = normalize_sha256(&asset.digest)?; + let name = asset.name; + Some(( + name.clone(), + ReleaseAsset { + name, + digest, + download_url: asset.browser_download_url, + }, + )) + }) + .collect()) +} + +fn trusted_asset( + spec: AssetSpec, + release: &Result, String>, + discover_current: bool, + state: &AppState, +) -> Result<(String, String), String> { + if let Ok(assets) = release + && let Some(asset) = select_release_asset(spec, assets, discover_current) + { + if asset.name != spec.name { + state.log( + "Info", + format!("Using latest T7 Patch release asset {}.", asset.name), + ); + } + let url = if asset.download_url.is_empty() { + spec.url.to_owned() + } else { + asset.download_url.clone() + }; + return Ok((url, asset.digest.clone())); + } + if let Some(digest) = spec.pinned_sha256 { + state.log( + "Warning", + format!( + "Release metadata digest unavailable for {}; using pinned trusted hash.", + spec.name + ), + ); + return Ok((spec.url.into(), digest.into())); + } + Err(match release { + Err(error) => format!("{error}; no trusted SHA-256 is pinned for {}", spec.name), + Ok(_) => format!("No GitHub SHA-256 digest available for {}", spec.name), + }) +} + +fn select_release_asset( + spec: AssetSpec, + assets: &HashMap, + discover_current: bool, +) -> Option<&ReleaseAsset> { + if let Some(asset) = assets.get(spec.name) { + return Some(asset); + } + if spec.name.to_ascii_lowercase().ends_with(".zip") { + let stem = &spec.name[..spec.name.len() - 4]; + let prefix = format!("{stem}.").to_ascii_lowercase(); + let mut versioned = assets + .values() + .filter(|asset| { + let name = asset.name.to_ascii_lowercase(); + name.strip_prefix(&prefix) + .and_then(|suffix| suffix.strip_suffix(".zip")) + .is_some_and(|version| !version.is_empty() && !version.contains('/')) + }) + .collect::>(); + if versioned.len() == 1 { + return versioned.pop(); + } + } + if !discover_current { + return None; + } + let mut zip_assets = assets + .values() + .filter(|asset| asset.name.to_ascii_lowercase().ends_with(".zip")) + .collect::>(); + let mut candidates = zip_assets + .iter() + .copied() + .filter(|asset| { + let name = asset.name.to_ascii_lowercase(); + if cfg!(windows) { + name.contains("windows") + } else { + name.contains("linux") || name.contains("steamdeck") + } + }) + .collect::>(); + if candidates.is_empty() { + candidates = zip_assets + .drain(..) + .filter(|asset| { + let name = asset.name.to_ascii_lowercase(); + !["windows", "linux", "steamdeck"] + .iter() + .any(|token| name.contains(token)) + }) + .collect(); + } + let universal = candidates + .iter() + .copied() + .filter(|asset| { + let name = asset.name.to_ascii_lowercase(); + name.contains("linux") && name.contains("windows") + }) + .collect::>(); + if !universal.is_empty() { + candidates = universal; + } + (candidates.len() == 1).then(|| candidates[0]) +} + +fn normalize_sha256(value: &str) -> Option { + let value = value.trim(); + let value = value + .split_once(':') + .filter(|(prefix, _)| prefix.eq_ignore_ascii_case("sha256")) + .map_or(value, |(_, digest)| digest) + .trim() + .to_ascii_lowercase(); + (value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())).then_some(value) +} + +fn find_patch_source(root: &Path) -> Result { + let mut directories = collect_directories(root)?; + directories.sort_by_key(|path| { + ( + if path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case("linux")) + { + 0 + } else { + 1 + }, + path.components().count(), + path.to_string_lossy().to_ascii_lowercase(), + ) + }); + directories + .into_iter() + .find(|directory| { + let names = fs::read_dir(directory) + .into_iter() + .flatten() + .filter_map(Result::ok) + .filter(|entry| regular_file(&entry.path())) + .map(|entry| entry.file_name().to_string_lossy().to_ascii_lowercase()) + .collect::>(); + T7_REQUIRED_FILES.iter().all(|name| names.contains(*name)) + }) + .ok_or_else(|| "T7 Patch archive did not contain the expected files".into()) +} + +fn find_lpc_files(root: &Path) -> Result, String> { + let mut directories = collect_directories(root)?; + directories.sort_by_key(|path| { + ( + if path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case("lpc")) + { + 0 + } else { + 1 + }, + path.components().count(), + path.to_string_lossy().to_ascii_lowercase(), + ) + }); + for directory in directories { + let mut files = fs::read_dir(&directory) + .map_err(|error| error.to_string())? + .filter_map(Result::ok) + .filter(|entry| { + regular_file(&entry.path()) + && entry + .file_name() + .to_str() + .is_some_and(|name| name.ends_with(".ff")) + }) + .map(|entry| entry.path()) + .collect::>(); + files.sort(); + if !files.is_empty() { + return Ok(files); + } + } + Err("LPC archive did not contain any .ff files".into()) +} + +fn collect_directories(root: &Path) -> Result, String> { + let mut directories = vec![root.to_path_buf()]; + let mut index = 0; + while index < directories.len() { + let directory = directories[index].clone(); + index += 1; + for entry in fs::read_dir(&directory).map_err(|error| error.to_string())? { + let entry = entry.map_err(|error| error.to_string())?; + let metadata = fs::symlink_metadata(entry.path()).map_err(|error| error.to_string())?; + if metadata.file_type().is_symlink() { + return Err("archive contains a link".into()); + } + if metadata.is_dir() { + directories.push(entry.path()); + } else if !metadata.is_file() { + return Err("archive contains a special file".into()); + } + } + } + Ok(directories) +} + +fn collect_files(root: &Path) -> Result, String> { + let mut files = Vec::new(); + let mut folded = HashSet::new(); + for directory in collect_directories(root)? { + for entry in fs::read_dir(&directory).map_err(|error| error.to_string())? { + let entry = entry.map_err(|error| error.to_string())?; + if !regular_file(&entry.path()) { + continue; + } + let relative = entry + .path() + .strip_prefix(root) + .map_err(|_| "archive path escaped extraction root".to_string())? + .to_path_buf(); + if !safe_relative(&relative) { + return Err("archive contains an unsafe path".into()); + } + let key = relative + .to_string_lossy() + .replace('\\', "/") + .to_ascii_lowercase(); + if !folded.insert(key) { + return Err("archive contains case-colliding paths".into()); + } + files.push((entry.path(), relative)); + } + } + files.sort_by(|left, right| left.1.cmp(&right.1)); + Ok(files) +} + +fn safe_relative(path: &Path) -> bool { + !path.as_os_str().is_empty() + && path + .components() + .all(|component| matches!(component, Component::Normal(_))) +} + +fn validate_game_dir(path: &Path) -> Result { + if path.as_os_str().is_empty() { + return Err("Game directory is not set".into()); + } + let path = path + .canonicalize() + .map_err(|error| format!("invalid game directory: {error}"))?; + if !path.is_dir() { + return Err("Game directory is not a directory".into()); + } + if !["BlackOpsIII.exe", "BlackOps3.exe"] + .iter() + .any(|name| regular_file(&path.join(name))) + { + return Err("Game directory does not contain a Black Ops III executable".into()); + } + Ok(path) +} + +fn validate_lpc_dir(game_dir: &Path) -> Result { + let lpc = game_dir.join("LPC"); + match fs::symlink_metadata(&lpc) { + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => { + Err(format!("refusing to use unsafe LPC path {}", lpc.display())) + } + Ok(_) => Ok(lpc), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(lpc), + Err(error) => Err(error.to_string()), + } +} + +fn safe_target(root: &Path, relative: &Path) -> Result { + if !safe_relative(relative) { + return Err("invalid game-relative path".into()); + } + let components = relative.components().collect::>(); + let mut target = root.to_path_buf(); + for (index, component) in components.iter().enumerate() { + let Component::Normal(component) = component else { + return Err("invalid game-relative path".into()); + }; + target.push(component); + if let Ok(metadata) = fs::symlink_metadata(&target) { + if metadata.file_type().is_symlink() { + return Err(format!( + "refusing to follow linked path {}", + target.display() + )); + } + if index + 1 < components.len() && !metadata.is_dir() { + return Err(format!("expected a directory at {}", target.display())); + } + if index + 1 == components.len() && !metadata.is_file() { + return Err(format!("expected a file at {}", target.display())); + } + } + } + Ok(target) +} + +fn regular_file(path: &Path) -> bool { + fs::symlink_metadata(path) + .map(|metadata| metadata.is_file() && !metadata.file_type().is_symlink()) + .unwrap_or(false) +} + +fn regular_directory(path: &Path) -> bool { + fs::symlink_metadata(path) + .map(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink()) + .unwrap_or(false) +} + +fn copy_file(source: &Path, target: &Path) -> Result<(), String> { + if let Some(parent) = target.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + fs::copy(source, target) + .map(|_| ()) + .map_err(|error| format!("failed to install {}: {error}", target.display())) +} + +fn create_backup_copy(source: &Path, target: &Path) -> Result<(), String> { + let mut input = fs::File::open(source).map_err(|error| error.to_string())?; + let mut output = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(target) + .map_err(|error| format!("failed to reserve backup {}: {error}", target.display()))?; + let result = std::io::copy(&mut input, &mut output) + .and_then(|_| output.sync_all()) + .map_err(|error| format!("failed to write backup {}: {error}", target.display())); + if result.is_err() { + drop(output); + let _ = fs::remove_file(target); + } + result +} + +fn ownership_manifest_relative() -> PathBuf { + Path::new(OWNERSHIP_DIRECTORY).join(OWNERSHIP_MANIFEST) +} + +fn ownership_manifest_path(game_dir: &Path) -> Result { + safe_target(game_dir, &ownership_manifest_relative()) +} + +fn folded_path(path: &Path) -> String { + path.to_string_lossy() + .replace('\\', "/") + .to_ascii_lowercase() +} + +fn valid_sha256(value: &str) -> bool { + value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +fn managed_relative(path: &Path) -> bool { + if !safe_relative(path) || path.starts_with(OWNERSHIP_DIRECTORY) { + return false; + } + let components = path.components().collect::>(); + match components.as_slice() { + [Component::Normal(name)] => name.to_str().is_some_and(|name| { + T7_GAME_FILES + .iter() + .any(|allowed| name.eq_ignore_ascii_case(allowed)) + }), + [Component::Normal(directory), Component::Normal(name)] => { + directory + .to_str() + .is_some_and(|directory| directory.eq_ignore_ascii_case("LPC")) + && name.to_str().is_some_and(|name| { + name.to_ascii_lowercase().ends_with(".ff") + && !name.ends_with(fs_ops::PATCHOPS_BACKUP_SUFFIX) + && !name.ends_with(fs_ops::LEGACY_BACKUP_SUFFIX) + }) + } + _ => false, + } +} + +fn owned_backup_relative(path: &Path) -> bool { + if !safe_relative(path) { + return false; + } + let components = path.components().collect::>(); + let [ + Component::Normal(owner), + Component::Normal(directory), + Component::Normal(generation), + Component::Normal(filename), + ] = components.as_slice() + else { + return false; + }; + owner == &std::ffi::OsStr::new(OWNERSHIP_DIRECTORY) + && directory == &std::ffi::OsStr::new(OWNERSHIP_BACKUP_DIRECTORY) + && generation.to_str().is_some_and(|value| { + !value.is_empty() + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + }) + && filename.to_str().is_some_and(|value| { + let digits = value + .strip_prefix("entry-") + .and_then(|value| value.strip_suffix(".original")); + digits.is_some_and(|digits| { + !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) + }) + }) +} + +fn validate_manifest(manifest: &OwnershipManifest) -> Result<(), String> { + if manifest.version != OWNERSHIP_VERSION { + return Err(format!( + "unsupported T7 ownership manifest version {}", + manifest.version + )); + } + let mut paths = HashSet::new(); + let mut backups = HashSet::new(); + for entry in &manifest.entries { + if !managed_relative(&entry.path) { + return Err(format!( + "T7 ownership manifest contains an invalid managed path: {}", + entry.path.display() + )); + } + if !paths.insert(folded_path(&entry.path)) { + return Err("T7 ownership manifest contains duplicate managed paths".into()); + } + if !valid_sha256(&entry.managed_sha256) { + return Err("T7 ownership manifest contains an invalid managed SHA-256".into()); + } + if let OriginalFile::Overwritten { + backup_path, + sha256, + .. + } = &entry.original + { + if !owned_backup_relative(backup_path) { + return Err(format!( + "T7 ownership manifest contains an invalid backup path: {}", + backup_path.display() + )); + } + if !backups.insert(folded_path(backup_path)) { + return Err("T7 ownership manifest reuses a backup path".into()); + } + if !valid_sha256(sha256) { + return Err("T7 ownership manifest contains an invalid backup SHA-256".into()); + } + } + } + Ok(()) +} + +fn load_manifest(game_dir: &Path) -> Result, String> { + let path = ownership_manifest_path(game_dir)?; + let metadata = match fs::symlink_metadata(&path) { + Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => metadata, + Ok(_) => { + return Err(format!( + "refusing to use unsafe T7 ownership manifest {}", + path.display() + )); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let state_dir = game_dir.join(OWNERSHIP_DIRECTORY); + return match fs::symlink_metadata(&state_dir) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(error.to_string()), + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => Err( + format!("refusing unsafe T7 ownership state {}", state_dir.display()), + ), + Ok(_) => { + let mut entries = + fs::read_dir(&state_dir).map_err(|error| error.to_string())?; + if entries.next().is_none() { + Ok(None) + } else { + Err(format!( + "T7 ownership state is incomplete; recovery files were retained at {}", + state_dir.display() + )) + } + } + }; + } + Err(error) => return Err(error.to_string()), + }; + if metadata.len() > MAX_MANIFEST_BYTES { + return Err("T7 ownership manifest is unexpectedly large".into()); + } + let mut body = Vec::new(); + fs::File::open(&path) + .map_err(|error| error.to_string())? + .take(MAX_MANIFEST_BYTES + 1) + .read_to_end(&mut body) + .map_err(|error| error.to_string())?; + if body.len() as u64 > MAX_MANIFEST_BYTES { + return Err("T7 ownership manifest is unexpectedly large".into()); + } + let manifest: OwnershipManifest = serde_json::from_slice(&body) + .map_err(|error| format!("invalid T7 ownership manifest: {error}"))?; + validate_manifest(&manifest)?; + Ok(Some(manifest)) +} + +fn write_manifest(game_dir: &Path, manifest: &mut OwnershipManifest) -> Result<(), String> { + manifest + .entries + .sort_by_key(|entry| folded_path(&entry.path)); + validate_manifest(manifest)?; + let path = ownership_manifest_path(game_dir)?; + let parent = path + .parent() + .ok_or_else(|| "T7 ownership manifest has no parent".to_string())?; + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + ownership_manifest_path(game_dir)?; + safe_target( + game_dir, + &Path::new(OWNERSHIP_DIRECTORY).join(format!(".{OWNERSHIP_MANIFEST}.patchops.tmp")), + )?; + let mut body = serde_json::to_vec_pretty(manifest).map_err(|error| error.to_string())?; + body.push(b'\n'); + fs_ops::atomic_write(&path, &body) +} + +fn verify_managed_entry(game_dir: &Path, entry: &ManagedFile) -> Result<(), String> { + let active = safe_target(game_dir, &entry.path)?; + let restore_only = matches!( + &entry.original, + OriginalFile::Overwritten { + sha256, + active_was_missing: true, + .. + } if sha256.eq_ignore_ascii_case(&entry.managed_sha256) + ); + match fs::symlink_metadata(&active) { + Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => { + let actual = fs_ops::sha256_file(&active)?; + if !actual.eq_ignore_ascii_case(&entry.managed_sha256) { + return Err(format!( + "T7 ownership conflict: {} changed outside PatchOpsIII; no files were removed", + active.display() + )); + } + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound && restore_only => {} + _ => { + return Err(format!( + "T7 ownership conflict: managed file is missing or unsafe: {}", + active.display() + )); + } + } + if let OriginalFile::Overwritten { + backup_path, + sha256, + .. + } = &entry.original + { + let backup = safe_target(game_dir, backup_path)?; + if !regular_file(&backup) { + return Err(format!( + "T7 recovery conflict: original backup is missing or unsafe: {}", + backup.display() + )); + } + let actual = fs_ops::sha256_file(&backup)?; + if !actual.eq_ignore_ascii_case(sha256) { + return Err(format!( + "T7 recovery conflict: original backup changed: {}", + backup.display() + )); + } + } + Ok(()) +} + +fn create_backup_generation(game_dir: &Path) -> Result { + let backup_root_relative = Path::new(OWNERSHIP_DIRECTORY).join(OWNERSHIP_BACKUP_DIRECTORY); + let probe = backup_root_relative.join(".patchops-directory-probe"); + safe_target(game_dir, &probe)?; + let backup_root = game_dir.join(&backup_root_relative); + fs::create_dir_all(&backup_root).map_err(|error| error.to_string())?; + safe_target(game_dir, &probe)?; + for _ in 0..100 { + let counter = STAGE_COUNTER.fetch_add(1, Ordering::Relaxed); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let generation = format!("{timestamp}-{}-{counter}", std::process::id()); + let path = backup_root.join(&generation); + match fs::create_dir(&path) { + Ok(()) => return Ok(generation), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => return Err(error.to_string()), + } + } + Err("could not allocate a unique T7 backup directory".into()) +} + +fn backup_relative(generation: &str, index: usize) -> PathBuf { + Path::new(OWNERSHIP_DIRECTORY) + .join(OWNERSHIP_BACKUP_DIRECTORY) + .join(generation) + .join(format!("entry-{index:04}.original")) +} + +fn expected_install_hashes( + operations: &[InstallOperation], +) -> Result, String> { + operations + .iter() + .map(|operation| { + Ok(( + folded_path(&operation.relative), + fs_ops::sha256_file(&operation.source)?, + )) + }) + .collect() +} + +fn known_t7_profile(marker_hashes: [&str; 2]) -> Option<&'static [(&'static str, &'static str)]> { + [ + KNOWN_CURRENT_FILES.as_slice(), + KNOWN_SCROPTSS_V3_03_FILES.as_slice(), + KNOWN_SCROPTSS_V3_02_FILES.as_slice(), + KNOWN_COMPATIBLE_FILES.as_slice(), + KNOWN_SHIVERSOFT_V2_03_FILES.as_slice(), + ] + .into_iter() + .find(|profile| { + T7_REQUIRED_FILES.iter().all(|name| { + let expected = profile + .iter() + .find(|(candidate, _)| candidate.eq_ignore_ascii_case(name)) + .map(|(_, hash)| *hash); + let actual = T7_REQUIRED_FILES + .iter() + .position(|candidate| candidate == name) + .map(|index| marker_hashes[index]); + expected.is_some() && expected == actual + }) + }) +} + +fn known_legacy_hashes(game_dir: &Path) -> Result>, String> { + let marker_hashes = T7_REQUIRED_FILES + .iter() + .map(|name| { + let target = safe_target(game_dir, Path::new(name))?; + if !target.exists() { + return Ok(None); + } + if !regular_file(&target) { + return Err(format!( + "refusing unsafe legacy T7 file {}", + target.display() + )); + } + fs_ops::sha256_file(&target).map(Some) + }) + .collect::, String>>()?; + if marker_hashes.iter().all(Option::is_none) { + return Ok(None); + } + if marker_hashes.iter().any(Option::is_none) { + return Err(format!( + "{UNTRACKED_UNINSTALL_ERROR}. The legacy T7 marker pair is incomplete" + )); + } + + let profile = known_t7_profile([ + marker_hashes[0] + .as_deref() + .expect("complete marker pair checked above"), + marker_hashes[1] + .as_deref() + .expect("complete marker pair checked above"), + ]) + .ok_or_else(|| { + format!( + "{UNTRACKED_UNINSTALL_ERROR}. The installed T7 payload does not match a release known to this build" + ) + })?; + + let mut expected = profile + .iter() + .map(|(path, hash)| (folded_path(Path::new(path)), (*hash).to_owned())) + .collect::>(); + expected.extend(KNOWN_LPC_FILES.iter().map(|(name, hash)| { + ( + folded_path(&Path::new("LPC").join(name)), + (*hash).to_owned(), + ) + })); + Ok(Some(expected)) +} + +fn suffixed_path(path: &Path, suffix: &str) -> PathBuf { + let mut value = path.as_os_str().to_os_string(); + value.push(suffix); + value.into() +} + +/// Enumerate only direct legacy LPC backups that the Python implementation +/// could have created. Matching is case-insensitive for ownership purposes, +/// while `.patchops.bak` wins over the older `.bak` when both exist. +fn legacy_lpc_backups(game_dir: &Path) -> Result, String> { + let lpc = validate_lpc_dir(game_dir)?; + match fs::symlink_metadata(&lpc) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => return Err(error.to_string()), + Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {} + Ok(_) => return Err(format!("refusing unsafe LPC directory {}", lpc.display())), + } + + let mut selected = HashMap::::new(); + for item in fs::read_dir(&lpc).map_err(|error| error.to_string())? { + let item = item.map_err(|error| error.to_string())?; + let Some(filename) = item.file_name().to_str().map(str::to_owned) else { + continue; + }; + let (base, priority) = + if let Some(base) = filename.strip_suffix(fs_ops::PATCHOPS_BACKUP_SUFFIX) { + (base, 2) + } else if let Some(base) = filename.strip_suffix(fs_ops::LEGACY_BACKUP_SUFFIX) { + (base, 1) + } else { + continue; + }; + if !base.to_ascii_lowercase().ends_with(".ff") { + continue; + } + let relative = Path::new("LPC").join(base); + if !managed_relative(&relative) { + return Err(format!( + "refusing unsafe legacy LPC backup {}", + item.path().display() + )); + } + let key = folded_path(&relative); + if let Some((existing_relative, _, existing_priority)) = selected.get(&key) { + if existing_relative != &relative { + return Err(format!( + "legacy LPC backups contain ambiguous path casing for {}", + relative.display() + )); + } + if priority <= *existing_priority { + continue; + } + } + selected.insert(key, (relative, item.path(), priority)); + } + let mut selected = selected + .into_values() + .map(|(relative, backup, _)| (relative, backup)) + .collect::>(); + selected.sort_by_key(|(relative, _)| folded_path(relative)); + Ok(selected) +} + +/// Convert an origin/main T7 install into the ownership format only when every +/// fixed payload byte matches a verified archive member. User-editable config +/// remains unowned. LPC suffix backups are copied into app-owned recovery +/// storage; the legacy copies are kept until the follow-on operation succeeds. +fn adopt_legacy_install( + game_dir: &Path, + expected: &HashMap, +) -> Result { + if load_manifest(game_dir)?.is_some() { + return Ok(false); + } + let marker_paths = T7_REQUIRED_FILES + .iter() + .map(|name| safe_target(game_dir, Path::new(name))) + .collect::, _>>()?; + if marker_paths.iter().all(|path| !path.exists()) { + return Ok(false); + } + if marker_paths.iter().any(|path| !regular_file(path)) { + return Err(format!( + "{UNTRACKED_UNINSTALL_ERROR}. The legacy T7 marker pair is incomplete or unsafe" + )); + } + for path in &marker_paths { + let relative = path + .strip_prefix(game_dir) + .map_err(|_| "legacy T7 path escaped the game directory".to_string())?; + let Some(expected_hash) = expected.get(&folded_path(relative)) else { + return Err(format!( + "{UNTRACKED_UNINSTALL_ERROR}. The installed T7 payload is not recognized by this build" + )); + }; + if !fs_ops::sha256_file(path)?.eq_ignore_ascii_case(expected_hash) { + return Err(format!( + "{UNTRACKED_UNINSTALL_ERROR}. {} does not match the verified legacy payload", + path.display() + )); + } + } + + let config = safe_target(game_dir, Path::new("t7patch.conf"))?; + if config.exists() && !regular_file(&config) { + return Err(format!( + "refusing unsafe legacy T7 config {}", + config.display() + )); + } + + let mut entries = Vec::new(); + for name in T7_GAME_FILES { + if name.eq_ignore_ascii_case("t7patch.conf") { + continue; + } + let relative = PathBuf::from(name); + let target = safe_target(game_dir, &relative)?; + if !target.exists() { + continue; + } + if !regular_file(&target) { + return Err(format!( + "refusing unsafe legacy T7 file {}", + target.display() + )); + } + let Some(expected_hash) = expected.get(&folded_path(&relative)) else { + return Err(format!( + "legacy T7 file {} is not part of the recognized payload; no files were changed", + target.display() + )); + }; + let actual = fs_ops::sha256_file(&target)?; + if !actual.eq_ignore_ascii_case(expected_hash) { + return Err(format!( + "legacy T7 file {} changed after installation; no files were changed", + target.display() + )); + } + entries.push(ManagedFile { + path: relative, + managed_sha256: actual, + original: OriginalFile::Created, + }); + } + + let mut generation = None; + let mut copied_backups = Vec::new(); + let result = (|| { + for (name, _) in KNOWN_LPC_FILES { + let relative = Path::new("LPC").join(name); + let Some(expected_hash) = expected.get(&folded_path(&relative)) else { + continue; + }; + let target = safe_target(game_dir, &relative)?; + if !target.exists() { + continue; + } + if !regular_file(&target) { + return Err(format!( + "refusing unsafe legacy LPC file {}", + target.display() + )); + } + let actual = fs_ops::sha256_file(&target)?; + if !actual.eq_ignore_ascii_case(expected_hash) { + return Err(format!( + "legacy LPC file {} changed after installation; no files were changed", + target.display() + )); + } + + let patchops = suffixed_path(&target, fs_ops::PATCHOPS_BACKUP_SUFFIX); + let legacy = suffixed_path(&target, fs_ops::LEGACY_BACKUP_SUFFIX); + let selected = if patchops.exists() { + Some(patchops) + } else if legacy.exists() { + Some(legacy) + } else { + None + }; + let original = if let Some(selected) = selected { + if !regular_file(&selected) { + return Err(format!( + "refusing unsafe legacy LPC backup {}", + selected.display() + )); + } + let original_sha256 = fs_ops::sha256_file(&selected)?; + if generation.is_none() { + generation = Some(create_backup_generation(game_dir)?); + } + let owned_relative = backup_relative( + generation + .as_deref() + .ok_or_else(|| "failed to allocate T7 backup generation".to_string())?, + copied_backups.len(), + ); + let owned = safe_target(game_dir, &owned_relative)?; + create_backup_copy(&selected, &owned)?; + if !fs_ops::sha256_file(&owned)?.eq_ignore_ascii_case(&original_sha256) { + return Err(format!( + "failed to verify adopted LPC backup {}", + owned.display() + )); + } + copied_backups.push(owned); + OriginalFile::Overwritten { + backup_path: owned_relative, + sha256: original_sha256, + active_was_missing: false, + } + } else { + OriginalFile::Created + }; + entries.push(ManagedFile { + path: relative, + managed_sha256: actual, + original, + }); + } + + let mut adopted_paths = entries + .iter() + .map(|entry| folded_path(&entry.path)) + .collect::>(); + for (relative, selected) in legacy_lpc_backups(game_dir)? { + let key = folded_path(&relative); + if adopted_paths.contains(&key) { + continue; + } + if !regular_file(&selected) { + return Err(format!( + "refusing unsafe legacy LPC backup {}", + selected.display() + )); + } + let target = safe_target(game_dir, &relative)?; + match fs::symlink_metadata(&target) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(error.to_string()), + Ok(_) => { + return Err(format!( + "legacy LPC target {} is not part of the recognized payload; no files were changed", + target.display() + )); + } + } + let original_sha256 = fs_ops::sha256_file(&selected)?; + if generation.is_none() { + generation = Some(create_backup_generation(game_dir)?); + } + let owned_relative = backup_relative( + generation + .as_deref() + .ok_or_else(|| "failed to allocate T7 backup generation".to_string())?, + copied_backups.len(), + ); + let owned = safe_target(game_dir, &owned_relative)?; + create_backup_copy(&selected, &owned)?; + if !fs_ops::sha256_file(&owned)?.eq_ignore_ascii_case(&original_sha256) { + return Err(format!( + "failed to verify adopted LPC backup {}", + owned.display() + )); + } + copied_backups.push(owned); + entries.push(ManagedFile { + path: relative, + managed_sha256: original_sha256.clone(), + original: OriginalFile::Overwritten { + backup_path: owned_relative, + sha256: original_sha256, + active_was_missing: true, + }, + }); + adopted_paths.insert(key); + } + write_manifest( + game_dir, + &mut OwnershipManifest { + version: OWNERSHIP_VERSION, + entries, + }, + ) + })(); + if let Err(error) = result { + for backup in copied_backups { + let _ = remove_file_if_present(&backup); + } + if let Some(generation) = generation { + let directory = game_dir + .join(OWNERSHIP_DIRECTORY) + .join(OWNERSHIP_BACKUP_DIRECTORY) + .join(generation); + let _ = fs::remove_dir(directory); + let _ = fs::remove_dir( + game_dir + .join(OWNERSHIP_DIRECTORY) + .join(OWNERSHIP_BACKUP_DIRECTORY), + ); + let _ = fs::remove_dir(game_dir.join(OWNERSHIP_DIRECTORY)); + } + return Err(error); + } + Ok(true) +} + +fn discard_adopted_manifest(game_dir: &Path) -> Result<(), String> { + let Some(manifest) = load_manifest(game_dir)? else { + return Ok(()); + }; + for entry in &manifest.entries { + if let OriginalFile::Overwritten { backup_path, .. } = &entry.original { + remove_file_if_present(&safe_target(game_dir, backup_path)?)?; + } + } + remove_file_if_present(&ownership_manifest_path(game_dir)?)?; + remove_empty_ownership_directories(game_dir); + Ok(()) +} + +fn remove_empty_ownership_directories(game_dir: &Path) { + let backup_root = game_dir + .join(OWNERSHIP_DIRECTORY) + .join(OWNERSHIP_BACKUP_DIRECTORY); + if regular_directory(&backup_root) { + if let Ok(entries) = fs::read_dir(&backup_root) { + for entry in entries.filter_map(Result::ok) { + let path = entry.path(); + if regular_directory(&path) { + let _ = fs::remove_dir(path); + } + } + } + let _ = fs::remove_dir(&backup_root); + } + let _ = fs::remove_dir(game_dir.join(OWNERSHIP_DIRECTORY)); +} + +fn reject_unsafe_profile_transition(game_dir: &Path, profile: PatchProfile) -> Result<(), String> { + if profile != PatchProfile::Current { + return Ok(()); + } + let Some(manifest) = load_manifest(game_dir)? else { + return Ok(()); + }; + let would_restore_compatible_file = manifest.entries.iter().any(|entry| { + entry + .path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| { + COMPATIBLE_ONLY_FILES + .iter() + .any(|candidate| candidate.eq_ignore_ascii_case(name)) + }) + && matches!(entry.original, OriginalFile::Overwritten { .. }) + }); + if would_restore_compatible_file { + Err("Cannot switch this managed T7 install to the Current profile because doing so would restore a pre-existing compatible-only DLL into the active game directory. Uninstall T7 Patch first and review the restored file before installing the Current profile.".into()) + } else { + Ok(()) + } +} + +fn rollback_transaction( + snapshot: &Snapshot, + error: String, + rollback_directory: &Path, +) -> Result<(), String> { + match snapshot.restore() { + Ok(()) => Err(format!("{error}; previous T7 files were restored")), + Err(rollback) => Err(format!( + "{error}; rollback failed: {rollback}. Recovery files remain in {}", + rollback_directory.display() + )), + } +} + +fn apply_managed_install( + game_dir: &Path, + operations: &[InstallOperation], + retained: &[PathBuf], + rollback_directory: &Path, +) -> Result<(), String> { + let previous = load_manifest(game_dir)?.unwrap_or(OwnershipManifest { + version: OWNERSHIP_VERSION, + entries: Vec::new(), + }); + let mut previous_by_path = previous + .entries + .into_iter() + .map(|entry| (folded_path(&entry.path), entry)) + .collect::>(); + let mut generation = None; + let mut desired_paths = HashSet::new(); + let mut prepared = Vec::new(); + let mut new_backup_index = 0_usize; + + for operation in operations { + if !regular_file(&operation.source) || !managed_relative(&operation.relative) { + return Err(format!( + "refusing unsafe T7 install operation for {}", + operation.relative.display() + )); + } + let key = folded_path(&operation.relative); + if !desired_paths.insert(key.clone()) { + return Err("T7 install contains duplicate target paths".into()); + } + let active = safe_target(game_dir, &operation.relative)?; + let managed_sha256 = fs_ops::sha256_file(&operation.source)?; + let (original, new_backup) = if let Some(previous) = previous_by_path.remove(&key) { + if previous.path.as_path() != operation.relative.as_path() { + return Err(format!( + "T7 ownership path casing changed from {} to {}; refusing an ambiguous update", + previous.path.display(), + operation.relative.display() + )); + } + verify_managed_entry(game_dir, &previous)?; + (previous.original, None) + } else if regular_file(&active) { + let original_sha256 = fs_ops::sha256_file(&active)?; + if generation.is_none() { + generation = Some(create_backup_generation(game_dir)?); + } + let generation = generation + .as_deref() + .ok_or_else(|| "failed to allocate T7 backup generation".to_string())?; + let relative = backup_relative(generation, new_backup_index); + new_backup_index += 1; + let backup = safe_target(game_dir, &relative)?; + match fs::symlink_metadata(&backup) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Ok(_) => return Err(format!("refusing to reuse backup {}", backup.display())), + Err(error) => return Err(error.to_string()), + } + ( + OriginalFile::Overwritten { + backup_path: relative, + sha256: original_sha256, + active_was_missing: false, + }, + Some(backup), + ) + } else { + (OriginalFile::Created, None) + }; + prepared.push(( + operation, + active, + ManagedFile { + path: operation.relative.clone(), + managed_sha256, + original, + }, + new_backup, + )); + } + + let mut entries = prepared + .iter() + .map(|(_, _, entry, _)| entry.clone()) + .collect::>(); + for relative in retained { + if !managed_relative(relative) { + return Err(format!( + "refusing unsafe retained T7 path {}", + relative.display() + )); + } + let key = folded_path(relative); + if !desired_paths.insert(key.clone()) { + return Err("T7 install contains duplicate retained paths".into()); + } + if let Some(previous) = previous_by_path.remove(&key) { + if previous.path.as_path() != relative.as_path() { + return Err(format!( + "T7 ownership path casing changed from {} to {}; refusing an ambiguous update", + previous.path.display(), + relative.display() + )); + } + verify_managed_entry(game_dir, &previous)?; + entries.push(previous); + } + } + + let retired = previous_by_path.into_values().collect::>(); + for entry in &retired { + verify_managed_entry(game_dir, entry)?; + } + + let manifest_path = ownership_manifest_path(game_dir)?; + let mut targets = vec![manifest_path]; + for (_, active, _, new_backup) in &prepared { + targets.push(active.clone()); + if let Some(backup) = new_backup { + targets.push(backup.clone()); + } + } + for entry in &retired { + targets.push(safe_target(game_dir, &entry.path)?); + } + let snapshot = Snapshot::capture(&targets, rollback_directory)?; + + let commit: Result<(), String> = (|| { + for (_, active, entry, new_backup) in &prepared { + if let Some(backup) = new_backup { + create_backup_copy(active, backup)?; + let expected = match &entry.original { + OriginalFile::Overwritten { sha256, .. } => sha256, + OriginalFile::Created => unreachable!(), + }; + if !fs_ops::sha256_file(backup)?.eq_ignore_ascii_case(expected) { + return Err(format!( + "failed to verify original T7 backup {}", + backup.display() + )); + } + } + } + for (operation, active, _, _) in &prepared { + copy_file(&operation.source, active)?; + } + for entry in &retired { + let active = safe_target(game_dir, &entry.path)?; + match &entry.original { + OriginalFile::Created => { + remove_file_if_present(&active)?; + if active.exists() { + return Err(format!( + "failed to retire managed T7 file {}", + active.display() + )); + } + } + OriginalFile::Overwritten { + backup_path, + sha256, + .. + } => { + let backup = safe_target(game_dir, backup_path)?; + copy_file(&backup, &active)?; + if !fs_ops::sha256_file(&active)?.eq_ignore_ascii_case(sha256) { + return Err(format!( + "failed to verify restored T7 file {}", + active.display() + )); + } + } + } + } + for entry in &entries { + let active = safe_target(game_dir, &entry.path)?; + if !regular_file(&active) + || !fs_ops::sha256_file(&active)?.eq_ignore_ascii_case(&entry.managed_sha256) + { + return Err(format!( + "T7 Patch install verification failed for {}", + active.display() + )); + } + } + write_manifest( + game_dir, + &mut OwnershipManifest { + version: OWNERSHIP_VERSION, + entries, + }, + ) + })(); + + if let Err(error) = commit { + return rollback_transaction(&snapshot, error, rollback_directory); + } + for entry in &retired { + if let OriginalFile::Overwritten { backup_path, .. } = &entry.original + && let Ok(backup) = safe_target(game_dir, backup_path) + { + let _ = remove_file_if_present(&backup); + } + } + remove_empty_ownership_directories(game_dir); + Ok(()) +} + +fn apply_managed_uninstall(game_dir: &Path, rollback_directory: &Path) -> Result<(), String> { + let manifest = load_manifest(game_dir)?.ok_or_else(|| UNTRACKED_UNINSTALL_ERROR.to_string())?; + for entry in &manifest.entries { + verify_managed_entry(game_dir, entry)?; + } + + let manifest_path = ownership_manifest_path(game_dir)?; + let mut targets = vec![manifest_path.clone()]; + for entry in &manifest.entries { + targets.push(safe_target(game_dir, &entry.path)?); + if let OriginalFile::Overwritten { backup_path, .. } = &entry.original { + targets.push(safe_target(game_dir, backup_path)?); + } + } + let snapshot = Snapshot::capture(&targets, rollback_directory)?; + let commit: Result<(), String> = (|| { + for entry in &manifest.entries { + let active = safe_target(game_dir, &entry.path)?; + match &entry.original { + OriginalFile::Created => { + remove_file_if_present(&active)?; + if active.exists() { + return Err(format!( + "failed to remove managed T7 file {}", + active.display() + )); + } + } + OriginalFile::Overwritten { + backup_path, + sha256, + .. + } => { + let backup = safe_target(game_dir, backup_path)?; + copy_file(&backup, &active)?; + if !fs_ops::sha256_file(&active)?.eq_ignore_ascii_case(sha256) { + return Err(format!( + "failed to verify restored T7 file {}", + active.display() + )); + } + remove_file_if_present(&backup)?; + } + } + } + remove_file_if_present(&manifest_path) + })(); + if let Err(error) = commit { + return rollback_transaction(&snapshot, error, rollback_directory); + } + remove_empty_ownership_directories(game_dir); + Ok(()) +} + +fn write_config_with_ownership( + game_dir: &Path, + contents: &[u8], + rollback_directory: &Path, +) -> Result<(), String> { + let relative = PathBuf::from("t7patch.conf"); + let path = safe_target(game_dir, &relative)?; + let Some(mut manifest) = load_manifest(game_dir)? else { + return fs_ops::atomic_write(&path, contents); + }; + let Some(index) = manifest + .entries + .iter() + .position(|entry| folded_path(&entry.path) == folded_path(&relative)) + else { + return fs_ops::atomic_write(&path, contents); + }; + verify_managed_entry(game_dir, &manifest.entries[index])?; + let manifest_path = ownership_manifest_path(game_dir)?; + let snapshot = Snapshot::capture(&[path.clone(), manifest_path], rollback_directory)?; + let commit: Result<(), String> = (|| { + fs_ops::atomic_write(&path, contents)?; + manifest.entries[index].managed_sha256 = fs_ops::sha256_file(&path)?; + write_manifest(game_dir, &mut manifest) + })(); + match commit { + Ok(()) => Ok(()), + Err(error) => rollback_transaction(&snapshot, error, rollback_directory), + } +} + +fn create_stage(state: &AppState, label: &str) -> Result { + let root = state.mod_files_dir(); + fs::create_dir_all(&root).map_err(|error| error.to_string())?; + for _ in 0..10 { + let id = STAGE_COUNTER.fetch_add(1, Ordering::Relaxed); + let stage = root.join(format!(".{label}-stage-{}-{id}", std::process::id())); + match fs::create_dir(&stage) { + Ok(()) => return Ok(stage), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => return Err(error.to_string()), + } + } + Err("could not create a unique staging directory".into()) +} + +fn clear_legacy_cache(state: &AppState) { + let root = state.mod_files_dir(); + for directory in [ + root.join("linux"), + root.join("Linux.Steamdeck.and.Manual.Windows.Install"), + root.join("T7Patch_extracted"), + ] { + if fs::symlink_metadata(&directory) + .is_ok_and(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink()) + { + let _ = fs::remove_dir_all(directory); + } + } + let archive = root.join("T7Patch.zip"); + if regular_file(&archive) { + let _ = fs::remove_file(archive); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + static TEST_COUNTER: AtomicU64 = AtomicU64::new(0); + + fn temp_dir(label: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "patchops-t7-{label}-{}-{}", + std::process::id(), + TEST_COUNTER.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + path + } + + fn operation(source_root: &Path, relative: &str, contents: &[u8]) -> InstallOperation { + let source = source_root.join(relative); + fs::create_dir_all(source.parent().unwrap()).unwrap(); + fs::write(&source, contents).unwrap(); + InstallOperation { + source, + relative: PathBuf::from(relative), + } + } + + #[test] + fn parses_and_updates_t7_config_without_losing_unknown_lines() { + let original = "unknown=keep\nplayername=Old\nnetworkpassword=old\nisfriendsonly=0\n"; + let updated = update_conf(original, Some("^2New"), Some("secret"), Some(true)); + assert_eq!( + updated, + "unknown=keep\nplayername=^2New\nnetworkpassword=secret\nisfriendsonly=1\n" + ); + let parsed = parse_conf(&updated); + assert_eq!(parsed.gamertag, "^2New"); + assert_eq!(parsed.color_code, "^2"); + assert_eq!(parsed.plain_name, "New"); + assert_eq!(parsed.network_password, "secret"); + assert!(parsed.friends_only); + + assert!(validate_gamertag("bad\nname", "^1").is_err()); + assert!(validate_gamertag("name", "^7").is_err()); + } + + #[test] + fn selects_patch_by_executable_profile_and_parses_github_digest() { + assert_eq!(patch_profile("current").unwrap(), PatchProfile::Current); + assert_eq!(patch_profile("enhanced").unwrap(), PatchProfile::Current); + assert_eq!( + patch_profile("compatible").unwrap(), + PatchProfile::Compatible + ); + assert!(patch_profile("unverified").is_err()); + + let game = temp_dir("profile"); + assert_eq!(mode_for_profile(&game, Some("compatible")), "Compatible"); + fs::remove_dir_all(game).unwrap(); + + let digests = release_digests_from_json( + br#"{"assets":[{"name":"Linux.Steamdeck.and.Manual.Windows.Install.zip","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","browser_download_url":"https://example.invalid/exact.zip"}]}"#, + ) + .unwrap(); + assert_eq!( + digests + .get(PATCH_ARCHIVE_NAME) + .map(|asset| asset.digest.as_str()), + Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + ); + } + + #[test] + fn resolves_versioned_latest_release_archive() { + let assets = release_digests_from_json( + br#"{"assets":[{"name":"Linux.Steamdeck.and.Manual.Windows.Install.3.4.1.zip","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","browser_download_url":"https://github.com/Scroptss/T7Patch/releases/download/v3.4.1/archive.zip"}]}"#, + ) + .unwrap(); + let selected = select_release_asset(CURRENT_ARCHIVE, &assets, true).unwrap(); + assert_eq!( + selected.name, + "Linux.Steamdeck.and.Manual.Windows.Install.3.4.1.zip" + ); + } + + #[test] + fn discovers_unambiguously_renamed_universal_release_archive() { + let assets = release_digests_from_json( + br#"{"assets":[ + {"name":"T7Patch-Windows.zip","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","browser_download_url":"https://example.invalid/windows.zip"}, + {"name":"T7Patch-Linux-Windows.zip","digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","browser_download_url":"https://example.invalid/universal.zip"}, + {"name":"checksums.txt","digest":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","browser_download_url":"https://example.invalid/checksums.txt"} + ]}"#, + ) + .unwrap(); + let selected = select_release_asset(CURRENT_ARCHIVE, &assets, true).unwrap(); + assert_eq!(selected.name, "T7Patch-Linux-Windows.zip"); + assert_eq!( + selected.download_url, + "https://example.invalid/universal.zip" + ); + } + + #[test] + fn rejects_ambiguous_renamed_release_archives() { + let assets = release_digests_from_json( + br#"{"assets":[ + {"name":"T7Patch-Linux-a.zip","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","browser_download_url":"https://example.invalid/a.zip"}, + {"name":"T7Patch-Linux-b.zip","digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","browser_download_url":"https://example.invalid/b.zip"} + ]}"#, + ) + .unwrap(); + assert!(select_release_asset(CURRENT_ARCHIVE, &assets, true).is_none()); + } + + #[test] + fn renamed_assets_need_valid_github_digests() { + let assets = release_digests_from_json(br#"{"assets":[ + {"name":"T7Patch-Linux-Windows.zip","digest":"","browser_download_url":"https://example.invalid/unsigned.zip"}, + {"name":"T7Patch-Linux.zip","digest":"sha256:not-a-digest"} + ]}"#).unwrap(); + assert!(assets.is_empty()); + assert!(select_release_asset(CURRENT_ARCHIVE, &assets, true).is_none()); + } + + #[test] + fn historical_t7_marker_pairs_select_only_their_exact_release_profile() { + assert_eq!( + known_t7_profile([ + KNOWN_SCROPTSS_V3_03_FILES[1].1, + KNOWN_SCROPTSS_V3_03_FILES[2].1, + ]), + Some(KNOWN_SCROPTSS_V3_03_FILES.as_slice()) + ); + assert_eq!( + known_t7_profile([ + KNOWN_SHIVERSOFT_V2_03_FILES[2].1, + KNOWN_SHIVERSOFT_V2_03_FILES[3].1, + ]), + Some(KNOWN_SHIVERSOFT_V2_03_FILES.as_slice()) + ); + assert!( + known_t7_profile([ + KNOWN_SCROPTSS_V3_03_FILES[1].1, + KNOWN_SHIVERSOFT_V2_03_FILES[3].1, + ]) + .is_none() + ); + } + + #[test] + fn ownership_manifest_preserves_originals_across_updates_and_covers_lpc() { + let game = temp_dir("ownership"); + let sources = temp_dir("ownership-sources"); + let rollback = temp_dir("ownership-rollback"); + fs::create_dir_all(game.join("LPC")).unwrap(); + fs::write(game.join("t7patch.dll"), b"original-patch").unwrap(); + fs::write(game.join("discord_game_sdk.dll"), b"original-discord").unwrap(); + fs::write(game.join("LPC/english.ff"), b"original-lpc").unwrap(); + + let unrelated_patchops = game.join("LPC/english.ff.patchops.bak"); + let unrelated_legacy = game.join("LPC/english.ff.bak"); + fs::write(&unrelated_patchops, b"unrelated-patchops-backup").unwrap(); + fs::write(&unrelated_legacy, b"unrelated-legacy-backup").unwrap(); + + let first = [ + operation(&sources, "first/t7patch.dll", b"managed-patch-v1"), + operation(&sources, "first/t7patchloader.dll", b"managed-loader-v1"), + operation( + &sources, + "first/discord_game_sdk.dll", + b"managed-discord-v1", + ), + operation(&sources, "first/english.ff", b"managed-lpc-v1"), + ]; + let first = [ + InstallOperation { + source: first[0].source.clone(), + relative: PathBuf::from("t7patch.dll"), + }, + InstallOperation { + source: first[1].source.clone(), + relative: PathBuf::from("t7patchloader.dll"), + }, + InstallOperation { + source: first[2].source.clone(), + relative: PathBuf::from("discord_game_sdk.dll"), + }, + InstallOperation { + source: first[3].source.clone(), + relative: PathBuf::from("LPC/english.ff"), + }, + ]; + apply_managed_install(&game, &first, &[], &rollback.join("first")).unwrap(); + + let first_manifest = load_manifest(&game).unwrap().unwrap(); + assert_eq!(first_manifest.entries.len(), 4); + let patch_entry = first_manifest + .entries + .iter() + .find(|entry| entry.path == Path::new("t7patch.dll")) + .unwrap(); + let OriginalFile::Overwritten { + backup_path: patch_backup, + sha256: patch_original_hash, + .. + } = &patch_entry.original + else { + panic!("existing patch file was not recorded as overwritten"); + }; + assert!(owned_backup_relative(patch_backup)); + assert!(!patch_backup.to_string_lossy().ends_with(".bak")); + assert_eq!( + fs_ops::sha256_file(&game.join(patch_backup)).unwrap(), + *patch_original_hash + ); + let preserved_patch_backup = patch_backup.clone(); + let lpc_entry = first_manifest + .entries + .iter() + .find(|entry| entry.path == Path::new("LPC/english.ff")) + .unwrap(); + assert!(matches!( + lpc_entry.original, + OriginalFile::Overwritten { .. } + )); + let loader_entry = first_manifest + .entries + .iter() + .find(|entry| entry.path == Path::new("t7patchloader.dll")) + .unwrap(); + assert!(matches!(loader_entry.original, OriginalFile::Created)); + + let second = [ + InstallOperation { + source: operation(&sources, "second/t7patch.dll", b"managed-patch-v2").source, + relative: PathBuf::from("t7patch.dll"), + }, + InstallOperation { + source: operation(&sources, "second/t7patchloader.dll", b"managed-loader-v2") + .source, + relative: PathBuf::from("t7patchloader.dll"), + }, + InstallOperation { + source: operation(&sources, "second/english.ff", b"managed-lpc-v2").source, + relative: PathBuf::from("LPC/english.ff"), + }, + ]; + apply_managed_install(&game, &second, &[], &rollback.join("second")).unwrap(); + + let second_manifest = load_manifest(&game).unwrap().unwrap(); + let updated_patch = second_manifest + .entries + .iter() + .find(|entry| entry.path == Path::new("t7patch.dll")) + .unwrap(); + let OriginalFile::Overwritten { backup_path, .. } = &updated_patch.original else { + panic!("updated patch lost its original backup"); + }; + assert_eq!(backup_path, &preserved_patch_backup); + assert_eq!(fs::read(game.join(backup_path)).unwrap(), b"original-patch"); + assert_eq!( + fs::read(game.join("discord_game_sdk.dll")).unwrap(), + b"original-discord" + ); + assert!( + second_manifest + .entries + .iter() + .all(|entry| entry.path != Path::new("discord_game_sdk.dll")) + ); + assert_eq!( + fs::read(&unrelated_patchops).unwrap(), + b"unrelated-patchops-backup" + ); + assert_eq!( + fs::read(&unrelated_legacy).unwrap(), + b"unrelated-legacy-backup" + ); + + apply_managed_uninstall(&game, &rollback.join("uninstall")).unwrap(); + assert_eq!( + fs::read(game.join("t7patch.dll")).unwrap(), + b"original-patch" + ); + assert_eq!( + fs::read(game.join("LPC/english.ff")).unwrap(), + b"original-lpc" + ); + assert!(!game.join("t7patchloader.dll").exists()); + assert_eq!( + fs::read(&unrelated_patchops).unwrap(), + b"unrelated-patchops-backup" + ); + assert_eq!( + fs::read(&unrelated_legacy).unwrap(), + b"unrelated-legacy-backup" + ); + assert!(load_manifest(&game).unwrap().is_none()); + + fs::remove_dir_all(game).unwrap(); + fs::remove_dir_all(sources).unwrap(); + fs::remove_dir_all(rollback).unwrap(); + } + + #[test] + fn exact_legacy_install_is_adopted_and_lpc_original_is_restored() { + let game = temp_dir("legacy-adopt"); + let rollback = temp_dir("legacy-adopt-rollback"); + fs::create_dir_all(game.join("LPC")).unwrap(); + fs::write(game.join("t7patch.dll"), b"legacy-patch").unwrap(); + fs::write(game.join("t7patchloader.dll"), b"legacy-loader").unwrap(); + fs::write(game.join("t7patch.conf"), b"playername=custom\n").unwrap(); + fs::write(game.join("LPC/en_core_ffotd_tu32_593.ff"), b"legacy-lpc").unwrap(); + let suffix_backup = game.join("LPC/en_core_ffotd_tu32_593.ff.patchops.bak"); + fs::write(&suffix_backup, b"original-lpc").unwrap(); + let extra_backup = game.join("LPC/custom_language.ff.patchops.bak"); + fs::write(&extra_backup, b"custom-original-lpc").unwrap(); + let expected = [ + PathBuf::from("t7patch.dll"), + PathBuf::from("t7patchloader.dll"), + PathBuf::from("LPC/en_core_ffotd_tu32_593.ff"), + ] + .into_iter() + .map(|relative| { + let hash = fs_ops::sha256_file(&game.join(&relative)).unwrap(); + (folded_path(&relative), hash) + }) + .collect::>(); + + assert!(adopt_legacy_install(&game, &expected).unwrap()); + let manifest = load_manifest(&game).unwrap().unwrap(); + assert!( + manifest + .entries + .iter() + .all(|entry| entry.path != Path::new("t7patch.conf")) + ); + let lpc = manifest + .entries + .iter() + .find(|entry| entry.path == Path::new("LPC/en_core_ffotd_tu32_593.ff")) + .unwrap(); + let OriginalFile::Overwritten { backup_path, .. } = &lpc.original else { + panic!("legacy LPC backup was not adopted"); + }; + assert_ne!(game.join(backup_path), suffix_backup); + assert_eq!(fs::read(game.join(backup_path)).unwrap(), b"original-lpc"); + let extra = manifest + .entries + .iter() + .find(|entry| entry.path == Path::new("LPC/custom_language.ff")) + .expect("backup-only legacy LPC original should be adopted"); + assert!(matches!( + extra.original, + OriginalFile::Overwritten { + active_was_missing: true, + .. + } + )); + assert!(!game.join("LPC/custom_language.ff").exists()); + + apply_managed_uninstall(&game, &rollback).unwrap(); + assert!(!game.join("t7patch.dll").exists()); + assert!(!game.join("t7patchloader.dll").exists()); + assert_eq!( + fs::read(game.join("t7patch.conf")).unwrap(), + b"playername=custom\n" + ); + assert_eq!( + fs::read(game.join("LPC/en_core_ffotd_tu32_593.ff")).unwrap(), + b"original-lpc" + ); + assert_eq!(fs::read(&suffix_backup).unwrap(), b"original-lpc"); + assert_eq!( + fs::read(game.join("LPC/custom_language.ff")).unwrap(), + b"custom-original-lpc" + ); + assert_eq!(fs::read(&extra_backup).unwrap(), b"custom-original-lpc"); + fs::remove_dir_all(game).unwrap(); + fs::remove_dir_all(rollback).unwrap(); + } + + #[test] + fn legacy_adoption_rejects_unknown_or_incomplete_markers_without_state() { + let game = temp_dir("legacy-reject"); + fs::write(game.join("t7patch.dll"), b"known-patch").unwrap(); + let expected = [( + folded_path(Path::new("t7patch.dll")), + fs_ops::sha256_file(&game.join("t7patch.dll")).unwrap(), + )] + .into_iter() + .collect::>(); + assert!(adopt_legacy_install(&game, &expected).is_err()); + assert!(!game.join(OWNERSHIP_DIRECTORY).exists()); + assert_eq!(fs::read(game.join("t7patch.dll")).unwrap(), b"known-patch"); + + fs::write(game.join("t7patchloader.dll"), b"unexpected-loader").unwrap(); + assert!(adopt_legacy_install(&game, &expected).is_err()); + assert!(!game.join(OWNERSHIP_DIRECTORY).exists()); + assert_eq!( + fs::read(game.join("t7patchloader.dll")).unwrap(), + b"unexpected-loader" + ); + fs::remove_dir_all(game).unwrap(); + } + + #[test] + fn clean_legacy_probe_is_an_idempotent_noop() { + let game = temp_dir("legacy-clean"); + assert!(known_legacy_hashes(&game).unwrap().is_none()); + assert!(!adopt_legacy_install(&game, &HashMap::new()).unwrap()); + assert!(!game.join(OWNERSHIP_DIRECTORY).exists()); + fs::remove_dir_all(game).unwrap(); + } + + #[test] + fn incomplete_ownership_state_is_not_treated_as_a_clean_install() { + let game = temp_dir("incomplete-state"); + let recovery = game + .join(OWNERSHIP_DIRECTORY) + .join(OWNERSHIP_BACKUP_DIRECTORY) + .join("retained"); + fs::create_dir_all(&recovery).unwrap(); + fs::write(recovery.join("entry-0000.original"), b"recovery").unwrap(); + let error = load_manifest(&game).unwrap_err(); + assert!(error.contains("ownership state is incomplete")); + assert_eq!( + fs::read(recovery.join("entry-0000.original")).unwrap(), + b"recovery" + ); + fs::remove_dir_all(game).unwrap(); + } + + #[test] + fn current_profile_refuses_to_restore_compatible_only_originals_mid_install() { + let game = temp_dir("profile-transition"); + let sources = temp_dir("profile-transition-sources"); + let rollback = temp_dir("profile-transition-rollback"); + fs::write(game.join("discord_game_sdk.dll"), b"pre-existing").unwrap(); + let operations = [InstallOperation { + source: operation(&sources, "discord_game_sdk.dll", b"compatible-managed").source, + relative: PathBuf::from("discord_game_sdk.dll"), + }]; + apply_managed_install(&game, &operations, &[], &rollback).unwrap(); + let before = fs::read(game.join("discord_game_sdk.dll")).unwrap(); + let error = reject_unsafe_profile_transition(&game, PatchProfile::Current).unwrap_err(); + assert!(error.contains("Uninstall T7 Patch first")); + assert_eq!(fs::read(game.join("discord_game_sdk.dll")).unwrap(), before); + fs::remove_dir_all(game).unwrap(); + fs::remove_dir_all(sources).unwrap(); + fs::remove_dir_all(rollback).unwrap(); + } + + #[test] + fn uninstall_and_update_refuse_modified_managed_bytes_without_losing_recovery_state() { + let game = temp_dir("conflict"); + let sources = temp_dir("conflict-sources"); + let rollback = temp_dir("conflict-rollback"); + fs::write(game.join("t7patch.dll"), b"original").unwrap(); + let first = [InstallOperation { + source: operation(&sources, "v1.dll", b"managed-v1").source, + relative: PathBuf::from("t7patch.dll"), + }]; + apply_managed_install(&game, &first, &[], &rollback.join("install")).unwrap(); + let manifest_path = ownership_manifest_path(&game).unwrap(); + let manifest_before = fs::read(&manifest_path).unwrap(); + let manifest = load_manifest(&game).unwrap().unwrap(); + let backup = match &manifest.entries[0].original { + OriginalFile::Overwritten { backup_path, .. } => game.join(backup_path), + OriginalFile::Created => panic!("expected an original backup"), + }; + let backup_before = fs::read(&backup).unwrap(); + fs::write(game.join("t7patch.dll"), b"user-modified").unwrap(); + + let update = [InstallOperation { + source: operation(&sources, "v2.dll", b"managed-v2").source, + relative: PathBuf::from("t7patch.dll"), + }]; + let update_error = + apply_managed_install(&game, &update, &[], &rollback.join("refused-update")) + .unwrap_err(); + assert!(update_error.contains("changed outside PatchOpsIII")); + let uninstall_error = + apply_managed_uninstall(&game, &rollback.join("refused-uninstall")).unwrap_err(); + assert!(uninstall_error.contains("changed outside PatchOpsIII")); + assert_eq!( + fs::read(game.join("t7patch.dll")).unwrap(), + b"user-modified" + ); + assert_eq!(fs::read(&manifest_path).unwrap(), manifest_before); + assert_eq!(fs::read(&backup).unwrap(), backup_before); + + fs::write(game.join("t7patch.dll"), b"managed-v1").unwrap(); + fs::write(&backup, b"tampered-backup").unwrap(); + let backup_error = + apply_managed_uninstall(&game, &rollback.join("refused-backup")).unwrap_err(); + assert!(backup_error.contains("original backup changed")); + assert_eq!(fs::read(game.join("t7patch.dll")).unwrap(), b"managed-v1"); + assert!(manifest_path.is_file()); + assert_eq!(fs::read(&backup).unwrap(), b"tampered-backup"); + + fs::remove_dir_all(game).unwrap(); + fs::remove_dir_all(sources).unwrap(); + fs::remove_dir_all(rollback).unwrap(); + } + + #[test] + fn configure_refreshes_the_owned_config_hash() { + let game = temp_dir("config-owned"); + let sources = temp_dir("config-owned-sources"); + let rollback = temp_dir("config-owned-rollback"); + let install = [InstallOperation { + source: operation(&sources, "t7patch.conf", b"playername=Old\nunknown=keep\n").source, + relative: PathBuf::from("t7patch.conf"), + }]; + apply_managed_install(&game, &install, &[], &rollback.join("install")).unwrap(); + let before = load_manifest(&game).unwrap().unwrap().entries[0] + .managed_sha256 + .clone(); + let updated = b"playername=^2New\nunknown=keep\n"; + write_config_with_ownership(&game, updated, &rollback.join("configure")).unwrap(); + let after = load_manifest(&game).unwrap().unwrap().entries[0] + .managed_sha256 + .clone(); + assert_ne!(before, after); + assert_eq!( + after, + fs_ops::sha256_file(&game.join("t7patch.conf")).unwrap() + ); + + apply_managed_uninstall(&game, &rollback.join("uninstall")).unwrap(); + assert!(!game.join("t7patch.conf").exists()); + fs::remove_dir_all(game).unwrap(); + fs::remove_dir_all(sources).unwrap(); + fs::remove_dir_all(rollback).unwrap(); + } + + #[test] + fn legacy_install_without_manifest_is_left_byte_for_byte_untouched() { + let game = temp_dir("legacy-untracked"); + let rollback = temp_dir("legacy-untracked-rollback"); + fs::create_dir_all(game.join("LPC")).unwrap(); + let active_patch = game.join("t7patch.dll"); + let active_lpc = game.join("LPC/english.ff"); + let patchops_backup = game.join("LPC/english.ff.patchops.bak"); + let legacy_backup = game.join("LPC/english.ff.bak"); + fs::write(&active_patch, b"legacy-managed-or-user-patch").unwrap(); + fs::write(&active_lpc, b"legacy-managed-or-user-lpc").unwrap(); + fs::write(&patchops_backup, b"ambiguous-patchops-backup").unwrap(); + fs::write(&legacy_backup, b"ambiguous-legacy-backup").unwrap(); + + let error = apply_managed_uninstall(&game, &rollback).unwrap_err(); + assert!(error.contains("legacy or untracked install")); + assert_eq!( + fs::read(&active_patch).unwrap(), + b"legacy-managed-or-user-patch" + ); + assert_eq!( + fs::read(&active_lpc).unwrap(), + b"legacy-managed-or-user-lpc" + ); + assert_eq!( + fs::read(&patchops_backup).unwrap(), + b"ambiguous-patchops-backup" + ); + assert_eq!( + fs::read(&legacy_backup).unwrap(), + b"ambiguous-legacy-backup" + ); + assert!(load_manifest(&game).unwrap().is_none()); + + fs::remove_dir_all(game).unwrap(); + fs::remove_dir_all(rollback).unwrap(); + } + + #[test] + fn manifest_cannot_claim_legacy_or_suffix_based_backups() { + let game = temp_dir("reject-suffix-backup"); + let rollback = temp_dir("reject-suffix-backup-rollback"); + let active = game.join("t7patch.dll"); + let unrelated = game.join("t7patch.dll.bak"); + fs::write(&active, b"managed").unwrap(); + fs::write(&unrelated, b"unrelated-original").unwrap(); + let manifest = OwnershipManifest { + version: OWNERSHIP_VERSION, + entries: vec![ManagedFile { + path: PathBuf::from("t7patch.dll"), + managed_sha256: fs_ops::sha256_file(&active).unwrap(), + original: OriginalFile::Overwritten { + backup_path: PathBuf::from("t7patch.dll.bak"), + sha256: fs_ops::sha256_file(&unrelated).unwrap(), + active_was_missing: false, + }, + }], + }; + let manifest_path = game.join(ownership_manifest_relative()); + fs::create_dir_all(manifest_path.parent().unwrap()).unwrap(); + fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + + let error = apply_managed_uninstall(&game, &rollback).unwrap_err(); + assert!(error.contains("invalid backup path")); + assert_eq!(fs::read(&active).unwrap(), b"managed"); + assert_eq!(fs::read(&unrelated).unwrap(), b"unrelated-original"); + assert!(manifest_path.is_file()); + + fs::remove_dir_all(game).unwrap(); + fs::remove_dir_all(rollback).unwrap(); + } + + #[test] + fn snapshot_restores_partial_file_changes() { + let root = temp_dir("rollback"); + let existing = root.join("existing.dll"); + let created = root.join("created.dll"); + fs::write(&existing, b"original").unwrap(); + let snapshot = Snapshot::capture( + &[existing.clone(), created.clone()], + &root.join("snapshots"), + ) + .unwrap(); + fs::write(&existing, b"partial").unwrap(); + fs::write(&created, b"partial").unwrap(); + snapshot.restore().unwrap(); + assert_eq!(fs::read(existing).unwrap(), b"original"); + assert!(!created.exists()); + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/native/gpui/Cargo.toml b/native/gpui/Cargo.toml index 393ec03..de2ac99 100644 --- a/native/gpui/Cargo.toml +++ b/native/gpui/Cargo.toml @@ -8,11 +8,8 @@ license = "MIT" [dependencies] anyhow = "1" env_logger = "0.11" +log = "0.4" gpui = "=0.2.2" gpui-component = "=0.5.0" -reqwest = { version = "0.12", default-features = false, features = ["blocking", "json"] } +patchops-core = { path = "../core" } serde_json = "1" - -[profile.release] -lto = "thin" -strip = "debuginfo" diff --git a/native/gpui/README.md b/native/gpui/README.md index 0332592..9ec42e8 100644 --- a/native/gpui/README.md +++ b/native/gpui/README.md @@ -1,59 +1,50 @@ # PatchOpsIII native desktop app -This is the primary PatchOpsIII desktop client for Windows x64 and Linux x64, -including Steam Deck desktop mode. It renders Rust/GPUI controls without -Chromium, React, Tauri, or a WebView. The established Python API still performs -game detection, downloads, backups, configuration, and launch operations. The -Electron/React client remains in the repository as a legacy fallback. +The primary Windows x64 and Linux x64 client is one Rust process: GPUI controls +and the toolkit-independent `patchops-core` library. No Python interpreter, +PyInstaller service, loopback HTTP server, Tauri, or WebView runs in the native +app. The legacy Electron client and its Python backend remain available. -The original framework research, architecture decision, comparison criteria, -implemented workflow inventory, and remaining parity work are retained in -[the native desktop migration record](../../docs/gpui-architecture.md). +See [the architecture and migration record](../../docs/gpui-architecture.md) +for the dispatcher contract, progress hook, operation differences, and hardware +validation gates. ## Install a release archive -The native build produces `PatchOpsIII-native-windows-x64.zip` and -`PatchOpsIII-native-linux-x64.zip`. Each archive has this layout: +Extract `PatchOpsIII-native-windows-x64.zip` or +`PatchOpsIII-native-linux-x64.zip`. Each contains: ```text PatchOpsIII/ ├── patchopsiii-gpui[.exe] -├── resources/ -│ ├── backend-bin/patchops-backend[.exe] -│ ├── package.json -│ ├── presets.json -│ ├── PatchOpsIII.ico -│ └── icon-512.png +├── resources/ # metadata, presets, application icons ├── LICENSE -├── THIRD-PARTY-NOTICES/ +├── THIRD-PARTY-NOTICES/Lucide.txt ├── README.md -└── NATIVE-README.md +├── NATIVE-README.md +└── docs/native-desktop.md ``` -Extract the whole folder and keep `resources` beside the native executable. On -Linux, make the native executable executable after extraction if the archive -tool did not preserve its mode: +Run `patchopsiii-gpui.exe` on Windows. On Linux: ```sh chmod +x PatchOpsIII/patchopsiii-gpui ./PatchOpsIII/patchopsiii-gpui ``` -On Windows, run `PatchOpsIII\patchopsiii-gpui.exe`. The native executable starts -the bundled PyInstaller backend on an OS-assigned loopback port, waits up to 30 -seconds for `/api/health`, opens the UI only after the service is ready, and -terminates and reaps the service on normal exit. No Python installation is -needed. Linux still needs a working X11 or Wayland desktop, fonts, and a -Vulkan-capable driver. +Keep the resources directory alongside the executable. Linux needs X11 or +Wayland, fonts, and a Vulkan-capable graphics driver. Native ZIPs do not provide +an automatic installer or updater. Update checks offer the matching native ZIP; +the legacy MSI/NSIS/AppImage assets remain separate. ## Build and run from source -Install Rust with rustup, Python 3.12, and the normal backend requirements. The -crate pins GPUI 0.2.2 and GPUI Component 0.5.0 with a committed `Cargo.lock` and -Rust 1.99.0. GPUI's upstream main branch has since changed its platform API; use -the pinned published APIs when editing this app. +Install Rust through rustup. `native/rust-toolchain.toml` pins Rust 1.99.0 with +rustfmt and Clippy. `native/Cargo.lock` locks the workspace; GPUI 0.2.2 and GPUI +Component 0.5.0 remain pinned. The original lockfile's libc 0.2.189 is retained +for GPUI's xattr 0.2.3 dependency. -Linux build dependencies on Ubuntu 24.04: +Ubuntu 24.04 build packages: ```sh sudo apt-get install libfontconfig1-dev libfreetype6-dev libxkbcommon-dev \ @@ -62,75 +53,74 @@ sudo apt-get install libfontconfig1-dev libfreetype6-dev libxkbcommon-dev \ ``` Windows needs Visual Studio Build Tools with Desktop development with C++, a -Windows SDK, and Rust's MSVC toolchain. +Windows SDK, and Rust's MSVC toolchain. Rust builds do not require Python or Bun: -From the repository root: +```sh +cargo +1.99.0 build --locked --manifest-path native/Cargo.toml -p patchopsiii-gpui +./native/target/debug/patchopsiii-gpui +``` + +Python's standard library is sufficient for the optional convenience scripts: ```sh -python -m venv .venv -# Linux: source .venv/bin/activate -# Windows PowerShell: .venv\Scripts\Activate.ps1 -python -m pip install -r requirements.txt python scripts/gpui.py build python scripts/gpui.py run +# Existing Bun aliases: bun run gpui:build / bun run gpui:run ``` -The launcher remains the recommended development command: it starts the source -API with an inherited bound socket, waits for health, injects the resulting URL, -and reaps both processes. A directly started development binary can also find -`backend/api.py` from the repository and uses `.venv` or -`PATCHOPSIII_PYTHON`. `PATCHOPSIII_GPUI_BACKEND_URL` selects an externally owned -loopback API; `PATCHOPSIII_BACKEND_PATH` selects a packaged backend executable. - -Electron and GPUI deliberately share backend settings, logs, caches, and game -files. Do not perform overlapping game mutations in the two apps. Native actions -display the selected operation and game directory for confirmation. HTTP -requests run sequentially on a worker; long downloads do not freeze navigation. -GET requests time out after 30 seconds and operations after 10 minutes. A timeout -does not guarantee that the Python operation stopped, so check logs and refresh -before retrying. Wait for file operations to finish before closing the app. +`CARGO_TARGET_DIR` selects a custom build directory; relative paths in the +launcher resolve against the repository root. Default output is `native/target`. +`PATCHOPSIII_DATA_DIR` optionally selects isolated settings/log/cache storage, +useful for disposable tests. The old Python/backend URL environment variables +are no longer used. The normal data directory still shares +`electron-settings.json`, logs and legacy ownership state with Electron. Avoid +simultaneous mutations from both clients. -## Build a distributable archive +Requests execute serially on a worker thread. Navigation remains responsive +while an operation runs. Download timeouts and integrity checks live in the +core; there is no HTTP request timeout between the UI and worker. Wait for file +operations to finish before closing: cancellation and joining the worker on +window close are not implemented. -The backend build commands are shared with the legacy Electron packaging so the -PyInstaller flags cannot drift. Install Bun plus PyInstaller in `.venv`, then: +## Build a distributable archive ```sh -# Linux -bun run gpui:build:release -bun run build:backend:linux +python scripts/gpui.py build --release python scripts/package_gpui.py \ - --executable native/gpui/target/release/patchopsiii-gpui \ - --backend dist/backend/patchops-backend \ + --executable native/target/release/patchopsiii-gpui \ --output dist/native/PatchOpsIII-native-linux-x64.zip - -# Windows PowerShell uses build:backend:win and the corresponding .exe paths. +# Windows: use native/target/release/patchopsiii-gpui.exe and the Windows ZIP name. ``` -`.github/workflows/gpui-build.yml` runs formatting, Clippy with warnings denied, -Rust and Python tests, the Linux window-close smoke check, both release builds, -and archive creation. Stable and beta release workflows attach both native ZIPs -without changing the existing Electron artifact names or tag behavior. +The archive contains one product executable and resources/docs. Presets and +release version are also embedded at compile time; release workflows derive the +runtime version from package.json. Native CI builds/tests the Rust workspace and +runs standard-library Python launcher/archive checks. Stable and beta release +workflows attach both native ZIPs under their existing artifact names. ## Validation ```sh +cargo +1.99.0 fmt --manifest-path native/Cargo.toml --all --check +cargo +1.99.0 clippy --locked --manifest-path native/Cargo.toml --workspace --all-targets -- -D warnings +cargo +1.99.0 test --locked --manifest-path native/Cargo.toml -p patchops-core +cargo +1.99.0 test --locked --manifest-path native/Cargo.toml -p patchopsiii-gpui python -m unittest scripts/test_gpui.py -python scripts/gpui.py check -python scripts/gpui.py test -cargo fmt --manifest-path native/gpui/Cargo.toml --check -cargo clippy --locked --manifest-path native/gpui/Cargo.toml --all-targets -- -D warnings -# Linux with xvfb, openbox, xcompmgr, xdotool and a Vulkan driver installed: +# Read-only status on a disposable fake game, without a desktop: +python scripts/smoke_gpui_linux.py --status-only --executable native/target/debug/patchopsiii-gpui +# With xvfb, openbox, xcompmgr, xdotool and a Vulkan driver: xvfb-run -a python scripts/smoke_gpui_linux.py --start-session \ - --executable native/gpui/target/release/patchopsiii-gpui + --executable native/target/debug/patchopsiii-gpui ``` -## Remaining gaps +The binary's `--smoke-status` option starts the real worker, prints status JSON, +and exits before creating a window. The smoke script selects a temporary data +directory and fake executable/config; it never requests a game mutation. + +## Remaining validation -The native app is the primary implementation, but promotion does not imply full -UI parity. Native installer integration, signing, automatic updates, streaming -WebSocket logs, some richer progress/detail states, accessibility/IME coverage, -and real-device Windows, Wayland, and Steam Deck validation remain open. The -legacy Electron installers stay available while those delivery and parity gaps -are closed. See the migration record for the detailed workflow matrix and manual -review checklist. +Native signing/installers, automatic updates, accessibility/IME, Windows UAC, +Wayland, Steam Deck, real downloads and restoration on disposable installations +need hardware/manual checks. PR #40 uses stricter ownership and verified +transactional backups than Python; legacy payloads not provably owned may require +manual recovery. The parity fixtures document exact Python behavior separately. diff --git a/native/gpui/src/backend.rs b/native/gpui/src/backend.rs index 64b79e4..842a123 100644 --- a/native/gpui/src/backend.rs +++ b/native/gpui/src/backend.rs @@ -1,325 +1,17 @@ -//! Blocking HTTP stays on one worker thread. The UI never waits for game operations. -use anyhow::{Context, Result, bail}; -use reqwest::blocking::Client; +//! In-process Rust backend hosted on one worker thread. +use anyhow::{Result, bail}; +use patchops_core::{AppState, Engine, EventCallback, models::LogEntry}; use serde_json::Value; -use std::{ - env, - ffi::OsString, - fs, - net::TcpListener, - path::{Path, PathBuf}, - process::{Child, Command, Stdio}, - sync::mpsc, - thread, - time::{Duration, Instant}, -}; +use std::{sync::mpsc, thread}; -const BACKEND_HOST: &str = "127.0.0.1"; -const BACKEND_START_TIMEOUT: Duration = Duration::from_secs(30); - -struct LaunchSpec { - command: PathBuf, - args: Vec, - app_root: PathBuf, - packaged: bool, -} - -/// Owns the local API process for the lifetime of the desktop application. -/// -/// `PATCHOPSIII_GPUI_BACKEND_URL` remains an escape hatch for the development -/// launcher and tests. In that mode the process that supplied the URL retains -/// ownership of the service. -pub struct BackendService { - url: String, - child: Option, -} - -impl BackendService { - pub fn launch() -> Result { - if let Ok(url) = env::var("PATCHOPSIII_GPUI_BACKEND_URL") { - return Ok(Self { - url: validate_url(&url)?, - child: None, - }); - } - - let spec = resolve_launch_spec()?; - let port = reserve_loopback_port()?; - let url = format!("http://{BACKEND_HOST}:{port}"); - let mut command = Command::new(&spec.command); - command - .args(&spec.args) - .current_dir(&spec.app_root) - .env("PATCHOPSIII_BACKEND_HOST", BACKEND_HOST) - .env("PATCHOPSIII_BACKEND_PORT", port.to_string()) - .env("PYTHONUNBUFFERED", "1") - .stdin(Stdio::null()); - if let Some(version) = application_version(&spec.app_root) { - command.env("PATCHOPSIII_VERSION", version); - } - if spec.packaged { - command.stdout(Stdio::null()).stderr(Stdio::null()); - } else { - command.stdout(Stdio::inherit()).stderr(Stdio::inherit()); - } - configure_child_process(&mut command); - - let mut child = command.spawn().with_context(|| { - format!( - "Unable to start the PatchOpsIII backend at {}", - spec.command.display() - ) - })?; - if let Err(error) = wait_until_healthy(&mut child, &url, BACKEND_START_TIMEOUT) { - stop_child(&mut child); - return Err(error); - } - Ok(Self { - url, - child: Some(child), - }) - } - - pub fn url(&self) -> &str { - &self.url - } -} - -impl Drop for BackendService { - fn drop(&mut self) { - if let Some(child) = self.child.as_mut() { - stop_child(child); - } - } -} - -fn backend_executable_name() -> &'static str { - if cfg!(windows) { - "patchops-backend.exe" - } else { - "patchops-backend" - } -} - -fn resolve_launch_spec() -> Result { - if let Some(path) = env::var_os("PATCHOPSIII_BACKEND_PATH").map(PathBuf::from) { - let path = if path.is_absolute() { - path - } else { - env::current_dir() - .context("Unable to resolve PATCHOPSIII_BACKEND_PATH")? - .join(path) - }; - if !path.is_file() { - bail!( - "PATCHOPSIII_BACKEND_PATH does not name a file: {}", - path.display() - ); - } - return packaged_launch(path); - } - - let executable = env::current_exe().context("Unable to locate the GPUI executable")?; - let executable_dir = executable - .parent() - .context("The GPUI executable has no parent directory")?; - let name = backend_executable_name(); - let packaged_candidates = [ - executable_dir - .join("resources") - .join("backend-bin") - .join(name), - executable_dir.join("backend-bin").join(name), - executable_dir - .parent() - .unwrap_or(executable_dir) - .join("Resources") - .join("backend-bin") - .join(name), - ]; - for path in packaged_candidates { - if path.is_file() { - return packaged_launch(path); - } - } - - let app_root = find_source_root(&executable)?; - let python = python_command(&app_root); - Ok(LaunchSpec { - command: python, - args: vec![app_root.join("backend").join("api.py").into_os_string()], - app_root, - packaged: false, - }) -} - -fn packaged_launch(path: PathBuf) -> Result { - ensure_backend_executable(&path)?; - let app_root = path - .parent() - .and_then(Path::parent) - .context("Packaged backend has no resources directory")? - .to_path_buf(); - Ok(LaunchSpec { - command: path, - args: Vec::new(), - app_root, - packaged: true, - }) -} - -#[cfg(unix)] -fn ensure_backend_executable(path: &Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - - let mut permissions = fs::metadata(path)?.permissions(); - let mode = permissions.mode(); - if mode & 0o100 == 0 { - permissions.set_mode(mode | 0o100); - fs::set_permissions(path, permissions).with_context(|| { - format!( - "Unable to make the packaged backend executable: {}", - path.display() - ) - })?; - } - Ok(()) -} - -#[cfg(not(unix))] -fn ensure_backend_executable(_path: &Path) -> Result<()> { - Ok(()) -} - -fn find_source_root(executable: &Path) -> Result { - let current_dir = env::current_dir().context("Unable to read the working directory")?; - for start in [Some(current_dir.as_path()), executable.parent()] - .into_iter() - .flatten() - { - for candidate in start.ancestors() { - if candidate.join("backend").join("api.py").is_file() - && candidate.join("presets.json").is_file() - { - return Ok(candidate.to_path_buf()); - } - } - } - bail!( - "PatchOpsIII backend not found. Install resources/backend-bin/{} next to the app, or run from the repository.", - backend_executable_name() - ) -} - -fn python_command(app_root: &Path) -> PathBuf { - if let Some(python) = env::var_os("PATCHOPSIII_PYTHON") { - return PathBuf::from(python); - } - let venv_python = if cfg!(windows) { - app_root.join(".venv").join("Scripts").join("python.exe") - } else { - app_root.join(".venv").join("bin").join("python") - }; - if venv_python.is_file() { - venv_python - } else { - PathBuf::from(if cfg!(windows) { "python" } else { "python3" }) - } -} - -fn application_version(app_root: &Path) -> Option { - if let Ok(version) = env::var("PATCHOPSIII_VERSION") - && !version.trim().is_empty() - { - return Some(version); - } - let package = fs::read_to_string(app_root.join("package.json")).ok()?; - serde_json::from_str::(&package).ok()?["version"] - .as_str() - .map(str::to_owned) -} - -fn reserve_loopback_port() -> Result { - let listener = TcpListener::bind((BACKEND_HOST, 0)) - .context("Unable to reserve a loopback port for the backend")?; - Ok(listener.local_addr()?.port()) -} - -fn wait_until_healthy(child: &mut Child, url: &str, timeout: Duration) -> Result<()> { - let client = Client::builder() - .no_proxy() - .redirect(reqwest::redirect::Policy::none()) - .connect_timeout(Duration::from_secs(1)) - .timeout(Duration::from_secs(1)) - .build()?; - let deadline = Instant::now() + timeout; - while Instant::now() < deadline { - if let Some(status) = child.try_wait().context("Unable to inspect the backend")? { - bail!("PatchOpsIII backend exited during startup with {status}"); - } - if let Ok(response) = client.get(format!("{url}/api/health")).send() - && response.status().is_success() - && response - .json::() - .ok() - .and_then(|body| body["ok"].as_bool()) - == Some(true) - { - return Ok(()); - } - thread::sleep(Duration::from_millis(100)); - } - bail!("PatchOpsIII backend did not become healthy within 30 seconds") -} - -#[cfg(windows)] -fn configure_child_process(command: &mut Command) { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x0800_0000; - command.creation_flags(CREATE_NO_WINDOW); -} - -#[cfg(not(windows))] -fn configure_child_process(_command: &mut Command) {} - -fn stop_child(child: &mut Child) { - if matches!(child.try_wait(), Ok(Some(_))) { - return; - } - #[cfg(windows)] - { - let pid = child.id().to_string(); - let _ = Command::new("taskkill") - .args(["/pid", pid.as_str(), "/t", "/f"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - } - #[cfg(not(windows))] - { - let pid = child.id().to_string(); - let _ = Command::new("kill") - .args(["-TERM", pid.as_str()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - let deadline = Instant::now() + Duration::from_secs(3); - while Instant::now() < deadline { - if matches!(child.try_wait(), Ok(Some(_))) { - return; - } - thread::sleep(Duration::from_millis(50)); - } - } - let _ = child.kill(); - let _ = child.wait(); -} +pub use patchops_core::models::OperationProgress; pub struct Request { pub path: String, pub body: Option, } +#[derive(Debug)] pub struct Reply { pub state: Option, pub message: String, @@ -327,24 +19,22 @@ pub struct Reply { pub is_status: bool, } +/// Streaming events emitted while a long-running operation is executing. +/// +/// The current UI obtains the same entries in the next state response. A UI +/// can subscribe to Backend::events to render logs/progress immediately. +#[derive(Clone, Debug)] +#[allow(dead_code)] // UI event drain is wired separately from the backend port. +pub enum BackendEvent { + Log(LogEntry), + Progress(OperationProgress), +} + pub struct Backend { pub requests: mpsc::Sender, pub replies: mpsc::Receiver, -} - -pub fn validate_url(url: &str) -> Result { - let parsed = reqwest::Url::parse(url)?; - if parsed.scheme() != "http" - || parsed.host_str() != Some("127.0.0.1") - || !parsed.username().is_empty() - || parsed.password().is_some() - || parsed.path() != "/" - || parsed.query().is_some() - || parsed.fragment().is_some() - { - bail!("GPUI backend must be an HTTP origin on 127.0.0.1"); - } - Ok(url.trim_end_matches('/').to_owned()) + #[allow(dead_code)] // Public subscription hook for the UI. + pub events: mpsc::Receiver, } pub fn decode_reply(path: &str, value: Value) -> Result { @@ -373,7 +63,7 @@ pub fn decode_reply(path: &str, value: Value) -> Result { value.get("state").cloned() }; let message = if path == "/api/status" { - "Connected to the local service".to_owned() + "Connected to the in-process Rust backend".to_owned() } else if let Some(update) = value.get("update") { format!("Update check: {update}") } else { @@ -390,42 +80,59 @@ pub fn decode_reply(path: &str, value: Value) -> Result { }) } -fn execute(client: &Client, url: &str, request: Request) -> Result { +fn execute(engine: &Engine, request: Request) -> Result { let path = request.path; - let builder = match request.body { - Some(body) => client.post(format!("{url}{path}")).json(&body), - None => client - .get(format!("{url}{path}")) - .timeout(Duration::from_secs(30)), - }; - let value = builder - .send() - .context("Local service request failed")? - .error_for_status() - .context("Local service returned an HTTP error")? - .json::() - .context("Local service returned invalid JSON")?; + let value = engine + .dispatch(&path, request.body) + .map_err(anyhow::Error::msg)?; decode_reply(&path, value) } impl Backend { - pub fn start(url: String) -> Result { - let url = validate_url(&url)?; - // Loopback requests must not use a developer's system HTTP proxy. - let client = Client::builder() - .no_proxy() - .redirect(reqwest::redirect::Policy::none()) - .connect_timeout(Duration::from_secs(3)) - .timeout(Duration::from_secs(600)) - .build()?; + pub fn start() -> Result { + Self::start_with(|callback| AppState::for_desktop(Some(callback))) + } + + fn start_with( + factory: impl FnOnce(EventCallback) -> std::result::Result + Send + 'static, + ) -> Result { let (requests, incoming) = mpsc::channel::(); let (outgoing, replies) = mpsc::channel(); + let (event_sender, events) = mpsc::channel(); thread::Builder::new() - .name("patchops-gpui-api".into()) + .name("patchops-gpui-core".into()) .spawn(move || { + let progress_sender = event_sender.clone(); + let callback = std::sync::Arc::new(move |entry| { + let _ = event_sender.send(BackendEvent::Log(entry)); + }); + let state = match factory(callback) { + Ok(state) => state, + Err(error) => { + for request in incoming { + let is_status = request.path == "/api/status"; + if outgoing + .send(Reply { + state: None, + message: error.clone(), + failed: true, + is_status, + }) + .is_err() + { + break; + } + } + return; + } + }; + state.set_progress_callback(std::sync::Arc::new(move |progress| { + let _ = progress_sender.send(BackendEvent::Progress(progress)); + })); + let engine = Engine::new(state); for request in incoming { let is_status = request.path == "/api/status"; - let reply = execute(&client, &url, request).unwrap_or_else(|error| Reply { + let reply = execute(&engine, request).unwrap_or_else(|error| Reply { state: None, message: format!("{error:#}"), failed: true, @@ -436,7 +143,11 @@ impl Backend { } } })?; - Ok(Self { requests, replies }) + Ok(Self { + requests, + replies, + events, + }) } } @@ -446,26 +157,16 @@ mod tests { use serde_json::json; #[test] - fn accepts_only_loopback_origins() { - assert_eq!( - validate_url("http://127.0.0.1:8767/").unwrap(), - "http://127.0.0.1:8767" - ); - for url in [ - "https://127.0.0.1", - "http://example.com", - "http://localhost", - "http://127.0.0.1/api", - "http://user@127.0.0.1", - "http://127.0.0.1?x=1", - ] { - assert!(validate_url(url).is_err(), "{url}"); - } - } - - #[test] - fn handles_http_200_operation_failures_and_depot_instructions() { - let error = decode_reply("/api/exe-swap/compatible", json!({"ok": false, "error": "Download depot first", "depotCommand": "download_depot 311210 311211 123"})).err().unwrap(); + fn handles_operation_failures_and_depot_instructions() { + let error = decode_reply( + "/api/exe-swap/compatible", + json!({ + "ok": false, + "error": "Download depot first", + "depotCommand": "download_depot 311210 311211 123" + }), + ) + .unwrap_err(); assert!( error .to_string() @@ -498,57 +199,39 @@ mod tests { } #[test] - fn worker_posts_exact_payload_and_surfaces_http_errors() { - use std::{ - io::{Read, Write}, - net::TcpListener, - }; - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let url = format!("http://{}", listener.local_addr().unwrap()); - let server = thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); - stream - .set_read_timeout(Some(Duration::from_secs(3))) - .unwrap(); - let mut request = Vec::new(); - let mut byte = [0u8; 1]; - while !request.ends_with(b"\r\n\r\n") { - stream.read_exact(&mut byte).unwrap(); - request.push(byte[0]); - } - let headers = String::from_utf8(request).unwrap(); - assert!(headers.starts_with("POST /api/config HTTP/1.1")); - let length: usize = headers - .lines() - .find_map(|line| { - line.to_lowercase() - .strip_prefix("content-length:") - .map(|s| s.trim().parse().unwrap()) - }) - .unwrap(); - let mut body = vec![0; length]; - stream.read_exact(&mut body).unwrap(); - assert_eq!( - serde_json::from_slice::(&body).unwrap(), - json!({"key": "MaxFPS", "value": 144}) - ); - stream.write_all(b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").unwrap(); - }); - let backend = Backend::start(url).unwrap(); + fn worker_returns_in_process_status() { + let root = + std::env::temp_dir().join(format!("patchops-worker-test-{}", std::process::id())); + let data = root.join("data"); + let game = root.join("game"); + std::fs::create_dir_all(&data).unwrap(); + std::fs::create_dir_all(game.join("players")).unwrap(); + std::fs::write(game.join("BlackOps3.exe"), b"fake game").unwrap(); + std::fs::write(game.join("players/config.ini"), b"FOV = \"90\"\n").unwrap(); + std::fs::write( + data.join("electron-settings.json"), + json!({"game_dir": game}).to_string(), + ) + .unwrap(); + let backend = + Backend::start_with(move |callback| AppState::new(data, None, Some(callback))).unwrap(); backend .requests .send(Request { - path: "/api/config".into(), - body: Some(json!({"key": "MaxFPS", "value": 144})), + path: "/api/status".into(), + body: None, }) .unwrap(); let reply = backend .replies - .recv_timeout(Duration::from_secs(5)) + .recv_timeout(std::time::Duration::from_secs(5)) .unwrap(); - assert!(reply.failed); - assert!(reply.message.contains("503")); - assert!(reply.state.is_none()); - server.join().unwrap(); + assert!(!reply.failed, "{}", reply.message); + let state = reply.state.unwrap(); + assert!(state["appVersion"].is_string()); + assert_eq!(state["gameDetected"], true); + assert_eq!(state["graphics"]["fov"], 90); + drop(backend); + std::fs::remove_dir_all(root).unwrap(); } } diff --git a/native/gpui/rust-toolchain.toml b/native/rust-toolchain.toml similarity index 100% rename from native/gpui/rust-toolchain.toml rename to native/rust-toolchain.toml diff --git a/scripts/gpui.py b/scripts/gpui.py index 8306c01..ec084cd 100644 --- a/scripts/gpui.py +++ b/scripts/gpui.py @@ -1,24 +1,20 @@ #!/usr/bin/env python3 -"""Build or run the GPUI desktop app with a source-backed development service.""" +"""Build or run the single-process Rust/GPUI desktop app.""" from __future__ import annotations import argparse -import json import os from pathlib import Path -import socket import subprocess import sys -import tempfile -import time -from urllib.request import ProxyHandler, build_opener ROOT = Path(__file__).resolve().parents[1] -MANIFEST = ROOT / "native" / "gpui" / "Cargo.toml" +WORKSPACE = ROOT / "native" +MANIFEST = WORKSPACE / "Cargo.toml" def target_directory() -> Path: - target = Path(os.environ.get("CARGO_TARGET_DIR", MANIFEST.parent / "target")) + target = Path(os.environ.get("CARGO_TARGET_DIR", WORKSPACE / "target")) return target if target.is_absolute() else ROOT / target @@ -32,86 +28,30 @@ def stop(process: subprocess.Popen) -> None: process.wait() -def serve(ready: Path) -> None: - # Bind once and hand the listening socket directly to Uvicorn. No port race, - # no collision with Electron, and no need to modify the normal API launcher. - import uvicorn - from backend.api import app - - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: - listener.bind(("127.0.0.1", 0)) - listener.listen(128) - port = listener.getsockname()[1] - temporary = ready.with_suffix(".tmp") - temporary.write_text(json.dumps({"url": f"http://127.0.0.1:{port}"}), encoding="utf-8") - temporary.replace(ready) - server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning")) - server.run(sockets=[listener]) - - -def wait_ready(process: subprocess.Popen, ready: Path, timeout: float = 30) -> str: - deadline = time.monotonic() + timeout - opener = build_opener(ProxyHandler({})) - while time.monotonic() < deadline: - if process.poll() is not None: - raise RuntimeError(f"Local service exited with code {process.returncode}") - if ready.exists(): - url = json.loads(ready.read_text(encoding="utf-8"))["url"] - try: - with opener.open(f"{url}/api/health", timeout=1) as response: - if json.load(response).get("ok") is True: - return url - except OSError: - pass - time.sleep(0.1) - raise RuntimeError("Local service did not become healthy within 30 seconds") - - def run(executable: Path) -> int: if not executable.is_file(): raise RuntimeError(f"GPUI executable not found: {executable}. Run the build command first.") - with tempfile.TemporaryDirectory(prefix="patchops-gpui-") as directory: - ready = Path(directory) / "ready.json" - # Source execution needs the repository root on Python's import path. - env = {**os.environ, "PYTHONPATH": str(ROOT)} - service = subprocess.Popen([sys.executable, str(Path(__file__).resolve()), "serve", "--ready", str(ready)], cwd=ROOT, env=env) - app = None - try: - url = wait_ready(service, ready) - app = subprocess.Popen([str(executable)], cwd=ROOT, env={**env, "PATCHOPSIII_GPUI_BACKEND_URL": url}) - while app.poll() is None: - if service.poll() is not None: - raise RuntimeError("The local service stopped while GPUI was running") - time.sleep(0.2) - return app.returncode - finally: - if app is not None: - stop(app) - stop(service) + app = subprocess.Popen([str(executable)], cwd=ROOT) + try: + return app.wait() + finally: + stop(app) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("command", choices=["build", "run", "serve", "test", "check"]) + parser.add_argument("command", choices=["build", "run", "test", "check"]) parser.add_argument("--release", action="store_true") parser.add_argument("--executable", type=Path, help="Run a prebuilt GPUI executable") - parser.add_argument("--ready", type=Path, help=argparse.SUPPRESS) args = parser.parse_args() - if args.command == "serve": - if args.ready is None: - parser.error("serve requires --ready") - # When this file is run directly, sys.path starts at scripts/. - sys.path.insert(0, str(ROOT)) - serve(args.ready) - return 0 if args.command in {"build", "test", "check"}: command = ["cargo", args.command, "--locked", "--manifest-path", str(MANIFEST)] + command += ["--workspace"] if args.command == "test" else ["-p", "patchopsiii-gpui"] if args.release: command.append("--release") env = {**os.environ, "CARGO_TARGET_DIR": str(target_directory())} - return subprocess.call(command, cwd=MANIFEST.parent, env=env) - target = target_directory() - executable = args.executable or target / ("release" if args.release else "debug") / ("patchopsiii-gpui.exe" if os.name == "nt" else "patchopsiii-gpui") + return subprocess.call(command, cwd=WORKSPACE, env=env) + executable = args.executable or target_directory() / ("release" if args.release else "debug") / ("patchopsiii-gpui.exe" if os.name == "nt" else "patchopsiii-gpui") return run(executable.resolve()) diff --git a/scripts/package_gpui.py b/scripts/package_gpui.py index 4ee1b47..62c837b 100644 --- a/scripts/package_gpui.py +++ b/scripts/package_gpui.py @@ -10,7 +10,7 @@ ARCHIVE_ROOT = PurePosixPath("PatchOpsIII") -def archive_manifest(executable: Path, backend: Path) -> list[tuple[Path, PurePosixPath]]: +def archive_manifest(executable: Path) -> list[tuple[Path, PurePosixPath]]: required = [ ROOT / "LICENSE", ROOT / "README.md", @@ -22,14 +22,13 @@ def archive_manifest(executable: Path, backend: Path) -> list[tuple[Path, PurePo ROOT / "native" / "gpui" / "README.md", ROOT / "docs" / "gpui-architecture.md", ] - missing = [path for path in [executable, backend, *required] if not path.is_file()] + missing = [path for path in [executable, *required] if not path.is_file()] if missing: names = ", ".join(str(path) for path in missing) raise FileNotFoundError(f"Required packaging input missing: {names}") return [ (executable, ARCHIVE_ROOT / executable.name), - (backend, ARCHIVE_ROOT / "resources" / "backend-bin" / backend.name), (ROOT / "presets.json", ARCHIVE_ROOT / "resources" / "presets.json"), (ROOT / "package.json", ARCHIVE_ROOT / "resources" / "package.json"), (ROOT / "PatchOpsIII.ico", ARCHIVE_ROOT / "resources" / "PatchOpsIII.ico"), @@ -51,10 +50,10 @@ def archive_manifest(executable: Path, backend: Path) -> list[tuple[Path, PurePo ] -def create_archive(executable: Path, backend: Path, output: Path) -> Path: +def create_archive(executable: Path, output: Path) -> Path: output.parent.mkdir(parents=True, exist_ok=True) with ZipFile(output, "w", ZIP_DEFLATED) as archive: - for source, destination in archive_manifest(executable, backend): + for source, destination in archive_manifest(executable): archive.write(source, destination.as_posix()) return output @@ -62,11 +61,10 @@ def create_archive(executable: Path, backend: Path, output: Path) -> Path: def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--executable", type=Path, required=True) - parser.add_argument("--backend", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() try: - output = create_archive(args.executable, args.backend, args.output) + output = create_archive(args.executable, args.output) except FileNotFoundError as error: parser.error(str(error)) print(output) diff --git a/scripts/smoke_gpui_linux.py b/scripts/smoke_gpui_linux.py index de307a3..2eff8c5 100644 --- a/scripts/smoke_gpui_linux.py +++ b/scripts/smoke_gpui_linux.py @@ -1,39 +1,19 @@ #!/usr/bin/env python3 -"""Render a native window and close it through X11 against a safe local API. +"""Check in-process status and native X11 window close against a disposable game. -Requires an X11 DISPLAY, xdotool, and a compositor/window manager. CI uses -xvfb-run with --start-session. By default a read-only fake API is injected; ---use-owned-backend exercises the packaged backend. No mutations are requested. +Requires DISPLAY, xdotool, and a window manager for the window check. Use +--status-only to verify the same archive binary without desktop automation. +No game mutations or external backend service are requested. """ import argparse -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json import os from pathlib import Path import subprocess -import threading +import tempfile import time -class ReadOnlyStatus(BaseHTTPRequestHandler): - def do_GET(self): - if self.path != "/api/status": - self.send_error(404) - return - payload = json.dumps({"appVersion": "GPUI smoke", "platform": "Linux", "gameDetected": False, "gameDir": None, "launchProfiles": [], "logs": []}).encode() - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(payload))) - self.end_headers() - self.wfile.write(payload) - - def do_POST(self): - self.send_error(405, "Smoke service is read-only") - - def log_message(self, *_): - pass - - def stop(process): if process.poll() is None: process.terminate() @@ -48,21 +28,34 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--executable", type=Path, required=True) parser.add_argument("--start-session", action="store_true") - parser.add_argument("--use-owned-backend", action="store_true") + parser.add_argument("--status-only", action="store_true") args = parser.parse_args() owned = [] - with ThreadingHTTPServer(("127.0.0.1", 0), ReadOnlyStatus) as server: - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() + with tempfile.TemporaryDirectory(prefix="patchops-native-smoke-") as directory: + temporary = Path(directory) + game = temporary / "game" + (game / "players").mkdir(parents=True) + (game / "BlackOps3.exe").write_bytes(b"disposable smoke executable") + (game / "players" / "config.ini").write_text('FOV = "90"\n', encoding="utf-8") + data = temporary / "data" + data.mkdir() + (data / "electron-settings.json").write_text(json.dumps({"game_dir": str(game)}), encoding="utf-8") + env = {**os.environ, "PATCHOPSIII_DATA_DIR": str(data)} + # A native status request needs neither Python nor an HTTP listener. + executable = str(args.executable.resolve()) + status_result = subprocess.run([executable, "--smoke-status"], env=env, capture_output=True, text=True, check=True, timeout=30) + status = json.loads(status_result.stdout) + if not status.get("gameDetected") or Path(status["gameDir"]) != game or status["graphics"]["fov"] != 90: + raise RuntimeError(f"Native core failed to load disposable game status: {status}") + print("GPUI in-process Rust status loaded from disposable game") + if args.status_only: + return try: if args.start_session: owned.append(subprocess.Popen(["openbox", "--sm-disable"])) owned.append(subprocess.Popen(["xcompmgr"])) time.sleep(1) - env = dict(os.environ) - if not args.use_owned_backend: - env["PATCHOPSIII_GPUI_BACKEND_URL"] = f"http://127.0.0.1:{server.server_port}" - app = subprocess.Popen([str(args.executable.resolve())], env=env) + app = subprocess.Popen([executable], env=env) owned.append(app) deadline = time.monotonic() + 30 window = None @@ -77,9 +70,7 @@ def main(): if window is None: raise RuntimeError("GPUI did not map its native window") time.sleep(1) - # Close this exact owned window through its WM keyboard shortcut. - # This reproduces the platform callback path that TestAppContext - # cannot exercise; direct XDestroyWindow is not normal app closure. + # Normal window close exercises the real platform callback. subprocess.run(["xdotool", "windowactivate", "--sync", window, "key", "alt+F4"], check=True, timeout=5) result = app.wait(timeout=10) if result != 0: @@ -88,8 +79,6 @@ def main(): finally: for process in reversed(owned): stop(process) - server.shutdown() - thread.join(timeout=5) if __name__ == "__main__": diff --git a/scripts/test_gpui.py b/scripts/test_gpui.py index 3011942..c2f60ee 100644 --- a/scripts/test_gpui.py +++ b/scripts/test_gpui.py @@ -1,4 +1,4 @@ -"""Lifecycle and packaging checks use stand-ins; no game installation is modified.""" +"""Native launcher and archive checks; no game installation is modified.""" import importlib.util import os from pathlib import Path @@ -12,13 +12,16 @@ spec = importlib.util.spec_from_file_location("gpui_launcher", Path(__file__).with_name("gpui.py")) launcher = importlib.util.module_from_spec(spec) spec.loader.exec_module(launcher) - package_spec = importlib.util.spec_from_file_location("gpui_packager", Path(__file__).with_name("package_gpui.py")) packager = importlib.util.module_from_spec(package_spec) package_spec.loader.exec_module(packager) class LifecycleTests(unittest.TestCase): + def test_default_target_is_workspace_target(self): + with patch.dict(os.environ, {}, clear=True): + self.assertEqual(launcher.target_directory(), launcher.ROOT / "native" / "target") + def test_relative_target_directory_is_consistent_for_build_and_run(self): with patch.dict(os.environ, {"CARGO_TARGET_DIR": "build/custom-gpui"}): self.assertEqual(launcher.target_directory(), launcher.ROOT / "build/custom-gpui") @@ -27,77 +30,56 @@ def test_stop_reaps_child(self): process = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) launcher.stop(process) self.assertIsNotNone(process.poll()) - launcher.stop(process) # idempotent - - def test_startup_failure_cleans_service(self): - process = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) - try: - with patch.object(launcher.subprocess, "Popen", return_value=process), patch.object(launcher, "wait_ready", side_effect=RuntimeError("unhealthy")): - with self.assertRaisesRegex(RuntimeError, "unhealthy"): - launcher.run(Path(sys.executable)) - self.assertIsNotNone(process.poll()) - finally: - launcher.stop(process) - - def test_early_service_exit_is_reported(self): - process = subprocess.Popen([sys.executable, "-c", "raise SystemExit(7)"]) - process.wait() - with tempfile.TemporaryDirectory() as directory: - with self.assertRaisesRegex(RuntimeError, "code 7"): - launcher.wait_ready(process, Path(directory) / "ready.json") + launcher.stop(process) - def test_missing_executable_does_not_start_service(self): + def test_missing_executable_starts_no_process(self): with patch.object(launcher.subprocess, "Popen") as popen: with self.assertRaisesRegex(RuntimeError, "not found"): launcher.run(Path("/missing/patchopsiii-gpui")) popen.assert_not_called() - def test_ui_failure_stops_owned_service(self): - service = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) - app = subprocess.Popen([sys.executable, "-c", "raise SystemExit(9)"]) - app.wait() + def test_runs_only_native_executable_and_preserves_exit_status(self): + process = subprocess.Popen([sys.executable, "-c", "raise SystemExit(9)"]) + process.wait() + with patch.object(launcher.subprocess, "Popen", return_value=process) as popen: + self.assertEqual(launcher.run(Path(sys.executable)), 9) + popen.assert_called_once_with([sys.executable], cwd=launcher.ROOT) + + def test_interrupted_wait_reaps_native_child(self): + process = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) try: - with patch.object(launcher.subprocess, "Popen", side_effect=[service, app]), patch.object(launcher, "wait_ready", return_value="http://127.0.0.1:12345"): - self.assertEqual(launcher.run(Path(sys.executable)), 9) - self.assertIsNotNone(service.poll()) + with patch.object(launcher.subprocess, "Popen", return_value=process), patch.object(process, "wait", side_effect=[KeyboardInterrupt, None]): + with self.assertRaises(KeyboardInterrupt): + launcher.run(Path(sys.executable)) + process.wait(timeout=5) + self.assertIsNotNone(process.poll()) finally: - launcher.stop(service) + launcher.stop(process) class PackagingTests(unittest.TestCase): - def test_archive_is_self_contained_and_matches_native_lookup_layout(self): + def test_archive_contains_one_rust_executable_and_no_python_backend(self): with tempfile.TemporaryDirectory() as directory: temporary = Path(directory) executable = temporary / ("patchopsiii-gpui.exe" if os.name == "nt" else "patchopsiii-gpui") - backend = temporary / ("patchops-backend.exe" if os.name == "nt" else "patchops-backend") executable.write_bytes(b"native") - backend.write_bytes(b"backend") output = temporary / "PatchOpsIII-native.zip" - - packager.create_archive(executable, backend, output) - + packager.create_archive(executable, output) with ZipFile(output) as archive: names = set(archive.namelist()) self.assertIn(f"PatchOpsIII/{executable.name}", names) - self.assertIn(f"PatchOpsIII/resources/backend-bin/{backend.name}", names) self.assertIn("PatchOpsIII/resources/presets.json", names) self.assertIn("PatchOpsIII/resources/package.json", names) self.assertIn("PatchOpsIII/resources/PatchOpsIII.ico", names) self.assertIn("PatchOpsIII/resources/icon-512.png", names) self.assertIn("PatchOpsIII/THIRD-PARTY-NOTICES/Lucide.txt", names) - self.assertFalse(any(name.endswith(".py") for name in names)) - self.assertEqual( - archive.read(f"PatchOpsIII/resources/backend-bin/{backend.name}"), - b"backend", - ) + self.assertFalse(any("backend-bin" in name or name.endswith(".py") or "patchops-backend" in name for name in names)) + self.assertEqual(archive.read(f"PatchOpsIII/{executable.name}"), b"native") - def test_archive_requires_both_product_executables(self): + def test_archive_requires_native_executable(self): with tempfile.TemporaryDirectory() as directory: - temporary = Path(directory) - executable = temporary / "patchopsiii-gpui" - executable.write_bytes(b"native") - with self.assertRaisesRegex(FileNotFoundError, "patchops-backend"): - packager.archive_manifest(executable, temporary / "patchops-backend") + with self.assertRaisesRegex(FileNotFoundError, "patchopsiii-gpui"): + packager.archive_manifest(Path(directory) / "patchopsiii-gpui") if __name__ == "__main__": From 691dbbcae4ecf7a76e4b55ef2dd44fa561e11bef Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:02:51 -0400 Subject: [PATCH 04/17] test(core): add Python-golden parity suite for patchops-core --- native/core/tests/parity.rs | 412 +++ native/core/tests/parity/README.md | 115 + native/core/tests/parity/RESULTS.md | 38 + .../tests/parity/config_append_and_crlf.json | 483 +++ .../config_graphics_advanced_write.json | 1632 ++++++++++ .../tests/parity/config_missing_file.json | 296 ++ .../parity/config_readonly_roundtrip.json | 873 +++++ .../core/tests/parity/dxvk_config_write.json | 550 ++++ .../exe_preserved_enhanced_detection.json | 476 +++ .../exe_untrusted_backup_detection.json | 539 ++++ .../game_directory_validation_invalid.json | 202 ++ .../game_directory_validation_valid.json | 481 +++ .../tests/parity/launch_options_apply.json | 800 +++++ .../parity/launch_options_unsupported.json | 482 +++ .../core/tests/parity/qol_restore_legacy.json | 1042 ++++++ .../tests/parity/qol_toggle_roundtrip.json | 2845 +++++++++++++++++ native/core/tests/parity/qol_toggles.json | 1042 ++++++ native/core/tests/parity/results.jsonl | 135 + .../core/tests/parity/results.provenance.json | 31 + .../parity/status_alternate_executable.json | 455 +++ .../tests/parity/status_compatible_build.json | 459 +++ .../tests/parity/status_config_variants.json | 455 +++ .../tests/parity/status_dxvk_installed.json | 518 +++ .../tests/parity/status_empty_config.json | 455 +++ .../parity/status_enhanced_installed.json | 560 ++++ native/core/tests/parity/status_no_game.json | 341 ++ .../parity/status_september_2026_current.json | 459 +++ .../status_steam_secondary_library.json | 458 +++ .../tests/parity/status_t7_installed.json | 518 +++ .../tests/parity/status_unverified_base.json | 455 +++ .../parity/t7_config_clear_and_append.json | 557 ++++ .../tests/parity/t7_config_invalid_name.json | 487 +++ .../core/tests/parity/t7_config_missing.json | 302 ++ native/core/tests/parity/t7_config_write.json | 557 ++++ .../parity/t7_release_asset_discovery.json | 177 + .../tests/parity/vram_target_roundtrip.json | 761 +++++ scripts/gen_parity_fixtures.py | 676 ++++ 37 files changed, 21124 insertions(+) create mode 100644 native/core/tests/parity.rs create mode 100644 native/core/tests/parity/README.md create mode 100644 native/core/tests/parity/RESULTS.md create mode 100644 native/core/tests/parity/config_append_and_crlf.json create mode 100644 native/core/tests/parity/config_graphics_advanced_write.json create mode 100644 native/core/tests/parity/config_missing_file.json create mode 100644 native/core/tests/parity/config_readonly_roundtrip.json create mode 100644 native/core/tests/parity/dxvk_config_write.json create mode 100644 native/core/tests/parity/exe_preserved_enhanced_detection.json create mode 100644 native/core/tests/parity/exe_untrusted_backup_detection.json create mode 100644 native/core/tests/parity/game_directory_validation_invalid.json create mode 100644 native/core/tests/parity/game_directory_validation_valid.json create mode 100644 native/core/tests/parity/launch_options_apply.json create mode 100644 native/core/tests/parity/launch_options_unsupported.json create mode 100644 native/core/tests/parity/qol_restore_legacy.json create mode 100644 native/core/tests/parity/qol_toggle_roundtrip.json create mode 100644 native/core/tests/parity/qol_toggles.json create mode 100644 native/core/tests/parity/results.jsonl create mode 100644 native/core/tests/parity/results.provenance.json create mode 100644 native/core/tests/parity/status_alternate_executable.json create mode 100644 native/core/tests/parity/status_compatible_build.json create mode 100644 native/core/tests/parity/status_config_variants.json create mode 100644 native/core/tests/parity/status_dxvk_installed.json create mode 100644 native/core/tests/parity/status_empty_config.json create mode 100644 native/core/tests/parity/status_enhanced_installed.json create mode 100644 native/core/tests/parity/status_no_game.json create mode 100644 native/core/tests/parity/status_september_2026_current.json create mode 100644 native/core/tests/parity/status_steam_secondary_library.json create mode 100644 native/core/tests/parity/status_t7_installed.json create mode 100644 native/core/tests/parity/status_unverified_base.json create mode 100644 native/core/tests/parity/t7_config_clear_and_append.json create mode 100644 native/core/tests/parity/t7_config_invalid_name.json create mode 100644 native/core/tests/parity/t7_config_missing.json create mode 100644 native/core/tests/parity/t7_config_write.json create mode 100644 native/core/tests/parity/t7_release_asset_discovery.json create mode 100644 native/core/tests/parity/vram_target_roundtrip.json create mode 100644 scripts/gen_parity_fixtures.py diff --git a/native/core/tests/parity.rs b/native/core/tests/parity.rs new file mode 100644 index 0000000..d510bac --- /dev/null +++ b/native/core/tests/parity.rs @@ -0,0 +1,412 @@ +//! Python main is the oracle. See parity/README.md for isolation and normalization. +use patchops_core::{AppState, Engine}; +use regex::Regex; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::{ + env, fs, + path::{Path, PathBuf}, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; + +fn decode64(text: &str) -> Vec { + let mut accumulator = 0u32; + let mut bits = 0; + let mut bytes = Vec::new(); + for byte in text.bytes().filter(|byte| *byte != b'=') { + let value = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .iter() + .position(|candidate| *candidate == byte) + .expect("valid fixture base64"); + accumulator = (accumulator << 6) | value as u32; + bits += 6; + if bits >= 8 { + bits -= 8; + bytes.push((accumulator >> bits) as u8); + } + } + bytes +} + +fn encode64(bytes: &[u8]) -> String { + let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut output = String::new(); + for chunk in bytes.chunks(3) { + let value = ((chunk[0] as u32) << 16) + | ((chunk.get(1).copied().unwrap_or(0) as u32) << 8) + | chunk.get(2).copied().unwrap_or(0) as u32; + for offset in (0..4).rev() { + let index = 3 - offset; + output.push(if index > chunk.len() { + '=' + } else { + alphabet[((value >> (6 * offset)) & 63) as usize] as char + }); + } + } + output +} + +fn normalize_text(text: &str, root: &Path) -> String { + let mut text = text.replace(root.to_str().unwrap(), "$ROOT"); + if let Ok(kernel) = fs::read_to_string("/proc/sys/kernel/osrelease") { + text = text.replace(kernel.trim(), "$KERNEL"); + } + Regex::new(r"\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\b") + .unwrap() + .replace_all(&text, regex::NoExpand("$TIMESTAMP")) + .into_owned() +} + +fn normalize(value: Value, root: &Path) -> Value { + match value { + Value::String(text) => Value::String(normalize_text(&text, root)), + Value::Array(items) => Value::Array( + items + .into_iter() + .map(|item| normalize(item, root)) + .collect(), + ), + Value::Object(items) => Value::Object( + items + .into_iter() + .map(|(key, value)| (normalize_text(&key, root), normalize(value, root))) + .collect(), + ), + value => value, + } +} + +fn recreate(root: &Path, fixture: &Value) { + fs::create_dir_all(root).unwrap(); + for directory in fixture["directories"].as_array().unwrap() { + fs::create_dir_all(root.join(directory.as_str().unwrap())).unwrap(); + } + for file in fixture["files"].as_array().unwrap() { + let path = root.join(file["path"].as_str().unwrap()); + let bytes = if let Some(text) = file["text"].as_str() { + text.replace("$ROOT", root.to_str().unwrap()).into_bytes() + } else { + decode64(file["base64"].as_str().unwrap()) + }; + fs::write(&path, bytes).unwrap(); + #[cfg(unix)] + fs::set_permissions( + path, + fs::Permissions::from_mode(file["mode"].as_u64().unwrap() as u32), + ) + .unwrap(); + } +} + +fn snapshot(root: &Path) -> Value { + fn visit(root: &Path, path: &Path, directories: &mut Vec, files: &mut Vec) { + let mut children: Vec<_> = fs::read_dir(path) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect(); + children.sort(); + for path in children { + let relative = path + .strip_prefix(root) + .unwrap() + .to_str() + .unwrap() + .replace('\\', "/"); + if path.is_dir() { + directories.push(relative); + visit(root, &path, directories, files); + } else { + let raw = fs::read(&path).unwrap(); + #[cfg(unix)] + let mode = path.metadata().unwrap().permissions().mode() & 0o777; + #[cfg(not(unix))] + let mode = 0o644; + let mut entry = json!({"path": relative, "mode": mode}); + let normalized = match String::from_utf8(raw.clone()) { + Ok(text) => { + let text = normalize_text(&text, root); + entry["text"] = json!(text); + text.into_bytes() + } + Err(_) => { + entry["base64"] = json!(encode64(&raw)); + raw + } + }; + entry["size"] = json!(normalized.len()); + entry["sha256"] = json!(format!("{:x}", Sha256::digest(&normalized))); + files.push(entry); + } + } + } + let mut directories = Vec::new(); + let mut files = Vec::new(); + visit(root, root, &mut directories, &mut files); + directories.sort(); + files.sort_by(|a, b| a["path"].as_str().cmp(&b["path"].as_str())); + json!({"directories": directories, "files": files}) +} + +fn differences(case: &str, location: &str, expected: &Value, actual: &Value, count: &mut usize) { + if expected == actual { + return; + } + match (expected, actual) { + (Value::Object(expected), Value::Object(actual)) => { + let keys: std::collections::BTreeSet<_> = + expected.keys().chain(actual.keys()).collect(); + for key in keys { + if expected.contains_key(key) && actual.contains_key(key) { + differences( + case, + &format!("{location}/{key}"), + &expected[key], + &actual[key], + count, + ); + } else { + *count += 1; + println!( + "PARITY_DIFF {}", + json!({"case": case, "path": format!("{location}/{key}"), "expected": expected.get(key), "actual": actual.get(key), "expectedPresent": expected.contains_key(key), "actualPresent": actual.contains_key(key)}) + ); + } + } + } + // Compare filesystem entries by path so one extra log/settings file doesn't + // shift every subsequent entry and hide the real changes. + (Value::Array(expected), Value::Array(actual)) if location.ends_with("/files") => { + let as_map = |files: &[Value]| -> Value { + Value::Object( + files + .iter() + .map(|file| (file["path"].as_str().unwrap().to_owned(), file.clone())) + .collect(), + ) + }; + differences(case, location, &as_map(expected), &as_map(actual), count); + } + (Value::Array(expected), Value::Array(actual)) if expected.len() == actual.len() => { + for (index, (expected, actual)) in expected.iter().zip(actual).enumerate() { + differences( + case, + &format!("{location}/{index}"), + expected, + actual, + count, + ); + } + } + _ => { + *count += 1; + println!( + "PARITY_DIFF {}", + json!({"case": case, "path": location, "expected": expected, "actual": actual}) + ); + } + } +} + +fn run_case(name: &str) { + let repo = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .unwrap(); + let golden: Value = serde_json::from_slice( + &fs::read(repo.join(format!("native/core/tests/parity/{name}.json"))).unwrap(), + ) + .unwrap(); + assert!( + golden.get("rustIgnore").is_none(), + "adapter missing: {}", + golden["rustIgnore"] + ); + if let Ok(root) = env::var("PATCHOPS_PARITY_CHILD_ROOT") { + let root = PathBuf::from(root); + let engine = Engine::new( + AppState::new(root.join("data"), Some(root.join("resources")), None).unwrap(), + ); + let mut count = 0; + for (index, operation) in golden["operations"].as_array().unwrap().iter().enumerate() { + let body = operation.get("body").map(|body| { + serde_json::from_str::( + &body.to_string().replace("$ROOT", root.to_str().unwrap()), + ) + .unwrap() + }); + let actual = engine + .dispatch(operation["path"].as_str().unwrap(), body) + .unwrap_or_else(|error| json!({"dispatchError": error})); + differences( + name, + &format!("responses/{index}"), + &golden["expected"]["responses"][index], + &normalize(actual, &root), + &mut count, + ); + differences( + name, + &format!("checkpoints/{index}"), + &golden["expected"]["checkpoints"][index], + &snapshot(&root), + &mut count, + ); + } + differences( + name, + "filesystem", + &golden["expected"]["filesystem"], + &snapshot(&root), + &mut count, + ); + assert_eq!( + count, 0, + "{name}: {count} parity differences; see PARITY_DIFF records" + ); + return; + } + assert!( + cfg!(target_os = "linux"), + "this harness needs Linux bubblewrap; supply platform isolation adapters elsewhere" + ); + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let scratch = env::temp_dir().join(format!( + "patchops-rust-parity-{}-{unique}-{name}", + std::process::id() + )); + let root = scratch.join("root"); + recreate(&root, &golden["fixture"]); + assert_eq!( + snapshot(&root), + golden["fixture"], + "fixture reconstruction must be lossless" + ); + let tools = scratch.join("tools"); + fs::create_dir_all(&tools).unwrap(); + for (name, exit) in [("steam", 0), ("pkill", 1)] { + let path = tools.join(name); + // Change comm to `true` before exiting: a zombie shell named `steam` + // would make the real /proc polling incorrectly report Steam running. + let script = if name == "steam" { + "#!/bin/sh\nexec /usr/bin/true\n".to_owned() + } else { + format!("#!/bin/sh\nexit {exit}\n") + }; + fs::write(&path, script).unwrap(); + #[cfg(unix)] + fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap(); + } + let home = env::var_os("HOME").expect("HOME is required for the existing Steam discovery API"); + let home = PathBuf::from(home); + let output = Command::new("bwrap") + .args([ + "--die-with-parent", + "--ro-bind", + "/", + "/", + "--unshare-pid", + "--proc", + "/proc", + "--dev", + "/dev", + ]) + .arg("--tmpfs") + .arg(&home) + .arg("--ro-bind") + .arg(&repo) + .arg(&repo) + .arg("--bind") + .arg(&scratch) + .arg(&scratch) + .arg("--dir") + .arg(home.join(".steam")) + .arg("--symlink") + .arg(root.join("steam")) + .arg(home.join(".steam/steam")) + .args(["/bin/sh", "-c", "umask 002; exec \"$@\"", "parity"]) + .arg(env::current_exe().unwrap()) + .args(["--exact", name, "--nocapture"]) + .env("PATCHOPS_PARITY_CHILD_ROOT", &root) + .env( + "PATH", + format!( + "{}:{}", + tools.display(), + env::var("PATH").unwrap_or_default() + ), + ) + .output() + .expect("bubblewrap must be installed; no unsafe host fallback is permitted"); + fs::remove_dir_all(scratch).unwrap(); + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + assert!( + output.status.success(), + "{name}: isolated parity test failed ({})", + output.status + ); +} + +macro_rules! case { + ($name:ident) => { + #[test] + #[cfg_attr( + not(target_os = "linux"), + ignore = "needs a platform filesystem/process isolation adapter" + )] + fn $name() { + run_case(stringify!($name)); + } + }; +} +case!(status_no_game); +case!(status_unverified_base); +case!(status_steam_secondary_library); +case!(game_directory_validation_invalid); +case!(game_directory_validation_valid); +case!(config_graphics_advanced_write); +case!(t7_config_write); +case!(dxvk_config_write); +case!(launch_options_apply); +case!(qol_toggles); +case!(status_t7_installed); +case!(status_dxvk_installed); +case!(status_enhanced_installed); +case!(status_empty_config); +case!(status_config_variants); +case!(config_append_and_crlf); +case!(config_missing_file); +case!(t7_config_missing); +case!(t7_config_clear_and_append); +case!(t7_config_invalid_name); +case!(launch_options_unsupported); +case!(qol_restore_legacy); +case!(qol_toggle_roundtrip); +case!(exe_untrusted_backup_detection); +case!(exe_preserved_enhanced_detection); +case!(status_alternate_executable); +case!(config_readonly_roundtrip); +case!(vram_target_roundtrip); + +#[test] +#[ignore = "needs injectable EXE SHA-256 provider; fake bytes cannot hash to the Sep 2026 Steam digest"] +fn status_september_2026_current() { + run_case("status_september_2026_current"); +} +#[test] +#[ignore = "needs injectable EXE SHA-256 provider; fake bytes cannot hash to the compatible Steam digest"] +fn status_compatible_build() { + run_case("status_compatible_build"); +} +#[test] +#[ignore = "needs public release-asset selection API or injectable HTTP transport; install otherwise downloads archives"] +fn t7_release_asset_discovery() { + run_case("t7_release_asset_discovery"); +} diff --git a/native/core/tests/parity/README.md b/native/core/tests/parity/README.md new file mode 100644 index 0000000..5e657dc --- /dev/null +++ b/native/core/tests/parity/README.md @@ -0,0 +1,115 @@ +# Python → Rust behavioral parity + +The case JSON files are goldens generated from the Python backend on `main`, not +handwritten Rust expectations. `sourceHashes` records the five Python modules +used. At generation, those modules were identical to local `main`. + +Run from the repository root: + +```sh +python3 scripts/gen_parity_fixtures.py +python3 scripts/gen_parity_fixtures.py --check +cd native +~/.cargo/bin/cargo test -p patchops-core --test parity -- --nocapture +``` + +The Python runner prefers FastAPI TestClient when FastAPI, Pydantic, and HTTPX +are available. This environment lacks FastAPI; the existing temporary venv +also has neither pip nor ensurepip. These goldens therefore use direct async +endpoint calls with minimal import/model shims, plus installed `requests` and +`vdf`. They exercise endpoint logic and filesystem effects, **not FastAPI +request validation or lifecycle hooks**. Installing the real dependencies may +change `runner` metadata; regenerate before using `--check` in that environment. + +## Coverage + +31 cases cover: + +- Status without a game, base game, secondary Steam library discovered through + `libraryfolders.vdf`, both executable names, empty/malformed/decimal graphics + configuration, installed T7/DXVK/Enhanced, current September 2026 and + compatible executable profiles. +- Valid and invalid game-directory selection; graphics/advanced writes; config + append and CRLF input; missing config; read-only/write/unlock; VRAM limit and + unlimited round trips. +- T7 gamertag/color/password/friends-only edits, missing keys, trimmed values, + empty/overlong names, missing configuration, preservation of unknown keys. +- DXVK settings writes with existing DLL/config files. +- Supported Steam launch-option application and preservation of existing Wine, + command marker, custom flags, and `fs_game`; unsupported options. +- Intro, all-intro, and compiler toggles, repeated enable, restoration, and + legacy `.bak` restoration. +- EXE integrity/state detection, untrusted build backups, and a preserved + Enhanced hash with real fixture bytes. +- Mock release metadata for version-suffixed legacy T7 archives and renamed + universal current archives (the #46/#47 behavior). + +Each golden records initial directories/files, operations, endpoint responses, +filesystem snapshots **after every operation**, and final filesystem state. +Files include relative path, mode, normalized byte size, SHA-256, and text or +base64 content. Fixture recreation is checked before invoking Rust. + +## Isolation + +Generation uses temporary roots, fixture settings/presets/Steam paths, disabled +Steam process operations, synchronous log capture, and a fail-closed Requests +network guard. Release discovery alone replaces `requests.get` with a fake +response. No game payload is downloaded or redistributed. + +The Rust tests target the public `AppState::new` and `Engine::dispatch` APIs. +On Linux they re-exec each case through **bubblewrap** with a read-only host +filesystem, a temporary home mount, a fixture Steam symlink, writable fixture +root, isolated PID namespace, and fake `steam`/`pkill` commands. The home +environment variable is read, never changed; real Steam files/processes cannot +be affected. No network-performing Rust dispatch is exercised. Missing or +unsupported bubblewrap fails explicitly; there is no host fallback. Tests on +other platforms are ignored pending equivalent filesystem/process isolation. + +## Normalization rules + +1. Replace each temporary root in responses, object keys, and UTF-8 file + contents with `$ROOT`. Paths in snapshots are relative and use `/`. +2. Replace generated `YYYY-MM-DD HH:MM:SS` log timestamps with `$TIMESTAMP`. + Seeded ISO Enhanced timestamps are fixed inputs and retained verbatim. +3. Replace the host kernel release in Python log headers with `$KERNEL`. + Version strings, platform naming, log header structure, messages, and + category/order are retained and compared. +4. Sort filesystem entries/directories and JSON object keys. Preserve response + array order, config comments, line endings, permission bits, VDF whitespace, + JSON file formatting, missing-vs-null fields, and every extra/deleted file. + JSON numbers are compared exactly, including integer versus float representation. +5. Text file size/hash describe normalized UTF-8 bytes, not the physical + temporary-path-expanded file. Binary files use unmodified bytes/base64. + File mtimes/ownership and temporary directory names are not recorded. Both + runners use fixed umask `002`; fixture files explicitly start at `0644`. + This catches atomic replacements that change existing permission bits. +6. Known current/compatible EXEs contain nonempty marker bytes and a + `logicalSha256` alias used **only by the Python hash seam**. Their physical + sizes/hashes are also recorded. Production code checks build identity by + SHA-256, not a fixed EXE byte size; fake bytes cannot have the real digest. + +## Missing Rust adapters (three explicit ignores) + +| Case | Adapter needed | +| --- | --- | +| `status_september_2026_current` | Injectable file-hash provider, covering the September 2026 hash and BuildID 24784313. | +| `status_compatible_build` | Same hash seam, covering the compatible digest and BuildID 10650222. | +| `t7_release_asset_discovery` | Expose a pure asset selector or injectable HTTP transport; the existing selector is private to installation/download. | + +These cases contain real Python expected output. Running them explicitly with +`--ignored` fails with the missing adapter reason; they cannot silently pass. + +## Results + +See [RESULTS.md](RESULTS.md) for concrete regressions and +[results.jsonl](results.jsonl) for **every** leaf mismatch (including intermediate +checkpoints and derivative size/hash differences). Dispatch errors are captured +as `{"dispatchError": ...}` diagnostic values: this wrapper is not a production +API response and is not accepted as equivalent to Python's `ok/error/state` +JSON. The strict suite deliberately remains red while these differences exist. + +Status-envelope differences do not prevent the harness from checking the +remaining response fields or filesystem changes. +Byte-only formatting differences are reported alongside functional differences +and should be assessed separately. Do not replace goldens with Rust output to +make the suite green. diff --git a/native/core/tests/parity/RESULTS.md b/native/core/tests/parity/RESULTS.md new file mode 100644 index 0000000..166f3ec --- /dev/null +++ b/native/core/tests/parity/RESULTS.md @@ -0,0 +1,38 @@ +# Latest parity results + +test result: FAILED. 17 passed; 11 failed; 3 ignored; 0 measured; 0 filtered out; finished in 10.53s + +135 leaf mismatches across 11 failing cases. Every failure is described below; [results.jsonl](results.jsonl) contains every expected/actual value, including intermediate checkpoints and derivative size/hash mismatches. [results.provenance.json](results.provenance.json) records source snapshots and the executed test binary hash. + +The core is being edited concurrently. These results describe the compiled test run; source hashes were sampled before launch and when writing this report, and the provenance file lists any changes between those samples. Later core edits require another run. Python module hashes are recorded in each golden and match local main. + +Generator verification: `python3 scripts/gen_parity_fixtures.py --check` passed for all 31 cases. This environment used the direct-endpoint fallback, not FastAPI request validation. + +| Failing case | Differences | Expected vs actual | Likely cause / location | +| --- | ---: | --- | --- | +| `config_append_and_crlf` | 6 | Python rewrites CRLF to LF; Rust retains CRLF. Values, comments and file modes now match. | `native/core/src/app.rs:169` — write_config_values deliberately retains the detected line ending. | +| `config_readonly_roundtrip` | 17 | Python uses 0400/0600; Rust uses 0444/0644. Rejected write expects the Python Permission denied message; Rust returns its own read-only explanation and logs it. | `native/core/src/app.rs:331`; `native/core/src/app.rs:169` — preserve-mode chmod and preflight rejection differ from Python chmod/write. | +| `dxvk_config_write` | 15 | Python returns ok=true and updates async, threads, FPS, latency, tear-free and HUD. Rust returns ok=false with “DXVK is not managed by PatchOpsIII…” and leaves dxvk.conf unchanged. | `native/core/src/dxvk.rs:175` — configure_managed requires an ownership manifest or recognized legacy DLL digests. This is an explicit safety policy difference; fake DLLs cannot satisfy legacy digest pins. | +| `game_directory_validation_invalid` | 6 | The error response now matches. Python logs “Selected directory is not a Black Ops III install: $ROOT/not-a-game”; Rust logs “BlackOps3.exe or BlackOpsIII.exe was not found.” | `native/core/src/app.rs:44`; `native/core/src/engine.rs:40` — generic dispatch error logging replaces the endpoint-specific message. | +| `launch_options_apply` | 31 | Current launch options and active profile match. Python updates the rolling localconfig backup on the second apply; Rust skips a no-op write and keeps oldmod in the backup. Rust also creates localconfig.vdf.patchops.bak, uses a tab between VDF key/value instead of a space, changes modes, and emits different logs. | `native/core/src/steam.rs:408`; `native/core/src/steam.rs:388`; `native/core/src/steam.rs:263` — no-op short circuit, persistent original backup and serializer. Some process-log differences depend on Python close/open being mocked; persistence differences do not. | +| `launch_options_unsupported` | 7 | Python returns ok=false/error plus state and logs Warning. Rust returns ok=false/error without state and logs Error. | `native/core/src/engine.rs:40` — generic mutation error conversion loses the endpoint-specific state and severity. | +| `qol_toggle_roundtrip` | 41 | Files and QoL flags match. Repeated compiler enable expects “Already using latest d3dcompiler.”; Rust says it renamed the DLL. Repeated all-intro enable expects “Intro videos were already skipped.”; Rust says “All intro videos skipped.” | `native/core/src/app.rs:402`; `native/core/src/app.rs:435` — success log selection ignores whether a file changed. | +| `status_enhanced_installed` | 4 | Legacy Enhanced state expects filesInstalled=4, backupStatus=Created and the two seeded timestamps. Rust reports 0/Not created/null/null, although installed=true matches. | `native/core/src/enhanced.rs:146`; `native/core/src/enhanced.rs:201` — legacy Python state has no game_directory; status discards unbound ownership metadata. Requires safe legacy migration or a documented intentional behavior change. | +| `t7_config_invalid_name` | 3 | Empty/overlong-name error strings now match, including periods. Python emits no log/file for validation failure; Rust creates an Error log file. | `native/core/src/engine.rs:40` — generic dispatch error logging adds validation side effects. | +| `t7_config_missing` | 2 | The missing-gamertag-config error text now matches. Python emits no log/file; Rust creates PatchOpsIII.log with an Error entry. | `native/core/src/engine.rs:40` — generic dispatch error logging adds a validation side effect. | +| `vram_target_roundtrip` | 3 | At 100%, Python writes VideoMemory="1.0"; Rust writes "1". Response values, final config text and permissions match. The intermediate checkpoint catches the byte difference. | `native/core/src/app.rs:292` — set_vram_target trims trailing zeros from the decimal value. | + +## Passing cases + +`config_graphics_advanced_write`, `config_missing_file`, `exe_preserved_enhanced_detection`, `exe_untrusted_backup_detection`, `game_directory_validation_valid`, `qol_restore_legacy`, `qol_toggles`, `status_alternate_executable`, `status_config_variants`, `status_dxvk_installed`, `status_empty_config`, `status_no_game`, `status_steam_secondary_library`, `status_t7_installed`, `status_unverified_base`, `t7_config_clear_and_append`, `t7_config_write`. + +Earlier failures in shared status JSON, config comments, settings serialization, preserved Enhanced T7 mode, T7 config logging/staging, and existing-file atomic permissions now pass after concurrent core fixes. No goldens were changed to accept Rust behavior. + +## Explicit adapter gaps + +- September 2026 current and compatible Steam EXE cases require an injectable hash provider. Their physical marker bytes cannot have the game executable digests. +- Versioned/renamed T7 release asset discovery requires a public pure selector or injectable HTTP transport. The private installer otherwise downloads archives. + +Functional response gaps (missing state, rejected DXVK config, legacy Enhanced metadata) merit implementation review. Byte/permission/log differences are also strict parity failures; some ownership/backup behavior is deliberately stricter in Rust. Steam process log comparisons are limited by Python’s disabled close/open hooks. No process lifecycle or network/download integration was validated. + +The test agent edited only the generator and parity test paths. No commits, pushes, or stashes were made. diff --git a/native/core/tests/parity/config_append_and_crlf.json b/native/core/tests/parity/config_append_and_crlf.json new file mode 100644 index 0000000..8ca7b68 --- /dev/null +++ b/native/core/tests/parity/config_append_and_crlf.json @@ -0,0 +1,483 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "5bbcfaa7b01899957078f15e497c18c301b1f36dba18a4b230d7171bec43a4ac", + "size": 41, + "text": "$TIMESTAMP - Success: Set MaxFPS to 200.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "88cdfbed938b27580ce91830d4f72a25e720dd529767ec6804f8f014dea678ed", + "size": 46, + "text": "FOV = \"80\"\nMaxFPS = \"200\" // fixture appended\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "5bbcfaa7b01899957078f15e497c18c301b1f36dba18a4b230d7171bec43a4ac", + "size": 41, + "text": "$TIMESTAMP - Success: Set MaxFPS to 200.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "88cdfbed938b27580ce91830d4f72a25e720dd529767ec6804f8f014dea678ed", + "size": 46, + "text": "FOV = \"80\"\nMaxFPS = \"200\" // fixture appended\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 75 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 200, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set MaxFPS to 200.", + "message": "Set MaxFPS to 200." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Set MaxFPS to 200.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "6a4e31f30c7a97d9bde8fe66f1e6b89e5b6370281053bf9c09f421f9e62fc9ed", + "size": 12, + "text": "FOV = \"80\"\r\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "config_append_and_crlf", + "operations": [ + { + "body": { + "comment": "fixture appended", + "key": "MaxFPS", + "value": 200 + }, + "method": "POST", + "path": "/api/config" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/config_graphics_advanced_write.json b/native/core/tests/parity/config_graphics_advanced_write.json new file mode 100644 index 0000000..1dfde05 --- /dev/null +++ b/native/core/tests/parity/config_graphics_advanced_write.json @@ -0,0 +1,1632 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "ce683c0fab28aadf089f749e1bcaec284f21a5db9e56935c11b2c7d84c0def44", + "size": 41, + "text": "$TIMESTAMP - Success: Set MaxFPS to 240.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "a24ed9ad7430b8e3f6e4130a40581aa109916c18e327a544c128552684ff1896", + "size": 481, + "text": "// fixture configuration\nMaxFPS = \"240\" // fixture max fps\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "af760606f4a1e70110ef950799bceb67801867b590513184f566cfbab46caf37", + "size": 79, + "text": "$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "226f162a28fed3951a7df4d3b574728a41907972ac7788f2afe67b45c53522f4", + "size": 485, + "text": "// fixture configuration\nMaxFPS = \"240\" // fixture max fps\nFOV = \"110\" // fixture fov\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "deaec6e85c14d457442eef071d07d854c4095549802f35c4c15919a57c607a29", + "size": 119, + "text": "$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n$TIMESTAMP - Success: Set DrawFPS to 1.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "5de3b12023957cef9c91e8d39838882f93665969afa2bfd5439c3f4a2b4daf02", + "size": 492, + "text": "// fixture configuration\nMaxFPS = \"240\" // fixture max fps\nFOV = \"110\" // fixture fov\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"1\" // fixture counter\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "e29c30c92ed2f927e25dccd5910155d0081d06929c7cdc127bd647f8bbb68d2b", + "size": 167, + "text": "$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n$TIMESTAMP - Success: Set DrawFPS to 1.\n$TIMESTAMP - Success: Set SerializeRender to 2.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "b0107b725502ac0217bc70e0fef6638dba7e1b8bb94a5eca0f1cc530fa07821d", + "size": 495, + "text": "// fixture configuration\nMaxFPS = \"240\" // fixture max fps\nFOV = \"110\" // fixture fov\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"1\" // fixture counter\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"2\" // fixture cpu\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "7f99bb8be5709868b1ef21dc26f4f1876a4af01218a14d9fa113171832aba700", + "size": 220, + "text": "$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n$TIMESTAMP - Success: Set DrawFPS to 1.\n$TIMESTAMP - Success: Set SerializeRender to 2.\n$TIMESTAMP - Success: Limited VRAM usage set to 85%.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "2a28ba0d29a3047b281da49c8b3d19bd9868a0fc25fdc1758caaf07f8d7c3b98", + "size": 497, + "text": "// fixture configuration\nMaxFPS = \"240\" // fixture max fps\nFOV = \"110\" // fixture fov\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"1\" // fixture counter\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"2\" // fixture cpu\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"0.85\" // 0.75 to 1\nStreamMinResident = \"1\" // 0 or 1\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "7f99bb8be5709868b1ef21dc26f4f1876a4af01218a14d9fa113171832aba700", + "size": 220, + "text": "$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n$TIMESTAMP - Success: Set DrawFPS to 1.\n$TIMESTAMP - Success: Set SerializeRender to 2.\n$TIMESTAMP - Success: Limited VRAM usage set to 85%.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "2a28ba0d29a3047b281da49c8b3d19bd9868a0fc25fdc1758caaf07f8d7c3b98", + "size": 497, + "text": "// fixture configuration\nMaxFPS = \"240\" // fixture max fps\nFOV = \"110\" // fixture fov\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"1\" // fixture counter\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"2\" // fixture cpu\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"0.85\" // 0.75 to 1\nStreamMinResident = \"1\" // 0 or 1\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 240, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set MaxFPS to 240.", + "message": "Set MaxFPS to 240." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Set MaxFPS to 240.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 110, + "maxFps": 240, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set MaxFPS to 240.", + "message": "Set MaxFPS to 240." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set FOV to 110.", + "message": "Set FOV to 110." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": true, + "fov": 110, + "maxFps": 240, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set MaxFPS to 240.", + "message": "Set MaxFPS to 240." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set FOV to 110.", + "message": "Set FOV to 110." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set DrawFPS to 1.", + "message": "Set DrawFPS to 1." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n$TIMESTAMP - Success: Set DrawFPS to 1.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": true, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": true, + "fov": 110, + "maxFps": 240, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set MaxFPS to 240.", + "message": "Set MaxFPS to 240." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set FOV to 110.", + "message": "Set FOV to 110." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set DrawFPS to 1.", + "message": "Set DrawFPS to 1." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set SerializeRender to 2.", + "message": "Set SerializeRender to 2." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n$TIMESTAMP - Success: Set DrawFPS to 1.\n$TIMESTAMP - Success: Set SerializeRender to 2.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": true, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": true, + "vramTarget": 85 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": true, + "fov": 110, + "maxFps": 240, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set MaxFPS to 240.", + "message": "Set MaxFPS to 240." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set FOV to 110.", + "message": "Set FOV to 110." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set DrawFPS to 1.", + "message": "Set DrawFPS to 1." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Set SerializeRender to 2.", + "message": "Set SerializeRender to 2." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Limited VRAM usage set to 85%.", + "message": "Limited VRAM usage set to 85%." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Set MaxFPS to 240.\n$TIMESTAMP - Success: Set FOV to 110.\n$TIMESTAMP - Success: Set DrawFPS to 1.\n$TIMESTAMP - Success: Set SerializeRender to 2.\n$TIMESTAMP - Success: Limited VRAM usage set to 85%.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "config_graphics_advanced_write", + "operations": [ + { + "body": { + "comment": "fixture max fps", + "key": "MaxFPS", + "value": 240 + }, + "method": "POST", + "path": "/api/config" + }, + { + "body": { + "comment": "fixture fov", + "key": "FOV", + "value": 110 + }, + "method": "POST", + "path": "/api/config" + }, + { + "body": { + "comment": "fixture counter", + "key": "DrawFPS", + "value": 1 + }, + "method": "POST", + "path": "/api/config" + }, + { + "body": { + "comment": "fixture cpu", + "key": "SerializeRender", + "value": 2 + }, + "method": "POST", + "path": "/api/config" + }, + { + "body": { + "limited": true, + "target": 85 + }, + "method": "POST", + "path": "/api/vram-target" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/config_missing_file.json b/native/core/tests/parity/config_missing_file.json new file mode 100644 index 0000000..05c8e9d --- /dev/null +++ b/native/core/tests/parity/config_missing_file.json @@ -0,0 +1,296 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "7887d478e9b567914954cabb07c8a4f3b7590487a7a90178f41b09380b6d1140", + "size": 74, + "text": "$TIMESTAMP - Error: config.ini not found at $ROOT/game/players/config.ini\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "7887d478e9b567914954cabb07c8a4f3b7590487a7a90178f41b09380b6d1140", + "size": 74, + "text": "$TIMESTAMP - Error: config.ini not found at $ROOT/game/players/config.ini\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "error": "config.ini not found at $ROOT/game/players/config.ini", + "ok": false + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "config_missing_file", + "operations": [ + { + "body": { + "key": "FOV", + "value": 90 + }, + "method": "POST", + "path": "/api/config" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/config_readonly_roundtrip.json b/native/core/tests/parity/config_readonly_roundtrip.json new file mode 100644 index 0000000..1952a8c --- /dev/null +++ b/native/core/tests/parity/config_readonly_roundtrip.json @@ -0,0 +1,873 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "0cbf6bdc3874ea5334e7d242b5008c0bd27467ad303be2db7b1d5f065b6d43e1", + "size": 51, + "text": "$TIMESTAMP - Success: config.ini set to read-only.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 256, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "27ed3c899b7c99c0dfb22609a80bc72c21e01b6c5910efb7ec06905af8e7b03f", + "size": 133, + "text": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 256, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "c32d57314e410d703bcf8c7c4483998639c0064fc45b9b9a3672076d815a7b52", + "size": 183, + "text": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n$TIMESTAMP - Success: config.ini set to writable.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 384, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "c32d57314e410d703bcf8c7c4483998639c0064fc45b9b9a3672076d815a7b52", + "size": 183, + "text": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n$TIMESTAMP - Success: config.ini set to writable.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 384, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": true, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: config.ini set to read-only.", + "message": "config.ini set to read-only." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: config.ini set to read-only.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "error": "[Errno 13] Permission denied: '$ROOT/game/players/config.ini'", + "ok": false + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: config.ini set to read-only.", + "message": "config.ini set to read-only." + }, + { + "category": "Error", + "line": "$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'", + "message": "[Errno 13] Permission denied: '$ROOT/game/players/config.ini'" + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: config.ini set to writable.", + "message": "config.ini set to writable." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n$TIMESTAMP - Success: config.ini set to writable.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "config_readonly_roundtrip", + "operations": [ + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/config-readonly" + }, + { + "body": { + "key": "FOV", + "value": 90 + }, + "method": "POST", + "path": "/api/config" + }, + { + "body": { + "enabled": false + }, + "method": "POST", + "path": "/api/config-readonly" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/dxvk_config_write.json b/native/core/tests/parity/dxvk_config_write.json new file mode 100644 index 0000000..41c475d --- /dev/null +++ b/native/core/tests/parity/dxvk_config_write.json @@ -0,0 +1,550 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "4fba4fb547a7790f00d93c27e3eac01805518dc8a1cdffdf564a91bc8cba0a42", + "size": 60, + "text": "$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3d11.dll", + "sha256": "dc5515425bf1f3f94009300d02aaab926e2c7b60a7c11dc1bd2e7d2005fcc510", + "size": 13, + "text": "fixture-d3d11" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "base64": "AP9maXh0dXJlLWR4Z2k=", + "mode": 420, + "path": "game/dxgi.dll", + "sha256": "e7e2f73af077301aedc20be31fc767cb105d7e99b2c7969429cc5074b85e3f0f", + "size": 14 + }, + { + "mode": 420, + "path": "game/dxvk.conf", + "sha256": "bf50c7e6c4bcb2f97c849cf7d854912de81121c539094eaf55dc8ceab1e3cdde", + "size": 145, + "text": "dxvk.enableAsync=false\ndxvk.numCompilerThreads=6\ndxgi.maxFrameRate=144\ndxgi.maxFrameLatency=2\ndxvk.tearFree=Auto\ndxvk.hud=fps,frametimes,gpuload\n" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "4fba4fb547a7790f00d93c27e3eac01805518dc8a1cdffdf564a91bc8cba0a42", + "size": 60, + "text": "$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3d11.dll", + "sha256": "dc5515425bf1f3f94009300d02aaab926e2c7b60a7c11dc1bd2e7d2005fcc510", + "size": 13, + "text": "fixture-d3d11" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "base64": "AP9maXh0dXJlLWR4Z2k=", + "mode": 420, + "path": "game/dxgi.dll", + "sha256": "e7e2f73af077301aedc20be31fc767cb105d7e99b2c7969429cc5074b85e3f0f", + "size": 14 + }, + { + "mode": 420, + "path": "game/dxvk.conf", + "sha256": "bf50c7e6c4bcb2f97c849cf7d854912de81121c539094eaf55dc8ceab1e3cdde", + "size": 145, + "text": "dxvk.enableAsync=false\ndxvk.numCompilerThreads=6\ndxgi.maxFrameRate=144\ndxgi.maxFrameLatency=2\ndxvk.tearFree=Auto\ndxvk.hud=fps,frametimes,gpuload\n" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": true, + "installed": true, + "settings": { + "enableAsync": false, + "gplAsyncCache": true, + "hudEnabled": true, + "maxFrameLatency": 2, + "maxFrameRate": 144, + "numCompilerThreads": 6, + "tearFree": "Auto" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.", + "message": "Updated dxvk.conf from DXVK settings." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": true, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3d11.dll", + "sha256": "dc5515425bf1f3f94009300d02aaab926e2c7b60a7c11dc1bd2e7d2005fcc510", + "size": 13, + "text": "fixture-d3d11" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "base64": "AP9maXh0dXJlLWR4Z2k=", + "mode": 420, + "path": "game/dxgi.dll", + "sha256": "e7e2f73af077301aedc20be31fc767cb105d7e99b2c7969429cc5074b85e3f0f", + "size": 14 + }, + { + "mode": 420, + "path": "game/dxvk.conf", + "sha256": "f0a2ee076de3a6b372fe91658dea9c64d8d0a2cd86bdacbf4082d17c9c60b524", + "size": 45, + "text": "dxvk.enableAsync=true\ndxgi.maxFrameLatency=1\n" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "dxvk_config_write", + "operations": [ + { + "body": { + "enableAsync": false, + "gplAsyncCache": false, + "hudEnabled": true, + "maxFrameLatency": 2, + "maxFrameRate": 144, + "numCompilerThreads": 6, + "tearFree": "Auto" + }, + "method": "POST", + "path": "/api/dxvk-config" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/exe_preserved_enhanced_detection.json b/native/core/tests/parity/exe_preserved_enhanced_detection.json new file mode 100644 index 0000000..efb93c2 --- /dev/null +++ b/native/core/tests/parity/exe_preserved_enhanced_detection.json @@ -0,0 +1,476 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4752ef2d60db6d6142b391f2b2901592ae8cb000dbac3bed0602e86284c62e1a", + "size": 192, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\",\n \"enhanced_exe_hashes\": {\n \"$ROOT/game\": [\n \"0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2\"\n ]\n }\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.enhanced.bak", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4752ef2d60db6d6142b391f2b2901592ae8cb000dbac3bed0602e86284c62e1a", + "size": 192, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\",\n \"enhanced_exe_hashes\": {\n \"$ROOT/game\": [\n \"0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2\"\n ]\n }\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.enhanced.bak", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Enhanced", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Executable is managed by BO3 Enhanced. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": true, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": true, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Executable is managed by BO3 Enhanced.", + "integrityStatus": "enhanced", + "latestAvailable": false, + "modeLabel": "Enhanced", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "enhanced", + "state": "enhanced", + "trustedExecutable": true + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4752ef2d60db6d6142b391f2b2901592ae8cb000dbac3bed0602e86284c62e1a", + "size": 192, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\",\n \"enhanced_exe_hashes\": {\n \"$ROOT/game\": [\n \"0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2\"\n ]\n }\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.enhanced.bak", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "exe_preserved_enhanced_detection", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/exe_untrusted_backup_detection.json b/native/core/tests/parity/exe_untrusted_backup_detection.json new file mode 100644 index 0000000..2a1530f --- /dev/null +++ b/native/core/tests/parity/exe_untrusted_backup_detection.json @@ -0,0 +1,539 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.10650222.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.24784313.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.enhanced.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe.patchops.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.10650222.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.24784313.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.enhanced.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe.patchops.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.10650222.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.24784313.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.enhanced.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe.patchops.bak", + "sha256": "6a3302a80b8135a133456c63760088d45d72bf053d07015781f07e8b49a8dd95", + "size": 24, + "text": "not-a-trusted-executable" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "exe_untrusted_backup_detection", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/game_directory_validation_invalid.json b/native/core/tests/parity/game_directory_validation_invalid.json new file mode 100644 index 0000000..13f85e7 --- /dev/null +++ b/native/core/tests/parity/game_directory_validation_invalid.json @@ -0,0 +1,202 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "55e49116f53272369a07a704208aa87457422dab49635004e6497bc269cbdf8a", + "size": 88, + "text": "$TIMESTAMP - Error: Selected directory is not a Black Ops III install: $ROOT/not-a-game\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "55e49116f53272369a07a704208aa87457422dab49635004e6497bc269cbdf8a", + "size": 88, + "text": "$TIMESTAMP - Error: Selected directory is not a Black Ops III install: $ROOT/not-a-game\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "error": "BlackOps3.exe or BlackOpsIII.exe was not found.", + "ok": false + } + ] + }, + "fixture": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "game_directory_validation_invalid", + "operations": [ + { + "body": { + "path": "$ROOT/not-a-game" + }, + "method": "POST", + "path": "/api/game-directory" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/game_directory_validation_valid.json b/native/core/tests/parity/game_directory_validation_valid.json new file mode 100644 index 0000000..6c2f32c --- /dev/null +++ b/native/core/tests/parity/game_directory_validation_valid.json @@ -0,0 +1,481 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "0737e37545c83c8fd0201864ecef447104acf15b268c0df598b38d879cc18f27", + "size": 55, + "text": "$TIMESTAMP - Success: Game directory set to $ROOT/game\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "0737e37545c83c8fd0201864ecef447104acf15b268c0df598b38d879cc18f27", + "size": 55, + "text": "$TIMESTAMP - Success: Game directory set to $ROOT/game\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Game directory set to $ROOT/game", + "message": "Game directory set to $ROOT/game" + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Game directory set to $ROOT/game\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "99e02132d604629c806d118cb06ecedc18295f28afa54906c43af1d730650210", + "size": 33, + "text": "{\n \"release_channel\": \"stable\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "game_directory_validation_valid", + "operations": [ + { + "body": { + "path": "$ROOT/game" + }, + "method": "POST", + "path": "/api/game-directory" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/launch_options_apply.json b/native/core/tests/parity/launch_options_apply.json new file mode 100644 index 0000000..abb5708 --- /dev/null +++ b/native/core/tests/parity/launch_options_apply.json @@ -0,0 +1,800 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "data/backups", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "5ee8d57f5546dd7782aa286df32a599208c4109b966f800c99d717489565325e", + "size": 205, + "text": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n" + }, + { + "mode": 420, + "path": "data/backups/localconfig_backup.vdf", + "sha256": "0b29ee605d61e62a073f542a48a59f68274178d1b3c07318b738939682f636c8", + "size": 279, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game oldmod\"\n }\n }\n }\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", + "size": 231, + "text": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n" + } + ] + }, + { + "directories": [ + "data", + "data/backups", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "95451fde46e60360d4f19d90ab4baa0fdbbee9c80009dfb9196dee25a742076b", + "size": 410, + "text": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n" + }, + { + "mode": 420, + "path": "data/backups/localconfig_backup.vdf", + "sha256": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", + "size": 231, + "text": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", + "size": 231, + "text": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "data/backups", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "95451fde46e60360d4f19d90ab4baa0fdbbee9c80009dfb9196dee25a742076b", + "size": 410, + "text": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n" + }, + { + "mode": 420, + "path": "data/backups/localconfig_backup.vdf", + "sha256": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", + "size": 231, + "text": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", + "size": 231, + "text": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "offline", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": false, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": true, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf", + "message": "Config backup created at $ROOT/data/backups/localconfig_backup.vdf" + }, + { + "category": "Info", + "line": "$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", + "message": "Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp" + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "offline", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": false, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": true, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf", + "message": "Config backup created at $ROOT/data/backups/localconfig_backup.vdf" + }, + { + "category": "Info", + "line": "$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", + "message": "Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp" + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf", + "message": "Config backup created at $ROOT/data/backups/localconfig_backup.vdf" + }, + { + "category": "Info", + "line": "$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", + "message": "Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp" + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "0b29ee605d61e62a073f542a48a59f68274178d1b3c07318b738939682f636c8", + "size": 279, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game oldmod\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "launch_options_apply", + "operations": [ + { + "body": { + "options": "+set fs_game offlinemp", + "preserve_fs_game": false + }, + "method": "POST", + "path": "/api/launch-options" + }, + { + "body": { + "options": "", + "preserve_fs_game": true + }, + "method": "POST", + "path": "/api/launch-options" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/launch_options_unsupported.json b/native/core/tests/parity/launch_options_unsupported.json new file mode 100644 index 0000000..ac8ae90 --- /dev/null +++ b/native/core/tests/parity/launch_options_unsupported.json @@ -0,0 +1,482 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "beb3040d711e2d595fbd43b6bf846eaa569bc71117cf351a59d8169d18d0a599", + "size": 49, + "text": "$TIMESTAMP - Warning: Unsupported launch option.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "beb3040d711e2d595fbd43b6bf846eaa569bc71117cf351a59d8169d18d0a599", + "size": 49, + "text": "$TIMESTAMP - Warning: Unsupported launch option.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "error": "Unsupported launch option.", + "ok": false, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Warning", + "line": "$TIMESTAMP - Warning: Unsupported launch option.", + "message": "Unsupported launch option." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Warning: Unsupported launch option.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "launch_options_unsupported", + "operations": [ + { + "body": { + "options": "-bad" + }, + "method": "POST", + "path": "/api/launch-options" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/qol_restore_legacy.json b/native/core/tests/parity/qol_restore_legacy.json new file mode 100644 index 0000000..ede4d1b --- /dev/null +++ b/native/core/tests/parity/qol_restore_legacy.json @@ -0,0 +1,1042 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "062c04e8bec5131e82db06546ef03acd6ea19d26c9227f4ae4c277b8413e0533", + "size": 44, + "text": "$TIMESTAMP - Success: Intro video restored.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "c89497a06934019d2c1d332cc92208512095c0edf17c2ee9e52d0b98d63d36e7", + "size": 95, + "text": "$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "cc0159a731f7ad6967c5d96d394a221966ab8b21f867f24285e55dc5ec188b73", + "size": 140, + "text": "$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "cc0159a731f7ad6967c5d96d394a221966ab8b21f867f24285e55dc5ec188b73", + "size": 140, + "text": "$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video restored.", + "message": "Intro video restored." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video restored.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": true, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video restored.", + "message": "Intro video restored." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Restored d3dcompiler_46.dll.", + "message": "Restored d3dcompiler_46.dll." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video restored.", + "message": "Intro video restored." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Restored d3dcompiler_46.dll.", + "message": "Restored d3dcompiler_46.dll." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro videos restored.", + "message": "Intro videos restored." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "qol_restore_legacy", + "operations": [ + { + "body": { + "enabled": false + }, + "method": "POST", + "path": "/api/intro-skip" + }, + { + "body": { + "enabled": false + }, + "method": "POST", + "path": "/api/d3dcompiler" + }, + { + "body": { + "enabled": false + }, + "method": "POST", + "path": "/api/all-intros-skip" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/qol_toggle_roundtrip.json b/native/core/tests/parity/qol_toggle_roundtrip.json new file mode 100644 index 0000000..82bd8bd --- /dev/null +++ b/native/core/tests/parity/qol_toggle_roundtrip.json @@ -0,0 +1,2845 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "680528705d5426430bdafe3c54deafbe9d2548a015247eab02ad7b87c8cb9df6", + "size": 43, + "text": "$TIMESTAMP - Success: Intro video skipped.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "ce266d1b7095c11f4e7ac3f9a40ff619dfb73ac05b2017981aa5789443e047c5", + "size": 114, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "835983fb48b9162bab97965ad1c70eb442647d6684420ebbf17cdb376a895734", + "size": 162, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "982f487ba00ba1dd90c51fc7fc35fbae3980703344cb540ce09ca6e6ae7c67fd", + "size": 205, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "ec98ed509c87a1b4400764cb10165b62ded7945305db0c7cbbf73b4b154d0b73", + "size": 261, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "1f7e01f0fb2633fb2cbc948432bd1678a14f3ccc9facbe18d7551ede78ce9cb5", + "size": 318, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "90bf0ac9bf3012850da5329873a982c952630cb789134e537d6dd9733db097fa", + "size": 362, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "542a71b70954e8cfc9482125557264817e194286f71e2c7e7b7cb8b4e0ff40a6", + "size": 413, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "3f0851b07e23f0d5cff15aa9905bc22775bd53b83f5ca1727ed90937d05f4931", + "size": 458, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "3f0851b07e23f0d5cff15aa9905bc22775bd53b83f5ca1727ed90937d05f4931", + "size": 458, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": true, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": true, + "d3dcompiler": true, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": true, + "d3dcompiler": true, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Already using latest d3dcompiler.", + "message": "Already using latest d3dcompiler." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": true, + "d3dcompiler": true, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Already using latest d3dcompiler.", + "message": "Already using latest d3dcompiler." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro videos were already skipped.", + "message": "Intro videos were already skipped." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": true, + "d3dcompiler": true, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Already using latest d3dcompiler.", + "message": "Already using latest d3dcompiler." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro videos were already skipped.", + "message": "Intro videos were already skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video restored.", + "message": "Intro video restored." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": true, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Already using latest d3dcompiler.", + "message": "Already using latest d3dcompiler." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro videos were already skipped.", + "message": "Intro videos were already skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video restored.", + "message": "Intro video restored." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Restored d3dcompiler_46.dll.", + "message": "Restored d3dcompiler_46.dll." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Already using latest d3dcompiler.", + "message": "Already using latest d3dcompiler." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro videos were already skipped.", + "message": "Intro videos were already skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video restored.", + "message": "Intro video restored." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Restored d3dcompiler_46.dll.", + "message": "Restored d3dcompiler_46.dll." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro videos restored.", + "message": "Intro videos restored." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "qol_toggle_roundtrip", + "operations": [ + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/intro-skip" + }, + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/d3dcompiler" + }, + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/all-intros-skip" + }, + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/intro-skip" + }, + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/d3dcompiler" + }, + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/all-intros-skip" + }, + { + "body": { + "enabled": false + }, + "method": "POST", + "path": "/api/intro-skip" + }, + { + "body": { + "enabled": false + }, + "method": "POST", + "path": "/api/d3dcompiler" + }, + { + "body": { + "enabled": false + }, + "method": "POST", + "path": "/api/all-intros-skip" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/qol_toggles.json b/native/core/tests/parity/qol_toggles.json new file mode 100644 index 0000000..56d6dfa --- /dev/null +++ b/native/core/tests/parity/qol_toggles.json @@ -0,0 +1,1042 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "680528705d5426430bdafe3c54deafbe9d2548a015247eab02ad7b87c8cb9df6", + "size": 43, + "text": "$TIMESTAMP - Success: Intro video skipped.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "ce266d1b7095c11f4e7ac3f9a40ff619dfb73ac05b2017981aa5789443e047c5", + "size": 114, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "835983fb48b9162bab97965ad1c70eb442647d6684420ebbf17cdb376a895734", + "size": 162, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "835983fb48b9162bab97965ad1c70eb442647d6684420ebbf17cdb376a895734", + "size": 162, + "text": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll.patchops.bak", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv.patchops.bak", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv.patchops.bak", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": true, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Intro video skipped.", + "message": "Intro video skipped." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", + "message": "Renamed d3dcompiler_46.dll to reduce stuttering." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: All intro videos skipped.", + "message": "All intro videos skipped." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": true, + "d3dcompiler": true, + "intro": true + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "qol_toggles", + "operations": [ + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/intro-skip" + }, + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/d3dcompiler" + }, + { + "body": { + "enabled": true + }, + "method": "POST", + "path": "/api/all-intros-skip" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/results.jsonl b/native/core/tests/parity/results.jsonl new file mode 100644 index 0000000..f4da4fd --- /dev/null +++ b/native/core/tests/parity/results.jsonl @@ -0,0 +1,135 @@ +{"actual": "a1dd932b86b13cb4bcf2e634ccb15a22de3921dd35edf26539e3bad342481cad", "case": "config_append_and_crlf", "expected": "88cdfbed938b27580ce91830d4f72a25e720dd529767ec6804f8f014dea678ed", "path": "checkpoints/0/files/game/players/config.ini/sha256"} +{"actual": 48, "case": "config_append_and_crlf", "expected": 46, "path": "checkpoints/0/files/game/players/config.ini/size"} +{"actual": "FOV = \"80\"\r\nMaxFPS = \"200\" // fixture appended\r\n", "case": "config_append_and_crlf", "expected": "FOV = \"80\"\nMaxFPS = \"200\" // fixture appended\n", "path": "checkpoints/0/files/game/players/config.ini/text"} +{"actual": "a1dd932b86b13cb4bcf2e634ccb15a22de3921dd35edf26539e3bad342481cad", "case": "config_append_and_crlf", "expected": "88cdfbed938b27580ce91830d4f72a25e720dd529767ec6804f8f014dea678ed", "path": "filesystem/files/game/players/config.ini/sha256"} +{"actual": 48, "case": "config_append_and_crlf", "expected": 46, "path": "filesystem/files/game/players/config.ini/size"} +{"actual": "FOV = \"80\"\r\nMaxFPS = \"200\" // fixture appended\r\n", "case": "config_append_and_crlf", "expected": "FOV = \"80\"\nMaxFPS = \"200\" // fixture appended\n", "path": "filesystem/files/game/players/config.ini/text"} +{"actual": 292, "case": "config_readonly_roundtrip", "expected": 256, "path": "checkpoints/0/files/game/players/config.ini/mode"} +{"actual": "f6eb8d3a609209ad97790b5dbe8bd4adc81959ead4bd5bc9719b3a0e931ac5f2", "case": "config_readonly_roundtrip", "expected": "27ed3c899b7c99c0dfb22609a80bc72c21e01b6c5910efb7ec06905af8e7b03f", "path": "checkpoints/1/files/data/PatchOpsIII.log/sha256"} +{"actual": 129, "case": "config_readonly_roundtrip", "expected": 133, "path": "checkpoints/1/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: config.ini is read-only. Unlock it before making changes.\n", "case": "config_readonly_roundtrip", "expected": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n", "path": "checkpoints/1/files/data/PatchOpsIII.log/text"} +{"actual": 292, "case": "config_readonly_roundtrip", "expected": 256, "path": "checkpoints/1/files/game/players/config.ini/mode"} +{"actual": "1ce5b075f40e582bc02e377ad01da6adf72f7b2e41b0f2964c50bbb25c552223", "case": "config_readonly_roundtrip", "expected": "c32d57314e410d703bcf8c7c4483998639c0064fc45b9b9a3672076d815a7b52", "path": "checkpoints/2/files/data/PatchOpsIII.log/sha256"} +{"actual": 179, "case": "config_readonly_roundtrip", "expected": 183, "path": "checkpoints/2/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: config.ini is read-only. Unlock it before making changes.\n$TIMESTAMP - Success: config.ini set to writable.\n", "case": "config_readonly_roundtrip", "expected": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n$TIMESTAMP - Success: config.ini set to writable.\n", "path": "checkpoints/2/files/data/PatchOpsIII.log/text"} +{"actual": 420, "case": "config_readonly_roundtrip", "expected": 384, "path": "checkpoints/2/files/game/players/config.ini/mode"} +{"actual": "1ce5b075f40e582bc02e377ad01da6adf72f7b2e41b0f2964c50bbb25c552223", "case": "config_readonly_roundtrip", "expected": "c32d57314e410d703bcf8c7c4483998639c0064fc45b9b9a3672076d815a7b52", "path": "filesystem/files/data/PatchOpsIII.log/sha256"} +{"actual": 179, "case": "config_readonly_roundtrip", "expected": 183, "path": "filesystem/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: config.ini is read-only. Unlock it before making changes.\n$TIMESTAMP - Success: config.ini set to writable.\n", "case": "config_readonly_roundtrip", "expected": "$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n$TIMESTAMP - Success: config.ini set to writable.\n", "path": "filesystem/files/data/PatchOpsIII.log/text"} +{"actual": 420, "case": "config_readonly_roundtrip", "expected": 384, "path": "filesystem/files/game/players/config.ini/mode"} +{"actual": "config.ini is read-only. Unlock it before making changes.", "case": "config_readonly_roundtrip", "expected": "[Errno 13] Permission denied: '$ROOT/game/players/config.ini'", "path": "responses/1/error"} +{"actual": "$TIMESTAMP - Error: config.ini is read-only. Unlock it before making changes.", "case": "config_readonly_roundtrip", "expected": "$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'", "path": "responses/2/state/logs/1/line"} +{"actual": "config.ini is read-only. Unlock it before making changes.", "case": "config_readonly_roundtrip", "expected": "[Errno 13] Permission denied: '$ROOT/game/players/config.ini'", "path": "responses/2/state/logs/1/message"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: config.ini is read-only. Unlock it before making changes.\n$TIMESTAMP - Success: config.ini set to writable.\n```", "case": "config_readonly_roundtrip", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: config.ini set to read-only.\n$TIMESTAMP - Error: [Errno 13] Permission denied: '$ROOT/game/players/config.ini'\n$TIMESTAMP - Success: config.ini set to writable.\n```", "path": "responses/2/state/maintenance/logPayload"} +{"actual": "0fa5c0d9f92b3b383df7f6a3c315a0d94b0d1af57961cfe67d0320643bb1df38", "case": "dxvk_config_write", "expected": "4fba4fb547a7790f00d93c27e3eac01805518dc8a1cdffdf564a91bc8cba0a42", "path": "checkpoints/0/files/data/PatchOpsIII.log/sha256"} +{"actual": 135, "case": "dxvk_config_write", "expected": 60, "path": "checkpoints/0/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Error: DXVK is not managed by PatchOpsIII; install it before applying settings so existing files can be backed up safely.\n", "case": "dxvk_config_write", "expected": "$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.\n", "path": "checkpoints/0/files/data/PatchOpsIII.log/text"} +{"actual": "f0a2ee076de3a6b372fe91658dea9c64d8d0a2cd86bdacbf4082d17c9c60b524", "case": "dxvk_config_write", "expected": "bf50c7e6c4bcb2f97c849cf7d854912de81121c539094eaf55dc8ceab1e3cdde", "path": "checkpoints/0/files/game/dxvk.conf/sha256"} +{"actual": 45, "case": "dxvk_config_write", "expected": 145, "path": "checkpoints/0/files/game/dxvk.conf/size"} +{"actual": "dxvk.enableAsync=true\ndxgi.maxFrameLatency=1\n", "case": "dxvk_config_write", "expected": "dxvk.enableAsync=false\ndxvk.numCompilerThreads=6\ndxgi.maxFrameRate=144\ndxgi.maxFrameLatency=2\ndxvk.tearFree=Auto\ndxvk.hud=fps,frametimes,gpuload\n", "path": "checkpoints/0/files/game/dxvk.conf/text"} +{"actual": "0fa5c0d9f92b3b383df7f6a3c315a0d94b0d1af57961cfe67d0320643bb1df38", "case": "dxvk_config_write", "expected": "4fba4fb547a7790f00d93c27e3eac01805518dc8a1cdffdf564a91bc8cba0a42", "path": "filesystem/files/data/PatchOpsIII.log/sha256"} +{"actual": 135, "case": "dxvk_config_write", "expected": 60, "path": "filesystem/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Error: DXVK is not managed by PatchOpsIII; install it before applying settings so existing files can be backed up safely.\n", "case": "dxvk_config_write", "expected": "$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.\n", "path": "filesystem/files/data/PatchOpsIII.log/text"} +{"actual": "f0a2ee076de3a6b372fe91658dea9c64d8d0a2cd86bdacbf4082d17c9c60b524", "case": "dxvk_config_write", "expected": "bf50c7e6c4bcb2f97c849cf7d854912de81121c539094eaf55dc8ceab1e3cdde", "path": "filesystem/files/game/dxvk.conf/sha256"} +{"actual": 45, "case": "dxvk_config_write", "expected": 145, "path": "filesystem/files/game/dxvk.conf/size"} +{"actual": "dxvk.enableAsync=true\ndxgi.maxFrameLatency=1\n", "case": "dxvk_config_write", "expected": "dxvk.enableAsync=false\ndxvk.numCompilerThreads=6\ndxgi.maxFrameRate=144\ndxgi.maxFrameLatency=2\ndxvk.tearFree=Auto\ndxvk.hud=fps,frametimes,gpuload\n", "path": "filesystem/files/game/dxvk.conf/text"} +{"actual": "DXVK is not managed by PatchOpsIII; install it before applying settings so existing files can be backed up safely.", "actualPresent": true, "case": "dxvk_config_write", "expected": null, "expectedPresent": false, "path": "responses/0/error"} +{"actual": false, "case": "dxvk_config_write", "expected": true, "path": "responses/0/ok"} +{"actual": null, "actualPresent": false, "case": "dxvk_config_write", "expected": {"activeLaunchProfile": "default", "advanced": {"configReadonly": false, "maxFrameLatency": 1, "reduceCpu": false, "smoothFramerate": false, "unlockOptions": false, "vramLimited": false, "vramTarget": 100}, "appVersion": "v1.3.4", "configExists": true, "currentLaunchOptions": "", "dxvk": {"confExists": true, "installed": true, "settings": {"enableAsync": false, "gplAsyncCache": true, "hudEnabled": true, "maxFrameLatency": 2, "maxFrameRate": 144, "numCompilerThreads": 6, "tearFree": "Auto"}}, "enhanced": {"acknowledgedAt": null, "backupStatus": "Not created", "detectedAt": null, "dumpSource": "", "filesInstalled": 0, "installed": false, "launchOptionsActive": false}, "exeSwap": {"activeBuildDate": "", "activeBuildId": "Unverified", "backupAvailable": false, "compatibleActive": false, "compatibleAvailable": false, "compatibleBuildDate": "Mar 3, 2023", "compatibleBuildId": "10650222", "currentBuildDate": "Sep 10, 2026", "currentBuildId": "24784313", "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", "enhancedActive": false, "enhancedAvailable": false, "enhancedBuildDate": "", "enhancedBuildId": "Enhanced", "enhancedExeActive": false, "executable": "$ROOT/game/BlackOpsIII.exe", "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", "executableName": "BlackOpsIII.exe", "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", "integrityStatus": "unverified", "latestAvailable": false, "modeLabel": "Unknown", "patchLabel": "T7 Patch Scroptss/T7Patch", "profile": "", "state": "unverified", "trustedExecutable": false}, "gameDetected": true, "gameDir": "$ROOT/game", "graphics": {"displayMode": 1, "drawFps": false, "fov": 80, "maxFps": 165, "refreshRate": 60.0, "renderResolution": 100, "resolution": "1920x1080", "vsync": true}, "launchProfiles": [{"active": true, "id": "default", "installed": true, "label": "Default (None)", "option": "", "state": "Ready", "subscribed": true}, {"active": false, "id": "offline", "installed": true, "label": "Play Offline", "option": "+set fs_game offlinemp", "state": "Ready", "subscribed": true}, {"active": false, "id": "all_around", "installed": false, "label": "All-around Enhancement Lite", "option": "+set fs_game 2994481309", "path": null, "state": "Not Subscribed", "subscribed": false}, {"active": false, "id": "ultimate", "installed": false, "label": "Ultimate Experience Mod", "option": "+set fs_game 2942053577", "path": null, "state": "Not Subscribed", "subscribed": false}, {"active": false, "id": "reforged", "installed": false, "label": "BO3 Reforged", "option": "+set fs_game 3667377161", "path": null, "state": "Not Subscribed", "subscribed": false}], "logPath": "$ROOT/data/PatchOpsIII.log", "logs": [{"category": "Success", "line": "$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.", "message": "Updated dxvk.conf from DXVK settings."}], "maintenance": {"logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Updated dxvk.conf from DXVK settings.\n```", "modFilesDir": "$ROOT/data/BO3 Mod Files"}, "mods": {"dxvk": true, "enhanced": false, "t7Patch": false}, "platform": "Linux", "presets": ["Smooth Preset", "Low-Latency Preset"], "qol": {"allIntros": false, "d3dcompiler": false, "intro": false}, "releaseChannel": "stable", "steamUserId": "76561198000000000", "t7": {"colorCode": "", "confExists": false, "friendsOnly": false, "gamertag": "", "installed": false, "mode": "Custom", "networkPassword": "", "plainName": ""}}, "expectedPresent": true, "path": "responses/0/state"} +{"actual": "f009f442d5749fad5f455cbec4e0cff61a986ed294cd3728353aeb61213f7d68", "case": "game_directory_validation_invalid", "expected": "55e49116f53272369a07a704208aa87457422dab49635004e6497bc269cbdf8a", "path": "checkpoints/0/files/data/PatchOpsIII.log/sha256"} +{"actual": 68, "case": "game_directory_validation_invalid", "expected": 88, "path": "checkpoints/0/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Error: BlackOps3.exe or BlackOpsIII.exe was not found.\n", "case": "game_directory_validation_invalid", "expected": "$TIMESTAMP - Error: Selected directory is not a Black Ops III install: $ROOT/not-a-game\n", "path": "checkpoints/0/files/data/PatchOpsIII.log/text"} +{"actual": "f009f442d5749fad5f455cbec4e0cff61a986ed294cd3728353aeb61213f7d68", "case": "game_directory_validation_invalid", "expected": "55e49116f53272369a07a704208aa87457422dab49635004e6497bc269cbdf8a", "path": "filesystem/files/data/PatchOpsIII.log/sha256"} +{"actual": 68, "case": "game_directory_validation_invalid", "expected": 88, "path": "filesystem/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Error: BlackOps3.exe or BlackOpsIII.exe was not found.\n", "case": "game_directory_validation_invalid", "expected": "$TIMESTAMP - Error: Selected directory is not a Black Ops III install: $ROOT/not-a-game\n", "path": "filesystem/files/data/PatchOpsIII.log/text"} +{"actual": "56e29d8549dbdbef24936b0e65e427ca5f8bb943f75906c58c15f2ac0408c3be", "case": "launch_options_apply", "expected": "5ee8d57f5546dd7782aa286df32a599208c4109b966f800c99d717489565325e", "path": "checkpoints/0/files/data/PatchOpsIII.log/sha256"} +{"actual": 199, "case": "launch_options_apply", "expected": 205, "path": "checkpoints/0/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n", "case": "launch_options_apply", "expected": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n", "path": "checkpoints/0/files/data/PatchOpsIII.log/text"} +{"actual": 436, "case": "launch_options_apply", "expected": 420, "path": "checkpoints/0/files/data/backups/localconfig_backup.vdf/mode"} +{"actual": {"mode": 436, "path": "steam/userdata/76561198000000000/config/localconfig.vdf.patchops.bak", "sha256": "0b29ee605d61e62a073f542a48a59f68274178d1b3c07318b738939682f636c8", "size": 279, "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game oldmod\"\n }\n }\n }\n }\n }\n}\n"}, "actualPresent": true, "case": "launch_options_apply", "expected": null, "expectedPresent": false, "path": "checkpoints/0/files/steam/userdata/76561198000000000/config/localconfig.vdf.patchops.bak"} +{"actual": "3953bbfc18ab3a48c98a9327f0959184c77540ad6b5a049b97deb593cea9ab78", "case": "launch_options_apply", "expected": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", "path": "checkpoints/0/files/steam/userdata/76561198000000000/config/localconfig.vdf/sha256"} +{"actual": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\"\t\"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "case": "launch_options_apply", "expected": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "path": "checkpoints/0/files/steam/userdata/76561198000000000/config/localconfig.vdf/text"} +{"actual": "2351a346411360b1dafdb8fd5814faee157341067b44efd132be2e8da105c55e", "case": "launch_options_apply", "expected": "95451fde46e60360d4f19d90ab4baa0fdbbee9c80009dfb9196dee25a742076b", "path": "checkpoints/1/files/data/PatchOpsIII.log/sha256"} +{"actual": 398, "case": "launch_options_apply", "expected": 410, "path": "checkpoints/1/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n", "case": "launch_options_apply", "expected": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n", "path": "checkpoints/1/files/data/PatchOpsIII.log/text"} +{"actual": 436, "case": "launch_options_apply", "expected": 420, "path": "checkpoints/1/files/data/backups/localconfig_backup.vdf/mode"} +{"actual": "0b29ee605d61e62a073f542a48a59f68274178d1b3c07318b738939682f636c8", "case": "launch_options_apply", "expected": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", "path": "checkpoints/1/files/data/backups/localconfig_backup.vdf/sha256"} +{"actual": 279, "case": "launch_options_apply", "expected": 231, "path": "checkpoints/1/files/data/backups/localconfig_backup.vdf/size"} +{"actual": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game oldmod\"\n }\n }\n }\n }\n }\n}\n", "case": "launch_options_apply", "expected": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "path": "checkpoints/1/files/data/backups/localconfig_backup.vdf/text"} +{"actual": {"mode": 436, "path": "steam/userdata/76561198000000000/config/localconfig.vdf.patchops.bak", "sha256": "0b29ee605d61e62a073f542a48a59f68274178d1b3c07318b738939682f636c8", "size": 279, "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game oldmod\"\n }\n }\n }\n }\n }\n}\n"}, "actualPresent": true, "case": "launch_options_apply", "expected": null, "expectedPresent": false, "path": "checkpoints/1/files/steam/userdata/76561198000000000/config/localconfig.vdf.patchops.bak"} +{"actual": "3953bbfc18ab3a48c98a9327f0959184c77540ad6b5a049b97deb593cea9ab78", "case": "launch_options_apply", "expected": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", "path": "checkpoints/1/files/steam/userdata/76561198000000000/config/localconfig.vdf/sha256"} +{"actual": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\"\t\"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "case": "launch_options_apply", "expected": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "path": "checkpoints/1/files/steam/userdata/76561198000000000/config/localconfig.vdf/text"} +{"actual": "2351a346411360b1dafdb8fd5814faee157341067b44efd132be2e8da105c55e", "case": "launch_options_apply", "expected": "95451fde46e60360d4f19d90ab4baa0fdbbee9c80009dfb9196dee25a742076b", "path": "filesystem/files/data/PatchOpsIII.log/sha256"} +{"actual": 398, "case": "launch_options_apply", "expected": 410, "path": "filesystem/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n", "case": "launch_options_apply", "expected": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n", "path": "filesystem/files/data/PatchOpsIII.log/text"} +{"actual": 436, "case": "launch_options_apply", "expected": 420, "path": "filesystem/files/data/backups/localconfig_backup.vdf/mode"} +{"actual": "0b29ee605d61e62a073f542a48a59f68274178d1b3c07318b738939682f636c8", "case": "launch_options_apply", "expected": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", "path": "filesystem/files/data/backups/localconfig_backup.vdf/sha256"} +{"actual": 279, "case": "launch_options_apply", "expected": 231, "path": "filesystem/files/data/backups/localconfig_backup.vdf/size"} +{"actual": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game oldmod\"\n }\n }\n }\n }\n }\n}\n", "case": "launch_options_apply", "expected": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "path": "filesystem/files/data/backups/localconfig_backup.vdf/text"} +{"actual": {"mode": 436, "path": "steam/userdata/76561198000000000/config/localconfig.vdf.patchops.bak", "sha256": "0b29ee605d61e62a073f542a48a59f68274178d1b3c07318b738939682f636c8", "size": 279, "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game oldmod\"\n }\n }\n }\n }\n }\n}\n"}, "actualPresent": true, "case": "launch_options_apply", "expected": null, "expectedPresent": false, "path": "filesystem/files/steam/userdata/76561198000000000/config/localconfig.vdf.patchops.bak"} +{"actual": "3953bbfc18ab3a48c98a9327f0959184c77540ad6b5a049b97deb593cea9ab78", "case": "launch_options_apply", "expected": "952221cf8d0a15dfaf77b5a7f9950160bb80bec724405df1685e5100cfc52e55", "path": "filesystem/files/steam/userdata/76561198000000000/config/localconfig.vdf/sha256"} +{"actual": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\"\t\"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "case": "launch_options_apply", "expected": "\"UserLocalConfigStore\"\n{\n\t\"Software\"\n\t{\n\t\t\"Valve\"\n\t\t{\n\t\t\t\"Steam\"\n\t\t\t{\n\t\t\t\t\"apps\"\n\t\t\t\t{\n\t\t\t\t\t\"311210\"\n\t\t\t\t\t{\n\t\t\t\t\t\t\"LaunchOptions\" \"WINEDLLOVERRIDES=\\\"dsound=n,b\\\" %command% -novid +set fs_game offlinemp\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n", "path": "filesystem/files/steam/userdata/76561198000000000/config/localconfig.vdf/text"} +{"actual": [{"category": "Info", "line": "$TIMESTAMP - Info: Steam was not running.", "message": "Steam was not running."}, {"category": "Success", "line": "$TIMESTAMP - Success: Opened Steam.", "message": "Opened Steam."}, {"category": "Success", "line": "$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", "message": "Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp"}], "case": "launch_options_apply", "expected": [{"category": "Success", "line": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf", "message": "Config backup created at $ROOT/data/backups/localconfig_backup.vdf"}, {"category": "Info", "line": "$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", "message": "Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp"}], "path": "responses/0/state/logs"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n```", "case": "launch_options_apply", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n```", "path": "responses/0/state/maintenance/logPayload"} +{"actual": [{"category": "Info", "line": "$TIMESTAMP - Info: Steam was not running.", "message": "Steam was not running."}, {"category": "Success", "line": "$TIMESTAMP - Success: Opened Steam.", "message": "Opened Steam."}, {"category": "Success", "line": "$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", "message": "Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp"}, {"category": "Info", "line": "$TIMESTAMP - Info: Steam was not running.", "message": "Steam was not running."}, {"category": "Success", "line": "$TIMESTAMP - Success: Opened Steam.", "message": "Opened Steam."}, {"category": "Success", "line": "$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", "message": "Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp"}], "case": "launch_options_apply", "expected": [{"category": "Success", "line": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf", "message": "Config backup created at $ROOT/data/backups/localconfig_backup.vdf"}, {"category": "Info", "line": "$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", "message": "Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp"}, {"category": "Success", "line": "$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf", "message": "Config backup created at $ROOT/data/backups/localconfig_backup.vdf"}, {"category": "Info", "line": "$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp", "message": "Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp"}], "path": "responses/1/state/logs"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Info: Steam was not running.\n$TIMESTAMP - Success: Opened Steam.\n$TIMESTAMP - Success: Set Steam launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n```", "case": "launch_options_apply", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n$TIMESTAMP - Success: Config backup created at $ROOT/data/backups/localconfig_backup.vdf\n$TIMESTAMP - Info: Setting launch options to: WINEDLLOVERRIDES=\"dsound=n,b\" %command% -novid +set fs_game offlinemp\n```", "path": "responses/1/state/maintenance/logPayload"} +{"actual": "5723de2150ff3fa101b52b6afbf7148e2f6180255d1494a0639afcecf8a3f549", "case": "launch_options_unsupported", "expected": "beb3040d711e2d595fbd43b6bf846eaa569bc71117cf351a59d8169d18d0a599", "path": "checkpoints/0/files/data/PatchOpsIII.log/sha256"} +{"actual": 47, "case": "launch_options_unsupported", "expected": 49, "path": "checkpoints/0/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Error: Unsupported launch option.\n", "case": "launch_options_unsupported", "expected": "$TIMESTAMP - Warning: Unsupported launch option.\n", "path": "checkpoints/0/files/data/PatchOpsIII.log/text"} +{"actual": "5723de2150ff3fa101b52b6afbf7148e2f6180255d1494a0639afcecf8a3f549", "case": "launch_options_unsupported", "expected": "beb3040d711e2d595fbd43b6bf846eaa569bc71117cf351a59d8169d18d0a599", "path": "filesystem/files/data/PatchOpsIII.log/sha256"} +{"actual": 47, "case": "launch_options_unsupported", "expected": 49, "path": "filesystem/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Error: Unsupported launch option.\n", "case": "launch_options_unsupported", "expected": "$TIMESTAMP - Warning: Unsupported launch option.\n", "path": "filesystem/files/data/PatchOpsIII.log/text"} +{"actual": null, "actualPresent": false, "case": "launch_options_unsupported", "expected": {"activeLaunchProfile": "default", "advanced": {"configReadonly": false, "maxFrameLatency": 1, "reduceCpu": false, "smoothFramerate": false, "unlockOptions": false, "vramLimited": false, "vramTarget": 100}, "appVersion": "v1.3.4", "configExists": true, "currentLaunchOptions": "", "dxvk": {"confExists": false, "installed": false, "settings": {"enableAsync": true, "gplAsyncCache": true, "hudEnabled": false, "maxFrameLatency": 1, "maxFrameRate": 0, "numCompilerThreads": 0, "tearFree": "True"}}, "enhanced": {"acknowledgedAt": null, "backupStatus": "Not created", "detectedAt": null, "dumpSource": "", "filesInstalled": 0, "installed": false, "launchOptionsActive": false}, "exeSwap": {"activeBuildDate": "", "activeBuildId": "Unverified", "backupAvailable": false, "compatibleActive": false, "compatibleAvailable": false, "compatibleBuildDate": "Mar 3, 2023", "compatibleBuildId": "10650222", "currentBuildDate": "Sep 10, 2026", "currentBuildId": "24784313", "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", "enhancedActive": false, "enhancedAvailable": false, "enhancedBuildDate": "", "enhancedBuildId": "Enhanced", "enhancedExeActive": false, "executable": "$ROOT/game/BlackOpsIII.exe", "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", "executableName": "BlackOpsIII.exe", "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", "integrityStatus": "unverified", "latestAvailable": false, "modeLabel": "Unknown", "patchLabel": "T7 Patch Scroptss/T7Patch", "profile": "", "state": "unverified", "trustedExecutable": false}, "gameDetected": true, "gameDir": "$ROOT/game", "graphics": {"displayMode": 1, "drawFps": false, "fov": 80, "maxFps": 165, "refreshRate": 60.0, "renderResolution": 100, "resolution": "1920x1080", "vsync": true}, "launchProfiles": [{"active": true, "id": "default", "installed": true, "label": "Default (None)", "option": "", "state": "Ready", "subscribed": true}, {"active": false, "id": "offline", "installed": true, "label": "Play Offline", "option": "+set fs_game offlinemp", "state": "Ready", "subscribed": true}, {"active": false, "id": "all_around", "installed": false, "label": "All-around Enhancement Lite", "option": "+set fs_game 2994481309", "path": null, "state": "Not Subscribed", "subscribed": false}, {"active": false, "id": "ultimate", "installed": false, "label": "Ultimate Experience Mod", "option": "+set fs_game 2942053577", "path": null, "state": "Not Subscribed", "subscribed": false}, {"active": false, "id": "reforged", "installed": false, "label": "BO3 Reforged", "option": "+set fs_game 3667377161", "path": null, "state": "Not Subscribed", "subscribed": false}], "logPath": "$ROOT/data/PatchOpsIII.log", "logs": [{"category": "Warning", "line": "$TIMESTAMP - Warning: Unsupported launch option.", "message": "Unsupported launch option."}], "maintenance": {"logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Warning: Unsupported launch option.\n```", "modFilesDir": "$ROOT/data/BO3 Mod Files"}, "mods": {"dxvk": false, "enhanced": false, "t7Patch": false}, "platform": "Linux", "presets": ["Smooth Preset", "Low-Latency Preset"], "qol": {"allIntros": false, "d3dcompiler": false, "intro": false}, "releaseChannel": "stable", "steamUserId": "76561198000000000", "t7": {"colorCode": "", "confExists": false, "friendsOnly": false, "gamertag": "", "installed": false, "mode": "Custom", "networkPassword": "", "plainName": ""}}, "expectedPresent": true, "path": "responses/0/state"} +{"actual": "700bc9e99e153ec5786253f92138c9101134c8600bdf89dd4e93c9d5b7d4b68a", "case": "qol_toggle_roundtrip", "expected": "ec98ed509c87a1b4400764cb10165b62ded7945305db0c7cbbf73b4b154d0b73", "path": "checkpoints/4/files/data/PatchOpsIII.log/sha256"} +{"actual": 276, "case": "qol_toggle_roundtrip", "expected": 261, "path": "checkpoints/4/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n", "path": "checkpoints/4/files/data/PatchOpsIII.log/text"} +{"actual": "257323953a925a454ffccab8d336bd6477f7358954b305c5f58d609a1d75d596", "case": "qol_toggle_roundtrip", "expected": "1f7e01f0fb2633fb2cbc948432bd1678a14f3ccc9facbe18d7551ede78ce9cb5", "path": "checkpoints/5/files/data/PatchOpsIII.log/sha256"} +{"actual": 324, "case": "qol_toggle_roundtrip", "expected": 318, "path": "checkpoints/5/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n", "path": "checkpoints/5/files/data/PatchOpsIII.log/text"} +{"actual": "c5bc2db4cdb08f98461effb5db571c2c756d3bc50a3e4638e1808b1cace4a012", "case": "qol_toggle_roundtrip", "expected": "90bf0ac9bf3012850da5329873a982c952630cb789134e537d6dd9733db097fa", "path": "checkpoints/6/files/data/PatchOpsIII.log/sha256"} +{"actual": 368, "case": "qol_toggle_roundtrip", "expected": 362, "path": "checkpoints/6/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video restored.\n", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n", "path": "checkpoints/6/files/data/PatchOpsIII.log/text"} +{"actual": "9e05655f31c81cfde44f2521d0a8fb45c0179cd6d5795796876ad0897530fac3", "case": "qol_toggle_roundtrip", "expected": "542a71b70954e8cfc9482125557264817e194286f71e2c7e7b7cb8b4e0ff40a6", "path": "checkpoints/7/files/data/PatchOpsIII.log/sha256"} +{"actual": 419, "case": "qol_toggle_roundtrip", "expected": 413, "path": "checkpoints/7/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n", "path": "checkpoints/7/files/data/PatchOpsIII.log/text"} +{"actual": "23a2296e7faabfd8671edecc327f15a566b5c0149529c5a12bd6a56c87ea73b4", "case": "qol_toggle_roundtrip", "expected": "3f0851b07e23f0d5cff15aa9905bc22775bd53b83f5ca1727ed90937d05f4931", "path": "checkpoints/8/files/data/PatchOpsIII.log/sha256"} +{"actual": 464, "case": "qol_toggle_roundtrip", "expected": 458, "path": "checkpoints/8/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n", "path": "checkpoints/8/files/data/PatchOpsIII.log/text"} +{"actual": "23a2296e7faabfd8671edecc327f15a566b5c0149529c5a12bd6a56c87ea73b4", "case": "qol_toggle_roundtrip", "expected": "3f0851b07e23f0d5cff15aa9905bc22775bd53b83f5ca1727ed90937d05f4931", "path": "filesystem/files/data/PatchOpsIII.log/sha256"} +{"actual": 464, "case": "qol_toggle_roundtrip", "expected": 458, "path": "filesystem/files/data/PatchOpsIII.log/size"} +{"actual": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n", "path": "filesystem/files/data/PatchOpsIII.log/text"} +{"actual": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Already using latest d3dcompiler.", "path": "responses/4/state/logs/4/line"} +{"actual": "Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "Already using latest d3dcompiler.", "path": "responses/4/state/logs/4/message"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n```", "case": "qol_toggle_roundtrip", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n```", "path": "responses/4/state/maintenance/logPayload"} +{"actual": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Already using latest d3dcompiler.", "path": "responses/5/state/logs/4/line"} +{"actual": "Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "Already using latest d3dcompiler.", "path": "responses/5/state/logs/4/message"} +{"actual": "$TIMESTAMP - Success: All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro videos were already skipped.", "path": "responses/5/state/logs/5/line"} +{"actual": "All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "Intro videos were already skipped.", "path": "responses/5/state/logs/5/message"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n```", "case": "qol_toggle_roundtrip", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n```", "path": "responses/5/state/maintenance/logPayload"} +{"actual": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Already using latest d3dcompiler.", "path": "responses/6/state/logs/4/line"} +{"actual": "Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "Already using latest d3dcompiler.", "path": "responses/6/state/logs/4/message"} +{"actual": "$TIMESTAMP - Success: All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro videos were already skipped.", "path": "responses/6/state/logs/5/line"} +{"actual": "All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "Intro videos were already skipped.", "path": "responses/6/state/logs/5/message"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video restored.\n```", "case": "qol_toggle_roundtrip", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n```", "path": "responses/6/state/maintenance/logPayload"} +{"actual": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Already using latest d3dcompiler.", "path": "responses/7/state/logs/4/line"} +{"actual": "Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "Already using latest d3dcompiler.", "path": "responses/7/state/logs/4/message"} +{"actual": "$TIMESTAMP - Success: All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro videos were already skipped.", "path": "responses/7/state/logs/5/line"} +{"actual": "All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "Intro videos were already skipped.", "path": "responses/7/state/logs/5/message"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n```", "case": "qol_toggle_roundtrip", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n```", "path": "responses/7/state/maintenance/logPayload"} +{"actual": "$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Already using latest d3dcompiler.", "path": "responses/8/state/logs/4/line"} +{"actual": "Renamed d3dcompiler_46.dll to reduce stuttering.", "case": "qol_toggle_roundtrip", "expected": "Already using latest d3dcompiler.", "path": "responses/8/state/logs/4/message"} +{"actual": "$TIMESTAMP - Success: All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "$TIMESTAMP - Success: Intro videos were already skipped.", "path": "responses/8/state/logs/5/line"} +{"actual": "All intro videos skipped.", "case": "qol_toggle_roundtrip", "expected": "Intro videos were already skipped.", "path": "responses/8/state/logs/5/message"} +{"actual": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n```", "case": "qol_toggle_roundtrip", "expected": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Renamed d3dcompiler_46.dll to reduce stuttering.\n$TIMESTAMP - Success: All intro videos skipped.\n$TIMESTAMP - Success: Intro video skipped.\n$TIMESTAMP - Success: Already using latest d3dcompiler.\n$TIMESTAMP - Success: Intro videos were already skipped.\n$TIMESTAMP - Success: Intro video restored.\n$TIMESTAMP - Success: Restored d3dcompiler_46.dll.\n$TIMESTAMP - Success: Intro videos restored.\n```", "path": "responses/8/state/maintenance/logPayload"} +{"actual": null, "case": "status_enhanced_installed", "expected": "2026-09-10T13:00:00Z", "path": "responses/0/enhanced/acknowledgedAt"} +{"actual": "Not created", "case": "status_enhanced_installed", "expected": "Created", "path": "responses/0/enhanced/backupStatus"} +{"actual": null, "case": "status_enhanced_installed", "expected": "2026-09-10T12:00:00Z", "path": "responses/0/enhanced/detectedAt"} +{"actual": 0, "case": "status_enhanced_installed", "expected": 4, "path": "responses/0/enhanced/filesInstalled"} +{"actual": {"mode": 436, "path": "data/PatchOpsIII.log", "sha256": "535a7583a912399bd58d8c8624d6229c35bdc208125e1b048ccbf3ae74c3aec8", "size": 46, "text": "$TIMESTAMP - Error: Gamertag cannot be empty.\n"}, "actualPresent": true, "case": "t7_config_invalid_name", "expected": null, "expectedPresent": false, "path": "checkpoints/0/files/data/PatchOpsIII.log"} +{"actual": {"mode": 436, "path": "data/PatchOpsIII.log", "sha256": "b64571d2437f9ffe15114d4da28c3c2485db088ab215703b8c29c2e6f463df9d", "size": 104, "text": "$TIMESTAMP - Error: Gamertag cannot be empty.\n$TIMESTAMP - Error: Gamertag cannot exceed 20 characters.\n"}, "actualPresent": true, "case": "t7_config_invalid_name", "expected": null, "expectedPresent": false, "path": "checkpoints/1/files/data/PatchOpsIII.log"} +{"actual": {"mode": 436, "path": "data/PatchOpsIII.log", "sha256": "b64571d2437f9ffe15114d4da28c3c2485db088ab215703b8c29c2e6f463df9d", "size": 104, "text": "$TIMESTAMP - Error: Gamertag cannot be empty.\n$TIMESTAMP - Error: Gamertag cannot exceed 20 characters.\n"}, "actualPresent": true, "case": "t7_config_invalid_name", "expected": null, "expectedPresent": false, "path": "filesystem/files/data/PatchOpsIII.log"} +{"actual": {"mode": 436, "path": "data/PatchOpsIII.log", "sha256": "24cb586cc4355a63d227f4af41999ae3c45ecda3bb2345fb87df6ab1ec2b0f5f", "size": 95, "text": "$TIMESTAMP - Error: t7patch.conf was not found. Install T7 Patch before updating the gamertag.\n"}, "actualPresent": true, "case": "t7_config_missing", "expected": null, "expectedPresent": false, "path": "checkpoints/0/files/data/PatchOpsIII.log"} +{"actual": {"mode": 436, "path": "data/PatchOpsIII.log", "sha256": "24cb586cc4355a63d227f4af41999ae3c45ecda3bb2345fb87df6ab1ec2b0f5f", "size": 95, "text": "$TIMESTAMP - Error: t7patch.conf was not found. Install T7 Patch before updating the gamertag.\n"}, "actualPresent": true, "case": "t7_config_missing", "expected": null, "expectedPresent": false, "path": "filesystem/files/data/PatchOpsIII.log"} +{"actual": "6ce55dd039c23ff633f99c5601499de5244b6c5b8fe12856d646b90f7278fd60", "case": "vram_target_roundtrip", "expected": "48d4eca182b3265fc933d1d2399f0e7c3f38d025047ae32bc4b4ab1768771851", "path": "checkpoints/0/files/game/players/config.ini/sha256"} +{"actual": 473, "case": "vram_target_roundtrip", "expected": 475, "path": "checkpoints/0/files/game/players/config.ini/size"} +{"actual": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // 0.75 to 1\nStreamMinResident = \"1\" // 0 or 1\n", "case": "vram_target_roundtrip", "expected": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1.0\" // 0.75 to 1\nStreamMinResident = \"1\" // 0 or 1\n", "path": "checkpoints/0/files/game/players/config.ini/text"} diff --git a/native/core/tests/parity/results.provenance.json b/native/core/tests/parity/results.provenance.json new file mode 100644 index 0000000..90c2721 --- /dev/null +++ b/native/core/tests/parity/results.provenance.json @@ -0,0 +1,31 @@ +{ + "mismatchCount": 135, + "rustSourcesAtReport": { + "native/core/src/app.rs": "d9349229ac31f1a1d14e5d7bd391cc610b88bab8f3f0e1d9122f3e3ff084eb29", + "native/core/src/dxvk.rs": "d47cb3d23491daf4519bdb84d90e71ea5c5e10318db98ed0660e6d2e390aaa93", + "native/core/src/engine.rs": "962fb6557c6045502a2c0a86691bdc2c953dc5861b6f816393695e399c0345f7", + "native/core/src/enhanced.rs": "b1b424b2515068204b640adf1776484b881ceb1ed51a8653b3236e0365cb0a55", + "native/core/src/exe.rs": "1d9579e324edfcc2c7c54771aa2c2092f41ae94bd75bb9a51ca78ae029af0384", + "native/core/src/fs_ops.rs": "5d588d9fa973ce64291f7ceb7b3b6295510876bf32acb58678fc4cb1d170a4e5", + "native/core/src/lib.rs": "5a2f5cd5035d166bdfc2d70e661b7f4a00ce413f79a86e49e35425ada9edfdb5", + "native/core/src/models.rs": "4cb649a262fd8d7f59d6c35b3c8ed1b206f37691310a785c594163a543665121", + "native/core/src/steam.rs": "9a5a993c86b2909c1d1b870bad0eebb5d61e4c4611b54b69551aa13c1e145c45", + "native/core/src/t7.rs": "688d0a0652a036d8df49891a7be6b68ebc23ce727db4e5df76106455cf74e38d" + }, + "rustSourcesBeforeLaunch": { + "native/core/src/app.rs": "d9349229ac31f1a1d14e5d7bd391cc610b88bab8f3f0e1d9122f3e3ff084eb29", + "native/core/src/dxvk.rs": "d47cb3d23491daf4519bdb84d90e71ea5c5e10318db98ed0660e6d2e390aaa93", + "native/core/src/engine.rs": "962fb6557c6045502a2c0a86691bdc2c953dc5861b6f816393695e399c0345f7", + "native/core/src/enhanced.rs": "b1b424b2515068204b640adf1776484b881ceb1ed51a8653b3236e0365cb0a55", + "native/core/src/exe.rs": "1d9579e324edfcc2c7c54771aa2c2092f41ae94bd75bb9a51ca78ae029af0384", + "native/core/src/fs_ops.rs": "5d588d9fa973ce64291f7ceb7b3b6295510876bf32acb58678fc4cb1d170a4e5", + "native/core/src/lib.rs": "5a2f5cd5035d166bdfc2d70e661b7f4a00ce413f79a86e49e35425ada9edfdb5", + "native/core/src/models.rs": "4cb649a262fd8d7f59d6c35b3c8ed1b206f37691310a785c594163a543665121", + "native/core/src/steam.rs": "9a5a993c86b2909c1d1b870bad0eebb5d61e4c4611b54b69551aa13c1e145c45", + "native/core/src/t7.rs": "688d0a0652a036d8df49891a7be6b68ebc23ce727db4e5df76106455cf74e38d" + }, + "rustSourcesChangedBetweenLaunchAndReport": [], + "summary": "test result: FAILED. 17 passed; 11 failed; 3 ignored; 0 measured; 0 filtered out; finished in 10.53s", + "testBinary": "native/target/debug/deps/parity-4cab83e5679c2c93", + "testBinarySha256": "21c8aebfb1d99126ce863b2675b43f38420313d58b07eacf3b179608e0074182" +} diff --git a/native/core/tests/parity/status_alternate_executable.json b/native/core/tests/parity/status_alternate_executable.json new file mode 100644 index 0000000..0d04f4b --- /dev/null +++ b/native/core/tests/parity/status_alternate_executable.json @@ -0,0 +1,455 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOps3.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOps3.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOps3.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOps3.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOps3.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_alternate_executable", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_compatible_build.json b/native/core/tests/parity/status_compatible_build.json new file mode 100644 index 0000000..bb2bab2 --- /dev/null +++ b/native/core/tests/parity/status_compatible_build.json @@ -0,0 +1,459 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "logicalSha256": "66b95eb4667bd5b3b3d230e7bed1d29ccd261d48ca2699f01216c863be24ff44", + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "2ce2978309fe6dd6e7d2b964aff297c1d377ef934a442cd3a61333de90f48af9", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:compatible\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "logicalSha256": "66b95eb4667bd5b3b3d230e7bed1d29ccd261d48ca2699f01216c863be24ff44", + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "2ce2978309fe6dd6e7d2b964aff297c1d377ef934a442cd3a61333de90f48af9", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:compatible\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "Mar 3, 2023", + "activeBuildId": "10650222", + "backupAvailable": false, + "compatibleActive": true, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Executable matches compatible BuildID 10650222. T7 installs will use T7 Patch 2.04.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "66b95eb4667bd5b3b3d230e7bed1d29ccd261d48ca2699f01216c863be24ff44", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Executable matches compatible BuildID 10650222.", + "integrityStatus": "trusted", + "latestAvailable": false, + "modeLabel": "Compatible EXE", + "patchLabel": "T7 Patch 2.04", + "profile": "compatible", + "state": "trusted", + "trustedExecutable": true + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "logicalSha256": "66b95eb4667bd5b3b3d230e7bed1d29ccd261d48ca2699f01216c863be24ff44", + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "2ce2978309fe6dd6e7d2b964aff297c1d377ef934a442cd3a61333de90f48af9", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:compatible\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_compatible_build", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "rustIgnore": "fixture uses a logical SHA-256 alias because redistributing the game EXE is impossible", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_config_variants.json b/native/core/tests/parity/status_config_variants.json new file mode 100644 index 0000000..403ecfe --- /dev/null +++ b/native/core/tests/parity/status_config_variants.json @@ -0,0 +1,455 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "4a2fe5a4005cd7e1eed0bbfc814e244f70b968e690de376c800cced1eb402155", + "size": 180, + "text": "// malformed and decimal input\nMaxFPS = \"144.9\"\nFOV = \"bad\"\nRefreshRate = \"143.98\"\nVideoMemory = \"0.85\"\nStreamMinResident = \"1\"\nRestrictGraphicsOptions = \"0\"\nSerializeRender = \"2\"\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "4a2fe5a4005cd7e1eed0bbfc814e244f70b968e690de376c800cced1eb402155", + "size": 180, + "text": "// malformed and decimal input\nMaxFPS = \"144.9\"\nFOV = \"bad\"\nRefreshRate = \"143.98\"\nVideoMemory = \"0.85\"\nStreamMinResident = \"1\"\nRestrictGraphicsOptions = \"0\"\nSerializeRender = \"2\"\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": true, + "smoothFramerate": false, + "unlockOptions": true, + "vramLimited": true, + "vramTarget": 85 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 144, + "refreshRate": 143.98, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "4a2fe5a4005cd7e1eed0bbfc814e244f70b968e690de376c800cced1eb402155", + "size": 180, + "text": "// malformed and decimal input\nMaxFPS = \"144.9\"\nFOV = \"bad\"\nRefreshRate = \"143.98\"\nVideoMemory = \"0.85\"\nStreamMinResident = \"1\"\nRestrictGraphicsOptions = \"0\"\nSerializeRender = \"2\"\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_config_variants", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_dxvk_installed.json b/native/core/tests/parity/status_dxvk_installed.json new file mode 100644 index 0000000..71bc78a --- /dev/null +++ b/native/core/tests/parity/status_dxvk_installed.json @@ -0,0 +1,518 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3d11.dll", + "sha256": "dc5515425bf1f3f94009300d02aaab926e2c7b60a7c11dc1bd2e7d2005fcc510", + "size": 13, + "text": "fixture-d3d11" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "base64": "AP9maXh0dXJlLWR4Z2k=", + "mode": 420, + "path": "game/dxgi.dll", + "sha256": "e7e2f73af077301aedc20be31fc767cb105d7e99b2c7969429cc5074b85e3f0f", + "size": 14 + }, + { + "mode": 420, + "path": "game/dxvk.conf", + "sha256": "f0a2ee076de3a6b372fe91658dea9c64d8d0a2cd86bdacbf4082d17c9c60b524", + "size": 45, + "text": "dxvk.enableAsync=true\ndxgi.maxFrameLatency=1\n" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3d11.dll", + "sha256": "dc5515425bf1f3f94009300d02aaab926e2c7b60a7c11dc1bd2e7d2005fcc510", + "size": 13, + "text": "fixture-d3d11" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "base64": "AP9maXh0dXJlLWR4Z2k=", + "mode": 420, + "path": "game/dxgi.dll", + "sha256": "e7e2f73af077301aedc20be31fc767cb105d7e99b2c7969429cc5074b85e3f0f", + "size": 14 + }, + { + "mode": 420, + "path": "game/dxvk.conf", + "sha256": "f0a2ee076de3a6b372fe91658dea9c64d8d0a2cd86bdacbf4082d17c9c60b524", + "size": 45, + "text": "dxvk.enableAsync=true\ndxgi.maxFrameLatency=1\n" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": true, + "installed": true, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": true, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3d11.dll", + "sha256": "dc5515425bf1f3f94009300d02aaab926e2c7b60a7c11dc1bd2e7d2005fcc510", + "size": 13, + "text": "fixture-d3d11" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "base64": "AP9maXh0dXJlLWR4Z2k=", + "mode": 420, + "path": "game/dxgi.dll", + "sha256": "e7e2f73af077301aedc20be31fc767cb105d7e99b2c7969429cc5074b85e3f0f", + "size": 14 + }, + { + "mode": 420, + "path": "game/dxvk.conf", + "sha256": "f0a2ee076de3a6b372fe91658dea9c64d8d0a2cd86bdacbf4082d17c9c60b524", + "size": 45, + "text": "dxvk.enableAsync=true\ndxgi.maxFrameLatency=1\n" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_dxvk_installed", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_empty_config.json b/native/core/tests/parity/status_empty_config.json new file mode 100644 index 0000000..9f91ec2 --- /dev/null +++ b/native/core/tests/parity/status_empty_config.json @@ -0,0 +1,455 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0, + "text": "" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0, + "text": "" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 75 + }, + "appVersion": "v1.3.4", + "configExists": false, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0, + "text": "" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_empty_config", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_enhanced_installed.json b/native/core/tests/parity/status_enhanced_installed.json new file mode 100644 index 0000000..2e2dfdd --- /dev/null +++ b/native/core/tests/parity/status_enhanced_installed.json @@ -0,0 +1,560 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/bo3_enhanced_state.json", + "sha256": "070b5e85e4b9b360468cf37790c7684f6a0b95bbbb37ed1890c1c6c96f3f433c", + "size": 206, + "text": "{\"installed\": true, \"detected_at\": \"2026-09-10T12:00:00Z\", \"acknowledged_at\": \"2026-09-10T13:00:00Z\", \"installed_files\": [\"T7InternalWS.dll\", \"T7WSBootstrapper.dll\", \"WindowsCodecs.dll\", \"steam_api65.dll\"]}" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/T7InternalWS.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/T7WSBootstrapper.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/WindowsCodecs.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/steam_api65.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/bo3_enhanced_state.json", + "sha256": "070b5e85e4b9b360468cf37790c7684f6a0b95bbbb37ed1890c1c6c96f3f433c", + "size": 206, + "text": "{\"installed\": true, \"detected_at\": \"2026-09-10T12:00:00Z\", \"acknowledged_at\": \"2026-09-10T13:00:00Z\", \"installed_files\": [\"T7InternalWS.dll\", \"T7WSBootstrapper.dll\", \"WindowsCodecs.dll\", \"steam_api65.dll\"]}" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/T7InternalWS.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/T7WSBootstrapper.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/WindowsCodecs.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/steam_api65.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": "2026-09-10T13:00:00Z", + "backupStatus": "Created", + "detectedAt": "2026-09-10T12:00:00Z", + "dumpSource": "", + "filesInstalled": 4, + "installed": true, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Enhanced", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Executable is managed by BO3 Enhanced. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": true, + "enhancedAvailable": true, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": true, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Executable is managed by BO3 Enhanced.", + "integrityStatus": "enhanced", + "latestAvailable": false, + "modeLabel": "Enhanced", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "enhanced", + "state": "enhanced", + "trustedExecutable": true + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": true, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Enhanced", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/bo3_enhanced_state.json", + "sha256": "070b5e85e4b9b360468cf37790c7684f6a0b95bbbb37ed1890c1c6c96f3f433c", + "size": 206, + "text": "{\"installed\": true, \"detected_at\": \"2026-09-10T12:00:00Z\", \"acknowledged_at\": \"2026-09-10T13:00:00Z\", \"installed_files\": [\"T7InternalWS.dll\", \"T7WSBootstrapper.dll\", \"WindowsCodecs.dll\", \"steam_api65.dll\"]}" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/T7InternalWS.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/T7WSBootstrapper.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/WindowsCodecs.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/steam_api65.dll", + "sha256": "cf3a76345cf49ba68f8e1f8476b3b5566e2c80cf70b7c60eb5bd2185df31779e", + "size": 16, + "text": "fixture-enhanced" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_enhanced_installed", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_no_game.json b/native/core/tests/parity/status_no_game.json new file mode 100644 index 0000000..7ebf16c --- /dev/null +++ b/native/core/tests/parity/status_no_game.json @@ -0,0 +1,341 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "99e02132d604629c806d118cb06ecedc18295f28afa54906c43af1d730650210", + "size": 33, + "text": "{\n \"release_channel\": \"stable\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "99e02132d604629c806d118cb06ecedc18295f28afa54906c43af1d730650210", + "size": 33, + "text": "{\n \"release_channel\": \"stable\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 75 + }, + "appVersion": "v1.3.4", + "configExists": false, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unknown", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "BlackOps3.exe or BlackOpsIII.exe was not found. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "", + "executableHash": "", + "executableName": "", + "integrityMessage": "BlackOps3.exe or BlackOpsIII.exe was not found.", + "integrityStatus": "missing", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "missing", + "trustedExecutable": false + }, + "gameDetected": false, + "gameDir": null, + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Unknown", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "99e02132d604629c806d118cb06ecedc18295f28afa54906c43af1d730650210", + "size": 33, + "text": "{\n \"release_channel\": \"stable\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_no_game", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_september_2026_current.json b/native/core/tests/parity/status_september_2026_current.json new file mode 100644 index 0000000..97350a5 --- /dev/null +++ b/native/core/tests/parity/status_september_2026_current.json @@ -0,0 +1,459 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "logicalSha256": "51ca63bbc660e0826943c60da67606f6bcb4b3b519528b5e0548c68c9423a323", + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "e9045b0361257ba833a2d0407c79a04fb795259c62df6d909c60a3d31073d66e", + "size": 26, + "text": "PATCHOPS_FAKE_EXE:current\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "logicalSha256": "51ca63bbc660e0826943c60da67606f6bcb4b3b519528b5e0548c68c9423a323", + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "e9045b0361257ba833a2d0407c79a04fb795259c62df6d909c60a3d31073d66e", + "size": 26, + "text": "PATCHOPS_FAKE_EXE:current\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "Sep 10, 2026", + "activeBuildId": "24784313", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Executable matches latest Steam BuildID 24784313. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "51ca63bbc660e0826943c60da67606f6bcb4b3b519528b5e0548c68c9423a323", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Executable matches latest Steam BuildID 24784313.", + "integrityStatus": "trusted", + "latestAvailable": false, + "modeLabel": "Current EXE", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "current", + "state": "trusted", + "trustedExecutable": true + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Default", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "logicalSha256": "51ca63bbc660e0826943c60da67606f6bcb4b3b519528b5e0548c68c9423a323", + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "e9045b0361257ba833a2d0407c79a04fb795259c62df6d909c60a3d31073d66e", + "size": 26, + "text": "PATCHOPS_FAKE_EXE:current\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_september_2026_current", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "rustIgnore": "fixture uses a logical SHA-256 alias because redistributing the game EXE is impossible", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_steam_secondary_library.json b/native/core/tests/parity/status_steam_secondary_library.json new file mode 100644 index 0000000..e391919 --- /dev/null +++ b/native/core/tests/parity/status_steam_secondary_library.json @@ -0,0 +1,458 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/common", + "secondary-library/steamapps/common/Call of Duty Black Ops III", + "secondary-library/steamapps/common/Call of Duty Black Ops III/players", + "secondary-library/steamapps/common/Call of Duty Black Ops III/video", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "99e02132d604629c806d118cb06ecedc18295f28afa54906c43af1d730650210", + "size": 33, + "text": "{\n \"release_channel\": \"stable\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/common", + "secondary-library/steamapps/common/Call of Duty Black Ops III", + "secondary-library/steamapps/common/Call of Duty Black Ops III/players", + "secondary-library/steamapps/common/Call of Duty Black Ops III/video", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "99e02132d604629c806d118cb06ecedc18295f28afa54906c43af1d730650210", + "size": 33, + "text": "{\n \"release_channel\": \"stable\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/secondary-library/steamapps/common/Call of Duty Black Ops III/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/secondary-library/steamapps/common/Call of Duty Black Ops III", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/common", + "secondary-library/steamapps/common/Call of Duty Black Ops III", + "secondary-library/steamapps/common/Call of Duty Black Ops III/players", + "secondary-library/steamapps/common/Call of Duty Black Ops III/video", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "99e02132d604629c806d118cb06ecedc18295f28afa54906c43af1d730650210", + "size": 33, + "text": "{\n \"release_channel\": \"stable\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/common/Call of Duty Black Ops III/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_steam_secondary_library", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_t7_installed.json b/native/core/tests/parity/status_t7_installed.json new file mode 100644 index 0000000..7cc5aa3 --- /dev/null +++ b/native/core/tests/parity/status_t7_installed.json @@ -0,0 +1,518 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": true + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "^1", + "confExists": true, + "friendsOnly": false, + "gamertag": "^1OldName", + "installed": true, + "mode": "Custom", + "networkPassword": "old", + "plainName": "OldName" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_t7_installed", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/status_unverified_base.json b/native/core/tests/parity/status_unverified_base.json new file mode 100644 index 0000000..3698cc2 --- /dev/null +++ b/native/core/tests/parity/status_unverified_base.json @@ -0,0 +1,455 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n(no log entries found)\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "status_unverified_base", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/t7_config_clear_and_append.json b/native/core/tests/parity/t7_config_clear_and_append.json new file mode 100644 index 0000000..f3f45b3 --- /dev/null +++ b/native/core/tests/parity/t7_config_clear_and_append.json @@ -0,0 +1,557 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "af3e30337b73eb5367be9f5b723ad00af06bd465dfadd1d1b8565dc8c6d5c655", + "size": 213, + "text": "$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^2^1ColorName'.\n$TIMESTAMP - Success: Cleared network password in t7patch.conf.\n$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to Off.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "68cb4086156bfe2ed6ff13a556ba781dbaf947042c602182fedb607422f7a5e3", + "size": 71, + "text": "isfriendsonly=0\nnetworkpassword=\nplayername=^2^1ColorName\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "af3e30337b73eb5367be9f5b723ad00af06bd465dfadd1d1b8565dc8c6d5c655", + "size": 213, + "text": "$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^2^1ColorName'.\n$TIMESTAMP - Success: Cleared network password in t7patch.conf.\n$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to Off.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "68cb4086156bfe2ed6ff13a556ba781dbaf947042c602182fedb607422f7a5e3", + "size": 71, + "text": "isfriendsonly=0\nnetworkpassword=\nplayername=^2^1ColorName\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^2^1ColorName'.", + "message": "Updated 'playername' in t7patch.conf to '^2^1ColorName'." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Cleared network password in t7patch.conf.", + "message": "Cleared network password in t7patch.conf." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to Off.", + "message": "Updated 'isfriendsonly' in t7patch.conf to Off." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^2^1ColorName'.\n$TIMESTAMP - Success: Cleared network password in t7patch.conf.\n$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to Off.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": true + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "^2", + "confExists": true, + "friendsOnly": false, + "gamertag": "^2^1ColorName", + "installed": true, + "mode": "Custom", + "networkPassword": "", + "plainName": "^1ColorName" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "e6853c6f5659890470dcee843f013d873b6783329eb38056448c07f708beb5b6", + "size": 13, + "text": "unknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "t7_config_clear_and_append", + "operations": [ + { + "body": { + "colorCode": "^2", + "friendsOnly": false, + "gamertag": " ^1ColorName ", + "networkPassword": " " + }, + "method": "POST", + "path": "/api/t7-config" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/t7_config_invalid_name.json b/native/core/tests/parity/t7_config_invalid_name.json new file mode 100644 index 0000000..6d33b65 --- /dev/null +++ b/native/core/tests/parity/t7_config_invalid_name.json @@ -0,0 +1,487 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "error": "Gamertag cannot be empty.", + "ok": false + }, + { + "error": "Gamertag cannot exceed 20 characters.", + "ok": false + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "t7_config_invalid_name", + "operations": [ + { + "body": { + "gamertag": " " + }, + "method": "POST", + "path": "/api/t7-config" + }, + { + "body": { + "gamertag": "aaaaaaaaaaaaaaaaaaaaa" + }, + "method": "POST", + "path": "/api/t7-config" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/t7_config_missing.json b/native/core/tests/parity/t7_config_missing.json new file mode 100644 index 0000000..bc930ee --- /dev/null +++ b/native/core/tests/parity/t7_config_missing.json @@ -0,0 +1,302 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "error": "t7patch.conf was not found. Install T7 Patch before updating the gamertag.", + "ok": false + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "t7_config_missing", + "operations": [ + { + "body": { + "gamertag": "Player" + }, + "method": "POST", + "path": "/api/t7-config" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/t7_config_write.json b/native/core/tests/parity/t7_config_write.json new file mode 100644 index 0000000..762ce48 --- /dev/null +++ b/native/core/tests/parity/t7_config_write.json @@ -0,0 +1,557 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "f33a755c74594bf6f2205d2a7e8819ee536d04b4e00a88e98379966631885f28", + "size": 214, + "text": "$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^3FixturePlayer'.\n$TIMESTAMP - Success: Updated network password in t7patch.conf.\n$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to On.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "7c00a8697b2a429a1cbfacf00bc3cd1d5fdaba7359ba6fa15dcfe719ced09a34", + "size": 79, + "text": "playername=^3FixturePlayer\nnetworkpassword=secret\nisfriendsonly=1\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "f33a755c74594bf6f2205d2a7e8819ee536d04b4e00a88e98379966631885f28", + "size": 214, + "text": "$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^3FixturePlayer'.\n$TIMESTAMP - Success: Updated network password in t7patch.conf.\n$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to On.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "7c00a8697b2a429a1cbfacf00bc3cd1d5fdaba7359ba6fa15dcfe719ced09a34", + "size": 79, + "text": "playername=^3FixturePlayer\nnetworkpassword=secret\nisfriendsonly=1\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^3FixturePlayer'.", + "message": "Updated 'playername' in t7patch.conf to '^3FixturePlayer'." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Updated network password in t7patch.conf.", + "message": "Updated network password in t7patch.conf." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to On.", + "message": "Updated 'isfriendsonly' in t7patch.conf to On." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Updated 'playername' in t7patch.conf to '^3FixturePlayer'.\n$TIMESTAMP - Success: Updated network password in t7patch.conf.\n$TIMESTAMP - Success: Updated 'isfriendsonly' in t7patch.conf to On.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": true + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "^3", + "confExists": true, + "friendsOnly": true, + "gamertag": "^3FixturePlayer", + "installed": true, + "mode": "Custom", + "networkPassword": "secret", + "plainName": "FixturePlayer" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/t7patch.conf", + "sha256": "1d753acbca3ca8d7bef8cb73831810f54d6d48ed990fb7e6242eb74c230fde9a", + "size": 70, + "text": "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n" + }, + { + "mode": 420, + "path": "game/t7patch.dll", + "sha256": "e41e48279440133a6f109cbd9b8486bd57b16b10c61a34bf7ed4547bc7f748a1", + "size": 10, + "text": "fixture-t7" + }, + { + "mode": 420, + "path": "game/t7patchloader.dll", + "sha256": "fc61197d6de766eaddc97b118f5e7aed71805be61dcc7de5ec351bbd5b94caca", + "size": 14, + "text": "fixture-loader" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "t7_config_write", + "operations": [ + { + "body": { + "colorCode": "^3", + "friendsOnly": true, + "gamertag": "FixturePlayer", + "networkPassword": "secret" + }, + "method": "POST", + "path": "/api/t7-config" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/t7_release_asset_discovery.json b/native/core/tests/parity/t7_release_asset_discovery.json new file mode 100644 index 0000000..788db13 --- /dev/null +++ b/native/core/tests/parity/t7_release_asset_discovery.json @@ -0,0 +1,177 @@ +{ + "expected": { + "checkpoints": [], + "filesystem": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "cf8ce9a988680020881fa8b5f7a2b496a83da7ee6812b1b7c8217ca173df24a2", + "size": 86, + "text": "$TIMESTAMP - Info: Using latest T7 Patch release asset T7Patch-Linux-Windows-3.0.zip.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + [ + { + "assetKey": "compatible_archive", + "label": "versioned legacy-compatible name", + "release": { + "assets": [ + { + "browser_download_url": "https://fixtures.invalid/t7-2.05.zip", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Linux.Steamdeck.and.Manual.Windows.Install.2.05.zip" + } + ] + }, + "resolved": { + "sha256": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "url": "https://fixtures.invalid/t7-2.05.zip" + } + }, + { + "assetKey": "current_archive", + "label": "renamed universal current asset", + "release": { + "assets": [ + { + "browser_download_url": "https://fixtures.invalid/t7-3.0.zip", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "T7Patch-Linux-Windows-3.0.zip" + } + ] + }, + "resolved": { + "sha256": [ + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "url": "https://fixtures.invalid/t7-3.0.zip" + } + } + ] + ] + }, + "fixture": { + "directories": [ + "data", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "internalOperation": "t7_release_asset_discovery", + "name": "t7_release_asset_discovery", + "operations": [ + { + "method": "GET", + "path": "/api/status" + } + ], + "runner": "direct endpoint functions", + "rustIgnore": "release selection is private to the Rust install path and would otherwise download an archive", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/native/core/tests/parity/vram_target_roundtrip.json b/native/core/tests/parity/vram_target_roundtrip.json new file mode 100644 index 0000000..104f731 --- /dev/null +++ b/native/core/tests/parity/vram_target_roundtrip.json @@ -0,0 +1,761 @@ +{ + "expected": { + "checkpoints": [ + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "88b8977ed20d4f1f766456c7c1056fee4ea4f1edb1b606ca1ff9716b713c751d", + "size": 54, + "text": "$TIMESTAMP - Success: Limited VRAM usage set to 100%.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "48d4eca182b3265fc933d1d2399f0e7c3f38d025047ae32bc4b4ab1768771851", + "size": 475, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1.0\" // 0.75 to 1\nStreamMinResident = \"1\" // 0 or 1\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "465ba17022b5798819d6181a51fcafe86bba4d19b599f6e1463e6c9a77295673", + "size": 101, + "text": "$TIMESTAMP - Success: Limited VRAM usage set to 100%.\n$TIMESTAMP - Success: Enabled full VRAM usage.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "bb242c8618e86a15941c2a6dea6d990a317c94b6a90f159b7f60b5fff8f25198", + "size": 473, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // 0.75 to 1\nStreamMinResident = \"0\" // 0 or 1\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + } + ], + "filesystem": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 436, + "path": "data/PatchOpsIII.log", + "sha256": "465ba17022b5798819d6181a51fcafe86bba4d19b599f6e1463e6c9a77295673", + "size": 101, + "text": "$TIMESTAMP - Success: Limited VRAM usage set to 100%.\n$TIMESTAMP - Success: Enabled full VRAM usage.\n" + }, + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "bb242c8618e86a15941c2a6dea6d990a317c94b6a90f159b7f60b5fff8f25198", + "size": 473, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // 0.75 to 1\nStreamMinResident = \"0\" // 0 or 1\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "responses": [ + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": true, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Limited VRAM usage set to 100%.", + "message": "Limited VRAM usage set to 100%." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Limited VRAM usage set to 100%.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + }, + { + "ok": true, + "state": { + "activeLaunchProfile": "default", + "advanced": { + "configReadonly": false, + "maxFrameLatency": 1, + "reduceCpu": false, + "smoothFramerate": false, + "unlockOptions": false, + "vramLimited": false, + "vramTarget": 100 + }, + "appVersion": "v1.3.4", + "configExists": true, + "currentLaunchOptions": "", + "dxvk": { + "confExists": false, + "installed": false, + "settings": { + "enableAsync": true, + "gplAsyncCache": true, + "hudEnabled": false, + "maxFrameLatency": 1, + "maxFrameRate": 0, + "numCompilerThreads": 0, + "tearFree": "True" + } + }, + "enhanced": { + "acknowledgedAt": null, + "backupStatus": "Not created", + "detectedAt": null, + "dumpSource": "", + "filesInstalled": 0, + "installed": false, + "launchOptionsActive": false + }, + "exeSwap": { + "activeBuildDate": "", + "activeBuildId": "Unverified", + "backupAvailable": false, + "compatibleActive": false, + "compatibleAvailable": false, + "compatibleBuildDate": "Mar 3, 2023", + "compatibleBuildId": "10650222", + "currentBuildDate": "Sep 10, 2026", + "currentBuildId": "24784313", + "displayLabel": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE. T7 installs will use T7 Patch Scroptss/T7Patch.", + "enhancedActive": false, + "enhancedAvailable": false, + "enhancedBuildDate": "", + "enhancedBuildId": "Enhanced", + "enhancedExeActive": false, + "executable": "$ROOT/game/BlackOpsIII.exe", + "executableHash": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "executableName": "BlackOpsIII.exe", + "integrityMessage": "Active executable hash 0f96666e646d does not match latest BuildID 24784313, compatible BuildID 10650222, or a PatchOpsIII-preserved Enhanced EXE.", + "integrityStatus": "unverified", + "latestAvailable": false, + "modeLabel": "Unknown", + "patchLabel": "T7 Patch Scroptss/T7Patch", + "profile": "", + "state": "unverified", + "trustedExecutable": false + }, + "gameDetected": true, + "gameDir": "$ROOT/game", + "graphics": { + "displayMode": 1, + "drawFps": false, + "fov": 80, + "maxFps": 165, + "refreshRate": 60.0, + "renderResolution": 100, + "resolution": "1920x1080", + "vsync": true + }, + "launchProfiles": [ + { + "active": true, + "id": "default", + "installed": true, + "label": "Default (None)", + "option": "", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "offline", + "installed": true, + "label": "Play Offline", + "option": "+set fs_game offlinemp", + "state": "Ready", + "subscribed": true + }, + { + "active": false, + "id": "all_around", + "installed": false, + "label": "All-around Enhancement Lite", + "option": "+set fs_game 2994481309", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "ultimate", + "installed": false, + "label": "Ultimate Experience Mod", + "option": "+set fs_game 2942053577", + "path": null, + "state": "Not Subscribed", + "subscribed": false + }, + { + "active": false, + "id": "reforged", + "installed": false, + "label": "BO3 Reforged", + "option": "+set fs_game 3667377161", + "path": null, + "state": "Not Subscribed", + "subscribed": false + } + ], + "logPath": "$ROOT/data/PatchOpsIII.log", + "logs": [ + { + "category": "Success", + "line": "$TIMESTAMP - Success: Limited VRAM usage set to 100%.", + "message": "Limited VRAM usage set to 100%." + }, + { + "category": "Success", + "line": "$TIMESTAMP - Success: Enabled full VRAM usage.", + "message": "Enabled full VRAM usage." + } + ], + "maintenance": { + "logPayload": "PatchOpsIII v1.3.4 - Linux $KERNEL (x86_64) logs:\n```\n$TIMESTAMP - Success: Limited VRAM usage set to 100%.\n$TIMESTAMP - Success: Enabled full VRAM usage.\n```", + "modFilesDir": "$ROOT/data/BO3 Mod Files" + }, + "mods": { + "dxvk": false, + "enhanced": false, + "t7Patch": false + }, + "platform": "Linux", + "presets": [ + "Smooth Preset", + "Low-Latency Preset" + ], + "qol": { + "allIntros": false, + "d3dcompiler": false, + "intro": false + }, + "releaseChannel": "stable", + "steamUserId": "76561198000000000", + "t7": { + "colorCode": "", + "confExists": false, + "friendsOnly": false, + "gamertag": "", + "installed": false, + "mode": "Custom", + "networkPassword": "", + "plainName": "" + } + } + } + ] + }, + "fixture": { + "directories": [ + "data", + "game", + "game/players", + "game/video", + "not-a-game", + "resources", + "secondary-library", + "secondary-library/steamapps", + "secondary-library/steamapps/workshop", + "steam", + "steam/steamapps", + "steam/userdata", + "steam/userdata/76561198000000000", + "steam/userdata/76561198000000000/config" + ], + "files": [ + { + "mode": 420, + "path": "data/electron-settings.json", + "sha256": "4c07124e4037a29057b1274d3d2dbe1df207b1350e19c28ec27bf8d4ae4338b2", + "size": 61, + "text": "{\n \"release_channel\": \"stable\",\n \"game_dir\": \"$ROOT/game\"\n}" + }, + { + "mode": 420, + "path": "game/BlackOpsIII.exe", + "sha256": "0f96666e646d0a5ccbce7257d91593556a6747502ccab65a3991b7e097e9d5d2", + "size": 29, + "text": "PATCHOPS_FAKE_EXE:unverified\n" + }, + { + "mode": 420, + "path": "game/d3dcompiler_46.dll", + "sha256": "0ae6e172362d4d6866dccf7d2de3b99d9964310054626fce519bde4e35295caa", + "size": 15, + "text": "legacy-compiler" + }, + { + "mode": 420, + "path": "game/players/config.ini", + "sha256": "31e5090dff0b200154b4d7f79766d0101c796eddb073e0b3ae4a3fae3da234b4", + "size": 474, + "text": "// fixture configuration\nMaxFPS = \"165\" // original\nFOV = \"80\" // original\nFullScreenMode = \"1\" // original\nWindowSize = \"1920x1080\" // original\nRefreshRate = \"60\" // original\nResolutionPercent = \"100\" // original\nVsync = \"1\" // original\nDrawFPS = \"0\" // original\nSmoothFramerate = \"0\" // original\nRestrictGraphicsOptions = \"1\" // original\nSerializeRender = \"0\" // original\nMaxFrameLatency = \"1\" // original\nVideoMemory = \"1\" // original\nStreamMinResident = \"0\" // original\n" + }, + { + "mode": 420, + "path": "game/video/BO3_Global_Logo_LogoSequence.mkv", + "sha256": "06cd6175508d0d415bfd1944079df661fe73691ad4dd17ccc1688a298e6ed083", + "size": 9, + "text": "intro-one" + }, + { + "mode": 420, + "path": "game/video/Frontend_Intro.mkv", + "sha256": "5d3568fc674ae38d3d215ec7e9e46e02fd1c31987199c7d0aab8b7a3a41f64a2", + "size": 9, + "text": "intro-two" + }, + { + "mode": 420, + "path": "resources/presets.json", + "sha256": "e29c9752c43f216794bbe7c82739e5f6821f1debb71a734e152df2b9fb14d5f2", + "size": 5082, + "text": "{\n \"Smooth Preset\": {\n \"SmoothFramerate\": [\"1\", \"Enable smoothing at the cost of some performance\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"4\", \"Driver can queue up to 4 frames\"],\n \"SerializeRender\": [\"0\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"1\", \"Target all of the available VRAM to reduce stuttering\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"1\", \"Vertical sync enabled\"],\n \"BackbufferCount\": [\"3\", \"Triple buffering enabled\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"0\", \"No reduction in minimum asset residency in VRAM\"]\n },\n \"Low-Latency Preset\": {\n \"SmoothFramerate\": [\"0\", \"Disable smoothing to minimize frame delay\"],\n \"CorpseCount\": [\"16\", \"Maximum number of corpses is 32, 16 is selected for a balanced approach\"],\n \"MaxFrameLatency\": [\"1\", \"Driver can only queue up to 1 frame\"],\n \"SerializeRender\": [\"2\", \"0 is the default for the best performance, 1 and 2 improve latency but require a powerful CPU.\"],\n \"VideoMemory\": [\"0.75\", \"Target 75% the available VRAM to increase performance\"],\n \"RestrictGraphicsOptions\": [\"0\", \"Advanced graphics options unlocked\"],\n \"Vsync\": [\"0\", \"Vertical sync disabled for lower latency\"],\n \"BackbufferCount\": [\"2\", \"Reset Vsync to double buffer\"],\n \"MeshQuality\": [\"0\", \"Highest quality meshes\"],\n \"TextureFilter\": [\"2\", \"High anisotropic filtering\"],\n \"TextureQuality\": [\"1\", \"High texture quality (balanced)\"],\n \"TextureQualityFX\": [\"1\", \"Improved effect quality\"],\n \"TextureQualityProbes\": [\"0\", \"Default reflection quality\"],\n \"TextureQualityBakedSunShadows\": [\"1\", \"Slightly reduced baked sun shadow quality\"],\n \"TextureLowDetailResident\": [\"0\", \"Lowest detail resident streaming disabled\"],\n \"DisableDynamicLightShadows\": [\"1\", \"Dynamic light shadows disabled\"],\n \"DisableDynamicSunShadows\": [\"1\", \"Dynamic sun shadows disabled\"],\n \"SpotShadowTextureSize\": [\"1024\", \"Spot light shadow resolution set to 1024\"],\n \"OmniShadowTextureSize\": [\"256\", \"Omni (point) light shadow resolution set to 256\"],\n \"ShadowFiltering\": [\"1\", \"Multi-sampled soft shadows enabled\"],\n \"ActorShadows\": [\"0\", \"Actor shadows disabled\"],\n \"VolumetricLightingEnabled\": [\"0\", \"Volumetric lighting disabled\"],\n \"VolumetricLightingMaxSunSamples\": [\"8\", \"Max sun samples for volumetric lighting\"],\n \"VolumetricLightingMaxLightSamples\": [\"40\", \"Max light samples for volumetric lighting\"],\n \"VolumetricLightingSkipSunSamples\": [\"1\", \"Skip alternate sun samples\"],\n \"VolumetricLightingSkipLightSamples\": [\"1\", \"Skip alternate light samples\"],\n \"OIT\": [\"1\", \"Order-independent transparency enabled\"],\n \"OITLayers\": [\"8\", \"8 transparency layers set\"],\n \"SSAOTechnique\": [\"GTAO Low Quality\", \"Ambient occlusion set to GTAO Low Quality\"],\n \"AATechnique\": [\"FXAA\", \"Anti-aliasing set to FXAA\"],\n \"MotionBlur\": [\"Off\", \"Motion blur disabled\"],\n \"MotionBlurQuality\": [\"Medium\", \"Medium quality motion blur (if enabled)\"],\n \"SubsurfaceScattering\": [\"0\", \"Subsurface scattering disabled\"],\n \"StreamMinResident\": [\"1\", \"Reduce the minimum asset residency in VRAM\"]\n }\n}\n" + }, + { + "mode": 420, + "path": "secondary-library/steamapps/workshop/appworkshop_311210.acf", + "sha256": "1e41744c2d76714baeae696764a96898a65315748ed695f05aa6890dd4f7a78a", + "size": 85, + "text": "\"AppWorkshop\"\n{\n \"WorkshopItemsInstalled\"\n {\n }\n \"WorkshopItemDetails\"\n {\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/steamapps/libraryfolders.vdf", + "sha256": "1225e4effb5115a68ff774612689de1eb40580299937a428b8aa5ff8d01a864d", + "size": 153, + "text": "\"libraryfolders\"\n{\n \"0\"\n {\n \"path\" \"$ROOT/steam\"\n }\n \"1\"\n {\n \"path\" \"$ROOT/secondary-library\"\n \"apps\"\n {\n \"311210\" \"1\"\n }\n }\n}\n" + }, + { + "mode": 420, + "path": "steam/userdata/76561198000000000/config/localconfig.vdf", + "sha256": "6c7fefd9df196943bb300cec2b60d2fdd528efc08d9bad7fa4dfdc7b9b51a093", + "size": 211, + "text": "\"UserLocalConfigStore\"\n{\n \"Software\"\n {\n \"Valve\"\n {\n \"Steam\"\n {\n \"apps\"\n {\n \"311210\"\n {\n \"LaunchOptions\" \"\"\n }\n }\n }\n }\n }\n}\n" + } + ] + }, + "name": "vram_target_roundtrip", + "operations": [ + { + "body": { + "limited": true, + "target": 100 + }, + "method": "POST", + "path": "/api/vram-target" + }, + { + "body": { + "limited": false, + "target": 85 + }, + "method": "POST", + "path": "/api/vram-target" + } + ], + "runner": "direct endpoint functions", + "schemaVersion": 1, + "sourceHashes": { + "backend/api.py": "080be5c4a3b929134298179cd3f4d1558c288a50dcdb38978ada831925a57a46", + "bo3_enhanced.py": "32a670c8a63a64216ee5c46aa000d2e3840ca348ca0fd5031c81699252cf57b8", + "dxvk_manager.py": "8e150458bc7480ac89cd9e64c558ba438f61a09611bbf24b962cba02d7e4a5e5", + "t7_patch.py": "73e66fadd1c0775742b7bb6d9c6c73a3c9df8304d3117abd8ac768dfc68cc3ea", + "utils.py": "313069c3f70b92888239c9ef15ce2379713b029e019426e76b386f088b556564" + }, + "sourceOfTruth": "Python backend on main" +} diff --git a/scripts/gen_parity_fixtures.py b/scripts/gen_parity_fixtures.py new file mode 100644 index 0000000..a3adddd --- /dev/null +++ b/scripts/gen_parity_fixtures.py @@ -0,0 +1,676 @@ +#!/usr/bin/env python3 +"""Generate deterministic Python-backend parity goldens for patchops-core. + +The generator deliberately uses only the Python standard library plus the +repository's existing ``requests`` and ``vdf`` dependencies. When FastAPI and +Pydantic are unavailable it installs tiny import-time shims and invokes the +same async endpoint functions directly. No network request or real Steam +process is used. +""" + +from __future__ import annotations + +import argparse +import asyncio +import base64 +import hashlib +import importlib +import json +import os +import platform +import re +import shutil +import sys +import tempfile +import types +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = REPO_ROOT / "native" / "core" / "tests" / "parity" +STEAM_ID = "76561198000000000" +CURRENT_EXE_SHA256 = "51ca63bbc660e0826943c60da67606f6bcb4b3b519528b5e0548c68c9423a323" +COMPATIBLE_EXE_SHA256 = "66b95eb4667bd5b3b3d230e7bed1d29ccd261d48ca2699f01216c863be24ff44" +TIMESTAMP_RE = re.compile(r"\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\b") + + +def _install_import_shims() -> bool: + """Return True when endpoint calls must use the direct-call fallback.""" + try: + import fastapi # noqa: F401 + import pydantic # noqa: F401 + + return False + except ImportError: + pass + + class FieldInfo: + def __init__(self, default: Any = ..., **_: Any) -> None: + self.default = default + + class BaseModel: + def __init__(self, **values: Any) -> None: + annotations: dict[str, Any] = {} + for cls in reversed(type(self).mro()): + annotations.update(getattr(cls, "__annotations__", {})) + for name in annotations: + if name in values: + value = values.pop(name) + else: + default = getattr(type(self), name, ...) + value = default.default if isinstance(default, FieldInfo) else default + if value is ...: + raise TypeError(f"missing required field: {name}") + setattr(self, name, value) + if values: + raise TypeError(f"unexpected fields: {', '.join(sorted(values))}") + + def Field(default: Any = ..., **kwargs: Any) -> FieldInfo: # noqa: N802 + return FieldInfo(default, **kwargs) + + class FastAPI: + def __init__(self, *_: Any, **__: Any) -> None: + pass + + def add_middleware(self, *_: Any, **__: Any) -> None: + pass + + def _decorator(self, *_: Any, **__: Any) -> Callable[[Any], Any]: + return lambda function: function + + get = post = websocket = on_event = _decorator + + class WebSocket: + pass + + class WebSocketDisconnect(Exception): + pass + + class CORSMiddleware: + pass + + async def run_in_threadpool(function: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: + return function(*args, **kwargs) + + fastapi = types.ModuleType("fastapi") + fastapi.FastAPI = FastAPI + fastapi.WebSocket = WebSocket + fastapi.WebSocketDisconnect = WebSocketDisconnect + concurrency = types.ModuleType("fastapi.concurrency") + concurrency.run_in_threadpool = run_in_threadpool + middleware = types.ModuleType("fastapi.middleware") + cors = types.ModuleType("fastapi.middleware.cors") + cors.CORSMiddleware = CORSMiddleware + pydantic = types.ModuleType("pydantic") + pydantic.BaseModel = BaseModel + pydantic.Field = Field + sys.modules.update( + { + "fastapi": fastapi, + "fastapi.concurrency": concurrency, + "fastapi.middleware": middleware, + "fastapi.middleware.cors": cors, + "pydantic": pydantic, + } + ) + return True + + +DIRECT_CALL_FALLBACK = _install_import_shims() +sys.path.insert(0, str(REPO_ROOT)) +api = importlib.import_module("backend.api") +utils = importlib.import_module("utils") +t7_patch = importlib.import_module("t7_patch") +enhanced = importlib.import_module("bo3_enhanced") + +# A fail-closed network guard: release discovery temporarily replaces requests.get +# with fixture responses, but any accidental real request is an error. +def reject_network(*_: Any, **__: Any) -> Any: + raise AssertionError("parity fixtures must not access the network") + +api.requests.sessions.Session.request = reject_network + +try: + if DIRECT_CALL_FALLBACK: + raise ImportError("FastAPI unavailable") + from fastapi.testclient import TestClient + CLIENT = TestClient(api.app) +except (ImportError, TypeError): + CLIENT = None + + +class ImmediateLogTarget: + """Keep endpoint log ordering stable when endpoints are called directly.""" + + def handle_write_log( + self, *, full_message: str, category: str, plain_message: str, **_: str + ) -> None: + api.log_bus._recent.append( + {"message": plain_message, "category": category, "line": full_message} + ) + + +@dataclass +class Harness: + root: Path + data: Path + steam: Path + library: Path + game: Path + logical_hashes: dict[Path, str] + + +def write(path: Path, contents: str | bytes, mode: int = 0o644) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(contents.encode("utf-8") if isinstance(contents, str) else contents) + path.chmod(mode) + + +def localconfig(options: str = "") -> str: + escaped = options.replace("\\", "\\\\").replace('"', '\\"') + return ( + '"UserLocalConfigStore"\n{\n' + ' "Software"\n {\n "Valve"\n {\n "Steam"\n {\n' + ' "apps"\n {\n "311210"\n {\n' + f' "LaunchOptions" "{escaped}"\n' + " }\n }\n }\n }\n }\n}\n" + ) + + +def base_config() -> str: + return """// fixture configuration +MaxFPS = "165" // original +FOV = "80" // original +FullScreenMode = "1" // original +WindowSize = "1920x1080" // original +RefreshRate = "60" // original +ResolutionPercent = "100" // original +Vsync = "1" // original +DrawFPS = "0" // original +SmoothFramerate = "0" // original +RestrictGraphicsOptions = "1" // original +SerializeRender = "0" // original +MaxFrameLatency = "1" // original +VideoMemory = "1" // original +StreamMinResident = "0" // original +""" + + +def make_harness(root: Path, *, saved_game: bool = True, game_in_library: bool = False) -> Harness: + data = root / "data" + steam = root / "steam" + library = root / "secondary-library" + game = ( + library / "steamapps" / "common" / "Call of Duty Black Ops III" + if game_in_library + else root / "game" + ) + data.mkdir(parents=True) + steam.mkdir(parents=True) + library.mkdir(parents=True) + settings: dict[str, Any] = {"release_channel": "stable"} + if saved_game: + settings["game_dir"] = str(game) + write(data / "electron-settings.json", json.dumps(settings, indent=2)) + write(root / "resources" / "presets.json", (REPO_ROOT / "presets.json").read_bytes()) + write( + steam / "steamapps" / "libraryfolders.vdf", + ( + '"libraryfolders"\n{\n' + f' "0"\n {{\n "path" "{steam}"\n }}\n' + f' "1"\n {{\n "path" "{library}"\n "apps"\n {{\n "311210" "1"\n }}\n }}\n' + '}\n' + ), + ) + write(steam / "userdata" / STEAM_ID / "config" / "localconfig.vdf", localconfig()) + write( + library / "steamapps" / "workshop" / "appworkshop_311210.acf", + '"AppWorkshop"\n{\n "WorkshopItemsInstalled"\n {\n }\n "WorkshopItemDetails"\n {\n }\n}\n', + ) + return Harness(root, data, steam, library, game, {}) + + +def add_game(harness: Harness, *, exe_profile: str = "unverified") -> None: + marker = f"PATCHOPS_FAKE_EXE:{exe_profile}\n".encode() + write(harness.game / "BlackOpsIII.exe", marker) + if exe_profile == "current": + harness.logical_hashes[harness.game / "BlackOpsIII.exe"] = CURRENT_EXE_SHA256 + elif exe_profile == "compatible": + harness.logical_hashes[harness.game / "BlackOpsIII.exe"] = COMPATIBLE_EXE_SHA256 + write(harness.game / "players" / "config.ini", base_config()) + write(harness.game / "video" / "BO3_Global_Logo_LogoSequence.mkv", b"intro-one") + write(harness.game / "video" / "Frontend_Intro.mkv", b"intro-two") + write(harness.game / "d3dcompiler_46.dll", b"legacy-compiler") + + +def configure_modules(harness: Harness) -> None: + api.SETTINGS_PATH = harness.data / "electron-settings.json" + api.MOD_FILES_DIR = harness.data / "BO3 Mod Files" + api.PRESETS_PATH = harness.root / "resources" / "presets.json" + api._settings_cache = None + api._game_dir_cache = None + api._preset_names_cache = None + api._presets_cache = None + api.log_bus._recent.clear() + api.log_target = ImmediateLogTarget() + utils.steam_userdata_path = str(harness.steam / "userdata") + utils.steam_exe_path = str(harness.steam / "steam") + utils._steam_library_paths_cache = None + utils._candidate_steam_roots = lambda: [str(harness.steam)] + # Isolate the API's fallback home-directory discovery without changing HOME. + Path.home = classmethod(lambda cls: harness.root / "home") + utils.get_app_data_dir = lambda: str(harness.data) + api.get_app_data_dir = lambda: str(harness.data) + utils.close_steam = lambda _target: None + utils.open_steam = lambda _target: None + + real_hash = utils.file_sha256 + + def fixture_hash(path: str | os.PathLike[str]) -> str | None: + candidate = Path(path) + logical = harness.logical_hashes.get(candidate) + return logical or real_hash(str(candidate)) + + api.file_sha256 = fixture_hash + + +def normalize_string(value: str, root: Path) -> str: + value = value.replace(str(root), "$ROOT") + value = value.replace(str(root).replace("/", "\\"), "$ROOT") + value = value.replace(platform.release(), "$KERNEL") + return TIMESTAMP_RE.sub("$TIMESTAMP", value) + + +def normalize(value: Any, root: Path) -> Any: + if isinstance(value, str): + return normalize_string(value, root) + if isinstance(value, list): + return [normalize(item, root) for item in value] + if isinstance(value, tuple): + return [normalize(item, root) for item in value] + if isinstance(value, dict): + return {normalize_string(key, root): normalize(item, root) for key, item in value.items()} + return value + + +def file_entry(path: Path, root: Path, logical_hashes: dict[Path, str]) -> dict[str, Any]: + raw = path.read_bytes() + entry: dict[str, Any] = { + "path": path.relative_to(root).as_posix(), + "mode": path.stat().st_mode & 0o777, + } + try: + text = raw.decode("utf-8") + except UnicodeDecodeError: + normalized_raw = raw + entry["base64"] = base64.b64encode(raw).decode("ascii") + else: + text = normalize_string(text, root) + normalized_raw = text.encode("utf-8") + entry["text"] = text + entry["size"] = len(normalized_raw) + entry["sha256"] = hashlib.sha256(normalized_raw).hexdigest() + if path in logical_hashes: + entry["logicalSha256"] = logical_hashes[path] + return entry + + +def snapshot(root: Path, logical_hashes: dict[Path, str]) -> dict[str, Any]: + directories = sorted( + path.relative_to(root).as_posix() + for path in root.rglob("*") + if path.is_dir() + ) + files = [ + file_entry(path, root, logical_hashes) + for path in sorted(root.rglob("*")) + if path.is_file() + ] + return {"directories": directories, "files": files} + + +ENDPOINTS: dict[tuple[str, str], tuple[Callable[..., Any], type[Any] | None]] = { + ("GET", "/api/status"): (api.status, None), + ("POST", "/api/game-directory"): (api.set_game_directory, api.GameDirectoryPayload), + ("POST", "/api/config"): (api.set_config, api.ConfigValuePayload), + ("POST", "/api/vram-target"): (api.vram_target, api.VramPayload), + ("POST", "/api/t7-config"): (api.set_t7_config, api.T7ConfigPayload), + ("POST", "/api/dxvk-config"): (api.dxvk_config, api.DxvkConfigPayload), + ("POST", "/api/launch-options"): (api.launch_options, api.LaunchOptionsPayload), + ("POST", "/api/intro-skip"): (api.intro_skip, api.TogglePayload), + ("POST", "/api/d3dcompiler"): (api.d3dcompiler, api.TogglePayload), + ("POST", "/api/all-intros-skip"): (api.all_intros_skip, api.TogglePayload), + ("POST", "/api/config-readonly"): (api.config_readonly, api.TogglePayload), +} + + +async def call_endpoint(operation: dict[str, Any]) -> Any: + function, model = ENDPOINTS[(operation["method"], operation["path"])] + if model is None: + return await function() + return await function(model(**operation.get("body", {}))) + + +def run_endpoint_operations(operations: list[dict[str, Any]]) -> list[Any]: + if CLIENT is not None: + results = [] + for operation in operations: + response = CLIENT.request(operation["method"], operation["path"], + json=operation.get("body")) + response.raise_for_status() + results.append(response.json()) + return results + async def run() -> list[Any]: + return [await call_endpoint(operation) for operation in operations] + + return asyncio.run(run()) + + +class FakeResponse: + def __init__(self, payload: dict[str, Any]) -> None: + self.payload = payload + + def raise_for_status(self) -> None: + return None + + def json(self) -> dict[str, Any]: + return self.payload + + +def resolve_asset_cases() -> list[dict[str, Any]]: + digest_a = "a" * 64 + digest_b = "b" * 64 + payloads = [ + { + "label": "versioned legacy-compatible name", + "assetKey": "compatible_archive", + "release": { + "assets": [ + { + "name": "Linux.Steamdeck.and.Manual.Windows.Install.2.05.zip", + "browser_download_url": "https://fixtures.invalid/t7-2.05.zip", + "digest": f"sha256:{digest_a}", + } + ] + }, + }, + { + "label": "renamed universal current asset", + "assetKey": "current_archive", + "release": { + "assets": [ + { + "name": "T7Patch-Linux-Windows-3.0.zip", + "browser_download_url": "https://fixtures.invalid/t7-3.0.zip", + "digest": f"sha256:{digest_b}", + } + ] + }, + }, + ] + results = [] + original_get = t7_patch.requests.get + try: + for item in payloads: + t7_patch._t7patch_release_assets_cache.clear() + t7_patch.requests.get = lambda *_args, payload=item["release"], **_kwargs: FakeResponse(payload) + url, hashes = t7_patch._resolve_t7patch_asset(item["assetKey"], api.log_target) + results.append( + { + "label": item["label"], + "assetKey": item["assetKey"], + "release": item["release"], + "resolved": {"url": url, "sha256": sorted(hashes)}, + } + ) + finally: + t7_patch.requests.get = original_get + t7_patch._t7patch_release_assets_cache.clear() + return results + + +def case_definitions() -> list[dict[str, Any]]: + cases = [ + {"name": "status_no_game", "saved_game": False, "add_game": False, + "operations": [{"method": "GET", "path": "/api/status"}]}, + {"name": "status_unverified_base", "exe_profile": "unverified", + "operations": [{"method": "GET", "path": "/api/status"}]}, + {"name": "status_september_2026_current", "exe_profile": "current", + "operations": [{"method": "GET", "path": "/api/status"}], + "rustIgnore": "fixture uses a logical SHA-256 alias because redistributing the game EXE is impossible"}, + {"name": "status_compatible_build", "exe_profile": "compatible", + "operations": [{"method": "GET", "path": "/api/status"}], + "rustIgnore": "fixture uses a logical SHA-256 alias because redistributing the game EXE is impossible"}, + {"name": "status_steam_secondary_library", "saved_game": False, + "game_in_library": True, "exe_profile": "unverified", + "operations": [{"method": "GET", "path": "/api/status"}]}, + {"name": "game_directory_validation_invalid", "add_game": False, + "operations": [{"method": "POST", "path": "/api/game-directory", + "body": {"path": "$ROOT/not-a-game"}}]}, + {"name": "game_directory_validation_valid", "saved_game": False, + "operations": [{"method": "POST", "path": "/api/game-directory", + "body": {"path": "$ROOT/game"}}]}, + {"name": "config_graphics_advanced_write", + "operations": [ + {"method": "POST", "path": "/api/config", "body": {"key": "MaxFPS", "value": 240, "comment": "fixture max fps"}}, + {"method": "POST", "path": "/api/config", "body": {"key": "FOV", "value": 110, "comment": "fixture fov"}}, + {"method": "POST", "path": "/api/config", "body": {"key": "DrawFPS", "value": 1, "comment": "fixture counter"}}, + {"method": "POST", "path": "/api/config", "body": {"key": "SerializeRender", "value": 2, "comment": "fixture cpu"}}, + {"method": "POST", "path": "/api/vram-target", "body": {"limited": True, "target": 85}}, + ]}, + {"name": "t7_config_write", "setup": "t7", + "operations": [{"method": "POST", "path": "/api/t7-config", + "body": {"gamertag": "FixturePlayer", "colorCode": "^3", + "networkPassword": "secret", "friendsOnly": True}}]}, + {"name": "dxvk_config_write", "setup": "dxvk", + "operations": [{"method": "POST", "path": "/api/dxvk-config", + "body": {"enableAsync": False, "gplAsyncCache": False, + "numCompilerThreads": 6, "maxFrameRate": 144, + "maxFrameLatency": 2, "tearFree": "Auto", "hudEnabled": True}}]}, + {"name": "launch_options_apply", "launch_options": 'WINEDLLOVERRIDES="dsound=n,b" %command% -novid +set fs_game oldmod', + "operations": [ + {"method": "POST", "path": "/api/launch-options", + "body": {"options": "+set fs_game offlinemp", "preserve_fs_game": False}}, + {"method": "POST", "path": "/api/launch-options", + "body": {"options": "", "preserve_fs_game": True}}, + ]}, + {"name": "qol_toggles", + "operations": [ + {"method": "POST", "path": "/api/intro-skip", "body": {"enabled": True}}, + {"method": "POST", "path": "/api/d3dcompiler", "body": {"enabled": True}}, + {"method": "POST", "path": "/api/all-intros-skip", "body": {"enabled": True}}, + ]}, + {"name": "t7_release_asset_discovery", "add_game": False, + "internal": "t7_release_asset_discovery", + "rustIgnore": "release selection is private to the Rust install path and would otherwise download an archive"}, + ] + cases += [ + {"name": "status_t7_installed", "setup": "t7"}, + {"name": "status_dxvk_installed", "setup": "dxvk"}, + {"name": "status_enhanced_installed", "setup": "enhanced"}, + {"name": "status_empty_config", "config": ""}, + {"name": "status_config_variants", "config": + '// malformed and decimal input\nMaxFPS = "144.9"\nFOV = "bad"\n' + 'RefreshRate = "143.98"\nVideoMemory = "0.85"\nStreamMinResident = "1"\n' + 'RestrictGraphicsOptions = "0"\nSerializeRender = "2"\n'}, + {"name": "config_append_and_crlf", "config": 'FOV = "80"\r\n', + "operations": [{"method": "POST", "path": "/api/config", + "body": {"key": "MaxFPS", "value": 200, "comment": "fixture appended"}}]}, + {"name": "config_missing_file", "remove_config": True, + "operations": [{"method": "POST", "path": "/api/config", + "body": {"key": "FOV", "value": 90}}]}, + {"name": "t7_config_missing", "operations": [ + {"method": "POST", "path": "/api/t7-config", "body": {"gamertag": "Player"}}]}, + {"name": "t7_config_clear_and_append", "setup": "t7", + "t7_config": 'unknown=keep\n', "operations": [ + {"method": "POST", "path": "/api/t7-config", "body": { + "gamertag": " ^1ColorName ", "colorCode": "^2", + "networkPassword": " ", "friendsOnly": False}}]}, + {"name": "t7_config_invalid_name", "setup": "t7", "operations": [ + {"method": "POST", "path": "/api/t7-config", "body": {"gamertag": " "}}, + {"method": "POST", "path": "/api/t7-config", "body": {"gamertag": "a" * 21}}]}, + {"name": "launch_options_unsupported", "operations": [ + {"method": "POST", "path": "/api/launch-options", "body": {"options": "-bad"}}]}, + {"name": "qol_restore_legacy", "setup": "legacy_backups", "operations": [ + {"method": "POST", "path": "/api/intro-skip", "body": {"enabled": False}}, + {"method": "POST", "path": "/api/d3dcompiler", "body": {"enabled": False}}, + {"method": "POST", "path": "/api/all-intros-skip", "body": {"enabled": False}}]}, + {"name": "qol_toggle_roundtrip", "operations": [ + {"method": "POST", "path": path, "body": {"enabled": enabled}} + for enabled in (True, True, False) + for path in ("/api/intro-skip", "/api/d3dcompiler", "/api/all-intros-skip")]}, + {"name": "exe_untrusted_backup_detection", "setup": "untrusted_backups"}, + {"name": "exe_preserved_enhanced_detection", "setup": "preserved_enhanced"}, + {"name": "status_alternate_executable", "alternate_exe": True}, + {"name": "config_readonly_roundtrip", "operations": [ + {"method": "POST", "path": "/api/config-readonly", "body": {"enabled": True}}, + {"method": "POST", "path": "/api/config", "body": {"key": "FOV", "value": 90}}, + {"method": "POST", "path": "/api/config-readonly", "body": {"enabled": False}}]}, + {"name": "vram_target_roundtrip", "operations": [ + {"method": "POST", "path": "/api/vram-target", "body": {"limited": True, "target": 100}}, + {"method": "POST", "path": "/api/vram-target", "body": {"limited": False, "target": 85}}]}, + ] + for case in cases: + case.setdefault("operations", [{"method": "GET", "path": "/api/status"}]) + return cases + + +def apply_setup(harness: Harness, definition: dict[str, Any]) -> None: + setup = definition.get("setup") + if setup == "t7": + write( + harness.game / "t7patch.conf", + "playername=^1OldName\nnetworkpassword=old\nisfriendsonly=0\nunknown=keep\n", + ) + write(harness.game / "t7patch.dll", b"fixture-t7") + write(harness.game / "t7patchloader.dll", b"fixture-loader") + elif setup == "dxvk": + write(harness.game / "dxgi.dll", b"\x00\xfffixture-dxgi") + write(harness.game / "d3d11.dll", b"fixture-d3d11") + write(harness.game / "dxvk.conf", "dxvk.enableAsync=true\ndxgi.maxFrameLatency=1\n") + elif setup == "enhanced": + for filename in sorted(enhanced.EXPECTED_ENHANCED_FILES): + write(harness.game / filename, b"fixture-enhanced") + write(harness.data / enhanced.STATE_FILENAME, json.dumps({ + "installed": True, "detected_at": "2026-09-10T12:00:00Z", + "acknowledged_at": "2026-09-10T13:00:00Z", + "installed_files": sorted(enhanced.EXPECTED_ENHANCED_FILES)})) + elif setup == "legacy_backups": + for name in ("video/BO3_Global_Logo_LogoSequence.mkv", "video/Frontend_Intro.mkv", "d3dcompiler_46.dll"): + path = harness.game / name + path.rename(str(path) + utils.LEGACY_BACKUP_SUFFIX) + elif setup == "untrusted_backups": + for name in ("BlackOpsIII.exe.patchops.bak", "BlackOpsIII.24784313.bak", "BlackOpsIII.10650222.bak", "BlackOpsIII.enhanced.bak"): + write(harness.game / name, b"not-a-trusted-executable") + elif setup == "preserved_enhanced": + raw = (harness.game / "BlackOpsIII.exe").read_bytes() + settings_path = harness.data / "electron-settings.json" + settings = json.loads(settings_path.read_text()) + settings["enhanced_exe_hashes"] = {str(harness.game): [hashlib.sha256(raw).hexdigest()]} + write(settings_path, json.dumps(settings, indent=2)) + write(harness.game / "BlackOpsIII.enhanced.bak", raw) + if "config" in definition: + write(harness.game / "players/config.ini", definition["config"]) + if definition.get("remove_config"): + (harness.game / "players/config.ini").unlink() + if "t7_config" in definition: + write(harness.game / "t7patch.conf", definition["t7_config"]) + if definition.get("alternate_exe"): + (harness.game / "BlackOpsIII.exe").rename(harness.game / "BlackOps3.exe") + + if "launch_options" in definition: + write( + harness.steam / "userdata" / STEAM_ID / "config" / "localconfig.vdf", + localconfig(definition["launch_options"]), + ) + + +def generate_case(definition: dict[str, Any], scratch: Path) -> dict[str, Any]: + root = scratch / definition["name"] + harness = make_harness( + root, + saved_game=definition.get("saved_game", True), + game_in_library=definition.get("game_in_library", False), + ) + if definition.get("add_game", True): + add_game(harness, exe_profile=definition.get("exe_profile", "unverified")) + (root / "not-a-game").mkdir(exist_ok=True) + apply_setup(harness, definition) + configure_modules(harness) + + fixture = snapshot(root, harness.logical_hashes) + operations = normalize(definition.get("operations", []), root) + runtime_operations = json.loads(json.dumps(operations).replace("$ROOT", str(root))) + if definition.get("internal") == "t7_release_asset_discovery": + responses: list[Any] = [resolve_asset_cases()] + checkpoints = [] + else: + responses = [] + checkpoints = [] + for operation in runtime_operations: + responses.extend(run_endpoint_operations([operation])) + checkpoints.append(snapshot(root, harness.logical_hashes)) + + result: dict[str, Any] = { + "schemaVersion": 1, + "name": definition["name"], + "sourceOfTruth": "Python backend on main", + "runner": "FastAPI TestClient" if CLIENT is not None else "direct endpoint functions", + "sourceHashes": {name: hashlib.sha256((REPO_ROOT / name).read_bytes()).hexdigest() + for name in ("backend/api.py", "utils.py", "t7_patch.py", "dxvk_manager.py", "bo3_enhanced.py")}, + "fixture": fixture, + "operations": operations, + "expected": { + "responses": normalize(responses, root), + "checkpoints": checkpoints, + "filesystem": snapshot(root, harness.logical_hashes), + }, + } + if definition.get("internal"): + result["internalOperation"] = definition["internal"] + if definition.get("rustIgnore"): + result["rustIgnore"] = definition["rustIgnore"] + return result + + +def main() -> int: + # Fixed group-writable creation mask makes new-file modes reproducible and + # detects atomic replacements that fail to preserve a fixture's 0644 mode. + os.umask(0o002) + assert CURRENT_EXE_SHA256 == t7_patch.DEFAULT_STEAM_EXE_SHA256 + assert COMPATIBLE_EXE_SHA256 in api.COMPATIBLE_BUILD_SHA256 + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="fail if checked-in goldens differ") + args = parser.parse_args() + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="patchops-parity-") as temporary: + scratch = Path(temporary) + generated = { + f"{definition['name']}.json": json.dumps( + generate_case(definition, scratch), indent=2, sort_keys=True + ) + + "\n" + for definition in case_definitions() + } + + failures = [] + for filename, contents in generated.items(): + target = OUTPUT_DIR / filename + if args.check: + if not target.exists() or target.read_text(encoding="utf-8") != contents: + failures.append(filename) + else: + target.write_text(contents, encoding="utf-8") + print(f"wrote {target.relative_to(REPO_ROOT)}") + if args.check and failures: + print("stale parity goldens: " + ", ".join(failures), file=sys.stderr) + return 1 + if args.check: + print(f"{len(generated)} parity goldens are deterministic and current") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 88f1c3bb378a235d41d1d2709b7d82b358fb5793 Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:02:51 -0400 Subject: [PATCH 05/17] feat(gpui): custom titlebar, translucent backdrop, progress UI and layout fixes --- native/gpui/assets/caption/close.svg | 3 + native/gpui/assets/caption/maximize.svg | 3 + native/gpui/assets/caption/minimize.svg | 3 + native/gpui/assets/caption/restore.svg | 3 + native/gpui/src/main.rs | 37 +- native/gpui/src/ui.rs | 267 +++++++++--- native/gpui/src/ui/assets.rs | 16 + native/gpui/src/ui/chrome.rs | 540 ++++++++++++++++++++++++ native/gpui/src/ui/components.rs | 16 +- native/gpui/src/ui/dashboard.rs | 4 +- native/gpui/src/ui/enhanced.rs | 9 +- native/gpui/src/ui/exe.rs | 31 +- native/gpui/src/ui/graphics.rs | 2 + native/gpui/src/ui/progress.rs | 459 ++++++++++++++++++++ native/gpui/src/ui/t7.rs | 31 +- native/gpui/src/ui/theme.rs | 120 +++++- native/gpui/src/ui/tools.rs | 69 +-- scripts/smoke_gpui_wayland.py | 162 +++++++ 18 files changed, 1644 insertions(+), 131 deletions(-) create mode 100644 native/gpui/assets/caption/close.svg create mode 100644 native/gpui/assets/caption/maximize.svg create mode 100644 native/gpui/assets/caption/minimize.svg create mode 100644 native/gpui/assets/caption/restore.svg create mode 100644 native/gpui/src/ui/chrome.rs create mode 100644 native/gpui/src/ui/progress.rs create mode 100644 scripts/smoke_gpui_wayland.py diff --git a/native/gpui/assets/caption/close.svg b/native/gpui/assets/caption/close.svg new file mode 100644 index 0000000..fca322d --- /dev/null +++ b/native/gpui/assets/caption/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/caption/maximize.svg b/native/gpui/assets/caption/maximize.svg new file mode 100644 index 0000000..f4631d5 --- /dev/null +++ b/native/gpui/assets/caption/maximize.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/caption/minimize.svg b/native/gpui/assets/caption/minimize.svg new file mode 100644 index 0000000..c90101b --- /dev/null +++ b/native/gpui/assets/caption/minimize.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/assets/caption/restore.svg b/native/gpui/assets/caption/restore.svg new file mode 100644 index 0000000..c4e4b4c --- /dev/null +++ b/native/gpui/assets/caption/restore.svg @@ -0,0 +1,3 @@ + + + diff --git a/native/gpui/src/main.rs b/native/gpui/src/main.rs index d8349cf..f5d62f8 100644 --- a/native/gpui/src/main.rs +++ b/native/gpui/src/main.rs @@ -7,8 +7,27 @@ use gpui_component::{Root, Theme, ThemeMode}; fn main() -> Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); - let service = backend::BackendService::launch()?; - let backend = backend::Backend::start(service.url().to_owned())?; + let backend = backend::Backend::start()?; + // Read-only diagnostic used by archive smoke checks, without opening a window. + if std::env::args().any(|argument| argument == "--smoke-status") { + backend.requests.send(backend::Request { + path: "/api/status".into(), + body: None, + })?; + let reply = backend + .replies + .recv_timeout(std::time::Duration::from_secs(30))?; + if reply.failed { + anyhow::bail!("{}", reply.message); + } + println!( + "{}", + reply + .state + .ok_or_else(|| anyhow::anyhow!("Missing status document"))? + ); + return Ok(()); + } let app = Application::new().with_assets(ui::Assets); app.run(move |cx: &mut App| { gpui_component::init(cx); @@ -36,16 +55,11 @@ fn main() -> Result<()> { }) .detach(); let bounds = Bounds::centered(None, size(px(1150.), px(820.)), cx); + ui::log_session(); cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - window_min_size: Some(size(px(800.), px(600.))), - titlebar: Some(TitlebarOptions { - title: Some("PatchOpsIII".into()), - ..Default::default() - }), - ..Default::default() - }, + // Custom titlebar, client-side decorations and a translucent + // backdrop where the platform supports them; see `ui/chrome.rs`. + ui::window_options(bounds), move |window, cx| { let view = cx.new(|cx| ui::ControlCenter::new(backend, window, cx)); cx.new(|cx| Root::new(view, window, cx)) @@ -54,6 +68,5 @@ fn main() -> Result<()> { .expect("Unable to open the GPUI window"); cx.activate(true); }); - drop(service); Ok(()) } diff --git a/native/gpui/src/ui.rs b/native/gpui/src/ui.rs index 0827f68..2088c87 100644 --- a/native/gpui/src/ui.rs +++ b/native/gpui/src/ui.rs @@ -1,17 +1,22 @@ //! The PatchOpsIII desktop UI. The layout mirrors the Electron renderer //! (`src/renderer/main.tsx` + `styles/app.css`); each page lives in its own module. mod assets; +mod chrome; mod components; mod dashboard; mod enhanced; mod exe; mod graphics; mod modals; +mod progress; mod t7; mod theme; mod tools; pub use assets::Assets; +pub use chrome::{log_session, window_options}; +// The backend's event hook builds these; see `ControlCenter::on_progress`. +pub use progress::{Operation, ProgressEvent}; use crate::backend::{Backend, Reply, Request}; use components::{Btn, Glyph, icon}; @@ -22,13 +27,21 @@ use gpui_component::{ scroll::ScrollableElement, slider::{SliderEvent, SliderState}, }; +use progress::OperationProgress; use serde_json::{Value, json}; use std::{ collections::{BTreeMap, VecDeque}, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; -const POLL_INTERVAL: Duration = Duration::from_secs(5); +/// How often the UI asks the backend for a fresh status document while idle. +/// Actions refresh explicitly (see `request_refresh`), so this is only the +/// safety net for changes made outside the app; raise it freely. +const STATUS_POLL_INTERVAL: Duration = Duration::from_secs(5); +/// How often the Steam depot prompt re-checks whether the depot has arrived. +const DEPOT_POLL_INTERVAL: Duration = Duration::from_secs(5); +/// How often the UI loop drains backend replies and events. +const UI_TICK: Duration = Duration::from_millis(100); const DEPOT_COMMAND_MARKER: &str = "Steam console command: "; #[derive(Clone, Copy, PartialEq, Eq)] @@ -143,6 +156,18 @@ pub struct ControlCenter { content_scroll: ScrollHandle, log_scroll: ScrollHandle, log_count: usize, + /// The long operation in flight (or just finished); see `progress.rs`. + progress: Option, + /// The operation behind `inflight`, when it gets a progress strip. + inflight_op: Option, + /// Log lines streamed through `on_progress` since the last snapshot. + live_logs: Vec, + /// A status refresh is wanted as soon as the backend is idle. + refresh_pending: bool, + /// Titlebar drag in progress; see `chrome::drag_region`. + title_drag: chrome::DragArmed, + /// Whether the first presented frame has been reported; see `render`. + first_frame: bool, } fn dxvk_recommended() -> Value { @@ -253,11 +278,21 @@ impl ControlCenter { cx.subscribe_in(slider, window, |_, _, _: &SliderEvent, _, cx| cx.notify()) .detach(); } + // Developer knob for screenshots: PATCHOPSIII_GPUI_PAGE=t7|exe|enhanced|graphics|dxvk|tools. + let (page, graphics_tab) = match std::env::var("PATCHOPSIII_GPUI_PAGE").as_deref() { + Ok("t7") => (Page::T7, GraphicsTab::Settings), + Ok("exe") => (Page::Exe, GraphicsTab::Settings), + Ok("enhanced") => (Page::Enhanced, GraphicsTab::Settings), + Ok("graphics") => (Page::Graphics, GraphicsTab::Settings), + Ok("dxvk") => (Page::Graphics, GraphicsTab::Dxvk), + Ok("tools") => (Page::Tools, GraphicsTab::Settings), + _ => (Page::Dashboard, GraphicsTab::Settings), + }; let mut view = Self { backend, state: Value::Null, - page: Page::Dashboard, - graphics_tab: GraphicsTab::Settings, + page, + graphics_tab, busy: false, connected: false, message: "Connecting to the local service…".into(), @@ -284,15 +319,32 @@ impl ControlCenter { content_scroll: ScrollHandle::new(), log_scroll: ScrollHandle::new(), log_count: 0, + progress: None, + inflight_op: None, + live_logs: Vec::new(), + refresh_pending: false, + title_drag: Default::default(), + first_frame: false, }; + // Pick the backdrop for the decorations the platform granted, and redo + // it if they change later; repaint when the frame (tiling, maximise) + // changes shape. + // GPUI only forwards `TitlebarOptions::title` on X11/Windows/macOS. + window.set_window_title("PatchOpsIII"); + chrome::settle(window); + cx.observe_window_appearance(window, |_, window, cx| { + chrome::settle(window); + cx.notify(); + }) + .detach(); + cx.observe_window_bounds(window, |_, _, cx| cx.notify()) + .detach(); view.send("/api/status", None, cx); // Drain replies and periodically refresh on GPUI's foreground executor. // The only blocking work lives on the dedicated HTTP worker. cx.spawn_in(window, async move |entity, cx| { loop { - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; + cx.background_executor().timer(UI_TICK).await; if entity .update_in(cx, |view, window, cx| { while let Ok(reply) = view.backend.replies.try_recv() { @@ -313,6 +365,20 @@ impl ControlCenter { fn handle_reply(&mut self, reply: Reply, window: &mut Window, cx: &mut Context) { self.busy = false; let path = std::mem::take(&mut self.inflight); + let (is_status, carried_state) = (reply.is_status, reply.state.is_some()); + // A depot prompt is a question for the user, not a failed operation. + let failed = reply.failed && !reply.message.contains(DEPOT_COMMAND_MARKER); + if let Some(op) = self.inflight_op.take() { + let message = reply.message.lines().next().unwrap_or_default().to_owned(); + self.on_progress( + ProgressEvent::Finished { + op, + ok: !failed, + message, + }, + cx, + ); + } if reply.failed { self.queue.clear(); self.failed = true; @@ -340,15 +406,41 @@ impl ControlCenter { } } self.last_refresh = Instant::now(); + if carried_state { + self.prune_live_logs(); + } + self.refresh_after(is_status, failed, carried_state); + self.follow_log(); + if let Some(next) = self.queue.pop_front() { + self.send(&next.path, next.body, cx); + } + cx.notify(); + } + + /// Keep the Activity Log pinned to its newest line when entries arrive. + fn follow_log(&mut self) { let entries = self.visible_logs().len(); if entries != self.log_count { self.log_count = entries; self.log_scroll.scroll_to_bottom(); } - if let Some(next) = self.queue.pop_front() { - self.send(&next.path, next.body, cx); + } + + /// Refresh-after-action policy, in one place. A successful action normally + /// answers with the new state document, so nothing more is needed. One that + /// answers without it, or fails (the backend may have changed state and + /// logged before failing), asks for a status refresh right away instead of + /// waiting out `STATUS_POLL_INTERVAL`. + fn refresh_after(&mut self, is_status: bool, failed: bool, carried_state: bool) { + if !is_status && (failed || !carried_state) { + self.request_refresh(); } - cx.notify(); + } + + /// Ask for a status refresh as soon as the backend is idle. Backend events + /// that mean "state changed outside a request" can call this too. + pub fn request_refresh(&mut self) { + self.refresh_pending = true; } /// Per-endpoint bookkeeping for a failed request. @@ -400,6 +492,7 @@ impl ControlCenter { }; } "/api/exe-swap/compatible" => self.depot = None, + "/api/logs/clear" => self.live_logs.clear(), "/api/t7-config" => { self.t7_password_touched = false; self.load_t7(window, cx); @@ -410,19 +503,20 @@ impl ControlCenter { /// Background work: status refresh and depot watching. fn poll(&mut self, cx: &mut Context) { + self.expire_progress(cx); if self.busy || self.pending.is_some() { return; } - let watching = self - .depot - .as_ref() - .is_some_and(|depot| depot.watching && depot.last_poll.elapsed() >= POLL_INTERVAL); + let watching = self.depot.as_ref().is_some_and(|depot| { + depot.watching && depot.last_poll.elapsed() >= DEPOT_POLL_INTERVAL + }); if watching { if let Some(depot) = self.depot.as_mut() { depot.last_poll = Instant::now(); } self.send("/api/exe-swap/compatible", Some(json!({})), cx); - } else if self.last_refresh.elapsed() >= POLL_INTERVAL { + } else if self.refresh_pending || self.last_refresh.elapsed() >= STATUS_POLL_INTERVAL { + self.refresh_pending = false; self.send("/api/status", None, cx); } } @@ -439,6 +533,14 @@ impl ControlCenter { self.busy = true; self.inflight = path.to_owned(); self.last_refresh = Instant::now(); + // The depot watcher re-sends the same request every few + // seconds; that is polling, not an operation to announce. + let watching = self.depot.as_ref().is_some_and(|depot| depot.watching); + self.inflight_op = Operation::from_path(path) + .filter(|op| !(watching && *op == Operation::ExeCompatible)); + if let Some(op) = self.inflight_op { + self.on_progress(ProgressEvent::Started(op), cx); + } } Err(_) => { self.failed = true; @@ -736,19 +838,35 @@ impl ControlCenter { } /// Latest log lines, oldest first, without the service start-up notice. + fn log_key(entry: &Value) -> String { + format!( + "{}|{}|{}", + entry["line"], entry["category"], entry["message"] + ) + } + + /// Streamed lines the status snapshot now contains no longer need keeping. + fn prune_live_logs(&mut self) { + let known: std::collections::HashSet = self.state["logs"] + .as_array() + .into_iter() + .flatten() + .map(Self::log_key) + .collect(); + self.live_logs + .retain(|entry| !known.contains(&Self::log_key(entry))); + } + + /// Snapshot entries followed by lines streamed since (`on_progress`). fn visible_logs(&self) -> Vec<(String, String)> { let mut seen = std::collections::HashSet::new(); let mut entries: Vec<(String, String)> = self.state["logs"] .as_array() .into_iter() .flatten() + .chain(&self.live_logs) .filter(|entry| entry["message"] != "PatchOpsIII local API started.") - .filter(|entry| { - seen.insert(format!( - "{}|{}|{}", - entry["line"], entry["category"], entry["message"] - )) - }) + .filter(|entry| seen.insert(Self::log_key(entry))) .map(|entry| { ( entry["category"].as_str().unwrap_or("Info").to_owned(), @@ -773,9 +891,15 @@ impl ControlCenter { } } - /// `.titlebar`: logo, name, version (click to check for updates) and - /// the connection state. - fn header(&self, cx: &mut Context) -> impl IntoElement { + /// `.titlebar`: logo, name, version (click to check for updates), the + /// connection state and the caption buttons. The whole strip is the + /// window's titlebar; everything except the buttons drags the window. + fn header( + &self, + frame: chrome::Frame, + window: &Window, + cx: &mut Context, + ) -> impl IntoElement { let version = self.string("/appVersion"); let version = if version.to_lowercase().starts_with('v') || version.is_empty() { version @@ -783,23 +907,25 @@ impl ControlCenter { format!("v{version}") }; let can_act = self.can_act(); - div() - .flex() - .items_center() - .flex_none() - .h(px(32.)) - .px(px(10.)) - .gap(px(8.)) - .bg(theme::bg_deep()) - .border_b_1() - .border_color(theme::white(0.08)) + let drag = &self.title_drag; + chrome::titlebar(frame) .child( - img("images/logo.png") - .size(px(18.)) + chrome::drag_region("titlebar-brand", drag, frame) + .flex() + .items_center() .flex_none() - .rounded(px(4.)), + .h_full() + .pl(px(10.)) + .pr(px(8.)) + .gap(px(8.)) + .child( + img("images/logo.png") + .size(px(18.)) + .flex_none() + .rounded(px(4.)), + ) + .child(div().text_size(px(theme::FONT_SM)).child("PatchOpsIII")), ) - .child(div().text_size(px(theme::FONT_SM)).child("PatchOpsIII")) .child( div() .id("check-updates") @@ -826,8 +952,19 @@ impl ControlCenter { .child(version) .child(icon(Glyph::Refresh, 12., theme::muted())), ) - .child(div().flex_1()) - .child(self.connection_badge()) + .child( + chrome::drag_region("titlebar-spacer", drag, frame) + .flex_1() + .h_full(), + ) + .child( + chrome::drag_region("titlebar-status", drag, frame) + .flex() + .items_center() + .h_full() + .px(px(8.)) + .child(self.connection_badge()), + ) .child( Btn::new("reload-values", "Reload Values") .tiny() @@ -837,6 +974,7 @@ impl ControlCenter { cx.notify(); })), ) + .child(div().w(px(8.)).h_full().flex_none()) .child( Btn::new("refresh", if self.busy { "Working…" } else { "Refresh" }) .tiny() @@ -847,6 +985,13 @@ impl ControlCenter { view.send("/api/status", None, cx); })), ) + .child( + chrome::drag_region("titlebar-end", drag, frame) + .flex_none() + .h_full() + .w(px(10.)), + ) + .children(chrome::caption_buttons(frame, window)) } fn connection_badge(&self) -> impl IntoElement { @@ -1006,7 +1151,7 @@ impl ControlCenter { } /// `.log-panel`: the last lines of the backend's activity log. - fn log_panel(&self, cx: &mut Context) -> impl IntoElement { + fn log_panel(&self, height: Pixels, cx: &mut Context) -> impl IntoElement { let entries = self.visible_logs(); let status_color = if self.failed { theme::warning() @@ -1017,7 +1162,7 @@ impl ControlCenter { .flex() .flex_col() .flex_none() - .h(px(180.)) + .h(height) .child( div() .flex() @@ -1037,7 +1182,7 @@ impl ControlCenter { .truncate() .text_size(px(theme::FONT_XS)) .text_color(status_color) - .child(self.message.clone()), + .child(self.status_line()), ), ) .child( @@ -1056,7 +1201,7 @@ impl ControlCenter { .border_1() .border_color(theme::white(0.)) .rounded(px(9.)) - .bg(theme::bg_deep().alpha(0.5)) + .bg(theme::log_surface()) .child( div() .id("log-scroll") @@ -1166,8 +1311,15 @@ impl ControlCenter { } impl Render for ControlCenter { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if !self.first_frame { + self.first_frame = true; + chrome::first_frame(window, cx); + } let ready = !self.state.is_null(); + // `.log-panel` is 180px tall with a 140px floor; give the pages the + // difference on short windows so the default 820px layout fits. + let log_height = px((f32::from(window.viewport_size().height) * 0.19).clamp(140., 180.)); let body = if ready { div() .flex() @@ -1206,12 +1358,21 @@ impl Render for ControlCenter { .flex_col() .gap(px(theme::PANEL_GAP)) .pr(px(8.)) - .child(self.page_view(cx)), + // Content must keep its natural height (flex-shrink + // would squash it into the viewport and clip the + // cards instead of scrolling). + .child( + div() + .flex() + .flex_col() + .flex_none() + .child(self.page_view(cx)), + ), ) .vertical_scrollbar(&self.content_scroll), ), ) - .child(self.log_panel(cx)), + .child(self.log_panel(log_height, cx)), ) } else { div() @@ -1221,7 +1382,8 @@ impl Render for ControlCenter { .min_h_0() .child(self.startup_screen(cx)) }; - div() + let frame = chrome::Frame::read(window); + let root = div() .relative() .flex() .flex_col() @@ -1234,7 +1396,7 @@ impl Render for ControlCenter { .font(theme::ui_font()) .text_color(theme::text()) .text_size(px(theme::FONT_MD)) - .child(self.header(cx)) + .child(self.header(frame, window, cx)) .child( div() .flex() @@ -1244,7 +1406,12 @@ impl Render for ControlCenter { .p(px(theme::APP_PAD)) .child(body), ) - .children(self.confirm_modal(cx)) - .children(self.depot_modal(cx)) + .children( + self.confirm_modal(cx) + .map(|modal| frame.round_overlay(modal)), + ) + .children(self.depot_modal(cx).map(|modal| frame.round_overlay(modal))); + // Rounded corners and the 1px outline of a client-decorated window. + frame.frame(root) } } diff --git a/native/gpui/src/ui/assets.rs b/native/gpui/src/ui/assets.rs index 06c70cb..0c8cfff 100644 --- a/native/gpui/src/ui/assets.rs +++ b/native/gpui/src/ui/assets.rs @@ -4,6 +4,22 @@ use gpui::{AssetSource, SharedString}; use std::borrow::Cow; const ASSETS: &[(&str, &[u8])] = &[ + ( + "caption/close.svg", + include_bytes!("../../assets/caption/close.svg"), + ), + ( + "caption/maximize.svg", + include_bytes!("../../assets/caption/maximize.svg"), + ), + ( + "caption/minimize.svg", + include_bytes!("../../assets/caption/minimize.svg"), + ), + ( + "caption/restore.svg", + include_bytes!("../../assets/caption/restore.svg"), + ), ( "icons/check.svg", include_bytes!("../../assets/icons/check.svg"), diff --git a/native/gpui/src/ui/chrome.rs b/native/gpui/src/ui/chrome.rs new file mode 100644 index 0000000..e12388a --- /dev/null +++ b/native/gpui/src/ui/chrome.rs @@ -0,0 +1,540 @@ +//! Window chrome: the custom titlebar, caption buttons, the rounded client-side +//! frame and the choice of translucent backdrop. +//! +//! The Electron app is frameless and draws its own titlebar (`TitleBar` in +//! `src/renderer/main.tsx`, `.titlebar`/`.window-controls` in `app.css`). +//! The native window does the same with the platform's own mechanics: +//! +//! * **Windows**: the strip is the real titlebar. `window_control_area` +//! (Drag/Min/Max/Close) answers `WM_NCHITTEST`, so dragging, double-click, +//! Aero snap, snap layouts on the maximise button and the caption-button +//! clicks are all handled by the OS. +//! * **Linux (Wayland and X11)**: the strip is a client-side titlebar. Moves +//! use `start_window_move`, double-click toggles maximise, right-click opens +//! the compositor's window menu, and the caption buttons call +//! minimize/zoom/remove. Resize edges, cursors and the shadow come from +//! gpui-component's `Root`; this module adds the rounded corners and edge +//! outline, which follow `window.window_decorations()` so they disappear +//! when maximised/tiled and when the compositor insists on server-side +//! decorations (then the caption buttons are hidden too). +//! * **macOS**: traffic lights (`appears_transparent`), no caption buttons. +use super::theme::{self, Backdrop}; +use gpui::{prelude::*, *}; +use std::{cell::Cell, rc::Rc, sync::OnceLock}; + +pub const TITLEBAR_HEIGHT: f32 = 32.; +/// `.window-controls` is three 46px columns. +const CAPTION_WIDTH: f32 = 46.; +/// Corner radius of a client-decorated window. +const FRAME_RADIUS: f32 = 10.; + +/// Set to `1` to force fully opaque surfaces. +const OPAQUE_ENV: &str = "PATCHOPSIII_GPUI_OPAQUE"; +/// Test hook: `opaque`, `translucent` or `blurred` overrides detection. +const BACKDROP_ENV: &str = "PATCHOPSIII_GPUI_BACKDROP"; +/// Test hook: `server` asks for compositor-drawn decorations (the fallback +/// path), `client` is the default on Linux. +const DECORATIONS_ENV: &str = "PATCHOPSIII_GPUI_DECORATIONS"; +/// Test hook: set to `1` to open the window maximised. +const MAXIMIZED_ENV: &str = "PATCHOPSIII_GPUI_MAXIMIZED"; + +/// The windowing system GPUI is running on. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Session { + Windows, + MacOs, + Wayland, + X11, + Other, +} + +impl Session { + pub fn name(self) -> &'static str { + match self { + Self::Windows => "Windows", + Self::MacOs => "macOS", + Self::Wayland => "Wayland", + Self::X11 => "X11", + Self::Other => "headless", + } + } +} + +pub fn session() -> Session { + static SESSION: OnceLock = OnceLock::new(); + *SESSION.get_or_init(|| { + if cfg!(target_os = "windows") { + Session::Windows + } else if cfg!(target_os = "macos") { + Session::MacOs + } else { + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + match gpui::guess_compositor() { + "Wayland" => return Session::Wayland, + "X11" => return Session::X11, + _ => {} + } + Session::Other + } + }) +} + +fn env_flag(name: &str) -> bool { + std::env::var(name).is_ok_and(|value| matches!(value.trim(), "1" | "true" | "yes" | "on")) +} + +/// `XDG_CURRENT_DESKTOP`, lower-cased (e.g. `kde`, `gnome`, `ubuntu:gnome`). +fn desktop() -> String { + std::env::var("XDG_CURRENT_DESKTOP") + .unwrap_or_default() + .to_lowercase() +} + +/// What to expect from the compositor before the window exists. X11's +/// compositor is only known once the window is open; `settle` corrects it. +pub fn preferred_backdrop(session: Session) -> Backdrop { + if env_flag(OPAQUE_ENV) { + return Backdrop::Opaque; + } + match std::env::var(BACKDROP_ENV).as_deref() { + Ok("opaque") => return Backdrop::Opaque, + Ok("translucent") => return Backdrop::Translucent, + Ok("blurred") => return Backdrop::Blurred, + _ => {} + } + match session { + // Acrylic on Windows 10/11, vibrancy on macOS. + Session::Windows | Session::MacOs => Backdrop::Blurred, + Session::Other => Backdrop::Opaque, + Session::Wayland | Session::X11 => { + let desktop = desktop(); + let see_through_is_ugly = ["gnome", "unity", "pantheon", "budgie", "cinnamon"] + .iter() + .any(|name| desktop.contains(name)); + if see_through_is_ugly { + // Mutter and friends composite alpha but never blur it. + Backdrop::Opaque + } else if session == Session::Wayland && desktop.contains("kde") { + // GPUI binds org_kde_kwin_blur for `Blurred` on Wayland. + Backdrop::Blurred + } else { + // Other compositors: alpha only (GPUI has no X11 blur, and + // wlroots-style compositors blur on their own, if at all). + Backdrop::Translucent + } + } + } +} + +fn background_for(backdrop: Backdrop, session: Session, csd: bool) -> WindowBackgroundAppearance { + match backdrop { + Backdrop::Blurred => WindowBackgroundAppearance::Blurred, + Backdrop::Translucent => WindowBackgroundAppearance::Transparent, + // Wayland makes client-decorated surfaces transparent by itself; X11 + // needs the flag for the rounded corners and shadow. + Backdrop::Opaque if session == Session::X11 && csd => { + WindowBackgroundAppearance::Transparent + } + Backdrop::Opaque => WindowBackgroundAppearance::Opaque, + } +} + +/// The options for the main window. +pub fn window_options(bounds: Bounds) -> WindowOptions { + let session = session(); + let backdrop = preferred_backdrop(session); + let linux = matches!(session, Session::Wayland | Session::X11); + WindowOptions { + window_bounds: Some(if env_flag(MAXIMIZED_ENV) { + WindowBounds::Maximized(bounds) + } else { + WindowBounds::Windowed(bounds) + }), + window_min_size: Some(size(px(800.), px(600.))), + titlebar: Some(TitlebarOptions { + title: Some("PatchOpsIII".into()), + // Windows/macOS: no OS titlebar; macOS keeps its traffic lights. + appears_transparent: true, + traffic_light_position: Some(point(px(12.), px(10.))), + }), + // Ask for client-side decorations; the compositor may refuse + // (`settle` reads the outcome) and X11 without a compositor falls + // back to server-side decorations by itself. + window_decorations: linux.then_some( + if std::env::var(DECORATIONS_ENV).is_ok_and(|value| value == "server") { + WindowDecorations::Server + } else { + WindowDecorations::Client + }, + ), + window_background: background_for(backdrop, session, true), + ..Default::default() + } +} + +/// What the window ended up with, as read from the platform. +#[derive(Clone, Copy, Debug)] +pub struct Frame { + /// The app draws the frame (and, on Linux, the caption buttons). + pub csd: bool, + pub tiling: Tiling, + pub maximized: bool, +} + +impl Frame { + pub fn read(window: &Window) -> Self { + let (csd, tiling) = match window.window_decorations() { + Decorations::Client { tiling } => (true, tiling), + Decorations::Server => (false, Tiling::default()), + }; + Self { + csd, + tiling, + maximized: window.is_maximized(), + } + } + + /// Whether the window draws its own caption buttons. + fn caption_buttons(&self) -> bool { + match session() { + Session::Windows => true, + Session::MacOs | Session::Other => false, + Session::Wayland | Session::X11 => self.csd, + } + } + + /// Round the corners that are not flush with a screen edge. + pub fn round(&self, mut element: E, radius: Pixels) -> E { + if !self.csd { + return element; + } + let tiling = self.tiling; + if !(tiling.top || tiling.left) { + element = element.rounded_tl(radius); + } + if !(tiling.top || tiling.right) { + element = element.rounded_tr(radius); + } + if !(tiling.bottom || tiling.left) { + element = element.rounded_bl(radius); + } + if !(tiling.bottom || tiling.right) { + element = element.rounded_br(radius); + } + element + } + + /// Rounded corners plus the 1px outline of a client-decorated window. + pub fn frame(&self, mut element: E) -> E { + if !self.csd { + return element; + } + let tiling = self.tiling; + element = self + .round(element, px(FRAME_RADIUS)) + .border_color(theme::window_edge()); + if !tiling.top { + element = element.border_t_1(); + } + if !tiling.bottom { + element = element.border_b_1(); + } + if !tiling.left { + element = element.border_l_1(); + } + if !tiling.right { + element = element.border_r_1(); + } + element + } + + /// Round a full-window overlay (a modal scrim) like the frame it covers. + pub fn round_overlay(&self, element: E) -> E { + self.round(element, px(FRAME_RADIUS - 1.)) + } +} + +/// Apply the backdrop for the decorations the platform actually granted, and +/// re-apply it when they change (e.g. a compositor answering a +/// client-side-decoration request with server-side). +pub fn settle(window: &mut Window) { + thread_local! { + static APPLIED: Cell> = + const { Cell::new(None) }; + } + let session = session(); + let frame = Frame::read(window); + let mut backdrop = preferred_backdrop(session); + if session == Session::X11 && !frame.csd { + // No compositor: a transparent X11 window would render black. + backdrop = Backdrop::Opaque; + } + let background = background_for(backdrop, session, frame.csd); + if APPLIED.get() == Some((backdrop, background)) { + return; + } + APPLIED.set(Some((backdrop, background))); + theme::set_backdrop(backdrop); + window.set_background_appearance(background); + info(format_args!( + "window frame: {} decorations, {backdrop:?} backdrop ({background:?})", + if frame.csd { + "client-side" + } else { + "server-side" + }, + )); +} + +/// An INFO line on stderr, shown when `RUST_LOG` enables info for this crate +/// (`RUST_LOG=info`, `debug`, `patchopsiii_gpui=info`, ...). The crate has no +/// direct `log` dependency, and the app's default filter is `warn`. +pub fn info(message: std::fmt::Arguments) { + let enabled = std::env::var("RUST_LOG").is_ok_and(|filter| { + filter.split(',').any(|directive| { + let (target, level) = directive.rsplit_once('=').unwrap_or(("", directive)); + matches!(level.trim(), "info" | "debug" | "trace") + && (target.is_empty() || "patchopsiii_gpui".starts_with(target.trim())) + }) + }); + if enabled { + eprintln!("[INFO patchopsiii_gpui::ui::chrome] {message}"); + } +} + +/// One line for the log: which windowing system GPUI picked. +pub fn log_session() { + let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); + info(format_args!( + "compositor: {} (desktop: {}; backdrop preference: {:?})", + session().name(), + if desktop.is_empty() { + "unknown" + } else { + &desktop + }, + preferred_backdrop(session()), + )); +} + +/// Shared between a titlebar's drag regions: set on mouse-down, consumed by +/// the first drag motion (or cleared on release), so a double-click still +/// reaches the maximise toggle. +pub type DragArmed = Rc>; + +fn toggle_maximize(window: &mut Window) { + if cfg!(target_os = "macos") { + window.titlebar_double_click(); + } else { + window.zoom_window(); + } +} + +/// An area of the titlebar that moves the window. +pub fn drag_region(id: impl Into, armed: &DragArmed, frame: Frame) -> Stateful
{ + let region = div().id(id).window_control_area(WindowControlArea::Drag); + if cfg!(target_os = "windows") { + // `WM_NCHITTEST` reports HTCAPTION: the OS owns move/double-click. + return region; + } + let csd = frame.csd; + region + .on_mouse_down(MouseButton::Left, { + let armed = armed.clone(); + move |event, window, _| { + if event.click_count >= 2 { + armed.set(false); + toggle_maximize(window); + } else { + armed.set(true); + } + } + }) + .on_mouse_move({ + let armed = armed.clone(); + move |event, window, _| { + if armed.get() && event.dragging() { + armed.set(false); + window.start_window_move(); + } + } + }) + .on_mouse_up(MouseButton::Left, { + let armed = armed.clone(); + move |_, _, _| armed.set(false) + }) + .on_mouse_up_out(MouseButton::Left, { + let armed = armed.clone(); + move |_, _, _| armed.set(false) + }) + .on_mouse_down(MouseButton::Right, move |event, window, _| { + // The compositor's own window menu (restore/move/resize/close). + if csd { + window.show_window_menu(event.position); + } + }) +} + +/// The 32px strip: `.titlebar`. +pub fn titlebar(frame: Frame) -> Div { + let mut bar = div() + .flex() + .items_center() + .flex_none() + .h(px(TITLEBAR_HEIGHT)) + .bg(theme::titlebar()) + .border_b_1() + .border_color(theme::white(0.08)) + .when(cfg!(target_os = "macos"), |this| this.pl(px(80.))); + if frame.csd { + // The strip sits inside the 1px frame outline. + let radius = px(FRAME_RADIUS - 1.); + let tiling = frame.tiling; + if !(tiling.top || tiling.left) { + bar = bar.rounded_tl(radius); + } + if !(tiling.top || tiling.right) { + bar = bar.rounded_tr(radius); + } + } + bar +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Caption { + Minimize, + Maximize, + Restore, + Close, +} + +impl Caption { + fn path(self) -> &'static str { + match self { + Self::Minimize => "caption/minimize.svg", + Self::Maximize => "caption/maximize.svg", + Self::Restore => "caption/restore.svg", + Self::Close => "caption/close.svg", + } + } + + fn label(self) -> &'static str { + match self { + Self::Minimize => "Minimize window", + Self::Maximize => "Maximize window", + Self::Restore => "Restore window", + Self::Close => "Close window", + } + } + + fn area(self) -> WindowControlArea { + match self { + Self::Minimize => WindowControlArea::Min, + Self::Maximize | Self::Restore => WindowControlArea::Max, + Self::Close => WindowControlArea::Close, + } + } + + fn act(self, window: &mut Window) { + match self { + Self::Minimize => window.minimize_window(), + Self::Maximize | Self::Restore => window.zoom_window(), + Self::Close => window.remove_window(), + } + } +} + +/// `.window-control`: 46x32, a 10px glyph, grey until hovered, red for close. +fn caption_button(kind: Caption, frame: Frame) -> Stateful
{ + let close = kind == Caption::Close; + let (hover_bg, hover_fg): (Hsla, Hsla) = if close { + (rgb(0xc91b17).into(), rgb(0xffffff).into()) + } else { + (theme::white(0.08), theme::text()) + }; + let mut button = div() + .id(kind.label()) + .group("caption") + .window_control_area(kind.area()) + .flex() + .items_center() + .justify_center() + .flex_none() + .w(px(CAPTION_WIDTH)) + .h(px(TITLEBAR_HEIGHT)) + .hover(move |style| style.bg(hover_bg)) + .child( + svg() + .path(kind.path()) + .size(px(10.)) + .flex_none() + .text_color(theme::text().alpha(0.76)) + .group_hover("caption", move |style| style.text_color(hover_fg)), + ); + if close && frame.csd && !(frame.tiling.top || frame.tiling.right) { + // The hover fill must not poke out of the window's rounded corner. + button = button.rounded_tr(px(FRAME_RADIUS - 1.)); + } + if cfg!(target_os = "windows") { + // The OS performs the action for HTMINBUTTON/HTMAXBUTTON/HTCLOSE. + button + } else { + button + .cursor_pointer() + .on_click(move |_, window, _| kind.act(window)) + } +} + +/// `.window-controls`, when this window draws its own. +pub fn caption_buttons(frame: Frame, window: &Window) -> Option
{ + if !frame.caption_buttons() { + return None; + } + let controls = window.window_controls(); + let maximize = if frame.maximized { + Caption::Restore + } else { + Caption::Maximize + }; + Some( + div() + .flex() + .flex_none() + .h_full() + .when(controls.minimize, |this| { + this.child(caption_button(Caption::Minimize, frame)) + }) + .when(controls.maximize, |this| { + this.child(caption_button(maximize, frame)) + }) + .child(caption_button(Caption::Close, frame)), + ) +} + +/// Called from the first `render`. GPUI only draws once the compositor has +/// configured the surface and asked for a frame, so reaching this means the +/// window is mapped; the line is what `scripts/smoke_gpui_wayland.py` waits for. +/// With `PATCHOPSIII_GPUI_SMOKE_EXIT=1` the window then closes itself (a normal +/// window close, so the process exits through the usual path with status 0). +pub fn first_frame(window: &Window, cx: &mut Context) { + let viewport = window.viewport_size(); + info(format_args!( + "first frame: window mapped, {}x{} px viewport, {} decorations", + f32::from(viewport.width) as i32, + f32::from(viewport.height) as i32, + if Frame::read(window).csd { + "client-side" + } else { + "server-side" + }, + )); + if env_flag("PATCHOPSIII_GPUI_SMOKE_EXIT") { + cx.spawn_in(window, async move |_, cx| { + cx.background_executor() + .timer(std::time::Duration::from_secs(1)) + .await; + cx.update(|window, _| window.remove_window()).ok(); + }) + .detach(); + } +} diff --git a/native/gpui/src/ui/components.rs b/native/gpui/src/ui/components.rs index 745dc9c..ce03578 100644 --- a/native/gpui/src/ui/components.rs +++ b/native/gpui/src/ui/components.rs @@ -237,7 +237,19 @@ impl RenderOnce for Btn { .when_some(self.glyph, |this, glyph| { this.child(icon(glyph, if self.compact { 15. } else { 16. }, color)) }) - .child(self.label) + // `white-space: normal` on `.small-button`: when equal-width buttons + // share a narrow row the label wraps instead of being clipped. GPUI + // only wraps text inside a definite width, hence `w_full`; content- + // sized buttons keep a bare label (a `w_full` child there collapses). + .child(if self.grow && self.glyph.is_none() { + div() + .w_full() + .text_center() + .child(self.label) + .into_any_element() + } else { + self.label.into_any_element() + }) .when(!enabled, |this| this.opacity(0.62).cursor_not_allowed()) .when(enabled, |this| { this.cursor_pointer() @@ -460,7 +472,7 @@ pub fn radio_row( .flex() .items_center() .gap(px(10.)) - .min_h(px(32.)) + .min_h(px(30.)) .min_w_0() .border_b_1() .border_color(theme::border()) diff --git a/native/gpui/src/ui/dashboard.rs b/native/gpui/src/ui/dashboard.rs index c9f2e51..fdf39c5 100644 --- a/native/gpui/src/ui/dashboard.rs +++ b/native/gpui/src/ui/dashboard.rs @@ -1,5 +1,6 @@ //! Dashboard: status overview, quality-of-life options and launch options. use super::components::{Btn, check_row, column, columns, hairline, panel, radio_row, status_row}; +use super::progress::Operation; use super::{ControlCenter, theme}; use crate::backend::Request; use gpui::{prelude::*, *}; @@ -248,7 +249,8 @@ impl ControlCenter { json!({"options": selected_option, "preserve_fs_game": false}), )), ), - ), + ) + .children(self.progress_strip(&[Operation::WorkshopInstall])), ) } diff --git a/native/gpui/src/ui/enhanced.rs b/native/gpui/src/ui/enhanced.rs index 8eda52a..65be55d 100644 --- a/native/gpui/src/ui/enhanced.rs +++ b/native/gpui/src/ui/enhanced.rs @@ -2,6 +2,7 @@ use super::components::{ Btn, Glyph, bare_panel, card, card_title, column, columns, field_label, icon, metric, }; +use super::progress::Operation; use super::{ControlCenter, Validation, format_timestamp, theme}; use gpui::{prelude::*, *}; use serde_json::json; @@ -103,7 +104,7 @@ impl ControlCenter { .justify_center() .gap(px(4.)) .flex_1() - .min_w(px(220.)) + .min_w(px(260.)) .min_h(px(120.)) .p(px(14.)) .border_1() @@ -141,7 +142,7 @@ impl ControlCenter { .flex_col() .gap(px(10.)) .flex_1() - .min_w(px(240.)) + .min_w(px(300.)) .child( div() .flex() @@ -212,6 +213,9 @@ impl ControlCenter { }); card() .child(card_title(Some(Glyph::Download), "Install Source")) + .children( + self.progress_strip(&[Operation::EnhancedValidate, Operation::EnhancedInstall]), + ) .child( div() .flex() @@ -364,6 +368,7 @@ impl ControlCenter { )) .child("Danger Zone"), ) + .children(self.progress_strip(&[Operation::EnhancedUninstall])) .when(open, |this| { this.child( div().flex().justify_end().child( diff --git a/native/gpui/src/ui/exe.rs b/native/gpui/src/ui/exe.rs index b140bc4..b7de306 100644 --- a/native/gpui/src/ui/exe.rs +++ b/native/gpui/src/ui/exe.rs @@ -1,5 +1,6 @@ //! EXE Swapper: the three selectable executables and their trade-offs. use super::components::{Btn, Glyph, bare_panel, columns, icon, status_pill, text_xs}; +use super::progress::Operation; use super::{ControlCenter, theme}; use gpui::{prelude::*, *}; use serde_json::json; @@ -17,26 +18,21 @@ fn verdict_line(verdict: Verdict, topic: &str, detail: &str) -> Div { Verdict::Warn => (Glyph::Alert, theme::caution()), Verdict::Con => (Glyph::Close, theme::accent()), }; + // One run of text with a bold lead-in so long details wrap inside the card. + let lead = format!("{topic}:"); + let bold = HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + }; + let text = StyledText::new(format!("{lead} {detail}")).with_highlights([(0..lead.len(), bold)]); div() .flex() - .items_center() + .items_start() .gap(px(8.)) .min_w_0() .text_size(px(theme::FONT_SM)) - .child(icon(glyph, 15., color)) - .child( - div().flex_1().min_w_0().child( - div() - .flex() - .gap(px(4.)) - .child( - div() - .font_weight(FontWeight::BOLD) - .child(format!("{topic}:")), - ) - .child(detail.to_owned()), - ), - ) + .child(div().pt(px(2.)).child(icon(glyph, 15., color))) + .child(div().flex_1().min_w_0().child(text)) } struct ExeOption<'a> { @@ -327,6 +323,11 @@ impl ControlCenter { .flex_col() .gap(px(theme::PANEL_GAP)) .child(hero) + .children(self.progress_strip(&[ + Operation::ExeCompatible, + Operation::ExeCurrent, + Operation::ExeEnhanced, + ])) .child(options) .child(details), ) diff --git a/native/gpui/src/ui/graphics.rs b/native/gpui/src/ui/graphics.rs index 81167bf..86cc699 100644 --- a/native/gpui/src/ui/graphics.rs +++ b/native/gpui/src/ui/graphics.rs @@ -4,6 +4,7 @@ use super::components::{ Btn, Glyph, bare_panel, chip, column, columns, handler, icon, inline_pill, segmented, switch, text_xs, }; +use super::progress::Operation; use super::{ControlCenter, GraphicsTab, dxvk_recommended, theme}; use gpui::{prelude::*, *}; use gpui_component::slider::Slider; @@ -776,6 +777,7 @@ impl ControlCenter { .flex_col() .gap(px(theme::PANEL_GAP)) .child(control_bar) + .children(self.progress_strip(&[Operation::DxvkInstall, Operation::DxvkUninstall])) .child( columns() .child(quick_row("Preset", presets)) diff --git a/native/gpui/src/ui/progress.rs b/native/gpui/src/ui/progress.rs new file mode 100644 index 0000000..60577ef --- /dev/null +++ b/native/gpui/src/ui/progress.rs @@ -0,0 +1,459 @@ +//! Long-running operation feedback: the model behind the progress strips on +//! each page and the entry point the backend's event hook feeds. +//! +//! PR #40 (`migrate/tauri-in-process-rust`) has exactly one backend event, +//! `patchops-log` carrying `{category, message, line}`, and a frontend-only +//! `busy` id per command (`t7-install`, `dxvk-install`, `enhanced-install`, +//! `workshop-install`, ...). It renders no progress bars: buttons are disabled +//! while `busy` is set and log lines are appended as they stream in. This +//! module keeps that contract (`ProgressEvent::Log`, [`Operation::id`]) and +//! adds an optional stage/fraction channel for when the core can report one. +use super::components::{Glyph, icon}; +use super::{ControlCenter, theme}; +use gpui::{prelude::*, *}; +use serde_json::json; +use std::time::{Duration, Instant}; + +/// How long a finished operation stays visible before its strip disappears. +const OUTCOME_LINGER: Duration = Duration::from_secs(4); +/// Live log lines kept between status snapshots. +const LIVE_LOG_LIMIT: usize = 300; + +/// A long-running operation. [`Operation::id`] matches PR #40's `busy` ids. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Operation { + T7Install, + T7Uninstall, + DxvkInstall, + DxvkUninstall, + EnhancedValidate, + EnhancedInstall, + EnhancedUninstall, + WorkshopInstall, + ExeCompatible, + ExeCurrent, + ExeEnhanced, + UpdateCheck, + ResetStock, + ClearModFiles, +} + +impl Operation { + /// The operation a request path starts, if it is worth a progress strip. + pub fn from_path(path: &str) -> Option { + Some(match path { + "/api/t7-install" => Self::T7Install, + "/api/t7-uninstall" => Self::T7Uninstall, + "/api/dxvk-install" => Self::DxvkInstall, + "/api/dxvk-uninstall" => Self::DxvkUninstall, + "/api/enhanced-validate" => Self::EnhancedValidate, + "/api/enhanced-install" => Self::EnhancedInstall, + "/api/enhanced-uninstall" => Self::EnhancedUninstall, + "/api/workshop-install" => Self::WorkshopInstall, + "/api/exe-swap/compatible" => Self::ExeCompatible, + "/api/exe-swap/current" => Self::ExeCurrent, + "/api/exe-swap/enhanced" => Self::ExeEnhanced, + "/api/update-check" => Self::UpdateCheck, + "/api/reset-stock" => Self::ResetStock, + "/api/mod-files/clear" => Self::ClearModFiles, + _ => return None, + }) + } + + /// The frontend `busy` id used by PR #40. + #[allow(dead_code)] + pub fn id(self) -> &'static str { + match self { + Self::T7Install => "t7-install", + Self::T7Uninstall => "t7-uninstall", + Self::DxvkInstall => "dxvk-install", + Self::DxvkUninstall => "dxvk-uninstall", + Self::EnhancedValidate => "enhanced-validate", + Self::EnhancedInstall => "enhanced-install", + Self::EnhancedUninstall => "enhanced-uninstall", + Self::WorkshopInstall => "workshop-install", + Self::ExeCompatible => "exe-compatible", + Self::ExeCurrent => "exe-current", + Self::ExeEnhanced => "exe-enhanced", + Self::UpdateCheck => "update-check", + Self::ResetStock => "reset-stock", + Self::ClearModFiles => "clear-mod-files", + } + } + + /// Inverse of [`Operation::id`], for events that carry the `busy` id. + #[allow(dead_code)] + pub fn from_id(id: &str) -> Option { + const ALL: [Operation; 14] = [ + Operation::T7Install, + Operation::T7Uninstall, + Operation::DxvkInstall, + Operation::DxvkUninstall, + Operation::EnhancedValidate, + Operation::EnhancedInstall, + Operation::EnhancedUninstall, + Operation::WorkshopInstall, + Operation::ExeCompatible, + Operation::ExeCurrent, + Operation::ExeEnhanced, + Operation::UpdateCheck, + Operation::ResetStock, + Operation::ClearModFiles, + ]; + ALL.into_iter().find(|op| op.id() == id) + } + + fn title(self) -> &'static str { + match self { + Self::T7Install => "Installing T7 Patch", + Self::T7Uninstall => "Uninstalling T7 Patch", + Self::DxvkInstall => "Installing DXVK-GPLAsync", + Self::DxvkUninstall => "Uninstalling DXVK-GPLAsync", + Self::EnhancedValidate => "Validating dump source", + Self::EnhancedInstall => "Installing BO3 Enhanced", + Self::EnhancedUninstall => "Uninstalling BO3 Enhanced", + Self::WorkshopInstall => "Installing workshop mod", + Self::ExeCompatible => "Switching to the Compatible build", + Self::ExeCurrent => "Switching to the Latest build", + Self::ExeEnhanced => "Switching to BO3 Enhanced", + Self::UpdateCheck => "Checking for updates", + Self::ResetStock => "Resetting to stock", + Self::ClearModFiles => "Clearing mod files", + } + } + + /// The stage a streamed log line implies, matching the messages PR #40's + /// core logs ("Downloading DXVK-GPLAsync...", "Fetching latest BO3 + /// Enhanced release...", "Installing BO3 Enhanced files..."). + fn stage_for_log(message: &str) -> Option<&'static str> { + let lower = message.to_lowercase(); + [ + ("download", "Downloading"), + ("fetching", "Fetching release"), + ("extract", "Extracting"), + ("verif", "Verifying"), + ("validat", "Validating"), + ("back", "Backing up"), + ("install", "Installing"), + ("remov", "Removing files"), + ("restor", "Restoring"), + ] + .into_iter() + .find(|(needle, _)| lower.contains(needle)) + .map(|(_, stage)| stage) + } +} + +/// What the UI shows for the operation in flight (or just finished). +pub struct OperationProgress { + pub op: Operation, + /// Current stage, e.g. "Downloading". Empty until the first stage is known. + pub stage: String, + /// `Some(0.0..=1.0)` draws a determinate bar, `None` an indeterminate one. + pub fraction: Option, + /// Latest log line while running; the outcome text once finished. + pub message: String, + /// `None` while running, `Some(success)` once finished. + pub outcome: Option, + started: Instant, + finished: Option, +} + +impl OperationProgress { + fn new(op: Operation) -> Self { + Self { + op, + stage: String::new(), + fraction: None, + message: String::new(), + outcome: None, + started: Instant::now(), + finished: None, + } + } + + fn running(&self) -> bool { + self.outcome.is_none() + } + + /// The one-line summary used by the strip and the Activity Log header. + pub fn headline(&self) -> String { + match self.outcome { + Some(true) => "Done".to_owned(), + Some(false) => "Failed".to_owned(), + None if self.stage.is_empty() => format!("{}…", self.op.title()), + None => format!("{} · {}…", self.op.title(), self.stage), + } + } +} + +/// An event from the backend (or the UI itself) about a running operation. +#[derive(Clone, Debug)] +pub enum ProgressEvent { + /// An operation began. The UI raises this itself from `send`. + Started(Operation), + /// A new stage and, when the core can measure it, how far along it is. + #[allow(dead_code)] + Stage { + op: Operation, + stage: String, + fraction: Option, + }, + /// One activity-log line, i.e. PR #40's `patchops-log` payload. + Log { + category: String, + message: String, + /// The timestamped line (`"
Dashboard
Dashboard