diff --git a/server/.clippy.toml b/server/.clippy.toml new file mode 100644 index 000000000..88f1e5b57 --- /dev/null +++ b/server/.clippy.toml @@ -0,0 +1,9 @@ +# Clippy configuration for settled-reach-server +# Enforces determinism-safe collection types in simulation code (D-030). + +# Ban std::collections::HashMap — non-deterministic iteration order breaks replay. +# Use BTreeMap (ordered by key) or IndexMap (insertion-ordered) instead. +disallowed-types = [ + { path = "std::collections::HashMap", reason = "HashMap iteration order is non-deterministic. Use BTreeMap or IndexMap for deterministic simulation." }, + { path = "std::collections::HashSet", reason = "HashSet iteration order is non-deterministic. Use BTreeSet or IndexSet." }, +] diff --git a/server/src/perception/query.rs b/server/src/perception/query.rs index 9789f884d..af6057345 100644 --- a/server/src/perception/query.rs +++ b/server/src/perception/query.rs @@ -4,6 +4,11 @@ //! (natural vision, thermal, EM, etc.) implements PerceptionQuery to //! provide mode-specific FOV and visibility sector computation. //! v0.1 implements only NaturalVision. +//! +//! Note: HashMap is used for `sector_lookup` — a per-frame scratch buffer +//! looked up only by key. Iteration order is irrelevant here. Not subject to +//! the simulation determinism constraint (see server/.clippy.toml). +#![allow(clippy::disallowed_types)] use std::collections::{BTreeSet, HashMap}; diff --git a/server/src/simulation/movement.rs b/server/src/simulation/movement.rs index 5fd5e7aec..027ffa166 100644 --- a/server/src/simulation/movement.rs +++ b/server/src/simulation/movement.rs @@ -6,7 +6,7 @@ use bevy_ecs::prelude::*; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::BTreeMap; /// Chunk size in tiles (32x32 per chunk) pub const CHUNK_SIZE: i32 = 32; @@ -22,7 +22,10 @@ pub struct PlayerCharacter; /// /// Examples: a Standing character can walk past a Seated NPC at a console, /// a Fixture (terminal) shares a tile with someone Seated at it. -#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +#[derive( + Component, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, + Deserialize, +)] pub enum TilePresence { /// Upright position — walking, standing, sprinting. Default for all entities. #[default] @@ -39,7 +42,7 @@ pub enum TilePresence { /// Tile position component for grid-based movement. /// Discrete integer coordinates used in simulation; converted to f32 /// at the bridge boundary for VisibleEntity wire format. -#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct TilePosition { pub x: i32, pub y: i32, @@ -116,7 +119,7 @@ impl TilePosition { } /// Chunk coordinate for chunk-based map storage (D-012). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ChunkCoord { pub cx: i32, pub cy: i32, @@ -161,14 +164,14 @@ impl ChunkData { /// Unloaded chunks are treated as unwalkable. #[derive(Resource, Debug, Clone)] pub struct WalkabilityMap { - chunks: HashMap, + chunks: BTreeMap, } impl WalkabilityMap { /// Create a walkability map covering a rectangular area with all tiles walkable. /// Generates chunks to cover the specified dimensions on z-level 0..z_levels. pub fn new(width: i32, height: i32, z_levels: i32) -> Self { - let mut chunks = HashMap::new(); + let mut chunks = BTreeMap::new(); let cx_max = (width + CHUNK_SIZE - 1) / CHUNK_SIZE; let cy_max = (height + CHUNK_SIZE - 1) / CHUNK_SIZE; for z in 0..z_levels { @@ -183,7 +186,7 @@ impl WalkabilityMap { /// Create a walkability map covering a rectangular area with all tiles blocked. pub fn new_blocked(width: i32, height: i32, z_levels: i32) -> Self { - let mut chunks = HashMap::new(); + let mut chunks = BTreeMap::new(); let cx_max = (width + CHUNK_SIZE - 1) / CHUNK_SIZE; let cy_max = (height + CHUNK_SIZE - 1) / CHUNK_SIZE; for z in 0..z_levels { @@ -279,7 +282,7 @@ pub fn validate_movement( // Collect layer slots occupied by stationary entities (no MoveIntent). // Key: (position, layer) — two entities can share a tile if different layers. - let mut occupied: HashMap<(TilePosition, TilePresence), Entity> = HashMap::new(); + let mut occupied: BTreeMap<(TilePosition, TilePresence), Entity> = BTreeMap::new(); for (entity, pos, presence) in stationary.iter() { let layer = presence.copied().unwrap_or_default(); occupied.insert((*pos, layer), entity);