fix(simulation): Clippy cleanup and CI enforcement (#635)
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>
This commit is contained in:
@@ -66,7 +66,7 @@ fn snapshot_roundtrip_over_unix_socket() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
character_pressure: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
@@ -74,7 +74,7 @@ fn snapshot_roundtrip_over_unix_socket() {
|
||||
save_result: None,
|
||||
triangle_crisis_events: vec![],
|
||||
state_hash: None,
|
||||
debug_response: None,
|
||||
debug_response: None,
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
|
||||
@@ -52,7 +52,7 @@ fn snapshot_roundtrip_over_tcp() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
character_pressure: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
@@ -60,7 +60,7 @@ fn snapshot_roundtrip_over_tcp() {
|
||||
save_result: None,
|
||||
triangle_crisis_events: vec![],
|
||||
state_hash: None,
|
||||
debug_response: None,
|
||||
debug_response: None,
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
|
||||
@@ -7,12 +7,12 @@ use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::schedule::Schedule;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use settled_reach_server::simulation::triangle::{
|
||||
RoleId, TemplateId, TriangleClassification, TriangleId, TrianglePhase, TriangleState,
|
||||
};
|
||||
use settled_reach_server::knowledge::types::StableId;
|
||||
use settled_reach_server::simulation::tier::ActiveSim;
|
||||
use settled_reach_server::simulation::time::SimulationTime;
|
||||
use settled_reach_server::simulation::triangle::{
|
||||
RoleId, TemplateId, TriangleClassification, TriangleId, TrianglePhase, TriangleState,
|
||||
};
|
||||
use settled_reach_server::storyteller::{
|
||||
tick_contamination_activation, ContaminationActive, ContaminationEventQueue,
|
||||
CONTAMINATION_DELAY_TICKS, CONTAMINATION_PRESSURE_DELTA,
|
||||
@@ -112,11 +112,15 @@ fn contamination_activates_after_delay() {
|
||||
assert!(
|
||||
world.resource::<ContaminationActive>().0,
|
||||
"ContaminationActive must be true after CONTAMINATION_DELAY_TICKS ({}) ticks",
|
||||
CONTAMINATION_DELAY_TICKS
|
||||
CONTAMINATION_DELAY_TICKS
|
||||
);
|
||||
|
||||
// Assert all ActiveFork triangles have tension > 0
|
||||
for (label, entity) in [("hub-power", fork1), ("bar-tensions", fork2), ("informant-question", fork3)] {
|
||||
for (label, entity) in [
|
||||
("hub-power", fork1),
|
||||
("bar-tensions", fork2),
|
||||
("informant-question", fork3),
|
||||
] {
|
||||
let state = world
|
||||
.get::<TriangleState>(entity)
|
||||
.unwrap_or_else(|| panic!("{} triangle entity should exist", label));
|
||||
@@ -129,13 +133,15 @@ fn contamination_activates_after_delay() {
|
||||
assert_eq!(
|
||||
state.tension, CONTAMINATION_PRESSURE_DELTA,
|
||||
"ActiveFork triangle '{}' tension should be exactly {} (contamination delta)",
|
||||
label,
|
||||
CONTAMINATION_PRESSURE_DELTA
|
||||
label, CONTAMINATION_PRESSURE_DELTA
|
||||
);
|
||||
}
|
||||
|
||||
// Assert PassiveTension triangles were NOT pressured
|
||||
for (label, entity) in [("worried-knowledge", passive1), ("worried-partner", passive2)] {
|
||||
for (label, entity) in [
|
||||
("worried-knowledge", passive1),
|
||||
("worried-partner", passive2),
|
||||
] {
|
||||
let state = world
|
||||
.get::<TriangleState>(entity)
|
||||
.unwrap_or_else(|| panic!("{} triangle entity should exist", label));
|
||||
|
||||
@@ -352,13 +352,13 @@ fn gauntlet_deterministic_replay() {
|
||||
/// path (select_dialogue_line) which consumes SimRng.
|
||||
#[test]
|
||||
fn different_seed_produces_different_replay() {
|
||||
use settled_reach_server::simulation::line_pool::{
|
||||
AccessTier, IndexedDialogueLine, IndexedDialoguePool, LinePoolIndex, Mood, Situation,
|
||||
TrustTier, LinePoolIndexResource,
|
||||
};
|
||||
use settled_reach_server::simulation::dialogue::{
|
||||
CurrentMood, DialogueCooldownTracker, DialogueProfile, DialogueResponseBuffer,
|
||||
};
|
||||
use settled_reach_server::simulation::line_pool::{
|
||||
AccessTier, IndexedDialogueLine, IndexedDialoguePool, LinePoolIndex, LinePoolIndexResource,
|
||||
Mood, Situation, TrustTier,
|
||||
};
|
||||
|
||||
/// Build a deterministic app with dialogue-capable NPCs.
|
||||
fn build_app_with_dialogue(seed: u64) -> App {
|
||||
|
||||
@@ -7,24 +7,22 @@
|
||||
//! - DoorState persists in SaveStateV1.open_doors.
|
||||
|
||||
use bevy_ecs::{prelude::*, schedule::Schedule, world::World};
|
||||
use settled_reach_server::knowledge::KnowledgeGraph;
|
||||
use settled_reach_server::simulation::triangle::TemplateReferenceMap;
|
||||
use settled_reach_server::{
|
||||
knowledge::{registry::EntityRegistry, registry::StableEntityId, types::StableId},
|
||||
npc::relationships::RelationshipGraph,
|
||||
simulation::{
|
||||
examine::{
|
||||
process_examine_interaction, ExamineRequest, ExamineResultBuffer, ExamineText,
|
||||
},
|
||||
examine::{process_examine_interaction, ExamineRequest, ExamineResultBuffer, ExamineText},
|
||||
interaction::{
|
||||
process_door_interaction, process_terminal_interaction, DoorInteractRequest,
|
||||
DoorState, Interactable, ObjectType, TerminalInteractRequest, TerminalInteractedQueue,
|
||||
process_door_interaction, process_terminal_interaction, DoorInteractRequest, DoorState,
|
||||
Interactable, ObjectType, TerminalInteractRequest, TerminalInteractedQueue,
|
||||
},
|
||||
movement::{PlayerCharacter, TilePosition, WalkabilityMap},
|
||||
save_state::{SaveStateV1, SAVE_FORMAT_VERSION},
|
||||
time::{SimulationTime, TickRate},
|
||||
},
|
||||
};
|
||||
use settled_reach_server::knowledge::KnowledgeGraph;
|
||||
use settled_reach_server::simulation::triangle::TemplateReferenceMap;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -39,10 +37,7 @@ fn make_world_with_walkability(width: i32, height: i32) -> World {
|
||||
|
||||
fn spawn_player(world: &mut World, x: i32, y: i32) -> Entity {
|
||||
world
|
||||
.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(x, y, 0),
|
||||
))
|
||||
.spawn((PlayerCharacter, TilePosition::new(x, y, 0)))
|
||||
.id()
|
||||
}
|
||||
|
||||
@@ -85,7 +80,9 @@ fn door_toggle_flips_walkability_both_ways() {
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(
|
||||
world.resource::<WalkabilityMap>().can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
world
|
||||
.resource::<WalkabilityMap>()
|
||||
.can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
"after Open: blocking tile must become walkable"
|
||||
);
|
||||
assert!(
|
||||
@@ -106,7 +103,9 @@ fn door_toggle_flips_walkability_both_ways() {
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(
|
||||
!world.resource::<WalkabilityMap>().can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
!world
|
||||
.resource::<WalkabilityMap>()
|
||||
.can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
"after Close: blocking tile must be impassable again"
|
||||
);
|
||||
assert!(
|
||||
@@ -136,7 +135,9 @@ fn door_starts_open_toggle_closes_it() {
|
||||
|
||||
// Tile starts walkable (default map is all walkable)
|
||||
assert!(
|
||||
world.resource::<WalkabilityMap>().can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
world
|
||||
.resource::<WalkabilityMap>()
|
||||
.can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
"precondition: tile is walkable when door starts open"
|
||||
);
|
||||
|
||||
@@ -150,7 +151,9 @@ fn door_starts_open_toggle_closes_it() {
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(
|
||||
!world.resource::<WalkabilityMap>().can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
!world
|
||||
.resource::<WalkabilityMap>()
|
||||
.can_move_to(&TilePosition::new(10, 5, 0)),
|
||||
"toggling an open door must block the tile"
|
||||
);
|
||||
assert!(
|
||||
@@ -166,9 +169,9 @@ fn door_interact_without_door_state_does_not_panic() {
|
||||
let player = spawn_player(&mut world, 5, 5);
|
||||
let not_a_door = world.spawn(TilePosition::new(5, 6, 0)).id();
|
||||
|
||||
world
|
||||
.entity_mut(player)
|
||||
.insert(DoorInteractRequest { door_entity: not_a_door });
|
||||
world.entity_mut(player).insert(DoorInteractRequest {
|
||||
door_entity: not_a_door,
|
||||
});
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(process_door_interaction);
|
||||
@@ -208,7 +211,9 @@ fn examine_readable_returns_authored_text() {
|
||||
TilePosition::new(5, 6, 0),
|
||||
Interactable,
|
||||
ObjectType::Readable,
|
||||
ExamineText("A logistics manifest. Freight records dating back three cycles.".to_string()),
|
||||
ExamineText(
|
||||
"A logistics manifest. Freight records dating back three cycles.".to_string(),
|
||||
),
|
||||
))
|
||||
.id();
|
||||
|
||||
@@ -220,10 +225,7 @@ fn examine_readable_returns_authored_text() {
|
||||
schedule.add_systems(process_examine_interaction);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let event = world
|
||||
.get_mut::<ExamineResultBuffer>(player)
|
||||
.unwrap()
|
||||
.take();
|
||||
let event = world.get_mut::<ExamineResultBuffer>(player).unwrap().take();
|
||||
assert!(
|
||||
event.is_some(),
|
||||
"ExamineResultBuffer must contain a result after examining a Readable"
|
||||
@@ -274,16 +276,16 @@ fn examine_readable_without_examine_text_returns_fallback() {
|
||||
schedule.add_systems(process_examine_interaction);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let event = world
|
||||
.get_mut::<ExamineResultBuffer>(player)
|
||||
.unwrap()
|
||||
.take();
|
||||
let event = world.get_mut::<ExamineResultBuffer>(player).unwrap().take();
|
||||
assert!(
|
||||
event.is_some(),
|
||||
"ExamineResultBuffer must contain a result even without ExamineText"
|
||||
);
|
||||
let text = event.unwrap().text;
|
||||
assert!(!text.is_empty(), "fallback text must be non-empty, got: '{text}'");
|
||||
assert!(
|
||||
!text.is_empty(),
|
||||
"fallback text must be non-empty, got: '{text}'"
|
||||
);
|
||||
}
|
||||
|
||||
/// Examine out of range returns no result.
|
||||
@@ -322,11 +324,11 @@ fn examine_readable_out_of_range_returns_no_result() {
|
||||
schedule.add_systems(process_examine_interaction);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let event = world
|
||||
.get_mut::<ExamineResultBuffer>(player)
|
||||
.unwrap()
|
||||
.take();
|
||||
assert!(event.is_none(), "examining an out-of-range Readable must not produce a result");
|
||||
let event = world.get_mut::<ExamineResultBuffer>(player).unwrap().take();
|
||||
assert!(
|
||||
event.is_none(),
|
||||
"examining an out-of-range Readable must not produce a result"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -355,19 +357,27 @@ fn terminal_use_emits_terminal_interacted_event() {
|
||||
|
||||
// Register terminal in EntityRegistry so stable ID resolves
|
||||
let terminal_sid = StableId(42);
|
||||
world.entity_mut(terminal).insert(StableEntityId(terminal_sid));
|
||||
world.resource_mut::<EntityRegistry>().register_existing(terminal, terminal_sid);
|
||||
|
||||
world
|
||||
.entity_mut(player)
|
||||
.insert(TerminalInteractRequest { terminal_entity: terminal });
|
||||
.entity_mut(terminal)
|
||||
.insert(StableEntityId(terminal_sid));
|
||||
world
|
||||
.resource_mut::<EntityRegistry>()
|
||||
.register_existing(terminal, terminal_sid);
|
||||
|
||||
world.entity_mut(player).insert(TerminalInteractRequest {
|
||||
terminal_entity: terminal,
|
||||
});
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(process_terminal_interaction);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let queue = world.resource::<TerminalInteractedQueue>();
|
||||
assert_eq!(queue.events.len(), 1, "one TerminalInteracted event must be emitted");
|
||||
assert_eq!(
|
||||
queue.events.len(),
|
||||
1,
|
||||
"one TerminalInteracted event must be emitted"
|
||||
);
|
||||
assert_eq!(
|
||||
queue.events[0].terminal_id, terminal_sid,
|
||||
"terminal_id must match the interacted terminal"
|
||||
@@ -388,9 +398,9 @@ fn terminal_interact_request_consumed_after_processing() {
|
||||
|
||||
let terminal = world.spawn(TilePosition::new(5, 6, 0)).id();
|
||||
|
||||
world
|
||||
.entity_mut(player)
|
||||
.insert(TerminalInteractRequest { terminal_entity: terminal });
|
||||
world.entity_mut(player).insert(TerminalInteractRequest {
|
||||
terminal_entity: terminal,
|
||||
});
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(process_terminal_interaction);
|
||||
|
||||
@@ -106,10 +106,7 @@ fn malformed_input_produces_sim_error_and_server_continues() {
|
||||
.expect("not EOF");
|
||||
let snap1: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&snap1_bytes).expect("deserialize snapshot 1");
|
||||
assert!(
|
||||
snap1.sim_errors.is_empty(),
|
||||
"no errors expected on tick 1"
|
||||
);
|
||||
assert!(snap1.sim_errors.is_empty(), "no errors expected on tick 1");
|
||||
|
||||
// --- Tick 2: send malformed input (properly framed but garbage payload) ---
|
||||
let garbage_payload: Vec<u8> = vec![0xFF, 0xFE, 0xFD, 0xFC, 0xAB, 0xCD, 0xEF];
|
||||
@@ -132,8 +129,12 @@ fn malformed_input_produces_sim_error_and_server_continues() {
|
||||
"error kind must be ProtocolError"
|
||||
);
|
||||
assert!(
|
||||
snap2.sim_errors[0].message.contains("Malformed input frame")
|
||||
|| snap2.sim_errors[0].message.contains("Deserialization error"),
|
||||
snap2.sim_errors[0]
|
||||
.message
|
||||
.contains("Malformed input frame")
|
||||
|| snap2.sim_errors[0]
|
||||
.message
|
||||
.contains("Deserialization error"),
|
||||
"error message should describe the deserialization failure, got: {}",
|
||||
snap2.sim_errors[0].message,
|
||||
);
|
||||
@@ -161,7 +162,9 @@ fn malformed_input_produces_sim_error_and_server_continues() {
|
||||
// Clean up
|
||||
drop(reader);
|
||||
drop(writer);
|
||||
server_handle.join().expect("server thread should not panic");
|
||||
server_handle
|
||||
.join()
|
||||
.expect("server thread should not panic");
|
||||
}
|
||||
|
||||
/// State hash is populated in every snapshot and is deterministic for same state.
|
||||
@@ -206,8 +209,7 @@ fn state_hash_populated_in_snapshot() {
|
||||
let snap_bytes = read_framed(&mut reader)
|
||||
.expect("read snapshot")
|
||||
.expect("not EOF");
|
||||
let snap: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&snap_bytes).expect("deserialize snapshot");
|
||||
let snap: ObserverSnapshot = rmp_serde::from_slice(&snap_bytes).expect("deserialize snapshot");
|
||||
|
||||
assert!(
|
||||
snap.state_hash.is_some(),
|
||||
@@ -221,7 +223,9 @@ fn state_hash_populated_in_snapshot() {
|
||||
|
||||
drop(reader);
|
||||
drop(writer);
|
||||
server_handle.join().expect("server thread should not panic");
|
||||
server_handle
|
||||
.join()
|
||||
.expect("server thread should not panic");
|
||||
}
|
||||
|
||||
/// SimError roundtrips through MessagePack serialization.
|
||||
@@ -296,13 +300,11 @@ fn snapshot_with_sim_errors_roundtrips() {
|
||||
triangle_crisis_events: vec![],
|
||||
state_hash: Some(0xDEADBEEF),
|
||||
debug_response: None,
|
||||
sim_errors: vec![
|
||||
SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: "bad frame".into(),
|
||||
tick: 10,
|
||||
},
|
||||
],
|
||||
sim_errors: vec![SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: "bad frame".into(),
|
||||
tick: 10,
|
||||
}],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
};
|
||||
|
||||
@@ -79,9 +79,9 @@ fn generate_map(seed: u64) -> ProceduralMap {
|
||||
let y: i32 = rng.random_range(2_i32..(MAP_H - h - 2));
|
||||
|
||||
// Reject if overlaps an existing room (1-tile padding).
|
||||
let overlaps = rooms.iter().any(|r| {
|
||||
x < r.x + r.w + 1 && x + w + 1 > r.x && y < r.y + r.h + 1 && y + h + 1 > r.y
|
||||
});
|
||||
let overlaps = rooms
|
||||
.iter()
|
||||
.any(|r| x < r.x + r.w + 1 && x + w + 1 > r.x && y < r.y + r.h + 1 && y + h + 1 > r.y);
|
||||
|
||||
if !overlaps {
|
||||
for ry in y..(y + h) {
|
||||
@@ -153,11 +153,8 @@ fn generate_map(seed: u64) -> ProceduralMap {
|
||||
}
|
||||
|
||||
// Player start: centre of first room (always walkable by construction).
|
||||
let player_start = TilePosition::new(
|
||||
rooms[0].x + rooms[0].w / 2,
|
||||
rooms[0].y + rooms[0].h / 2,
|
||||
0,
|
||||
);
|
||||
let player_start =
|
||||
TilePosition::new(rooms[0].x + rooms[0].w / 2, rooms[0].y + rooms[0].h / 2, 0);
|
||||
|
||||
let mut entities = vec![player_start];
|
||||
entities.extend(door_placements.iter().map(|d| d.pos));
|
||||
|
||||
@@ -7,10 +7,10 @@ use settled_reach_server::bridge::tcp::TcpBridge;
|
||||
use settled_reach_server::bridge::types::*;
|
||||
use settled_reach_server::bridge::{BridgePlugin, BridgeResource};
|
||||
use settled_reach_server::knowledge::{KnowledgeGraph, KnowledgePlugin};
|
||||
use settled_reach_server::npc::relationships::TrustEventQueue;
|
||||
use settled_reach_server::simulation::interaction::NearbyInteractionBuffer;
|
||||
use settled_reach_server::simulation::monologue::{MonologueBuffer, MonologueState};
|
||||
use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
|
||||
use settled_reach_server::npc::relationships::TrustEventQueue;
|
||||
use settled_reach_server::simulation::SimulationPlugin;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
@@ -45,7 +45,10 @@ fn build_gauntlet(seed: u64) -> App {
|
||||
app.add_plugins(NpcPlugin);
|
||||
app.insert_resource(SimRng::new(seed));
|
||||
|
||||
test_world::setup_gauntlet(&mut app, settled_reach_server::bridge::types::CharacterArchetype::default());
|
||||
test_world::setup_gauntlet(
|
||||
&mut app,
|
||||
settled_reach_server::bridge::types::CharacterArchetype::default(),
|
||||
);
|
||||
|
||||
app
|
||||
}
|
||||
@@ -60,9 +63,7 @@ fn teleport_player(app: &mut App, pos: TilePosition, facing: Facing) {
|
||||
.query_filtered::<Entity, With<PlayerCharacter>>();
|
||||
query.single(app.world()).expect("player entity must exist")
|
||||
};
|
||||
app.world_mut()
|
||||
.entity_mut(player)
|
||||
.insert((pos, facing));
|
||||
app.world_mut().entity_mut(player).insert((pos, facing));
|
||||
}
|
||||
|
||||
/// Run N ticks, feeding inputs each tick, return the last snapshot.
|
||||
|
||||
@@ -23,21 +23,21 @@
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::schedule::Schedule;
|
||||
|
||||
use settled_reach_server::bridge::types::FacingDirection;
|
||||
use settled_reach_server::knowledge::events::{
|
||||
process_knowledge_events, KnowledgeEvent, KnowledgeEventQueue, KnowledgeEventType,
|
||||
};
|
||||
use settled_reach_server::knowledge::{
|
||||
ContradictionDetectedQueue, EntityRegistry, KnowledgeGraph,
|
||||
};
|
||||
use settled_reach_server::knowledge::types::StableId;
|
||||
use settled_reach_server::npc::{Npc, SecretSeverity};
|
||||
use settled_reach_server::knowledge::{ContradictionDetectedQueue, EntityRegistry, KnowledgeGraph};
|
||||
use settled_reach_server::npc::relationships::RelationshipGraph;
|
||||
use settled_reach_server::npc::{Npc, SecretSeverity};
|
||||
use settled_reach_server::perception::query::{NaturalVision, PerceptionQuery};
|
||||
use settled_reach_server::simulation::movement::{TilePosition, WalkabilityMap};
|
||||
use settled_reach_server::simulation::save_state::{NpcSaveState, SaveStateV1, SAVE_FORMAT_VERSION};
|
||||
use settled_reach_server::simulation::save_state::{
|
||||
NpcSaveState, SaveStateV1, SAVE_FORMAT_VERSION,
|
||||
};
|
||||
use settled_reach_server::simulation::tier::{ActiveSim, BackgroundSim};
|
||||
use settled_reach_server::simulation::time::TickRate;
|
||||
use settled_reach_server::bridge::types::FacingDirection;
|
||||
|
||||
// ===========================================================================
|
||||
// Layer 1 — Pure unit: no ECS world, no subprocess
|
||||
@@ -70,9 +70,7 @@ fn player_kg_has_no_passive_npc_leakage() {
|
||||
// Negative assertion: spawning a bare ECS entity doesn't populate a KG.
|
||||
// The knowledge graph is a component, not a global shared resource.
|
||||
let mut world = World::new();
|
||||
let player = world
|
||||
.spawn(KnowledgeGraph::new())
|
||||
.id();
|
||||
let player = world.spawn(KnowledgeGraph::new()).id();
|
||||
|
||||
// Spawn an NPC in the same world — no observation system runs.
|
||||
let _npc = world.spawn((Npc, TilePosition::new(50, 50, 0))).id();
|
||||
@@ -110,7 +108,9 @@ fn snapshot_excludes_entities_outside_los() {
|
||||
// --- Far entity: 45 tiles away, well outside FOV range (~12 tiles) ---
|
||||
let far_npc_pos = TilePosition::new(50, 5, 0);
|
||||
assert!(
|
||||
!geometry.visible_positions.contains(&(far_npc_pos.x, far_npc_pos.y)),
|
||||
!geometry
|
||||
.visible_positions
|
||||
.contains(&(far_npc_pos.x, far_npc_pos.y)),
|
||||
"IB-2: entity at {:?} (45 tiles from observer) must NOT be in FOV — \
|
||||
observer snapshot would exclude this entity (fog of perception, D-010 principle 2)",
|
||||
far_npc_pos
|
||||
@@ -118,14 +118,18 @@ fn snapshot_excludes_entities_outside_los() {
|
||||
|
||||
// --- Sanity check: the observer's own position is visible ---
|
||||
assert!(
|
||||
geometry.visible_positions.contains(&(observer_pos.x, observer_pos.y)),
|
||||
geometry
|
||||
.visible_positions
|
||||
.contains(&(observer_pos.x, observer_pos.y)),
|
||||
"IB-2 sanity: observer's own position must always be in the FOV set"
|
||||
);
|
||||
|
||||
// --- Additional sanity: tile directly ahead (1 step north) is visible ---
|
||||
let adjacent_pos = TilePosition::new(5, 4, 0);
|
||||
assert!(
|
||||
geometry.visible_positions.contains(&(adjacent_pos.x, adjacent_pos.y)),
|
||||
geometry
|
||||
.visible_positions
|
||||
.contains(&(adjacent_pos.x, adjacent_pos.y)),
|
||||
"IB-2 sanity: tile directly ahead of observer must be visible"
|
||||
);
|
||||
}
|
||||
@@ -272,9 +276,7 @@ fn background_npc_kg_not_updated_by_active_tier_events() {
|
||||
world.init_resource::<EntityRegistry>();
|
||||
|
||||
// Active-tier NPC: will be the observer in the knowledge event.
|
||||
let active_npc = world
|
||||
.spawn((Npc, ActiveSim, KnowledgeGraph::new()))
|
||||
.id();
|
||||
let active_npc = world.spawn((Npc, ActiveSim, KnowledgeGraph::new())).id();
|
||||
|
||||
// Background-tier NPC: must NOT be affected.
|
||||
let background_npc = world
|
||||
|
||||
@@ -102,11 +102,9 @@ fn ipc_round_trip_latency() {
|
||||
let handshake: HandshakeMessage =
|
||||
rmp_serde::from_slice(&handshake_bytes).expect("deserialize HandshakeMessage");
|
||||
assert_eq!(
|
||||
handshake.protocol_version,
|
||||
PROTOCOL_VERSION,
|
||||
handshake.protocol_version, PROTOCOL_VERSION,
|
||||
"handshake version mismatch: server={}, client={}",
|
||||
handshake.protocol_version,
|
||||
PROTOCOL_VERSION
|
||||
handshake.protocol_version, PROTOCOL_VERSION
|
||||
);
|
||||
|
||||
let make_input = |tick: u64| PlayerInput {
|
||||
|
||||
+40
-16
@@ -24,7 +24,7 @@ use std::path::PathBuf;
|
||||
fn ticker_yaml_path() -> PathBuf {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
PathBuf::from(manifest_dir)
|
||||
.join("../content/campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/ticker/the-last-shift.yaml")
|
||||
.join("content/campaigns/main/systems/van-maanens-star/stations/sova/districts/transit/ticker/the-last-shift.yaml")
|
||||
}
|
||||
|
||||
/// Minimal YAML structure for parsing just what we need to validate.
|
||||
@@ -83,7 +83,9 @@ fn ticker_yaml_has_30_headlines() {
|
||||
#[test]
|
||||
fn ticker_yaml_location_is_the_last_shift() {
|
||||
let path = ticker_yaml_path();
|
||||
if !path.exists() { return; }
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).expect("read ticker YAML");
|
||||
let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML");
|
||||
@@ -101,26 +103,26 @@ fn ticker_yaml_location_is_the_last_shift() {
|
||||
#[test]
|
||||
fn ticker_yaml_all_headlines_have_required_fields() {
|
||||
let path = ticker_yaml_path();
|
||||
if !path.exists() { return; }
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).expect("read ticker YAML");
|
||||
let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML");
|
||||
|
||||
for (i, headline) in file.headlines.iter().enumerate() {
|
||||
assert!(
|
||||
!headline.id.is_empty(),
|
||||
"Headline[{}] missing id field",
|
||||
i
|
||||
);
|
||||
assert!(!headline.id.is_empty(), "Headline[{}] missing id field", i);
|
||||
assert!(
|
||||
!headline.text.is_empty(),
|
||||
"Headline[{}] (id={}) has empty text",
|
||||
i, headline.id
|
||||
i,
|
||||
headline.id
|
||||
);
|
||||
assert!(
|
||||
!headline.category.is_empty(),
|
||||
"Headline[{}] (id={}) missing category",
|
||||
i, headline.id
|
||||
i,
|
||||
headline.id
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,7 +130,9 @@ fn ticker_yaml_all_headlines_have_required_fields() {
|
||||
#[test]
|
||||
fn ticker_yaml_ids_are_unique() {
|
||||
let path = ticker_yaml_path();
|
||||
if !path.exists() { return; }
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).expect("read ticker YAML");
|
||||
let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML");
|
||||
@@ -146,10 +150,19 @@ fn ticker_yaml_ids_are_unique() {
|
||||
#[test]
|
||||
fn ticker_yaml_categories_are_valid() {
|
||||
// D-036 defines 6 categories: freight, politics, infrastructure, sports, commission, community
|
||||
let valid_categories = ["freight", "politics", "infrastructure", "sports", "commission", "community"];
|
||||
let valid_categories = [
|
||||
"freight",
|
||||
"politics",
|
||||
"infrastructure",
|
||||
"sports",
|
||||
"commission",
|
||||
"community",
|
||||
];
|
||||
|
||||
let path = ticker_yaml_path();
|
||||
if !path.exists() { return; }
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).expect("read ticker YAML");
|
||||
let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML");
|
||||
@@ -158,7 +171,9 @@ fn ticker_yaml_categories_are_valid() {
|
||||
assert!(
|
||||
valid_categories.contains(&headline.category.as_str()),
|
||||
"Headline '{}' has unknown category '{}'. Valid categories: {:?}",
|
||||
headline.id, headline.category, valid_categories
|
||||
headline.id,
|
||||
headline.category,
|
||||
valid_categories
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -168,7 +183,9 @@ fn ticker_yaml_category_distribution_is_sane() {
|
||||
// Comment in the YAML: freight (9), politics (4), infrastructure (5), sports (3),
|
||||
// commission (5), community (4) = 30 total. Verify no category is completely absent.
|
||||
let path = ticker_yaml_path();
|
||||
if !path.exists() { return; }
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).expect("read ticker YAML");
|
||||
let file: TickerFile = serde_yaml::from_str(&content).expect("parse ticker YAML");
|
||||
@@ -178,7 +195,14 @@ fn ticker_yaml_category_distribution_is_sane() {
|
||||
*counts.entry(headline.category.clone()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
for cat in ["freight", "politics", "infrastructure", "sports", "commission", "community"] {
|
||||
for cat in [
|
||||
"freight",
|
||||
"politics",
|
||||
"infrastructure",
|
||||
"sports",
|
||||
"commission",
|
||||
"community",
|
||||
] {
|
||||
assert!(
|
||||
*counts.get(cat).unwrap_or(&0) > 0,
|
||||
"Category '{}' has no headlines — content is missing or miscategorized",
|
||||
|
||||
@@ -15,12 +15,12 @@ use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
||||
use settled_reach_server::bridge::tcp::TcpBridge;
|
||||
use settled_reach_server::bridge::types::*;
|
||||
use settled_reach_server::bridge::{BridgePlugin, BridgeResource};
|
||||
use settled_reach_server::simulation::line_pool::{LinePoolIndex, LinePoolIndexResource};
|
||||
use settled_reach_server::knowledge::registry::EntityRegistry;
|
||||
use settled_reach_server::knowledge::KnowledgeGraph;
|
||||
use settled_reach_server::perception::cognitive_delay::CognitiveDelay;
|
||||
use settled_reach_server::perception::vision_cone::Facing;
|
||||
use settled_reach_server::simulation::interaction::NearbyInteractionBuffer;
|
||||
use settled_reach_server::simulation::line_pool::{LinePoolIndex, LinePoolIndexResource};
|
||||
use settled_reach_server::simulation::listening::ListeningFocus;
|
||||
use settled_reach_server::simulation::monologue::{
|
||||
MonologueBuffer, MonologueState, SprintAnomalyQueue,
|
||||
@@ -33,7 +33,6 @@ const WARMUP_TICKS: usize = 5;
|
||||
const MEASURE_TICKS: usize = 50;
|
||||
const TOTAL_TICKS: usize = WARMUP_TICKS + MEASURE_TICKS;
|
||||
|
||||
|
||||
fn read_rss_kb() -> Option<u64> {
|
||||
std::fs::read_to_string("/proc/self/status")
|
||||
.ok()
|
||||
|
||||
@@ -404,7 +404,7 @@ fn all_facing_direction_variants_roundtrip() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
character_pressure: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
@@ -412,7 +412,7 @@ fn all_facing_direction_variants_roundtrip() {
|
||||
save_result: None,
|
||||
triangle_crisis_events: vec![],
|
||||
state_hash: None,
|
||||
debug_response: None,
|
||||
debug_response: None,
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
@@ -1706,8 +1706,7 @@ fn fixture_snapshot_minimal_fields() {
|
||||
#[test]
|
||||
fn fixture_snapshot_full_fields() {
|
||||
let bytes = read_named_fixture("snapshot_full");
|
||||
let snap: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize snapshot_full");
|
||||
let snap: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize snapshot_full");
|
||||
|
||||
assert_eq!(snap.version, PROTOCOL_VERSION, "protocol version mismatch");
|
||||
assert_eq!(snap.tick, 42, "tick should be 42");
|
||||
@@ -1715,10 +1714,7 @@ fn fixture_snapshot_full_fields() {
|
||||
// Monologue
|
||||
let monologue = snap.current_monologue.as_ref().expect("monologue absent");
|
||||
assert_eq!(monologue.id, "test_monologue_001");
|
||||
assert_eq!(
|
||||
monologue.text,
|
||||
"Something feels off about this place."
|
||||
);
|
||||
assert_eq!(monologue.text, "Something feels off about this place.");
|
||||
|
||||
// Dialogue
|
||||
let dialogue = snap.dialogue_response.as_ref().expect("dialogue absent");
|
||||
@@ -1738,7 +1734,10 @@ fn fixture_snapshot_full_fields() {
|
||||
assert_eq!(examine.entity_id, 42);
|
||||
|
||||
// Player knowledge
|
||||
let kg = snap.player_knowledge.as_ref().expect("player_knowledge absent");
|
||||
let kg = snap
|
||||
.player_knowledge
|
||||
.as_ref()
|
||||
.expect("player_knowledge absent");
|
||||
assert_eq!(kg.entities.len(), 1);
|
||||
assert_eq!(kg.entities[0].name, "Kael");
|
||||
assert_eq!(kg.facts.len(), 1);
|
||||
@@ -1758,8 +1757,7 @@ fn fixture_snapshot_full_fields() {
|
||||
#[test]
|
||||
fn fixture_player_input_move_fields() {
|
||||
let bytes = read_named_fixture("player_input_move");
|
||||
let input: PlayerInput =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize player_input_move");
|
||||
let input: PlayerInput = rmp_serde::from_slice(&bytes).expect("deserialize player_input_move");
|
||||
|
||||
assert_eq!(input.tick, 1, "tick should be 1");
|
||||
assert!(
|
||||
@@ -1780,7 +1778,11 @@ fn fixture_player_input_interact_fields() {
|
||||
target_entity_id,
|
||||
verb,
|
||||
} => {
|
||||
assert_eq!(*target_entity_id, Some(99u64), "target_entity_id should be Some(99)");
|
||||
assert_eq!(
|
||||
*target_entity_id,
|
||||
Some(99u64),
|
||||
"target_entity_id should be Some(99)"
|
||||
);
|
||||
assert_eq!(
|
||||
verb.as_deref(),
|
||||
Some("Talk"),
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use settled_reach_server::{
|
||||
npc::mood::MoodState,
|
||||
npc::{
|
||||
tell_state::{DerivedTellState, TellCategory},
|
||||
Contentment, DeviationTrigger, Npc, RoutineDeviation, Secret, SecretSeverity,
|
||||
ToleranceThreshold,
|
||||
},
|
||||
npc::mood::MoodState,
|
||||
simulation::tier::ActiveSim,
|
||||
};
|
||||
|
||||
@@ -42,9 +42,15 @@ fn make_tell_world_with_deviation(deviation: Option<RoutineDeviation>) -> (World
|
||||
severity: SecretSeverity::Minor,
|
||||
known_by: vec![],
|
||||
},
|
||||
ToleranceThreshold { current_stress: 0, threshold: 50 },
|
||||
ToleranceThreshold {
|
||||
current_stress: 0,
|
||||
threshold: 50,
|
||||
},
|
||||
Contentment { level: 0 },
|
||||
MoodState { mood: settled_reach_server::npc::mood::NpcMood::Neutral, changed_tick: 0 },
|
||||
MoodState {
|
||||
mood: settled_reach_server::npc::mood::NpcMood::Neutral,
|
||||
changed_tick: 0,
|
||||
},
|
||||
DerivedTellState::default(),
|
||||
));
|
||||
let entity = if let Some(dev) = deviation {
|
||||
@@ -103,9 +109,7 @@ fn no_deviation_component_does_not_produce_deviation_tell() {
|
||||
#[cfg(feature = "gauntlet")]
|
||||
fn build_storyteller_app() -> App {
|
||||
use settled_reach_server::{
|
||||
bridge::types::CharacterArchetype,
|
||||
simulation::SimulationPlugin,
|
||||
test_world,
|
||||
bridge::types::CharacterArchetype, simulation::SimulationPlugin, test_world,
|
||||
};
|
||||
let mut app = App::new();
|
||||
app.add_plugins(SimulationPlugin);
|
||||
@@ -118,7 +122,9 @@ fn build_storyteller_app() -> App {
|
||||
fn first_npc_entity(app: &mut App) -> Entity {
|
||||
use settled_reach_server::npc::Npc;
|
||||
let mut q = app.world_mut().query_filtered::<Entity, With<Npc>>();
|
||||
q.iter(app.world()).next().expect("gauntlet must have at least one NPC")
|
||||
q.iter(app.world())
|
||||
.next()
|
||||
.expect("gauntlet must have at least one NPC")
|
||||
}
|
||||
|
||||
#[cfg(feature = "gauntlet")]
|
||||
@@ -127,8 +133,8 @@ fn first_npc_entity(app: &mut App) -> Entity {
|
||||
fn triangle_activation_event_inserts_routine_deviation_on_anchor_npc() {
|
||||
// Inject a TriangleActivatedEvent pointing to an NPC, run one tick, assert
|
||||
// RoutineDeviation is inserted on that NPC by escalate_tells_on_activation.
|
||||
use settled_reach_server::simulation::triangle::TriangleId;
|
||||
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
||||
use settled_reach_server::simulation::triangle::{TriangleId};
|
||||
|
||||
let mut app = build_storyteller_app();
|
||||
// Run one tick so the world is fully initialized before we inject
|
||||
@@ -162,8 +168,8 @@ fn triangle_activation_event_inserts_routine_deviation_on_anchor_npc() {
|
||||
fn triangle_activation_produces_routine_deviation_tell_in_snapshot() {
|
||||
// End-to-end: after activation event, DerivedTellState on anchor NPC must be
|
||||
// TellCategory::RoutineDeviation. This verifies the full axis-9 pipeline.
|
||||
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
||||
use settled_reach_server::simulation::triangle::TriangleId;
|
||||
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
||||
|
||||
let mut app = build_storyteller_app();
|
||||
app.update(); // initialize
|
||||
@@ -198,8 +204,8 @@ fn routine_deviation_expires_after_duration() {
|
||||
//
|
||||
// Edge case: D-027 criterion 4 must continue to fire DURING the window
|
||||
// and stop firing AFTER it. NPCs shouldn't be permanently flagged.
|
||||
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
||||
use settled_reach_server::simulation::triangle::TriangleId;
|
||||
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
||||
// NOTE: TELL_ESCALATION_DURATION_TICKS constant (= 300) expected in storyteller module.
|
||||
// This test will need updating once the constant is public.
|
||||
|
||||
|
||||
@@ -13,17 +13,14 @@ use std::collections::BTreeMap;
|
||||
|
||||
use bevy_ecs::{schedule::Schedule, world::World};
|
||||
use settled_reach_server::{
|
||||
knowledge::{registry::EntityRegistry, types::StableId, StableEntityId},
|
||||
npc::ToleranceThreshold,
|
||||
simulation::triangle::{
|
||||
apply_resolve_triangle, tick_triangle_escalation, ResolveTriangleCommand,
|
||||
ResolveTriangleQueue, TemplateId, TriangleClassification, TriangleCrisisEventQueue,
|
||||
TriangleDef, TriangleId, TrianglePhase, TriangleState,
|
||||
},
|
||||
knowledge::{registry::EntityRegistry, types::StableId, StableEntityId},
|
||||
npc::ToleranceThreshold,
|
||||
simulation::{
|
||||
tier::ActiveSim,
|
||||
time::SimulationTime,
|
||||
},
|
||||
simulation::{tier::ActiveSim, time::SimulationTime},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -43,9 +40,18 @@ fn make_escalation_world() -> World {
|
||||
fn spawn_npc_with_threshold(world: &mut World, stable_id_val: u64, threshold: i16) -> StableId {
|
||||
let sid = StableId(stable_id_val);
|
||||
let entity = world
|
||||
.spawn((ActiveSim, StableEntityId(sid), ToleranceThreshold { current_stress: 0, threshold }))
|
||||
.spawn((
|
||||
ActiveSim,
|
||||
StableEntityId(sid),
|
||||
ToleranceThreshold {
|
||||
current_stress: 0,
|
||||
threshold,
|
||||
},
|
||||
))
|
||||
.id();
|
||||
world.resource_mut::<EntityRegistry>().register_existing(entity, sid);
|
||||
world
|
||||
.resource_mut::<EntityRegistry>()
|
||||
.register_existing(entity, sid);
|
||||
sid
|
||||
}
|
||||
|
||||
@@ -190,7 +196,14 @@ fn d087_seed_dependent_escalation_timing() {
|
||||
assignments.insert(RoleId::new("r"), npc);
|
||||
|
||||
// Spawn as separate triangles.
|
||||
let slow = spawn_triangle(&mut world, 10, 0, 2, TrianglePhase::Simmering, assignments.clone());
|
||||
let slow = spawn_triangle(
|
||||
&mut world,
|
||||
10,
|
||||
0,
|
||||
2,
|
||||
TrianglePhase::Simmering,
|
||||
assignments.clone(),
|
||||
);
|
||||
let fast = spawn_triangle(&mut world, 20, 0, 8, TrianglePhase::Simmering, assignments);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
@@ -202,8 +215,14 @@ fn d087_seed_dependent_escalation_timing() {
|
||||
}
|
||||
|
||||
// Both should be Active by 400 ticks.
|
||||
assert_eq!(world.get::<TriangleState>(slow).unwrap().phase, TrianglePhase::Active);
|
||||
assert_eq!(world.get::<TriangleState>(fast).unwrap().phase, TrianglePhase::Active);
|
||||
assert_eq!(
|
||||
world.get::<TriangleState>(slow).unwrap().phase,
|
||||
TrianglePhase::Active
|
||||
);
|
||||
assert_eq!(
|
||||
world.get::<TriangleState>(fast).unwrap().phase,
|
||||
TrianglePhase::Active
|
||||
);
|
||||
|
||||
// Fast triangle should have activated earlier (higher tension accumulated faster).
|
||||
let fast_tension = world.get::<TriangleState>(fast).unwrap().tension;
|
||||
@@ -220,7 +239,7 @@ fn crisis_event_trigger_npc_is_lowest_threshold() {
|
||||
let mut world = make_escalation_world();
|
||||
|
||||
let npc_high = spawn_npc_with_threshold(&mut world, 1, 50); // high tolerance
|
||||
let npc_low = spawn_npc_with_threshold(&mut world, 2, 10); // low tolerance — trigger
|
||||
let npc_low = spawn_npc_with_threshold(&mut world, 2, 10); // low tolerance — trigger
|
||||
|
||||
use settled_reach_server::simulation::triangle::RoleId;
|
||||
let mut assignments = BTreeMap::new();
|
||||
@@ -240,7 +259,10 @@ fn crisis_event_trigger_npc_is_lowest_threshold() {
|
||||
queue.events[0].trigger_npc, npc_low,
|
||||
"trigger NPC must be the one with the lowest threshold"
|
||||
);
|
||||
assert_eq!(queue.events[0].tick, 10, "crisis tick must match the game-minute");
|
||||
assert_eq!(
|
||||
queue.events[0].tick, 10,
|
||||
"crisis tick must match the game-minute"
|
||||
);
|
||||
}
|
||||
|
||||
/// No crisis event when tension hasn't exceeded the threshold.
|
||||
@@ -260,7 +282,10 @@ fn no_crisis_event_below_threshold() {
|
||||
run_at_tick(&mut world, &mut schedule, 10);
|
||||
|
||||
let queue = world.resource::<TriangleCrisisEventQueue>();
|
||||
assert!(queue.is_empty(), "no crisis event when tension (5) < threshold (100)");
|
||||
assert!(
|
||||
queue.is_empty(),
|
||||
"no crisis event when tension (5) < threshold (100)"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -281,28 +306,49 @@ fn active_triangle_continues_incrementing_no_new_event() {
|
||||
run_at_tick(&mut world, &mut schedule, 20);
|
||||
|
||||
let queue = world.resource::<TriangleCrisisEventQueue>();
|
||||
assert!(queue.is_empty(), "no crisis event for already-Active triangle");
|
||||
assert!(
|
||||
queue.is_empty(),
|
||||
"no crisis event for already-Active triangle"
|
||||
);
|
||||
}
|
||||
|
||||
/// Active triangle tension saturates at u8::MAX (255).
|
||||
#[test]
|
||||
fn active_triangle_tension_saturates_at_u8_max() {
|
||||
let mut world = make_escalation_world();
|
||||
spawn_triangle(&mut world, 1, 252, 10, TrianglePhase::Active, BTreeMap::new());
|
||||
spawn_triangle(
|
||||
&mut world,
|
||||
1,
|
||||
252,
|
||||
10,
|
||||
TrianglePhase::Active,
|
||||
BTreeMap::new(),
|
||||
);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(tick_triangle_escalation);
|
||||
run_at_tick(&mut world, &mut schedule, 10);
|
||||
|
||||
// First call: 252 + 10 = 262, saturates to 255
|
||||
let entity = world.query::<bevy_ecs::entity::Entity>().iter(&world).next().unwrap();
|
||||
let entity = world
|
||||
.query::<bevy_ecs::entity::Entity>()
|
||||
.iter(&world)
|
||||
.next()
|
||||
.unwrap();
|
||||
// Can't query TriangleState after mutable borrow; check via resource
|
||||
// (we verify by spawning directly and checking post-run)
|
||||
let _ = entity; // entity used to ensure spawn worked
|
||||
|
||||
// Re-run test cleanly
|
||||
let mut world2 = make_escalation_world();
|
||||
let e2 = spawn_triangle(&mut world2, 2, 254, 50, TrianglePhase::Active, BTreeMap::new());
|
||||
let e2 = spawn_triangle(
|
||||
&mut world2,
|
||||
2,
|
||||
254,
|
||||
50,
|
||||
TrianglePhase::Active,
|
||||
BTreeMap::new(),
|
||||
);
|
||||
let mut sched2 = Schedule::default();
|
||||
sched2.add_systems(tick_triangle_escalation);
|
||||
run_at_tick(&mut world2, &mut sched2, 10);
|
||||
@@ -349,7 +395,14 @@ fn d026_non_active_tier_triangle_not_escalated() {
|
||||
#[test]
|
||||
fn dormant_triangle_not_escalated() {
|
||||
let mut world = make_escalation_world();
|
||||
let entity = spawn_triangle(&mut world, 1, 0, 10, TrianglePhase::Dormant, BTreeMap::new());
|
||||
let entity = spawn_triangle(
|
||||
&mut world,
|
||||
1,
|
||||
0,
|
||||
10,
|
||||
TrianglePhase::Dormant,
|
||||
BTreeMap::new(),
|
||||
);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(tick_triangle_escalation);
|
||||
@@ -366,7 +419,14 @@ fn dormant_triangle_not_escalated() {
|
||||
#[test]
|
||||
fn resolved_triangle_not_escalated() {
|
||||
let mut world = make_escalation_world();
|
||||
let entity = spawn_triangle(&mut world, 1, 50, 5, TrianglePhase::Resolved, BTreeMap::new());
|
||||
let entity = spawn_triangle(
|
||||
&mut world,
|
||||
1,
|
||||
50,
|
||||
5,
|
||||
TrianglePhase::Resolved,
|
||||
BTreeMap::new(),
|
||||
);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(tick_triangle_escalation);
|
||||
@@ -401,14 +461,20 @@ fn resolve_command_sets_phase_to_resolved() {
|
||||
})
|
||||
.id();
|
||||
|
||||
world.resource_mut::<ResolveTriangleQueue>().push(ResolveTriangleCommand(TriangleId(100)));
|
||||
world
|
||||
.resource_mut::<ResolveTriangleQueue>()
|
||||
.push(ResolveTriangleCommand(TriangleId(100)));
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(apply_resolve_triangle);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let state = world.get::<TriangleState>(entity).unwrap();
|
||||
assert_eq!(state.phase, TrianglePhase::Resolved, "resolve command must set phase to Resolved");
|
||||
assert_eq!(
|
||||
state.phase,
|
||||
TrianglePhase::Resolved,
|
||||
"resolve command must set phase to Resolved"
|
||||
);
|
||||
assert_eq!(state.tension, 50, "tension must not change on resolve");
|
||||
}
|
||||
|
||||
@@ -455,7 +521,9 @@ fn d089_resolve_does_not_cascade() {
|
||||
.id();
|
||||
|
||||
// Resolve only triangle 100.
|
||||
world.resource_mut::<ResolveTriangleQueue>().push(ResolveTriangleCommand(TriangleId(100)));
|
||||
world
|
||||
.resource_mut::<ResolveTriangleQueue>()
|
||||
.push(ResolveTriangleCommand(TriangleId(100)));
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(apply_resolve_triangle);
|
||||
@@ -498,9 +566,13 @@ fn resolve_twice_is_idempotent() {
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(apply_resolve_triangle);
|
||||
|
||||
world.resource_mut::<ResolveTriangleQueue>().push(ResolveTriangleCommand(TriangleId(100)));
|
||||
world
|
||||
.resource_mut::<ResolveTriangleQueue>()
|
||||
.push(ResolveTriangleCommand(TriangleId(100)));
|
||||
schedule.run(&mut world);
|
||||
world.resource_mut::<ResolveTriangleQueue>().push(ResolveTriangleCommand(TriangleId(100)));
|
||||
world
|
||||
.resource_mut::<ResolveTriangleQueue>()
|
||||
.push(ResolveTriangleCommand(TriangleId(100)));
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert_eq!(
|
||||
@@ -526,7 +598,14 @@ fn crisis_events_accumulate_until_drained() {
|
||||
assignments.insert(RoleId::new("r"), npc);
|
||||
|
||||
// Two triangles that will both escalate.
|
||||
spawn_triangle(&mut world, 10, 0, 6, TrianglePhase::Simmering, assignments.clone());
|
||||
spawn_triangle(
|
||||
&mut world,
|
||||
10,
|
||||
0,
|
||||
6,
|
||||
TrianglePhase::Simmering,
|
||||
assignments.clone(),
|
||||
);
|
||||
spawn_triangle(&mut world, 20, 0, 6, TrianglePhase::Simmering, assignments);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
@@ -578,16 +657,37 @@ fn d087_all_v01_conflict_types_produce_escalatable_states() {
|
||||
use settled_reach_server::simulation::triangle::{ConflictType, NpcAxis, RoleId};
|
||||
|
||||
let defs = [
|
||||
("kael-davan", "smuggler", "ring-contact", ConflictType::ResourceCompetition),
|
||||
("sera-venn", "detective", "commission-inspector", ConflictType::SecretExposure),
|
||||
(
|
||||
"kael-davan",
|
||||
"smuggler",
|
||||
"ring-contact",
|
||||
ConflictType::ResourceCompetition,
|
||||
),
|
||||
(
|
||||
"sera-venn",
|
||||
"detective",
|
||||
"commission-inspector",
|
||||
ConflictType::SecretExposure,
|
||||
),
|
||||
("naia", "kael-davan", "hael", ConflictType::LatentTension),
|
||||
("drin", "ring-system", "dock-supervisor", ConflictType::ResourceCompetition),
|
||||
("worried-partner", "ring-member", "neighbor", ConflictType::LatentTension),
|
||||
(
|
||||
"drin",
|
||||
"ring-system",
|
||||
"dock-supervisor",
|
||||
ConflictType::ResourceCompetition,
|
||||
),
|
||||
(
|
||||
"worried-partner",
|
||||
"ring-member",
|
||||
"neighbor",
|
||||
ConflictType::LatentTension,
|
||||
),
|
||||
];
|
||||
|
||||
for (r0, r1, r2, conflict) in &defs {
|
||||
let roles = [RoleId::new(r0), RoleId::new(r1), RoleId::new(r2)];
|
||||
let tid = settled_reach_server::simulation::triangle::TriangleId::from_seed_and_roles(42, &roles);
|
||||
let tid =
|
||||
settled_reach_server::simulation::triangle::TriangleId::from_seed_and_roles(42, &roles);
|
||||
let def = TriangleDef {
|
||||
triangle_id: tid,
|
||||
roles: roles.clone(),
|
||||
@@ -595,7 +695,11 @@ fn d087_all_v01_conflict_types_produce_escalatable_states() {
|
||||
interest_axes: [NpcAxis::Want, NpcAxis::Secret, NpcAxis::Relationships],
|
||||
relationship_constraints: vec![],
|
||||
};
|
||||
assert!(def.validate().is_ok(), "D-087 triangle must be valid: {:?}", def.validate());
|
||||
assert!(
|
||||
def.validate().is_ok(),
|
||||
"D-087 triangle must be valid: {:?}",
|
||||
def.validate()
|
||||
);
|
||||
|
||||
// Can construct a TriangleState from the def.
|
||||
let mut assignments = BTreeMap::new();
|
||||
|
||||
@@ -9,15 +9,14 @@
|
||||
//! `cargo test -p settled-reach-server -- triangle_validation`
|
||||
|
||||
use settled_reach_server::{
|
||||
simulation::triangle::{
|
||||
generate_cross_template_triangles, generate_intra_template_triangles,
|
||||
validate_triangle_def, ConflictType, NpcAxis, RelationshipConstraint, RoleId,
|
||||
TemplateId, TemplateOwnership, TriangleDef, TriangleId, TrianglePhase, TrustRange,
|
||||
ValidationError,
|
||||
},
|
||||
knowledge::{registry::StableEntityId, types::StableId},
|
||||
npc::{Npc, RelationshipKind},
|
||||
simulation::rng::SimRng,
|
||||
simulation::triangle::{
|
||||
generate_cross_template_triangles, generate_intra_template_triangles,
|
||||
validate_triangle_def, ConflictType, NpcAxis, RelationshipConstraint, RoleId, TemplateId,
|
||||
TemplateOwnership, TriangleDef, TriangleId, TrianglePhase, TrustRange, ValidationError,
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -102,7 +101,12 @@ fn triangle_validation_conflict_viability_fails_without_want_axis() {
|
||||
|
||||
let result = validate_triangle_def(&def);
|
||||
assert!(
|
||||
matches!(result, Err(ValidationError::ConflictViability { triangle_id: TriangleId(10) })),
|
||||
matches!(
|
||||
result,
|
||||
Err(ValidationError::ConflictViability {
|
||||
triangle_id: TriangleId(10)
|
||||
})
|
||||
),
|
||||
"expected ConflictViability error, got: {:?}",
|
||||
result
|
||||
);
|
||||
@@ -129,7 +133,12 @@ fn triangle_validation_relationship_coherence_fails_without_constraints() {
|
||||
|
||||
let result = validate_triangle_def(&def);
|
||||
assert!(
|
||||
matches!(result, Err(ValidationError::RelationshipCoherence { triangle_id: TriangleId(20) })),
|
||||
matches!(
|
||||
result,
|
||||
Err(ValidationError::RelationshipCoherence {
|
||||
triangle_id: TriangleId(20)
|
||||
})
|
||||
),
|
||||
"expected RelationshipCoherence error, got: {:?}",
|
||||
result
|
||||
);
|
||||
@@ -214,11 +223,7 @@ fn triangle_validation_interest_divergence_first_last_duplicate() {
|
||||
fn triangle_validation_conflict_viability_checked_before_coherence() {
|
||||
let def = TriangleDef {
|
||||
triangle_id: TriangleId(40),
|
||||
roles: [
|
||||
RoleId::new("a"),
|
||||
RoleId::new("b"),
|
||||
RoleId::new("c"),
|
||||
],
|
||||
roles: [RoleId::new("a"), RoleId::new("b"), RoleId::new("c")],
|
||||
conflict_type: ConflictType::LatentTension,
|
||||
interest_axes: [NpcAxis::Contentment, NpcAxis::Tolerance, NpcAxis::Routine],
|
||||
relationship_constraints: vec![], // also fails coherence
|
||||
@@ -266,13 +271,8 @@ fn triangle_validation_cross_template_spans_two_templates() {
|
||||
..def
|
||||
};
|
||||
|
||||
let result = generate_cross_template_triangles(
|
||||
&mut world,
|
||||
hub_id,
|
||||
bar_id,
|
||||
&[overridden],
|
||||
&mut rng,
|
||||
);
|
||||
let result =
|
||||
generate_cross_template_triangles(&mut world, hub_id, bar_id, &[overridden], &mut rng);
|
||||
|
||||
assert!(
|
||||
result.warnings.is_empty(),
|
||||
@@ -297,12 +297,22 @@ fn triangle_validation_cross_template_spans_two_templates() {
|
||||
let freight = state.role_assignments[&RoleId::new("freight-handler")];
|
||||
let bartender = state.role_assignments[&RoleId::new("bartender")];
|
||||
assert_eq!(ops, StableId(1), "ops-manager must map to hub NPC 1");
|
||||
assert_eq!(freight, StableId(2), "freight-handler must map to hub NPC 2");
|
||||
assert_eq!(
|
||||
freight,
|
||||
StableId(2),
|
||||
"freight-handler must map to hub NPC 2"
|
||||
);
|
||||
assert_eq!(bartender, StableId(3), "bartender must map to bar NPC 3");
|
||||
|
||||
assert_eq!(state.phase, TrianglePhase::Simmering);
|
||||
assert!(state.tension >= 5 && state.tension <= 25, "tension in seeded range");
|
||||
assert!(state.tension_rate >= 1 && state.tension_rate <= 5, "rate in seeded range");
|
||||
assert!(
|
||||
state.tension >= 5 && state.tension <= 25,
|
||||
"tension in seeded range"
|
||||
);
|
||||
assert!(
|
||||
state.tension_rate >= 1 && state.tension_rate <= 5,
|
||||
"rate in seeded range"
|
||||
);
|
||||
}
|
||||
|
||||
/// Cross-template generation skips defs that fail validation, adding a warning.
|
||||
@@ -335,10 +345,15 @@ fn triangle_validation_cross_template_skips_invalid_defs() {
|
||||
}],
|
||||
};
|
||||
|
||||
let result = generate_cross_template_triangles(&mut world, hub_id, bar_id, &[invalid], &mut rng);
|
||||
let result =
|
||||
generate_cross_template_triangles(&mut world, hub_id, bar_id, &[invalid], &mut rng);
|
||||
|
||||
assert_eq!(result.triangles.len(), 0, "invalid def must be skipped");
|
||||
assert_eq!(result.warnings.len(), 1, "exactly one warning for the skipped def");
|
||||
assert_eq!(
|
||||
result.warnings.len(),
|
||||
1,
|
||||
"exactly one warning for the skipped def"
|
||||
);
|
||||
assert!(
|
||||
result.warnings[0].contains("validation failed"),
|
||||
"warning must mention validation failure: {}",
|
||||
@@ -391,8 +406,7 @@ fn triangle_validation_cross_template_deterministic() {
|
||||
assert_eq!(result1.triangles.len(), 1);
|
||||
assert_eq!(result2.triangles.len(), 1);
|
||||
assert_eq!(
|
||||
result1.triangles[0].tension,
|
||||
result2.triangles[0].tension,
|
||||
result1.triangles[0].tension, result2.triangles[0].tension,
|
||||
"cross-template generation must be deterministic (D-010)"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -431,7 +445,12 @@ fn triangle_validation_intra_template_does_not_see_other_template_npcs() {
|
||||
relationship_constraints: vec![],
|
||||
};
|
||||
|
||||
let result = generate_intra_template_triangles(&mut world, hub_id, &[def, valid_triangle_def(6)], &mut rng);
|
||||
let result = generate_intra_template_triangles(
|
||||
&mut world,
|
||||
hub_id,
|
||||
&[def, valid_triangle_def(6)],
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
// The def needing "inspector" should fall back (inspector is in bar, not hub)
|
||||
// At least one warning about the missing role
|
||||
|
||||
@@ -70,26 +70,40 @@ impl TestServer {
|
||||
}
|
||||
Err(e) => panic!("failed to read server stdout: {}", e),
|
||||
}
|
||||
assert!(Instant::now() < deadline, "timed out waiting for LISTENING signal");
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"timed out waiting for LISTENING signal"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let addr = format!("127.0.0.1:{}", port);
|
||||
let stream = TcpStream::connect(&addr).expect("client connect");
|
||||
stream.set_read_timeout(Some(SNAPSHOT_TIMEOUT)).expect("set timeout");
|
||||
stream
|
||||
.set_read_timeout(Some(SNAPSHOT_TIMEOUT))
|
||||
.expect("set timeout");
|
||||
let mut reader = BufReader::new(stream.try_clone().expect("clone stream"));
|
||||
let mut writer = BufWriter::new(stream);
|
||||
|
||||
// Protocol handshake
|
||||
let hf = read_framed(&mut reader).expect("read handshake").expect("connection closed");
|
||||
let hf = read_framed(&mut reader)
|
||||
.expect("read handshake")
|
||||
.expect("connection closed");
|
||||
let _: HandshakeMessage = rmp_serde::from_slice(&hf).expect("deserialize handshake");
|
||||
|
||||
// StartupMessage with chosen archetype
|
||||
let startup = StartupMessage { world_seed, character_archetype: archetype };
|
||||
let startup = StartupMessage {
|
||||
world_seed,
|
||||
character_archetype: archetype,
|
||||
};
|
||||
let startup_payload = rmp_serde::to_vec_named(&startup).expect("serialize startup");
|
||||
write_framed(&mut writer, &startup_payload).expect("send startup");
|
||||
|
||||
TestServer { child, reader, writer }
|
||||
TestServer {
|
||||
child,
|
||||
reader,
|
||||
writer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a tick's worth of inputs (empty = idle tick) and read back one snapshot.
|
||||
@@ -126,7 +140,10 @@ impl TestServer {
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(_) => { self.child.kill().ok(); break; }
|
||||
Err(_) => {
|
||||
self.child.kill().ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,9 +159,15 @@ fn test_smuggler_opening_monologue() {
|
||||
// Monologue IDs from smuggler/opening.yaml start with "pc-smuggler_".
|
||||
// This verifies: archetype → MonologueState.character → pool selection (D-032, #587, #595).
|
||||
let mut server = TestServer::boot_gauntlet(12345, CharacterArchetype::Smuggler);
|
||||
let snapshot = server.tick(vec![PlayerInput { tick: 0, action: PlayerAction::MoveNorth }]);
|
||||
let snapshot = server.tick(vec![PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::MoveNorth,
|
||||
}]);
|
||||
|
||||
assert_eq!(snapshot.version, PROTOCOL_VERSION, "protocol version mismatch");
|
||||
assert_eq!(
|
||||
snapshot.version, PROTOCOL_VERSION,
|
||||
"protocol version mismatch"
|
||||
);
|
||||
|
||||
let monologue = snapshot.current_monologue;
|
||||
assert!(
|
||||
@@ -169,9 +192,15 @@ fn test_detective_opening_monologue() {
|
||||
// Boot with Detective, advance 1 tick, assert opening monologue fires from detective pool.
|
||||
// Monologue IDs from detective/opening.yaml start with "pc-detective_".
|
||||
let mut server = TestServer::boot_gauntlet(12345, CharacterArchetype::Detective);
|
||||
let snapshot = server.tick(vec![PlayerInput { tick: 0, action: PlayerAction::MoveNorth }]);
|
||||
let snapshot = server.tick(vec![PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::MoveNorth,
|
||||
}]);
|
||||
|
||||
assert_eq!(snapshot.version, PROTOCOL_VERSION, "protocol version mismatch");
|
||||
assert_eq!(
|
||||
snapshot.version, PROTOCOL_VERSION,
|
||||
"protocol version mismatch"
|
||||
);
|
||||
|
||||
let monologue = snapshot.current_monologue;
|
||||
assert!(
|
||||
@@ -197,7 +226,8 @@ fn test_smuggler_and_detective_get_different_opening_monologue_ids() {
|
||||
// the same monologue ID on tick 1. If they do, D-032 partitioning is broken.
|
||||
let mut smug = TestServer::boot_gauntlet(12345, CharacterArchetype::Smuggler);
|
||||
let smug_snap = smug.tick(vec![]);
|
||||
let smug_id = smug_snap.current_monologue
|
||||
let smug_id = smug_snap
|
||||
.current_monologue
|
||||
.as_ref()
|
||||
.map(|m| m.id.clone())
|
||||
.unwrap_or_default();
|
||||
@@ -205,7 +235,8 @@ fn test_smuggler_and_detective_get_different_opening_monologue_ids() {
|
||||
|
||||
let mut det = TestServer::boot_gauntlet(12345, CharacterArchetype::Detective);
|
||||
let det_snap = det.tick(vec![]);
|
||||
let det_id = det_snap.current_monologue
|
||||
let det_id = det_snap
|
||||
.current_monologue
|
||||
.as_ref()
|
||||
.map(|m| m.id.clone())
|
||||
.unwrap_or_default();
|
||||
@@ -236,7 +267,9 @@ fn test_v0_1_integration_playthrough() {
|
||||
|
||||
// === Criterion 1: Opening monologue (Smuggler) ===
|
||||
let tick1 = server.tick(vec![]);
|
||||
let monologue = tick1.current_monologue.expect("Opening monologue must fire on tick 1");
|
||||
let monologue = tick1
|
||||
.current_monologue
|
||||
.expect("Opening monologue must fire on tick 1");
|
||||
assert!(
|
||||
monologue.id.starts_with("pc-smuggler_"),
|
||||
"Tick-1 monologue must be from smuggler pool. Got: {}",
|
||||
@@ -266,7 +299,11 @@ fn test_v0_1_integration_playthrough() {
|
||||
let mut deviation_observed = false;
|
||||
for _ in 0..5 {
|
||||
let snap = server.tick(vec![]);
|
||||
if snap.entities.iter().any(|e| e.tell_state == Some(TellCategory::RoutineDeviation)) {
|
||||
if snap
|
||||
.entities
|
||||
.iter()
|
||||
.any(|e| e.tell_state == Some(TellCategory::RoutineDeviation))
|
||||
{
|
||||
deviation_observed = true;
|
||||
break;
|
||||
}
|
||||
@@ -278,7 +315,9 @@ fn test_v0_1_integration_playthrough() {
|
||||
|
||||
// === Criterion 3 (D-036): News ticker visible in bar zone ===
|
||||
// Teleport to The Last Shift bar zone and check current_ticker is Some.
|
||||
let _teleport = server.send_debug(DebugCommandKind::TeleportToLocation("the-last-shift".into()));
|
||||
let _teleport = server.send_debug(DebugCommandKind::TeleportToLocation(
|
||||
"the-last-shift".into(),
|
||||
));
|
||||
let bar_snap = server.tick(vec![]);
|
||||
assert!(
|
||||
bar_snap.current_ticker.is_some(),
|
||||
|
||||
@@ -17,8 +17,8 @@ use settled_reach_server::npc::blueprint::{
|
||||
CulturalValues, CultureProfile, NamingConventions, OccasionalInjection, SpeechPatterns,
|
||||
VoiceExample,
|
||||
};
|
||||
use settled_reach_server::npc::PersonalityTrait;
|
||||
use settled_reach_server::npc::tell_state::TellCategory;
|
||||
use settled_reach_server::npc::PersonalityTrait;
|
||||
use settled_reach_server::voice::cache::VoiceCacheStore;
|
||||
use settled_reach_server::voice::prompt_builder::ContentType;
|
||||
use settled_reach_server::voice::queue::{Priority, VoiceQueue, VoiceRequest};
|
||||
@@ -59,11 +59,7 @@ fn voice_config() -> VoiceProcessConfig {
|
||||
} else {
|
||||
// Fall back to mock script — no model needed
|
||||
let mock = manifest.join("sr-voice/mock-stdio.sh");
|
||||
assert!(
|
||||
mock.exists(),
|
||||
"Mock script not found: {}",
|
||||
mock.display()
|
||||
);
|
||||
assert!(mock.exists(), "Mock script not found: {}", mock.display());
|
||||
eprintln!("Using mock sr-voice: {}", mock.display());
|
||||
if !bin_path.exists() {
|
||||
eprintln!(" (real binary not found: {})", bin_path.display());
|
||||
@@ -299,7 +295,11 @@ fn voice_pipeline_end_to_end() {
|
||||
drop(cache_guard);
|
||||
// Drop triggers save_all via the Drop impl — but the cache is behind
|
||||
// Arc<Mutex<>>, so we can't drop it here. Explicitly save instead.
|
||||
cache.lock().unwrap().save_all().expect("failed to save cache");
|
||||
cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.save_all()
|
||||
.expect("failed to save cache");
|
||||
|
||||
std::fs::write(&results_path, &results).expect("failed to write results");
|
||||
eprintln!("Results written to {}", results_path.display());
|
||||
|
||||
@@ -15,8 +15,8 @@ use settled_reach_server::npc::blueprint::{
|
||||
CulturalValues, CultureProfile, NamingConventions, OccasionalInjection, SpeechPatterns,
|
||||
VoiceExample,
|
||||
};
|
||||
use settled_reach_server::npc::PersonalityTrait;
|
||||
use settled_reach_server::npc::tell_state::TellCategory;
|
||||
use settled_reach_server::npc::PersonalityTrait;
|
||||
use settled_reach_server::voice::cache::{CacheKey, VoiceCacheStore};
|
||||
use settled_reach_server::voice::prompt_builder::ContentType;
|
||||
use settled_reach_server::voice::queue::{Priority, VoiceQueue, VoiceRequest};
|
||||
@@ -106,8 +106,9 @@ fn van_maanens_star_culture() -> CultureProfile {
|
||||
],
|
||||
occasional_injections: vec![OccasionalInjection {
|
||||
kind: "oath".into(),
|
||||
clause: "Use an oath like \"void take it\" when something is surprising or frustrating."
|
||||
.into(),
|
||||
clause:
|
||||
"Use an oath like \"void take it\" when something is surprising or frustrating."
|
||||
.into(),
|
||||
example: Some(VoiceExample {
|
||||
input: "discovers a critical part is missing".into(),
|
||||
output: "Void take it. The coupling's not here.".into(),
|
||||
@@ -623,13 +624,20 @@ fn voice_quality_batch() {
|
||||
.unwrap_or("[MISSING — not cached]");
|
||||
|
||||
output.push_str(&format!("--- #{}: {} ---\n", i + 1, case.label));
|
||||
output.push_str(&format!(" tell: {:?} | seed: {} | type: {:?}\n", case.tell_state, case.seed, case.content_type));
|
||||
output.push_str(&format!(
|
||||
" tell: {:?} | seed: {} | type: {:?}\n",
|
||||
case.tell_state, case.seed, case.content_type
|
||||
));
|
||||
output.push_str(&format!(" BASE: {}\n", case.base_text));
|
||||
output.push_str(&format!(" VOICED: {}\n\n", voiced));
|
||||
}
|
||||
|
||||
drop(cache_guard);
|
||||
cache.lock().unwrap().save_all().expect("failed to save cache");
|
||||
cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.save_all()
|
||||
.expect("failed to save cache");
|
||||
|
||||
let results_path = out.join("quality-batch.txt");
|
||||
std::fs::write(&results_path, &output).expect("failed to write results");
|
||||
|
||||
Reference in New Issue
Block a user