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:
2026-06-12 17:07:36 +02:00
co-authored by Claude Fable 5
parent 3a23378a88
commit 257d979aed
23 changed files with 4819 additions and 4735 deletions
+253
View File
@@ -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"));
}
}