feat(ci): ban HashMap in simulation crate via clippy (#343)

Add clippy::disallowed_types for std::collections::HashMap scoped to
the simulation crate. Replace HashMap with BTreeMap in movement.rs for
deterministic iteration order. Allow exception in perception/query.rs
where iteration order is irrelevant (per-frame scratch buffer).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 13:34:04 +01:00
co-authored by Claude Opus 4.6
parent f7e5b7eb63
commit 0f5a990f82
3 changed files with 25 additions and 8 deletions
+9
View File
@@ -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." },
]
+5
View File
@@ -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};
+11 -8
View File
@@ -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<ChunkCoord, ChunkData>,
chunks: BTreeMap<ChunkCoord, ChunkData>,
}
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);