refactor(simulation): split the input.rs and dialogue.rs dispatchers (T-1062)
input.rs 2,739 → 858 lines: per-domain action handlers moved to their owning modules (inventory, movement, stance, examine, follow, interaction, save_io, settings, bridge::debug, economy, vision_cone, bookmark, test_world reset + new teleport.rs); input.rs keeps the queue, the thin dispatch table, and pause/cooldown glue. All 9 type_complexity allows dissolved via one PlayerInputQuery alias. dialogue.rs → dialogue/ directory module: selection (631), response (1,473), confrontation (714), mod.rs (226, shared session components + re-exports — public paths preserved). Documented seam deviation: process_walk_away lives with confrontation (D-064/D-063 share the same world-response shape). Mechanical, zero behavior change: determinism + golden_suite byte-identical (independently re-verified); 1,504 lib tests unchanged — 23 input tests moved with their subjects, 53 dialogue tests redistributed, zero deleted. System scheduling registrations untouched (input_plugin.rs 0-line diff). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::system::SystemParam;
|
||||
|
||||
use crate::bridge::types::{BookmarkCatalog, BookmarkWire, CareerKindWire, SnapshotBuffer};
|
||||
use crate::knowledge::{resolve_culture, CultureResolver};
|
||||
@@ -174,6 +175,102 @@ fn prime_initial_catalog(
|
||||
tracing::debug!("BookmarkPlugin: initial catalog staged for tick-0 snapshot");
|
||||
}
|
||||
|
||||
/// Bundled SystemParam for bookmark-related input handling.
|
||||
///
|
||||
/// Bevy's blanket `IntoSystem` impl covers functions up to 16 parameters.
|
||||
/// Bundling the bookmark params keeps `process_player_input` at exactly 16.
|
||||
#[derive(SystemParam)]
|
||||
pub struct BookmarkInputParams<'w> {
|
||||
pub registry: Option<Res<'w, BookmarkRegistry>>,
|
||||
pub selected: Option<ResMut<'w, SelectedBookmark>>,
|
||||
pub snapshot_buf: Option<ResMut<'w, SnapshotBuffer>>,
|
||||
pub sim_error_buf: Option<ResMut<'w, crate::bridge::types::SimErrorBuffer>>,
|
||||
pub culture: Option<Res<'w, crate::knowledge::CultureResolverResource>>,
|
||||
}
|
||||
|
||||
/// Handle RequestBookmarkCatalog: stage the catalog in `SnapshotBuffer` for
|
||||
/// delivery in the next snapshot.
|
||||
pub fn handle_request_bookmark_catalog(bookmark: &mut BookmarkInputParams) {
|
||||
if let (Some(ref registry), Some(ref mut buf)) =
|
||||
(&bookmark.registry, &mut bookmark.snapshot_buf)
|
||||
{
|
||||
let resolver = bookmark.culture.as_deref().map(|c| &c.0);
|
||||
buf.pending_bookmark_catalog = Some(registry.build_catalog(resolver));
|
||||
tracing::debug!("RequestBookmarkCatalog: catalog staged");
|
||||
} else {
|
||||
tracing::warn!("RequestBookmarkCatalog: BookmarkRegistry or SnapshotBuffer not available");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle ConfirmBookmark: validate id + starting location against the registry
|
||||
/// and record the selection. Invalid or duplicate confirms emit a SimError.
|
||||
pub fn handle_confirm_bookmark(
|
||||
bookmark_id: String,
|
||||
starting_location_id: String,
|
||||
registry: &Option<Res<BookmarkRegistry>>,
|
||||
selected_bookmark: &mut Option<ResMut<SelectedBookmark>>,
|
||||
sim_error_buf: &mut Option<ResMut<crate::bridge::types::SimErrorBuffer>>,
|
||||
tick: u64,
|
||||
) {
|
||||
use crate::bridge::types::{SimError, SimErrorKind};
|
||||
|
||||
let Some(ref registry) = registry else {
|
||||
tracing::warn!("ConfirmBookmark: BookmarkRegistry not available");
|
||||
return;
|
||||
};
|
||||
let Some(defn) = registry.get(&bookmark_id) else {
|
||||
let msg = format!("ConfirmBookmark: unknown bookmark_id {:?}", bookmark_id);
|
||||
tracing::warn!("{}", msg);
|
||||
if let Some(ref mut buf) = sim_error_buf {
|
||||
buf.push(SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: msg,
|
||||
tick,
|
||||
});
|
||||
}
|
||||
return;
|
||||
};
|
||||
if !defn.allowed_locations.contains(&starting_location_id) {
|
||||
let msg = format!(
|
||||
"ConfirmBookmark: starting_location_id {:?} not in allowed_locations for {:?}",
|
||||
starting_location_id, bookmark_id
|
||||
);
|
||||
tracing::warn!("{}", msg);
|
||||
if let Some(ref mut buf) = sim_error_buf {
|
||||
buf.push(SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: msg,
|
||||
tick,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(ref mut sel) = selected_bookmark {
|
||||
if sel.bookmark_id.is_some() {
|
||||
let msg = format!(
|
||||
"ConfirmBookmark: bookmark already confirmed ({}), ignoring retry",
|
||||
sel.bookmark_id.as_deref().unwrap_or("?")
|
||||
);
|
||||
tracing::warn!("{}", msg);
|
||||
if let Some(ref mut buf) = sim_error_buf {
|
||||
buf.push(SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: msg,
|
||||
tick,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
sel.bookmark_id = Some(bookmark_id.clone());
|
||||
sel.starting_location_id = Some(starting_location_id.clone());
|
||||
tracing::info!(
|
||||
bookmark_id,
|
||||
starting_location_id,
|
||||
"ConfirmBookmark: selection recorded"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register all v0.2 bookmarks.
|
||||
///
|
||||
/// Hard-coded for now (single entry). Promote to TOML loading in Sprint 38+
|
||||
@@ -264,4 +361,160 @@ mod tests {
|
||||
.allowed_locations
|
||||
.contains(&"Unknown System".to_string()));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ConfirmBookmark input-handler tests (moved from input.rs, T-1062)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
use crate::bridge::types::{PlayerAction, PlayerInput, SimErrorBuffer};
|
||||
use crate::simulation::input::{process_player_input, InputQueue};
|
||||
use crate::simulation::movement::{PlayerCharacter, TilePosition};
|
||||
use crate::simulation::time::SimulationTime;
|
||||
|
||||
fn make_bookmark_world() -> bevy_ecs::world::World {
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(InputQueue::default());
|
||||
world.insert_resource(SimulationTime::default());
|
||||
world.init_resource::<crate::knowledge::EntityRegistry>();
|
||||
world.init_resource::<SelectedBookmark>();
|
||||
world.init_resource::<SimErrorBuffer>();
|
||||
|
||||
let mut registry = BookmarkRegistry::default();
|
||||
registry.insert(BookmarkDefinition {
|
||||
id: BookmarkId("test_bookmark".to_string()),
|
||||
title: "Test Bookmark".into(),
|
||||
subtitle: String::new(),
|
||||
flavor: String::new(),
|
||||
default_location: "Loc A".into(),
|
||||
allowed_locations: vec!["Loc A".into(), "Loc B".into()],
|
||||
career: CareerKind::Tycoon,
|
||||
starting_capital_tractus: 1_000,
|
||||
available: true,
|
||||
});
|
||||
world.insert_resource(registry);
|
||||
|
||||
world.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)));
|
||||
world
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_bookmark_unknown_id_emits_sim_error() {
|
||||
let mut world = make_bookmark_world();
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::ConfirmBookmark {
|
||||
bookmark_id: "no_such_bookmark".into(),
|
||||
starting_location_id: "Loc A".into(),
|
||||
},
|
||||
});
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(process_player_input);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let errors = world.resource_mut::<SimErrorBuffer>().drain();
|
||||
assert_eq!(
|
||||
errors.len(),
|
||||
1,
|
||||
"expected one SimError for unknown bookmark"
|
||||
);
|
||||
assert_eq!(
|
||||
errors[0].kind,
|
||||
crate::bridge::types::SimErrorKind::ProtocolError
|
||||
);
|
||||
assert!(errors[0].message.contains("unknown bookmark_id"));
|
||||
assert!(
|
||||
world.resource::<SelectedBookmark>().bookmark_id.is_none(),
|
||||
"SelectedBookmark must not be set on error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_bookmark_invalid_location_emits_sim_error() {
|
||||
let mut world = make_bookmark_world();
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::ConfirmBookmark {
|
||||
bookmark_id: "test_bookmark".into(),
|
||||
starting_location_id: "Not A Location".into(),
|
||||
},
|
||||
});
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(process_player_input);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let errors = world.resource_mut::<SimErrorBuffer>().drain();
|
||||
assert_eq!(
|
||||
errors.len(),
|
||||
1,
|
||||
"expected one SimError for invalid location"
|
||||
);
|
||||
assert_eq!(
|
||||
errors[0].kind,
|
||||
crate::bridge::types::SimErrorKind::ProtocolError
|
||||
);
|
||||
assert!(errors[0].message.contains("not in allowed_locations"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_bookmark_valid_inputs_populate_selected_bookmark() {
|
||||
let mut world = make_bookmark_world();
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::ConfirmBookmark {
|
||||
bookmark_id: "test_bookmark".into(),
|
||||
starting_location_id: "Loc B".into(),
|
||||
},
|
||||
});
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(process_player_input);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let errors = world.resource_mut::<SimErrorBuffer>().drain();
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"no errors expected for valid ConfirmBookmark"
|
||||
);
|
||||
|
||||
let sel = world.resource::<SelectedBookmark>();
|
||||
assert_eq!(sel.bookmark_id.as_deref(), Some("test_bookmark"));
|
||||
assert_eq!(sel.starting_location_id.as_deref(), Some("Loc B"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_bookmark_double_confirm_emits_sim_error() {
|
||||
let mut world = make_bookmark_world();
|
||||
|
||||
// Both confirms at tick=0: processed in order within the same run.
|
||||
// First succeeds; second hits the idempotency guard.
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::ConfirmBookmark {
|
||||
bookmark_id: "test_bookmark".into(),
|
||||
starting_location_id: "Loc A".into(),
|
||||
},
|
||||
});
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::ConfirmBookmark {
|
||||
bookmark_id: "test_bookmark".into(),
|
||||
starting_location_id: "Loc B".into(),
|
||||
},
|
||||
});
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(process_player_input);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let errors = world.resource_mut::<SimErrorBuffer>().drain();
|
||||
assert_eq!(errors.len(), 1, "double-confirm must emit ProtocolError");
|
||||
assert_eq!(
|
||||
errors[0].kind,
|
||||
crate::bridge::types::SimErrorKind::ProtocolError
|
||||
);
|
||||
assert!(errors[0].message.contains("already confirmed"));
|
||||
|
||||
// Selection must remain the original, not overwritten
|
||||
let sel = world.resource::<SelectedBookmark>();
|
||||
assert_eq!(sel.starting_location_id.as_deref(), Some("Loc A"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user