Files
lanspread/crates/lanspread-peer
ddidderr 09709cc008 feat(peer): stamp launcher settings on first play, add PersonaName rewrite
Some games ship a SmartSteamEmu.ini somewhere under their installed
local/ tree with a `PersonaName = ...` line that must carry the player's
configured username. They also ship account_name.txt and language.txt
files that the launcher already overwrote with the username/language.

Previously that account_name.txt/language.txt overwrite happened inside
the install transaction, so it only applied to freshly (re)installed
games — games already installed by an older build never got fixed up,
and the SmartSteamEmu.ini PersonaName line was not handled at all.

This moves all per-user setting application out of install and into a
single one-shot step performed the first time a game is played, gated by
a new per-game marker `games/<id>/launch_settings_applied` under the
state dir. On first play we search the whole local/ tree and stamp:

  - the username into the first account_name.txt,
  - the language into the first language.txt,
  - the username into the first SmartSteamEmu.ini PersonaName line,
    preserving that line's existing line ending (\n or \r\n) and its
    surrounding whitespace, leaving sibling lines untouched.

The marker only records that we *tried*: it is written unconditionally
after the first play, so a game with none of these files is still marked
done and never rescanned. Because already-installed games have no marker
yet, they are fixed up on their next play rather than only on reinstall.

To keep the marker honest across version changes, the install and update
transactions now clear it on success, so a freshly extracted local/ is
re-stamped on the next play.

Behavior changes from the user's perspective:
  - The first time you press Play after this change, your username/
    language are (re)applied to an existing install, including games you
    installed before this feature existed.
  - SmartSteamEmu.ini's PersonaName now reflects the launcher username.

Plumbing: account_name/language are removed from PeerCommand::InstallGame
/DownloadGameFiles[WithOptions] and the whole install handler chain, and
the Tauri pending_install_settings bookkeeping is gone — the launcher now
computes the values at play time in run_game and calls
lanspread_peer::apply_launch_settings_once. The headless harness gains a
`play` command exposing the same step for scripted testing.

Test Plan
  - just test: new lanspread_peer::launch_settings unit tests cover the
    PersonaName rewrite, \n/\r\n preservation, first-match search, the
    unconditional marker, and the no-op-once-applied path; a transaction
    test covers the install marker reset. Whole workspace is green.
  - just clippy clean; the change adds no new clippy warnings (incl.
    --tests).
  - S38 (new in PEER_CLI_SCENARIOS.md): host run of lanspread-peer-cli
    against the new fixture-persona/css RAR .eti (with --unrar) installs
    css, then `play css` stamps the deeply-buried CRLF PersonaName line,
    account_name.txt, and language.txt and creates the marker; a second
    `play` is a no-op even after the values are reset externally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 06:51:59 +02:00
..

lanspread-peer

lanspread-peer is the networking runtime that lets Lanspread nodes find each other on the local network, exchange library metadata, and transfer game files. It is designed to run headless other crates (most notably lanspread-tauri-deno-ts) embed it and drive it through a channel-based API.

Runtime Overview

  • start_peer(game_dir, tx_events, peer_game_db, unpacker, catalog) boots the asynchronous runtime in the background and returns a PeerRuntimeHandle whose sender controls the peer. The injected Unpacker keeps archive extraction out of the peer crate's platform layer, and the catalog set gates which local game roots are announced or served.
  • PeerCommand represents the small control surface exposed to the UI layer: ListGames, GetGame, FetchLatestFromPeers, DownloadGameFiles, InstallGame, UninstallGame, RemoveDownloadedGame, CancelDownload, SetGameDir, and GetPeerCount.
  • PeerEvent enumerates everything the peer runtime reports back to the UI: library snapshots, download/install/uninstall lifecycle updates, runtime failures, and peer membership changes.
  • PeerGameDB collects remote peer metadata. It aggregates discovered peers Game definitions, tracks the latest ETI version per title, and keeps the last seen list of GameFileDescription entries for each peer.

Internally the peer runtime owns four long-lived tasks that run for the lifetime of the process:

  1. Server component (run_server_component) listens for QUIC connections, advertises via mDNS, and serves Request::ListGames, Request::GetGame, Request::GetGameFileData, and Request::GetGameFileChunk by reading from the local game directory.
  2. Discovery loop (run_peer_discovery) uses the lanspread-mdns helper to discover other peers. The blocking mDNS work is executed on a dedicated thread via tokio::task::spawn_blocking so that the Tokio runtime remains responsive.
  3. Ping service (run_ping_service) periodically issues QUIC ping requests to keep peer liveness up to date and prunes stale entries from PeerGameDB.
  4. Local game monitor (run_local_game_monitor) watches the configured game directory and each game root non-recursively, gates per-ID rescans while operations are active, emits local-library changes separately from active operation snapshots, and runs a 300-second fallback scan for missed events.

scan_local_library maintains a lightweight on-disk index and produces both a GameDB and protocol summaries. A game is downloaded only when its root-level version.ini sentinel exists; local/ being a directory is the install signal.

Networking and File Transfer

  • Transport is handled by s2n-quic; TLS cert/key material is compiled in from the repository root.
  • Protocol messages are JSON-encoded structures defined in lanspread-proto::{Request, Response}.
  • File transfers stream raw bytes over dedicated bidirectional QUIC streams. peer::send_game_file_data sends entire files, while peer::send_game_file_chunk services ranged requests.

Download Pipeline

When the UI asks to download a game:

  1. The UI first issues PeerCommand::GetGame for a new download, or PeerCommand::FetchLatestFromPeers for an update that must bypass local archives. The selected peers are queried via request_game_details_from_peer, and their file manifests are merged inside PeerGameDB.
  2. Once the UI receives PeerEvent::GotGameFiles, it forwards the selected file list back with PeerCommand::DownloadGameFiles.
  3. download_game_files starts a version-sentinel transaction, parks any old version.ini as .version.ini.discarded, prepares non-sentinel files, emits PeerEvent::DownloadGameFilesBegin, and builds a per-peer plan (build_peer_plans) that round-robins file chunks across the available peers that advertise the latest version.
  4. Each plan is executed in its own task (download_from_peer). Chunk requests use per-chunk QUIC streams and write into pre-created files. The chunk writer keeps existing data intact and only truncates when we intentionally fall back to a full file transfer, which prevents corruption when multiple peers fill different regions of the same file.
  5. DownloadProgressTracker samples byte counters, transfer speed, and the number of unique peers that are actively streaming chunks. The Tauri UI sees those values together through the regular download-progress event.
  6. version.ini chunks are buffered in memory and committed last via .version.ini.tmp followed by an atomic rename. Failures are accumulated and retried (up to MAX_RETRY_COUNT) via retry_failed_chunks; failed downloads sweep .version.ini.tmp and .version.ini.discarded without restoring the previous sentinel. Cancelled downloads also discard the peer-owned download payload while preserving local/ and install transaction metadata.
  7. After a successful sentinel commit, PeerEvent::DownloadGameFilesFinished is emitted and the peer auto-runs the install transaction.

PeerCommand::CancelDownload cancels the tracked download token for an active transfer. The transfer task remains responsible for clearing active_operations, discarding partial payload files, and refreshing the settled local snapshot, so the UI continues to treat active-operation snapshots as the single source of truth for whether a download is still running.

Install Transactions

Install, update, uninstall, downloaded-file removal, and startup recovery live under src/install/. Install-side operation intent is stored atomically under the configured peer state directory, at games/<game_id>/install_intent.json. Game roots still use Lanspread-owned .local.installing/ and .local.backup/ directories marked by .lanspread_owned. Startup recovery combines the recorded intent with the observed filesystem state and only deletes reserved directories when intent or marker ownership proves they belong to Lanspread. Downloaded-file removal is deliberately separate from uninstall: it only accepts catalog IDs that are direct children of the configured game directory, refuses installed or in-flight roots, and deletes the whole game root only after finding a regular root-level version.ini sentinel.

Legacy launcher-owned files in game directories are migrated by a dedicated pre-start phase. Normal install, recovery, scan, and transfer paths use only the configured state directory for launcher-owned metadata.

Integration with lanspread-tauri-deno-ts

The Tauri application embeds this crate in crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:

  • LanSpreadState holds onto the peer control channel, the latest aggregated GameDB, per-game operation state, the catalog set, and the user-selected game directory.
  • The Tauri commands (request_games, install_game, update_game, remove_downloaded_game, and update_game_directory) translate UI actions into PeerCommands. In particular, update_game_directory validates the filesystem path before storing it, loads the bundled catalog on first use, kicks off the peer runtime on demand, and mirrors the installed/uninstalled state into the UI-facing database.
  • A background task consumes PeerEvents and fans them out to the front-end via Tauri publish/subscribe events (games-list-updated, game-download-*, game-install-*, game-uninstall-*, peer-*). The Tauri crate now only provides the unrar sidecar through the injected Unpacker; rollback and cleanup live in the peer transaction code.

Security & Operational Notes

  • All QUIC connections are TLS encrypted; the shipped certificates are suitable for local-network trust but should be rotated for production deployments.
  • Peer discovery is restricted to the local link via mDNS.
  • Long-running blocking mDNS calls are isolated on dedicated threads which keeps the async runtime responsive even when discovery takes a long time.
  • File writes are chunk-safe: partial chunk downloads open files without truncating existing data, and root-level version.ini is written only after the rest of the download has succeeded.

Known Limitations

  • PeerGameDB currently models the latest metadata that other peers advertise. If the UI needs to surface titles that only exist locally, additional merging with the locally scanned GameDB will be required.
  • The download planner uses a simple round-robin and does not yet take per-peer throughput or failures into account when distributing work.

Refer to the source (particularly src/lib.rs) for the exact message shapes and state machines.