feat(game): broadcast game presence (GOSSIP_PROTO 3) + per-game background
Steps 5-6 of game detection. BREAKING wire change — bump everyone. Wire (step 5): - PeerState.game: Option<String> (display label only — never appid/source). - SelfPresence.game + to_state carry it (single self-state builder). - GOSSIP_PROTO 2->3, GOSSIP_SIG_DOMAIN v3, version comment bumped together; Cargo MINOR 0.3.0 -> 0.4.0 per VERSIONING.md. v2/v3 isolate into different topics + signature domains, so a coordinated redeploy is required (same as the W4 avatar bump). - Gossip ingest sanitizes incoming game via sanitize_game_label (bidi/ control strip, 64-char/256-byte cap); empty -> None. - Bonus security fix (Codex find): reject inbound gossip frames over a 128KB cap BEFORE serde_json::from_slice — a legit Announce with a full 48KB avatar is ~49KB, so this bounds allocation abuse with headroom. Core wiring: - Spawns the detector at startup; consumes its watch channel in the main select. Detection runs continuously (for the local background); the broadcast is gated by game_presence_enabled (opt-in, default OFF). New commands: SetGamePresenceEnabled (immediate publish/clear, D8), SetGameOverride, SetGameProcessMap. New event: GameChanged. - game_presence_label sanitizes the outgoing label too. Background switch (step 6): - GUI handles GameChanged: stores current_game, swaps background to the per-game override (config.game_backgrounds[id]) or falls back to the W16 default; reuses the existing cached-handle path (no redraw flicker). 397 lib tests (all green), clippy --all-targets clean, full binary builds. Remaining: step 7 UI (opt-in toggle, roster 'Playing' text, manual override control, Settings game-backgrounds + process-map editors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -381,6 +381,11 @@ pub struct AppState {
|
||||
/// doesn't read the file from disk on every redraw. Loaded on startup and
|
||||
/// refreshed when the background is changed/removed. `None` = no custom bg.
|
||||
background_image: Option<bytes::Bytes>,
|
||||
/// The locally-detected running game (game detection), as reported by core.
|
||||
/// Drives the per-game background (W18) and a local "Playing …" indicator.
|
||||
/// `None` = nothing detected. Independent of whether we broadcast it to peers
|
||||
/// (that's `config.game_presence_enabled`).
|
||||
current_game: Option<crate::game::DetectedGame>,
|
||||
peers: HashMap<EndpointId, PeerState>,
|
||||
audio_levels: HashMap<EndpointId, f32>,
|
||||
/// Peers we've locally muted (their audio isn't mixed into our output).
|
||||
@@ -565,6 +570,7 @@ impl Default for AppState {
|
||||
selected_output,
|
||||
config,
|
||||
background_image,
|
||||
current_game: None,
|
||||
peers: HashMap::new(),
|
||||
audio_levels: HashMap::new(),
|
||||
locally_muted: HashSet::new(),
|
||||
@@ -624,6 +630,23 @@ fn load_background_bytes(config: &AppConfig) -> Option<bytes::Bytes> {
|
||||
std::fs::read(path).ok().map(bytes::Bytes::from)
|
||||
}
|
||||
|
||||
/// The effective background for the current state: the per-game override (W18) when
|
||||
/// the running `game` has a mapping in `config.game_backgrounds`, otherwise the
|
||||
/// single custom background (W16). A mapped-but-missing/unreadable per-game file
|
||||
/// falls back to the default WITHOUT forgetting the mapping (the file may return).
|
||||
fn effective_background_bytes(
|
||||
config: &AppConfig,
|
||||
game: Option<&crate::game::DetectedGame>,
|
||||
) -> Option<bytes::Bytes> {
|
||||
if let Some(g) = game
|
||||
&& let Some(path) = config.game_backgrounds.get(&g.id)
|
||||
&& let Ok(bytes) = std::fs::read(path)
|
||||
{
|
||||
return Some(bytes::Bytes::from(bytes));
|
||||
}
|
||||
load_background_bytes(config)
|
||||
}
|
||||
|
||||
pub fn run_gui() -> iced::Result {
|
||||
// Restore the last window size (saved on close). Position is restored too,
|
||||
// but only on X11 — Wayland's xdg-shell gives clients no way to set their own
|
||||
@@ -1144,6 +1167,15 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
format!("Presence mode stayed {mode}")
|
||||
};
|
||||
}
|
||||
UiEvent::GameChanged(detected) => {
|
||||
// The locally-detected game changed: switch the per-game
|
||||
// background (W18) if one is mapped, else fall back to the
|
||||
// default. Presence broadcasting is handled in core, gated by
|
||||
// the opt-in toggle; this is purely local presentation.
|
||||
state.current_game = detected;
|
||||
state.background_image =
|
||||
effective_background_bytes(&state.config, state.current_game.as_ref());
|
||||
}
|
||||
UiEvent::ShutdownComplete => {
|
||||
if state.closing {
|
||||
return iced::exit();
|
||||
|
||||
Reference in New Issue
Block a user