Fix all Clippy warnings across the server codebase (2411 insertions, 1341 deletions). Raise type-complexity-threshold to 750 and too-many-arguments to 12 in .clippy.toml for idiomatic Bevy ECS system signatures. The server now passes `cargo clippy -- --deny warnings` cleanly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
7.0 KiB
Rust
187 lines
7.0 KiB
Rust
//! Generate snapshot fixtures from the Gauntlet test world (D-030 full pipeline).
|
|
//!
|
|
//! Runs the full Gauntlet simulation pipeline — same code path as --test-mode —
|
|
//! then serializes ObserverSnapshots to both MessagePack (wire format) and JSON
|
|
//! (human-readable debug) for client-side visual testing.
|
|
//!
|
|
//! The .msgpack files are the actual wire-format bytes the server sends over IPC.
|
|
//! Client visual tests load them via Protocol.decode_snapshot() → GameState.apply_snapshot(),
|
|
//! exercising the exact same pipeline as the live game.
|
|
//!
|
|
//! The .json files are for human inspection only.
|
|
//!
|
|
//! Run with: cargo test --test gen_gauntlet_fixtures -- --ignored
|
|
//! Or: make fixtures-gauntlet
|
|
//!
|
|
//! Output: tests/fixtures/gauntlet/*.{msgpack,json} (relative to repo root)
|
|
|
|
use settled_reach_server::bridge::types::*;
|
|
use settled_reach_server::bridge::{BridgePlugin, SnapshotBuffer};
|
|
use settled_reach_server::knowledge::KnowledgePlugin;
|
|
use settled_reach_server::npc::NpcPlugin;
|
|
use settled_reach_server::perception::vision_cone::Facing;
|
|
use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition};
|
|
use settled_reach_server::simulation::rng::SimRng;
|
|
use settled_reach_server::simulation::SimulationPlugin;
|
|
use settled_reach_server::test_world;
|
|
|
|
use bevy_app::prelude::*;
|
|
use bevy_ecs::prelude::*;
|
|
use serde_json::Value;
|
|
use std::collections::BTreeMap;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
const SEED: u64 = 42;
|
|
const FIXTURE_DIR: &str = "../tests/fixtures/gauntlet";
|
|
|
|
/// Build a deterministic Gauntlet simulation app.
|
|
/// Identical to what the server runs in --test-mode.
|
|
fn build_gauntlet(seed: u64) -> App {
|
|
let mut app = App::new();
|
|
app.add_plugins(SimulationPlugin);
|
|
app.add_plugins(BridgePlugin);
|
|
app.add_plugins(KnowledgePlugin);
|
|
app.add_plugins(NpcPlugin);
|
|
app.insert_resource(SimRng::new(seed));
|
|
|
|
test_world::setup_gauntlet(
|
|
&mut app,
|
|
settled_reach_server::bridge::types::CharacterArchetype::default(),
|
|
);
|
|
|
|
app
|
|
}
|
|
|
|
/// Teleport the player to a specific position and facing.
|
|
/// Directly modifies ECS components — same effect as PlayerAction::Teleport
|
|
/// but without needing a target room action.
|
|
fn teleport_player(app: &mut App, pos: TilePosition, facing: Facing) {
|
|
let player = {
|
|
let mut query = app
|
|
.world_mut()
|
|
.query_filtered::<Entity, With<PlayerCharacter>>();
|
|
query.single(app.world()).expect("player entity must exist")
|
|
};
|
|
app.world_mut().entity_mut(player).insert((pos, facing));
|
|
}
|
|
|
|
/// Run N ticks, feeding inputs each tick, return the last snapshot.
|
|
fn run_ticks(app: &mut App, inputs: &[Vec<PlayerInput>]) -> ObserverSnapshot {
|
|
let mut last_snapshot: Option<ObserverSnapshot> = None;
|
|
|
|
for tick_inputs in inputs {
|
|
{
|
|
let mut queue = app
|
|
.world_mut()
|
|
.resource_mut::<settled_reach_server::simulation::input::InputQueue>();
|
|
for input in tick_inputs {
|
|
queue.push(input.clone());
|
|
}
|
|
}
|
|
|
|
app.update();
|
|
|
|
let buffer = app.world().resource::<SnapshotBuffer>();
|
|
if let Some(snapshot) = &buffer.snapshot {
|
|
last_snapshot = Some(snapshot.clone());
|
|
}
|
|
}
|
|
|
|
last_snapshot.expect("no snapshot produced")
|
|
}
|
|
|
|
/// Recursively sort all object keys for deterministic JSON output.
|
|
fn sort_json_keys(value: &Value) -> Value {
|
|
match value {
|
|
Value::Object(map) => {
|
|
let sorted: BTreeMap<String, Value> = map
|
|
.iter()
|
|
.map(|(k, v)| (k.clone(), sort_json_keys(v)))
|
|
.collect();
|
|
Value::Object(sorted.into_iter().collect())
|
|
}
|
|
Value::Array(arr) => Value::Array(arr.iter().map(sort_json_keys).collect()),
|
|
other => other.clone(),
|
|
}
|
|
}
|
|
|
|
fn write_fixture(name: &str, snapshot: &ObserverSnapshot) {
|
|
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_DIR);
|
|
fs::create_dir_all(&dir).expect("create fixture dir");
|
|
|
|
// MessagePack — exact wire-format bytes the server sends over IPC.
|
|
// Client visual tests load these via Protocol.decode_snapshot().
|
|
let msgpack = rmp_serde::to_vec_named(snapshot).expect("serialize to MessagePack");
|
|
let msgpack_path = dir.join(format!("{}.msgpack", name));
|
|
fs::write(&msgpack_path, &msgpack).expect("write msgpack fixture");
|
|
|
|
// JSON — human-readable debug companion (not loaded by tests).
|
|
let value: Value = serde_json::to_value(snapshot).expect("serialize to JSON");
|
|
let sorted = sort_json_keys(&value);
|
|
let json = serde_json::to_string_pretty(&sorted).expect("format JSON") + "\n";
|
|
let json_path = dir.join(format!("{}.json", name));
|
|
fs::write(&json_path, &json).expect("write json fixture");
|
|
|
|
eprintln!(
|
|
"Wrote {} ({} bytes msgpack, {} bytes json, {} visible_tiles, {} entities)",
|
|
name,
|
|
msgpack.len(),
|
|
json.len(),
|
|
snapshot.visible_tiles.len(),
|
|
snapshot.entities.len(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore] // Run manually: cargo test --test gen_gauntlet_fixtures -- --ignored
|
|
fn generate_gauntlet_snapshot_fixtures() {
|
|
// --- Hub (default spawn position) ---
|
|
// Player at (50, 58) facing North — tests fog rendering at the starting location.
|
|
// This is the exact state a new player sees on connect.
|
|
{
|
|
let mut app = build_gauntlet(SEED);
|
|
// Run 3 idle ticks to stabilize (cognitive delay, vision cone init)
|
|
let inputs: Vec<Vec<PlayerInput>> = vec![vec![], vec![], vec![]];
|
|
let snapshot = run_ticks(&mut app, &inputs);
|
|
write_fixture("hub_spawn", &snapshot);
|
|
}
|
|
|
|
// --- Fog Theater (observer position) ---
|
|
// Player at (56, 18) facing South — large open room with NPCs at varying distances.
|
|
// Tests visibility cone, fog layers, and distance-based fog rendering.
|
|
{
|
|
let mut app = build_gauntlet(SEED);
|
|
let fog_theater = test_world::constants::FOG_THEATER;
|
|
teleport_player(&mut app, fog_theater.observer, fog_theater.observer_facing);
|
|
let inputs: Vec<Vec<PlayerInput>> = vec![vec![], vec![], vec![]];
|
|
let snapshot = run_ticks(&mut app, &inputs);
|
|
write_fixture("fog_theater", &snapshot);
|
|
}
|
|
|
|
// --- Hub after movement (explored tiles + visible tiles differ) ---
|
|
// Player moves south from hub, creating a mix of explored-but-not-visible
|
|
// and currently-visible tiles — the exact fog boundary condition.
|
|
{
|
|
let mut app = build_gauntlet(SEED);
|
|
let inputs: Vec<Vec<PlayerInput>> = vec![
|
|
vec![],
|
|
vec![PlayerInput {
|
|
tick: 1,
|
|
action: PlayerAction::MoveSouth,
|
|
}],
|
|
vec![PlayerInput {
|
|
tick: 2,
|
|
action: PlayerAction::MoveSouth,
|
|
}],
|
|
vec![PlayerInput {
|
|
tick: 3,
|
|
action: PlayerAction::MoveSouth,
|
|
}],
|
|
vec![], // idle — snapshot has explored + visible tiles that differ
|
|
];
|
|
let snapshot = run_ticks(&mut app, &inputs);
|
|
write_fixture("hub_after_movement", &snapshot);
|
|
}
|
|
}
|