diff --git a/.gitignore b/.gitignore index 0adb1fb12..ae1470453 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ server/target/ tooling/content-converter/target/ tooling/line-previewer/target/ +tooling/test-client/target/ content-ron/ # Godot client (further ignores managed by client team) diff --git a/CHANGELOG.md b/CHANGELOG.md index 075a3aefa..f7be43e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- Test client binary scaffolding (#480) — standalone crate at `tooling/test-client/` with CLI (--connect, --replay, --text, --json, --quiet, --golden, --ticks), exit codes (0/1/2), golden file JSON diff, JSONL replay loader +- Snapshot text renderer (#481) — `format_snapshot_text(&ObserverSnapshot)` pub-exported from server crate, entity labels as kind:entity_id sorted by distance, room name stub, 10 unit tests - Sprint 9 (Gauntlet) briefing files — server, client, CI, audio, joint — 23 tickets across 4 teams - -### Added - Weapon aim lock audio (`sfx_weapon_aim_lock.ogg`) — clinical targeting confirmation tone for weapon aim state (#440) - Stance change audio (`sfx_stance_change.ogg`) — subtle mechanical click for stance toggle feedback (#440) - `make pre-pr` target — full pre-PR verification chain: lint → build → test → content validation → fixture staleness (#460, #465) diff --git a/server/src/bridge/mod.rs b/server/src/bridge/mod.rs index 5c5d93478..21d8aa8a5 100644 --- a/server/src/bridge/mod.rs +++ b/server/src/bridge/mod.rs @@ -9,6 +9,7 @@ use bevy_ecs::schedule::IntoScheduleConfigs; pub mod framing; pub mod local; pub mod tcp; +pub mod text_renderer; pub mod types; pub use types::*; diff --git a/server/src/bridge/tcp.rs b/server/src/bridge/tcp.rs index 2a1bb56dd..d4ddf2a88 100644 --- a/server/src/bridge/tcp.rs +++ b/server/src/bridge/tcp.rs @@ -46,9 +46,9 @@ impl TcpBridge { // Set non-blocking so receive_inputs doesn't stall the game loop. // read_framed handles WouldBlock by returning Ok(None). - stream.set_nonblocking(true).map_err(|e| { - BridgeError::Transport(format!("failed to set non-blocking: {}", e)) - })?; + stream + .set_nonblocking(true) + .map_err(|e| BridgeError::Transport(format!("failed to set non-blocking: {}", e)))?; // Clone stream for reader and writer let reader_stream = stream.try_clone().map_err(|e| { @@ -81,9 +81,9 @@ impl TcpBridge { local_addr ); - stream.set_nonblocking(true).map_err(|e| { - BridgeError::Transport(format!("failed to set non-blocking: {}", e)) - })?; + stream + .set_nonblocking(true) + .map_err(|e| BridgeError::Transport(format!("failed to set non-blocking: {}", e)))?; let reader_stream = stream.try_clone().map_err(|e| { BridgeError::Transport(format!("failed to clone stream for reader: {}", e)) diff --git a/server/src/bridge/text_renderer.rs b/server/src/bridge/text_renderer.rs new file mode 100644 index 000000000..d80d4bcd5 --- /dev/null +++ b/server/src/bridge/text_renderer.rs @@ -0,0 +1,411 @@ +// Text renderer for ObserverSnapshot — structured human-readable output. +// Library code callable by test-client crate and server integration tests. +// The server binary never references this module. + +use std::fmt::Write; + +use crate::bridge::types::*; +use crate::knowledge::types::{EntityVisibility, RelationshipState}; + +/// Format an ObserverSnapshot as structured text for human verification. +/// +/// Output format matches the test-client --text specification: +/// header, game time, room, entities (sorted by distance), pending +/// recognitions, tiles, interactions, inventory, monologue, dialogue. +pub fn format_snapshot_text(snapshot: &ObserverSnapshot) -> String { + let mut out = String::with_capacity(2048); + + // Find player entity for position reference + let player = snapshot + .entities + .iter() + .find(|e| matches!(e.kind, EntityKind::Player)); + let (px, py) = player.map(|p| (p.x as i32, p.y as i32)).unwrap_or((-1, -1)); + + // Header + writeln!( + out, + "=== Tick {} | Player ({},{}) facing {:?} | Stance: {:?} | TickRate: {:?} ===", + snapshot.tick, + px, + py, + snapshot.player_facing, + snapshot.player_stance, + snapshot.game_time.tick_rate, + ) + .ok(); + + // Game time + let hours = (snapshot.game_time.time_of_day / 60) % 24; + let minutes = snapshot.game_time.time_of_day % 60; + writeln!( + out, + "Game time: Day {}, {:02}:{:02} ({:?})", + snapshot.game_time.day, hours, minutes, snapshot.game_time.day_phase, + ) + .ok(); + + // Room name — stub until Gauntlet room constants are implemented (Sprint 9) + writeln!(out, "Room: (unknown)").ok(); + + // Non-player entities sorted by distance then entity_id + let mut entities: Vec<&VisibleEntity> = snapshot + .entities + .iter() + .filter(|e| !matches!(e.kind, EntityKind::Player)) + .collect(); + entities.sort_by_key(|e| { + let dist = (e.x as i32 - px).unsigned_abs() + (e.y as i32 - py).unsigned_abs(); + (dist, e.entity_id) + }); + + if !entities.is_empty() { + writeln!(out, "Entities ({}):", entities.len()).ok(); + for e in &entities { + let dist = (e.x as i32 - px).unsigned_abs() + (e.y as i32 - py).unsigned_abs(); + writeln!( + out, + " {}:{:<8} ({},{}) {:<8} rel:{:<16} vis:{:<12} d={}", + kind_label(e.kind), + e.entity_id, + e.x as i32, + e.y as i32, + sector_label(e.visibility), + relationship_label(e.relationship), + observation_label(&e.observation), + dist, + ) + .ok(); + } + } + + // Pending recognitions + if !snapshot.pending_recognitions.is_empty() { + write!( + out, + "Pending recognitions: {}", + snapshot.pending_recognitions.len() + ) + .ok(); + for pr in &snapshot.pending_recognitions { + let elapsed = pr.total_delay_ticks - pr.remaining_ticks; + write!( + out, + " [npc:{} at ({},{}) {}/{} ticks]", + pr.entity_id, pr.x as i32, pr.y as i32, elapsed, pr.total_delay_ticks, + ) + .ok(); + } + writeln!(out).ok(); + } + + // Tiles + writeln!(out, "Tiles: {} visible", snapshot.visible_tiles.len()).ok(); + + // Interactions + if !snapshot.nearby_interactions.is_empty() { + writeln!( + out, + "Interactions ({}):", + snapshot.nearby_interactions.len() + ) + .ok(); + for ni in &snapshot.nearby_interactions { + let verbs: Vec = ni + .verbs + .iter() + .map(|v| format!("{}({})", v.label, v.priority)) + .collect(); + writeln!( + out, + " {}:{} [{}] distance={}", + kind_label(ni.entity_type), + ni.entity_id, + verbs.join(", "), + ni.distance, + ) + .ok(); + } + } + + // Inventory + if !snapshot.player_inventory.is_empty() { + let slots: Vec = snapshot + .player_inventory + .iter() + .map(|item| format!("item:{}(slot-{})", item.item_id, item.slot)) + .collect(); + writeln!( + out, + "Inventory: {}/9 [{}]", + snapshot.player_inventory.len(), + slots.join(", "), + ) + .ok(); + } + + // Monologue + if let Some(ref mono) = snapshot.current_monologue { + writeln!(out, "Monologue: \"{}\"", mono.text).ok(); + } + + // Dialogue response + if let Some(ref dialogue) = snapshot.dialogue_response { + writeln!( + out, + "Dialogue: [npc:{}] \"{}\"", + dialogue.speaker_entity_id, dialogue.text + ) + .ok(); + } + + writeln!(out, "===").ok(); + out +} + +fn kind_label(kind: EntityKind) -> &'static str { + match kind { + EntityKind::Player => "player", + EntityKind::Npc => "npc", + EntityKind::Object => "obj", + EntityKind::Terrain => "terrain", + } +} + +fn sector_label(sector: VisibilitySector) -> &'static str { + match sector { + VisibilitySector::Forward => "Forward", + VisibilitySector::Peripheral => "Periph", + } +} + +fn relationship_label(rel: RelationshipState) -> &'static str { + match rel { + RelationshipState::Unknown => "Unknown", + RelationshipState::Known => "Known", + RelationshipState::Friendly => "Friendly", + RelationshipState::PersonOfInterest => "POI", + RelationshipState::Hostile => "Hostile", + } +} + +fn observation_label(obs: &EntityVisibility) -> &'static str { + match obs { + EntityVisibility::Visible => "Visible", + EntityVisibility::Remembered { .. } => "Remembered", + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::simulation::time::{DayPhase, TickRate}; + + fn make_snapshot() -> ObserverSnapshot { + ObserverSnapshot { + version: PROTOCOL_VERSION, + tick: 42, + game_time: GameTime { + day: 0, + time_of_day: 252, + day_phase: DayPhase::Morning, + tick_rate: TickRate::Full, + }, + player_facing: FacingDirection::East, + player_stance: MovementStance::Walk, + player_inventory: vec![], + entities: vec![ + VisibleEntity { + entity_id: 1, + x: 15.0, + y: 10.0, + z: 0, + kind: EntityKind::Player, + visibility: VisibilitySector::Forward, + relationship: RelationshipState::Unknown, + observation: EntityVisibility::Visible, + }, + VisibleEntity { + entity_id: 100, + x: 18.0, + y: 10.0, + z: 0, + kind: EntityKind::Npc, + visibility: VisibilitySector::Forward, + relationship: RelationshipState::Known, + observation: EntityVisibility::Visible, + }, + VisibleEntity { + entity_id: 200, + x: 16.0, + y: 9.0, + z: 0, + kind: EntityKind::Object, + visibility: VisibilitySector::Forward, + relationship: RelationshipState::Unknown, + observation: EntityVisibility::Visible, + }, + ], + visible_tiles: vec![VisibleTile { + x: 15, + y: 10, + z: 0, + visibility: VisibilitySector::Forward, + tile_kind: TileKind::Floor, + }], + nearby_interactions: vec![NearbyInteraction { + entity_id: 100, + entity_type: EntityKind::Npc, + distance: 3, + verbs: vec![ + VerbOption { + kind: VerbKind::Talk, + label: "Talk".into(), + priority: 1, + available: true, + }, + VerbOption { + kind: VerbKind::ExamineNpc, + label: "ExamineNpc".into(), + priority: 2, + available: true, + }, + ], + object_type: None, + contradicted: false, + }], + current_monologue: Some(MonologueEvent { + id: "mono_test_1".into(), + text: "Something about this manifest doesn't add up.".into(), + duration_seconds: 3.0, + }), + pending_recognitions: vec![], + dialogue_response: None, + } + } + + #[test] + fn header_contains_tick_and_position() { + let text = format_snapshot_text(&make_snapshot()); + assert!(text.contains("Tick 42")); + assert!(text.contains("Player (15,10)")); + assert!(text.contains("facing East")); + assert!(text.contains("Stance: Walk")); + } + + #[test] + fn game_time_formatted() { + let text = format_snapshot_text(&make_snapshot()); + assert!(text.contains("Day 0, 04:12 (Morning)")); + } + + #[test] + fn entities_sorted_by_distance() { + let text = format_snapshot_text(&make_snapshot()); + // obj:200 at (16,9) is distance 2 from player (15,10) + // npc:100 at (18,10) is distance 3 from player (15,10) + let obj_pos = text.find("obj:200").unwrap(); + let npc_pos = text.find("npc:100").unwrap(); + assert!( + obj_pos < npc_pos, + "obj:200 (d=2) should appear before npc:100 (d=3)" + ); + } + + #[test] + fn player_excluded_from_entity_list() { + let text = format_snapshot_text(&make_snapshot()); + assert!(text.contains("Entities (2):")); + assert!(!text.contains("player:1")); + } + + #[test] + fn interactions_rendered() { + let text = format_snapshot_text(&make_snapshot()); + assert!(text.contains("Interactions (1):")); + assert!(text.contains("Talk(1)")); + assert!(text.contains("distance=3")); + } + + #[test] + fn monologue_rendered() { + let text = format_snapshot_text(&make_snapshot()); + assert!(text.contains("Monologue: \"Something about this manifest doesn't add up.\"")); + } + + #[test] + fn dialogue_rendered_when_present() { + let mut snap = make_snapshot(); + snap.dialogue_response = Some(DialogueResponseEvent { + line_id: "line_test".into(), + text: "Welcome to the docks.".into(), + speaker_entity_id: 100, + }); + let text = format_snapshot_text(&snap); + assert!(text.contains("Dialogue: [npc:100] \"Welcome to the docks.\"")); + } + + #[test] + fn pending_recognitions_rendered() { + let mut snap = make_snapshot(); + snap.pending_recognitions = vec![PendingRecognitionWire { + entity_id: 104, + x: 19.0, + y: 12.0, + z: 0, + remaining_ticks: 5, + total_delay_ticks: 8, + }]; + let text = format_snapshot_text(&snap); + assert!(text.contains("Pending recognitions: 1")); + assert!(text.contains("npc:104 at (19,12) 3/8 ticks")); + } + + #[test] + fn inventory_rendered() { + let mut snap = make_snapshot(); + snap.player_inventory = vec![ + InventoryItem { + item_id: 300, + name: "Manifest".into(), + slot: 0, + }, + InventoryItem { + item_id: 301, + name: "Keycard".into(), + slot: 3, + }, + ]; + let text = format_snapshot_text(&snap); + assert!(text.contains("Inventory: 2/9")); + assert!(text.contains("item:300(slot-0)")); + assert!(text.contains("item:301(slot-3)")); + } + + #[test] + fn empty_snapshot_no_panic() { + let snap = ObserverSnapshot { + version: PROTOCOL_VERSION, + tick: 0, + game_time: GameTime { + day: 0, + time_of_day: 0, + day_phase: DayPhase::Morning, + tick_rate: TickRate::Full, + }, + player_facing: FacingDirection::North, + player_stance: MovementStance::Walk, + player_inventory: vec![], + entities: vec![], + visible_tiles: vec![], + nearby_interactions: vec![], + current_monologue: None, + pending_recognitions: vec![], + dialogue_response: None, + }; + let text = format_snapshot_text(&snap); + assert!(text.contains("Tick 0")); + assert!(text.contains("Player (-1,-1)")); + assert!(text.contains("Tiles: 0 visible")); + } +} diff --git a/server/src/content/hot_reload.rs b/server/src/content/hot_reload.rs index 257924499..bff2d2ce9 100644 --- a/server/src/content/hot_reload.rs +++ b/server/src/content/hot_reload.rs @@ -87,7 +87,11 @@ const MAX_WALK_DEPTH: usize = 100; /// Stops recursing at MAX_WALK_DEPTH to guard against symlink loops. fn walk_yaml(dir: &Path, timestamps: &mut BTreeMap, depth: usize) { if depth >= MAX_WALK_DEPTH { - tracing::warn!("walk_yaml: max depth {} reached at {:?}, stopping", MAX_WALK_DEPTH, dir); + tracing::warn!( + "walk_yaml: max depth {} reached at {:?}, stopping", + MAX_WALK_DEPTH, + dir + ); return; } let Ok(entries) = std::fs::read_dir(dir) else { diff --git a/server/src/content/spawn.rs b/server/src/content/spawn.rs index 072151692..47be62c0e 100644 --- a/server/src/content/spawn.rs +++ b/server/src/content/spawn.rs @@ -197,9 +197,10 @@ fn spawn_npc(world: &mut World, profile: &types::NpcProfile, result: &mut SpawnR // Register in EntityRegistry for StableId mapping let stable_id = world.resource_mut::().register(entity); - world - .entity_mut(entity) - .insert((StableEntityId(stable_id), ContentSlug(profile.canonical_id.clone()))); + world.entity_mut(entity).insert(( + StableEntityId(stable_id), + ContentSlug(profile.canonical_id.clone()), + )); result .npc_ids diff --git a/server/src/knowledge/registry.rs b/server/src/knowledge/registry.rs index d5bc0c277..32628f693 100644 --- a/server/src/knowledge/registry.rs +++ b/server/src/knowledge/registry.rs @@ -214,11 +214,7 @@ mod tests { id_first, id_second, "Re-registration must assign a new StableId" ); - assert_eq!( - id_second, - StableId(1), - "Counter advances monotonically" - ); + assert_eq!(id_second, StableId(1), "Counter advances monotonically"); assert_eq!(registry.len(), 1); // New mapping is bidirectionally correct diff --git a/server/src/main.rs b/server/src/main.rs index 3a7f18150..3433036e1 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -162,7 +162,9 @@ fn setup_proof_room(app: &mut App) { use settled_reach_server::simulation::monologue::{ MonologueBuffer, MonologueState, SprintAnomalyQueue, }; - use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; + use settled_reach_server::simulation::movement::{ + PlayerCharacter, TilePosition, WalkabilityMap, + }; use settled_reach_server::simulation::path_follow::MovementSpeed; use settled_reach_server::simulation::stance::{MovementProfile, PlayerMoveCooldown}; use settled_reach_server::simulation::time::DayPhase; diff --git a/server/src/perception/mod.rs b/server/src/perception/mod.rs index ee9003379..722c88465 100644 --- a/server/src/perception/mod.rs +++ b/server/src/perception/mod.rs @@ -26,10 +26,8 @@ impl Plugin for PerceptionPlugin { .add_systems( Update, ( - anomaly::clear_anomaly_markers - .before(anomaly::detect_anomalies), - anomaly::detect_anomalies - .before(observation::emit_observation_events), + anomaly::clear_anomaly_markers.before(anomaly::detect_anomalies), + anomaly::detect_anomalies.before(observation::emit_observation_events), cognitive_delay::process_cognitive_delay .after(observation::emit_observation_events) .before(crate::knowledge::events::process_knowledge_events), diff --git a/server/src/perception/observer/mod.rs b/server/src/perception/observer/mod.rs index 02a9d0d66..8e7d91571 100644 --- a/server/src/perception/observer/mod.rs +++ b/server/src/perception/observer/mod.rs @@ -15,9 +15,9 @@ use crate::knowledge::{EntityRegistry, KnowledgeGraph, StableId}; use crate::perception::cognitive_delay::CognitiveDelay; use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry}; use crate::perception::vision_cone::Facing; +use crate::simulation::dialogue::DialogueResponseBuffer; use crate::simulation::interaction::NearbyInteractionBuffer; use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName}; -use crate::simulation::dialogue::DialogueResponseBuffer; use crate::simulation::monologue::{MonologueBuffer, SprintAnomalyQueue}; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use crate::simulation::stance::Stance; diff --git a/server/src/perception/observer/tests.rs b/server/src/perception/observer/tests.rs index 3cb1eda33..d2e579d01 100644 --- a/server/src/perception/observer/tests.rs +++ b/server/src/perception/observer/tests.rs @@ -1948,7 +1948,11 @@ fn equidistant_npcs_produce_stable_snapshot_ordering() { .map(|e| e.entity_id) .collect(); - assert_eq!(npc_ids.len(), 3, "all three equidistant NPCs should be visible"); + assert_eq!( + npc_ids.len(), + 3, + "all three equidistant NPCs should be visible" + ); // Entity IDs must be in strictly ascending order (Fix B sort guarantee) for i in 1..npc_ids.len() { diff --git a/server/src/simulation/dialogue.rs b/server/src/simulation/dialogue.rs index 7cf965195..1fda1b31e 100644 --- a/server/src/simulation/dialogue.rs +++ b/server/src/simulation/dialogue.rs @@ -19,7 +19,9 @@ use bevy_ecs::prelude::*; use rand::Rng; use crate::bridge::types::{DialogueResponseEvent, RelationshipState}; -use crate::content::line_pool::{AccessTier, IndexedDialogueLine, Mood, Situation, Topic, TrustTier}; +use crate::content::line_pool::{ + AccessTier, IndexedDialogueLine, Mood, Situation, Topic, TrustTier, +}; use crate::content::LinePoolIndexResource; use crate::knowledge::{EntityRegistry, KnowledgeGraph}; use crate::simulation::movement::PlayerCharacter; @@ -219,7 +221,11 @@ pub fn derive_situations( /// - Topic match: +2 per matching topic /// /// Returns 0 only for lines on cooldown (caller handles). -pub fn score_line(line: &IndexedDialogueLine, npc_mood: Option, active_topics: &[Topic]) -> u32 { +pub fn score_line( + line: &IndexedDialogueLine, + npc_mood: Option, + active_topics: &[Topic], +) -> u32 { let mut score: u32 = 1; // Base score — no line is excluded by Layer 4 // Mood match @@ -318,8 +324,14 @@ pub fn process_talk_interaction( return; }; - let Ok((player_entity, observer_kg, talk_request, mut response_buffer, mut cooldown, active_dialogue_opt)) = - player_query.single_mut() + let Ok(( + player_entity, + observer_kg, + talk_request, + mut response_buffer, + mut cooldown, + active_dialogue_opt, + )) = player_query.single_mut() else { return; }; @@ -784,9 +796,14 @@ mod tests { let mut match_count = 0; for seed in 0..100 { let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(seed); - if let Some(line) = - select_dialogue_line(&candidates, Some(Mood::Worried), &[], &cooldown, 0, &mut rng) - { + if let Some(line) = select_dialogue_line( + &candidates, + Some(Mood::Worried), + &[], + &cooldown, + 0, + &mut rng, + ) { if line.id == "matched" { match_count += 1; } @@ -869,9 +886,10 @@ mod tests { role: "dock-worker".to_string(), lines, }; - index - .dialogue - .insert(("the-terminal".to_string(), "dock-worker".to_string()), pool); + index.dialogue.insert( + ("the-terminal".to_string(), "dock-worker".to_string()), + pool, + ); index } @@ -1009,10 +1027,11 @@ mod tests { let mut seen_ids: Vec = Vec::new(); for seed in 0..20 { // Reset for each iteration - world.get_mut::(player).unwrap().response = None; world - .entity_mut(player) - .insert(TalkRequest { target: npc }); + .get_mut::(player) + .unwrap() + .response = None; + world.entity_mut(player).insert(TalkRequest { target: npc }); world.insert_resource(SimRng::new(seed)); let mut schedule = bevy_ecs::schedule::Schedule::default(); @@ -1020,7 +1039,11 @@ mod tests { schedule.run(&mut world); world.flush(); - if let Some(resp) = &world.get::(player).unwrap().response { + if let Some(resp) = &world + .get::(player) + .unwrap() + .response + { if !seen_ids.contains(&resp.line_id) { seen_ids.push(resp.line_id.clone()); } @@ -1046,9 +1069,7 @@ mod tests { world.insert_resource(LinePoolIndexResource(index)); // NPC without DialogueProfile - let npc = world - .spawn((Npc, TilePosition::new(5, 5, 0))) - .id(); + let npc = world.spawn((Npc, TilePosition::new(5, 5, 0))).id(); world.resource_mut::().register(npc); let player = world @@ -1142,10 +1163,11 @@ mod tests { ); // Second talk — same tick, line on cooldown - world.get_mut::(player).unwrap().response = None; world - .entity_mut(player) - .insert(TalkRequest { target: npc }); + .get_mut::(player) + .unwrap() + .response = None; + world.entity_mut(player).insert(TalkRequest { target: npc }); let mut schedule2 = bevy_ecs::schedule::Schedule::default(); schedule2.add_systems(process_talk_interaction); diff --git a/server/src/simulation/input.rs b/server/src/simulation/input.rs index 0954c1ce0..2e4f6a367 100644 --- a/server/src/simulation/input.rs +++ b/server/src/simulation/input.rs @@ -102,12 +102,7 @@ pub fn process_player_input( for input in inputs { // Discard all gameplay actions while paused (D-052, R2-OQ-01). // Only Pause/Unpause are processed — everything else is discarded. - if paused - && !matches!( - input.action, - PlayerAction::Pause | PlayerAction::Unpause - ) - { + if paused && !matches!(input.action, PlayerAction::Pause | PlayerAction::Unpause) { continue; } match input.action { @@ -190,14 +185,20 @@ pub fn process_player_input( handle_place(&mut commands, ®istry, &player_query, target_entity_id); } Some("Talk") => { - handle_talk(&mut commands, ®istry, &player_query, &all_positions, target_entity_id); + handle_talk( + &mut commands, + ®istry, + &player_query, + &all_positions, + target_entity_id, + ); } _ => { tracing::info!( - "Interact: target={:?}, verb={:?} — logged only", - target_entity_id, - verb, - ); + "Interact: target={:?}, verb={:?} — logged only", + target_entity_id, + verb, + ); } }, PlayerAction::WalkAway => { @@ -363,7 +364,9 @@ fn handle_talk( // Server-side range check: reject Talk if target is beyond close range if let Ok(target_pos) = all_positions.get(target_entity) { - let distance = player_pos.manhattan_distance(target_pos).unwrap_or(u32::MAX); + let distance = player_pos + .manhattan_distance(target_pos) + .unwrap_or(u32::MAX); if distance > crate::simulation::interaction::CLOSE_RANGE { tracing::info!( target_id, @@ -1312,11 +1315,7 @@ mod tests { ); let mut query = world.query::<&Stance>(); let stance = query.single(&world).unwrap(); - assert_eq!( - stance.0, - MovementStance::Walk, - "Stance unchanged in batch" - ); + assert_eq!(stance.0, MovementStance::Walk, "Stance unchanged in batch"); assert_eq!( world.resource::().tick_rate, TickRate::Paused, diff --git a/server/src/simulation/monologue.rs b/server/src/simulation/monologue.rs index cc4aaf8bd..2d5b02878 100644 --- a/server/src/simulation/monologue.rs +++ b/server/src/simulation/monologue.rs @@ -39,18 +39,12 @@ pub(crate) const ANOMALY_DELAY_TICKS: u64 = 90; /// Fire DURING cognitive delay (when grey blob appears). Future: move to /// content pools with trigger="observe_anomaly" + character match. const RECOGNITION_LINES: &[(&str, &str)] = &[ - ( - "recognition_01", - "Wait \u{2014} I know that walk.", - ), + ("recognition_01", "Wait \u{2014} I know that walk."), ( "recognition_02", "Those footsteps... I've heard that pattern before.", ), - ( - "recognition_03", - "Something about that silhouette...", - ), + ("recognition_03", "Something about that silhouette..."), ]; /// Hardcoded v0.1 sprint anomaly "double-take" lines. @@ -258,9 +252,9 @@ pub fn trigger_recognition_monologue( let pending = cognitive_delay.pending_mut(); let target_idx = { // First pass: anomalous + unfired - let anomaly_idx = pending.iter().position(|p| { - !p.monologue_fired && anomaly_markers.get(p.target).is_ok() - }); + let anomaly_idx = pending + .iter() + .position(|p| !p.monologue_fired && anomaly_markers.get(p.target).is_ok()); if let Some(idx) = anomaly_idx { Some(idx) } else { @@ -872,10 +866,10 @@ mod tests { // trigger_recognition_monologue tests (#451, D-060) // ----------------------------------------------------------------------- + use crate::knowledge::types::StableId; use crate::perception::cognitive_delay::{ CognitiveDelay, PendingRecognition, RecognitionTrigger, NORMAL_DELAY_TICKS, }; - use crate::knowledge::types::StableId; fn setup_recognition_world() -> World { let mut world = World::new(); @@ -957,7 +951,12 @@ mod tests { // First tick: fires schedule.run(&mut world); assert!( - world.query::<&MonologueBuffer>().single(&world).unwrap().event.is_some(), + world + .query::<&MonologueBuffer>() + .single(&world) + .unwrap() + .event + .is_some(), "first tick should fire" ); @@ -967,7 +966,12 @@ mod tests { // Second tick: should NOT fire (monologue_fired = true) schedule.run(&mut world); assert!( - world.query::<&MonologueBuffer>().single(&world).unwrap().event.is_none(), + world + .query::<&MonologueBuffer>() + .single(&world) + .unwrap() + .event + .is_none(), "second tick should not fire (already fired for this recognition)" ); } @@ -1053,9 +1057,7 @@ mod tests { let mut world = setup_recognition_world(); let normal_target = world.spawn_empty().id(); - let anomalous_target = world - .spawn(crate::perception::anomaly::AnomalyMarker) - .id(); + let anomalous_target = world.spawn(crate::perception::anomaly::AnomalyMarker).id(); let mut cd = CognitiveDelay::default(); // Normal entity added first diff --git a/server/src/simulation/movement.rs b/server/src/simulation/movement.rs index e0eea10fc..5fd5e7aec 100644 --- a/server/src/simulation/movement.rs +++ b/server/src/simulation/movement.rs @@ -917,12 +917,14 @@ mod tests { // Entity with lower bits processes first and claims the target assert_eq!( - pos_lower, target, + pos_lower, + target, "entity with lower Entity::to_bits() ({}) should win the tile", lower.to_bits() ); assert_eq!( - pos_higher, higher_origin, + pos_higher, + higher_origin, "entity with higher Entity::to_bits() ({}) should stay at origin", higher.to_bits() ); diff --git a/server/tests/bridge_tcp.rs b/server/tests/bridge_tcp.rs index d189b8f2f..93b8d884d 100644 --- a/server/tests/bridge_tcp.rs +++ b/server/tests/bridge_tcp.rs @@ -86,7 +86,10 @@ fn input_roundtrip_over_tcp() { match bridge.receive_inputs() { Ok(inputs) if !inputs.is_empty() => break inputs, Ok(_) => { - assert!(std::time::Instant::now() < deadline, "timed out waiting for inputs"); + assert!( + std::time::Instant::now() < deadline, + "timed out waiting for inputs" + ); thread::sleep(std::time::Duration::from_millis(1)); } Err(e) => panic!("failed to receive inputs: {}", e), @@ -150,7 +153,10 @@ fn tcp_bridge_eof_returns_error() { match bridge.receive_inputs() { Ok(inputs) if inputs.is_empty() => { // WouldBlock — client hasn't disconnected yet, retry - assert!(std::time::Instant::now() < deadline, "timed out waiting for EOF"); + assert!( + std::time::Instant::now() < deadline, + "timed out waiting for EOF" + ); thread::sleep(std::time::Duration::from_millis(1)); } Ok(inputs) => panic!("expected Disconnected error, got {} inputs", inputs.len()), diff --git a/server/tests/determinism.rs b/server/tests/determinism.rs index a71164e04..06d17090d 100644 --- a/server/tests/determinism.rs +++ b/server/tests/determinism.rs @@ -16,8 +16,8 @@ use settled_reach_server::knowledge::registry::EntityRegistry; use settled_reach_server::knowledge::{KnowledgeGraph, KnowledgePlugin}; use settled_reach_server::npc::relationships::{RelationshipEdge, RelationshipGraph}; use settled_reach_server::npc::{ - Contentment, DailyRoutine, Npc, NpcPlugin, RelationshipKind, RoutineEntry, - ToleranceThreshold, Want, WantKind, + Contentment, DailyRoutine, Npc, NpcPlugin, RelationshipKind, RoutineEntry, ToleranceThreshold, + Want, WantKind, }; use settled_reach_server::perception::cognitive_delay::CognitiveDelay; use settled_reach_server::perception::vision_cone::Facing; @@ -213,10 +213,7 @@ fn build_deterministic_app(seed: u64) -> App { /// Run the simulation for a fixed number of ticks with predetermined inputs. /// Returns serialized snapshots for each tick. -fn run_simulation( - seed: u64, - inputs: &[Vec], -) -> Vec> { +fn run_simulation(seed: u64, inputs: &[Vec]) -> Vec> { let mut app = build_deterministic_app(seed); let mut snapshots = Vec::with_capacity(inputs.len()); @@ -334,7 +331,8 @@ fn gauntlet_deterministic_replay() { for (tick, (s1, s2)) in run1.iter().zip(run2.iter()).enumerate() { assert_eq!( - s1, s2, + s1, + s2, "snapshot at tick {} differs between runs ({} vs {} bytes)", tick, s1.len(), diff --git a/server/tests/gen_fixtures.rs b/server/tests/gen_fixtures.rs index f6461afd9..7becb342b 100644 --- a/server/tests/gen_fixtures.rs +++ b/server/tests/gen_fixtures.rs @@ -279,11 +279,11 @@ fn generate_msgpack_fixtures() { // Tests that GDScript can decode full ObserverSnapshot structs when the tick // field crosses encoding format boundaries. let boundary_snapshots: [(u64, &str); 5] = [ - (0, "snapshot_boundary_tick_0"), // pos fixint - (127, "snapshot_boundary_tick_127"), // pos fixint max - (32767, "snapshot_boundary_tick_32767"), // int16/uint16 asymmetry + (0, "snapshot_boundary_tick_0"), // pos fixint + (127, "snapshot_boundary_tick_127"), // pos fixint max + (32767, "snapshot_boundary_tick_32767"), // int16/uint16 asymmetry (2147483647, "snapshot_boundary_tick_2b31m1"), // int32/uint32 asymmetry - (4294967296, "snapshot_boundary_tick_2b32"), // int64 minimum + (4294967296, "snapshot_boundary_tick_2b32"), // int64 minimum ]; for (tick, name) in &boundary_snapshots { diff --git a/server/tests/serialization.rs b/server/tests/serialization.rs index 28650c31f..7efdae655 100644 --- a/server/tests/serialization.rs +++ b/server/tests/serialization.rs @@ -139,8 +139,9 @@ fn all_fixtures_deserialize() { if name.starts_with("snapshot_boundary") { // Boundary snapshot fixtures (#472): tick may exceed PROTOCOL_VERSION check - rmp_serde::from_slice::(&bytes) - .unwrap_or_else(|e| panic!("deserialize boundary snapshot fixture {}: {}", name, e)); + rmp_serde::from_slice::(&bytes).unwrap_or_else(|e| { + panic!("deserialize boundary snapshot fixture {}: {}", name, e) + }); } else if name.starts_with("snapshot") { let snap = rmp_serde::from_slice::(&bytes) .unwrap_or_else(|e| panic!("deserialize snapshot fixture {}: {}", name, e)); @@ -694,19 +695,48 @@ fn nearby_interaction_contradicted_roundtrip() { /// int 32 (-32769 to -2^31), int 64 (-2^31-1 to -2^63). const BOUNDARY_VALUES: [i64; 41] = [ // Positive boundaries (25 values) - 0, 1, 126, 127, // pos fixint - 128, 129, 254, 255, // uint 8 - 256, 257, 32766, 32767, // int 16 / uint 16 asymmetry - 32768, 32769, 65534, 65535, // uint 16 - 65536, 65537, 2147483646, 2147483647, // int 32 / uint 32 asymmetry - 2147483648, 4294967294, 4294967295, // uint 32 - 4294967296, i64::MAX, // int 64 + 0, + 1, + 126, + 127, // pos fixint + 128, + 129, + 254, + 255, // uint 8 + 256, + 257, + 32766, + 32767, // int 16 / uint 16 asymmetry + 32768, + 32769, + 65534, + 65535, // uint 16 + 65536, + 65537, + 2147483646, + 2147483647, // int 32 / uint 32 asymmetry + 2147483648, + 4294967294, + 4294967295, // uint 32 + 4294967296, + i64::MAX, // int 64 // Negative boundaries (16 values) - -1, -31, -32, // neg fixint - -33, -34, -127, -128, // int 8 - -129, -130, -32767, -32768, // int 16 - -32769, -2147483647, -2147483648, // int 32 - -2147483649, i64::MIN, // int 64 + -1, + -31, + -32, // neg fixint + -33, + -34, + -127, + -128, // int 8 + -129, + -130, + -32767, + -32768, // int 16 + -32769, + -2147483647, + -2147483648, // int 32 + -2147483649, + i64::MIN, // int 64 ]; #[test] diff --git a/tooling/test-client/Cargo.lock b/tooling/test-client/Cargo.lock new file mode 100644 index 000000000..95813535b --- /dev/null +++ b/tooling/test-client/Cargo.lock @@ -0,0 +1,1707 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bevy_app" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2271a0123a7cc355c3fe98754360c75aa84b29f2a6b1a9f8c00aac427570d174" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "cfg-if", + "ctrlc", + "downcast-rs", + "log", + "thiserror", + "variadics_please", +] + +[[package]] +name = "bevy_derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b6a05c31f54c83d681f1b8699bbaf581f06b25a40c9a6bb815625f731f5ba9" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn", +] + +[[package]] +name = "bevy_ecs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24637a7c8643cab493f4085cda6bde4895f0e0816699c59006f18819da2ca0b8" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags", + "bumpalo", + "concurrent-queue", + "derive_more", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eb14c18ca71e11c69fbae873c2db129064efac6d52e48d0127d37bfba1acfa8" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7272fca0bf30d8ca2571a803598856104b63e5c596d52850f811ed37c5e1e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "toml_edit", +] + +[[package]] +name = "bevy_platform" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b29ea749a8e85f98186ab662f607b885b97c804bb14cdb0cdf838164496d474" +dependencies = [ + "critical-section", + "foldhash 0.2.0", + "futures-channel", + "hashbrown 0.16.1", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "bevy_ptr" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f98cbc6d34bbdb58240b72ed1731931b4991a893b3a3238bb7c42ae054aa676" + +[[package]] +name = "bevy_reflect" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2a977e2b8dba65b6e9c11039c5f9ef108be428f036b3d1cac13ad86ec59f9c" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs", + "erased-serde", + "foldhash 0.2.0", + "glam", + "indexmap", + "serde", + "smallvec", + "smol_str", + "thiserror", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "067af30072b1611fda1a577f1cb678b8ea2c9226133068be808dd49aac30cef0" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_tasks" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990ffedd374dd2c4fe8f0fd4bcefd5617d1ee59164b6c3fcc356a69b48e26e8e" +dependencies = [ + "async-channel", + "async-executor", + "async-task", + "atomic-waker", + "bevy_platform", + "crossbeam-queue", + "derive_more", + "futures-lite", + "heapless", + "pin-project", +] + +[[package]] +name = "bevy_utils" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e258c44d869f9c41ac0f88a16815c67f2569eb9fff4716828a40273d127b6f84" +dependencies = [ + "bevy_platform", + "disqualified", + "thread_local", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys", +] + +[[package]] +name = "deprecate-until" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704" +dependencies = [ + "proc-macro2", + "quote", + "semver", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "equivalent", + "serde", + "serde_core", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32", + "portable-atomic", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pathfinding" +version = "4.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ac35caa284c08f3721fb33c2741b5f763decaf42d080c8a6a722154347017e" +dependencies = [ + "deprecate-until", + "indexmap", + "integer-sqrt", + "num-traits", + "rustc-hash", + "thiserror", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "settled-reach-server" +version = "0.1.0" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bincode", + "pathfinding", + "rand", + "rand_chacha", + "rmp-serde", + "serde", + "serde_yaml", + "thiserror", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "settled-reach-test-client" +version = "0.1.0" +dependencies = [ + "clap", + "rmp-serde", + "serde", + "serde_json", + "settled-reach-server", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "serde", + "thiserror", + "web-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tooling/test-client/Cargo.toml b/tooling/test-client/Cargo.toml new file mode 100644 index 000000000..5e59ee4ab --- /dev/null +++ b/tooling/test-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "settled-reach-test-client" +version = "0.1.0" +edition = "2021" +description = "Test client for the Settled Reach simulation server. Connects via TCP, receives ObserverSnapshots, sends replay inputs." + +[[bin]] +name = "settled-reach-test-client" +path = "src/main.rs" + +[dependencies] +settled-reach-server = { path = "../../server" } +clap = { version = "4", features = ["derive"] } +serde_json = "1" +serde = { version = "1", features = ["derive"] } +rmp-serde = "1" diff --git a/tooling/test-client/src/golden.rs b/tooling/test-client/src/golden.rs new file mode 100644 index 000000000..cb797969b --- /dev/null +++ b/tooling/test-client/src/golden.rs @@ -0,0 +1,104 @@ +// Golden file comparison for test-client. +// Compares the final ObserverSnapshot (as JSON) against a golden file. +// Reports field-by-field differences with JSON paths. + +use serde_json::Value; +use settled_reach_server::bridge::types::ObserverSnapshot; +use std::path::Path; + +/// Compare an ObserverSnapshot against a golden JSON file. +/// Returns a list of difference descriptions (empty = match). +pub fn compare_golden( + golden_path: &Path, + snapshot: &ObserverSnapshot, +) -> Result, String> { + let golden_str = std::fs::read_to_string(golden_path).map_err(|e| { + format!( + "failed to read golden file {}: {}", + golden_path.display(), + e + ) + })?; + let golden: Value = serde_json::from_str(&golden_str) + .map_err(|e| format!("failed to parse golden file: {}", e))?; + let actual: Value = serde_json::to_value(snapshot) + .map_err(|e| format!("failed to serialize snapshot: {}", e))?; + + let mut diffs = Vec::new(); + diff_values("", &golden, &actual, &mut diffs); + Ok(diffs) +} + +fn diff_values(path: &str, expected: &Value, actual: &Value, diffs: &mut Vec) { + match (expected, actual) { + (Value::Object(e), Value::Object(a)) => { + for key in e.keys() { + let child_path = if path.is_empty() { + format!(".{}", key) + } else { + format!("{}.{}", path, key) + }; + match a.get(key) { + Some(av) => diff_values(&child_path, &e[key], av, diffs), + None => diffs.push(format!( + "{}: expected {}, got ", + child_path, + format_value(&e[key]) + )), + } + } + for key in a.keys() { + if !e.contains_key(key) { + let child_path = if path.is_empty() { + format!(".{}", key) + } else { + format!("{}.{}", path, key) + }; + diffs.push(format!( + "{}: expected , got {}", + child_path, + format_value(&a[key]) + )); + } + } + } + (Value::Array(e), Value::Array(a)) => { + let max_len = e.len().max(a.len()); + for i in 0..max_len { + let child_path = format!("{}[{}]", path, i); + match (e.get(i), a.get(i)) { + (Some(ev), Some(av)) => diff_values(&child_path, ev, av, diffs), + (Some(ev), None) => diffs.push(format!( + "{}: expected {}, got ", + child_path, + format_value(ev) + )), + (None, Some(av)) => diffs.push(format!( + "{}: expected , got {}", + child_path, + format_value(av) + )), + (None, None) => unreachable!(), + } + } + } + _ => { + if expected != actual { + diffs.push(format!( + "{}: expected {}, got {}", + path, + format_value(expected), + format_value(actual) + )); + } + } + } +} + +fn format_value(v: &Value) -> String { + match v { + Value::String(s) => format!("{:?}", s), + Value::Null => "null".to_string(), + other => other.to_string(), + } +} diff --git a/tooling/test-client/src/main.rs b/tooling/test-client/src/main.rs new file mode 100644 index 000000000..c6b537bd4 --- /dev/null +++ b/tooling/test-client/src/main.rs @@ -0,0 +1,188 @@ +// test-client: Test client for the Settled Reach simulation server. +// +// Connects via TCP, receives ObserverSnapshots, optionally sends replay +// inputs, and outputs snapshots as text/JSON. Supports golden file +// comparison for deterministic regression testing. +// +// Exit codes: +// 0 = success +// 1 = golden file mismatch +// 2 = connection/protocol error + +use clap::Parser; +use std::io::{BufReader, BufWriter}; +use std::net::TcpStream; +use std::path::PathBuf; +use std::process; + +use settled_reach_server::bridge::framing::{read_framed, write_framed}; +use settled_reach_server::bridge::text_renderer::format_snapshot_text; +use settled_reach_server::bridge::types::ObserverSnapshot; + +mod golden; +mod replay; + +#[derive(Parser)] +#[command( + name = "test-client", + about = "Test client for the Settled Reach simulation server" +)] +struct Cli { + /// Server address (host:port) + #[arg(long, default_value = "127.0.0.1:9876")] + connect: String, + + /// JSONL replay file (one JSON array of PlayerInput per tick) + #[arg(long)] + replay: Option, + + /// Output format: structured text to stdout (default) + #[arg(long)] + text: bool, + + /// Output format: JSON snapshots to stdout + #[arg(long)] + json: bool, + + /// No output (CI assertions only) + #[arg(long)] + quiet: bool, + + /// Compare final snapshot against golden JSON file, exit 1 on diff + #[arg(long)] + golden: Option, + + /// Disconnect after N ticks + #[arg(long)] + ticks: Option, +} + +enum OutputMode { + Text, + Json, + Quiet, +} + +fn main() { + let cli = Cli::parse(); + + let output_mode = if cli.quiet { + OutputMode::Quiet + } else if cli.json { + OutputMode::Json + } else { + OutputMode::Text + }; + + // Connect to server + let stream = match TcpStream::connect(&cli.connect) { + Ok(s) => s, + Err(e) => { + eprintln!("Connection error: {} (address: {})", e, cli.connect); + process::exit(2); + } + }; + + let mut reader = BufReader::new(stream.try_clone().unwrap_or_else(|e| { + eprintln!("Failed to clone TCP stream: {}", e); + process::exit(2); + })); + let mut writer = BufWriter::new(stream); + + // Load replay inputs if provided + let replay_inputs = cli.replay.as_ref().map(|path| { + replay::load_replay(path).unwrap_or_else(|e| { + eprintln!("Replay load error: {}", e); + process::exit(2); + }) + }); + + let mut tick_count: u64 = 0; + let mut last_snapshot: Option = None; + + loop { + // Receive snapshot (blocking read) + let payload = match read_framed(&mut reader) { + Ok(Some(data)) => data, + Ok(None) => { + // Server closed connection — normal shutdown + break; + } + Err(e) => { + eprintln!("Read error: {}", e); + process::exit(2); + } + }; + + let snapshot: ObserverSnapshot = match rmp_serde::from_slice(&payload) { + Ok(s) => s, + Err(e) => { + eprintln!("Deserialization error: {}", e); + process::exit(2); + } + }; + + tick_count += 1; + + // Output snapshot + match output_mode { + OutputMode::Text => { + print!("{}", format_snapshot_text(&snapshot)); + } + OutputMode::Json => { + let json = serde_json::to_string_pretty(&snapshot).unwrap(); + println!("{}", json); + } + OutputMode::Quiet => {} + } + + last_snapshot = Some(snapshot); + + // Check tick limit before sending next input + if let Some(max_ticks) = cli.ticks { + if tick_count >= max_ticks { + break; + } + } + + // Send inputs for next tick + let inputs = replay_inputs + .as_ref() + .and_then(|r| r.get(tick_count as usize - 1)) + .cloned() + .unwrap_or_default(); + + let input_payload = rmp_serde::to_vec(&inputs).unwrap_or_else(|e| { + eprintln!("Input serialization error: {}", e); + process::exit(2); + }); + if let Err(e) = write_framed(&mut writer, &input_payload) { + eprintln!("Write error: {}", e); + process::exit(2); + } + } + + // Golden file comparison + if let Some(golden_path) = &cli.golden { + let snapshot = match last_snapshot { + Some(s) => s, + None => { + eprintln!("No snapshot received for golden comparison"); + process::exit(2); + } + }; + + let diffs = golden::compare_golden(golden_path, &snapshot).unwrap_or_else(|e| { + eprintln!("Golden file error: {}", e); + process::exit(2); + }); + + if !diffs.is_empty() { + eprintln!("GOLDEN FILE MISMATCH: {}", golden_path.display()); + for diff in &diffs { + eprintln!(" {}", diff); + } + process::exit(1); + } + } +} diff --git a/tooling/test-client/src/replay.rs b/tooling/test-client/src/replay.rs new file mode 100644 index 000000000..45c01b4f1 --- /dev/null +++ b/tooling/test-client/src/replay.rs @@ -0,0 +1,24 @@ +// JSONL replay file loader for test-client. +// Each line is a JSON array of PlayerInput for one tick. +// Empty array = idle tick (no input sent). + +use settled_reach_server::bridge::types::PlayerInput; +use std::path::Path; + +/// Load a JSONL replay file. Returns one Vec per tick. +pub fn load_replay(path: &Path) -> Result>, String> { + let content = std::fs::read_to_string(path) + .map_err(|e| format!("failed to read replay file {}: {}", path.display(), e))?; + + let mut ticks = Vec::new(); + for (i, line) in content.lines().enumerate() { + let line = line.trim(); + if line.is_empty() { + continue; + } + let inputs: Vec = + serde_json::from_str(line).map_err(|e| format!("replay line {}: {}", i + 1, e))?; + ticks.push(inputs); + } + Ok(ticks) +}