Refactor some things and make it safer
CI / build-and-test (pull_request) Successful in 2m0s
CI / production-build (pull_request) Successful in 11m1s

This commit is contained in:
2026-09-27 12:31:57 +02:00
parent 8efef087d4
commit ccf2306d0e
4 changed files with 52 additions and 29 deletions
+5 -1
View File
@@ -6,7 +6,7 @@ use crate::{
FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits, FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits,
FighterSolvedHurtboxes, FighterStaleMoveQueue, FighterSolvedHurtboxes, FighterStaleMoveQueue,
}, baked_animation::FighterBoneMatrices, state::FighterState, }, baked_animation::FighterBoneMatrices, state::FighterState,
}, input::FighterInput, player::Player, stage::line::StageCollision, }, input::FighterInput, match_runtime::MatchState, player::Player, stage::line::StageCollision,
}; };
#[derive(Resource, Default)] #[derive(Resource, Default)]
@@ -19,6 +19,7 @@ struct Checkpoint {
fighters: Vec<FighterCheckpoint>, fighters: Vec<FighterCheckpoint>,
stage_collision: StageCollision, stage_collision: StageCollision,
pending_hits: FighterPendingHits, pending_hits: FighterPendingHits,
match_state: MatchState,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -90,10 +91,12 @@ pub(super) fn save_or_restore(world: &mut World) {
checkpoint_fighters.push(checkpoint); checkpoint_fighters.push(checkpoint);
} }
let pending_hits = world.resource::<FighterPendingHits>().clone(); let pending_hits = world.resource::<FighterPendingHits>().clone();
let match_state = world.resource::<MatchState>().clone();
world.resource_mut::<DebugSaveState>().checkpoint = Some(Checkpoint { world.resource_mut::<DebugSaveState>().checkpoint = Some(Checkpoint {
fighters: checkpoint_fighters, fighters: checkpoint_fighters,
stage_collision, stage_collision,
pending_hits, pending_hits,
match_state,
}); });
info!("Saved debug game state"); info!("Saved debug game state");
return; return;
@@ -149,6 +152,7 @@ pub(super) fn save_or_restore(world: &mut World) {
} }
world.insert_resource(checkpoint.pending_hits.clone()); world.insert_resource(checkpoint.pending_hits.clone());
world.insert_resource(checkpoint.stage_collision); world.insert_resource(checkpoint.stage_collision);
world.insert_resource(checkpoint.match_state);
for mut match_camera in world.query::<&mut MatchCamera>().iter_mut(world) { for mut match_camera in world.query::<&mut MatchCamera>().iter_mut(world) {
match_camera.needs_snap = true; match_camera.needs_snap = true;
} }
+17 -10
View File
@@ -28,7 +28,7 @@ pub enum LocalInputSource {
Gamepad(Entity), Gamepad(Entity),
} }
/// Maps an input source to a player index /// Persistent mapping from physical/menu controller slots to input sources.
#[derive(Resource, Clone, Debug, Default, Deref, DerefMut)] #[derive(Resource, Clone, Debug, Default, Deref, DerefMut)]
pub struct LocalInputAssignments(pub Vec<(usize, LocalInputSource)>); pub struct LocalInputAssignments(pub Vec<(usize, LocalInputSource)>);
@@ -50,6 +50,16 @@ impl LocalInputAssignments {
} }
} }
/// Match-local mapping from dense GGRS handles to physical input sources.
#[derive(Resource, Clone, Debug, Default, Deref, DerefMut)]
pub struct MatchInputAssignments(pub Vec<Option<LocalInputSource>>);
impl MatchInputAssignments {
pub fn get_for_handle(&self, handle: usize) -> Option<LocalInputSource> {
self.0.get(handle).copied().flatten()
}
}
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
use crate::math::int::FGi32; use crate::math::int::FGi32;
use crate::netcode::GGRSCfg; use crate::netcode::GGRSCfg;
@@ -244,20 +254,17 @@ fn read_local_inputs(
input_map: Res<BaseInputMap>, input_map: Res<BaseInputMap>,
gc_ports: Res<gamecube::GcPorts>, gc_ports: Res<gamecube::GcPorts>,
gamepads: Query<&Gamepad>, gamepads: Query<&Gamepad>,
assignments: Res<LocalInputAssignments>, assignments: Res<MatchInputAssignments>,
local_players: Res<LocalPlayers>, local_players: Res<LocalPlayers>,
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
) { ) {
let mut local_inputs = HashMap::new(); let mut local_inputs = HashMap::new();
for handle in &local_players.0 { for handle in &local_players.0 {
let assigned = assignments let assigned = assignments.get_for_handle(*handle);
.0
.iter()
.find(|(player_handle, _)| player_handle == handle);
let gamepad = match assigned { let gamepad = match assigned {
Some((_, LocalInputSource::Gamepad(entity))) => gamepads.get(*entity).ok(), Some(LocalInputSource::Gamepad(entity)) => gamepads.get(entity).ok(),
Some((_, LocalInputSource::Keyboard)) => None, Some(LocalInputSource::Keyboard) => None,
None => gc_ports None => gc_ports
.entities .entities
.get(*handle) .get(*handle)
@@ -267,8 +274,8 @@ fn read_local_inputs(
}; };
let mut input_frame = match assigned { let mut input_frame = match assigned {
Some((_, LocalInputSource::Keyboard)) => keyboard::sample_keyboard(&key, &input_map), Some(LocalInputSource::Keyboard) => keyboard::sample_keyboard(&key, &input_map),
Some((_, LocalInputSource::Gamepad(_))) => gamepad Some(LocalInputSource::Gamepad(_)) => gamepad
.map(|pad| gamepad::sample_gamepad(pad, &input_map)) .map(|pad| gamepad::sample_gamepad(pad, &input_map))
.unwrap_or_default(), .unwrap_or_default(),
None => FighterInputFrame::default(), None => FighterInputFrame::default(),
+26 -18
View File
@@ -19,7 +19,7 @@ use crate::{
spawn_fighter, spawn_fighter,
visual::FighterAnimations, visual::FighterAnimations,
}, },
input::{LocalInputAssignments, LocalInputSource}, input::{LocalInputAssignments, LocalInputSource, MatchInputAssignments},
match_runtime::MatchState, match_runtime::MatchState,
netcode::{ netcode::{
GGRSCfg, GGRSCfg,
@@ -36,7 +36,7 @@ use crate::{
pub struct MatchPlayer { pub struct MatchPlayer {
/// Dense index used by GGRS and rollback-simulated player state. /// Dense index used by GGRS and rollback-simulated player state.
pub handle: usize, pub handle: usize,
/// Physical controller port selected for a single-player match. /// Physical controller port selected in the menu.
pub controller_port: usize, pub controller_port: usize,
pub fighter: FighterId, pub fighter: FighterId,
} }
@@ -282,7 +282,7 @@ pub fn initiate_default_match(
pub fn prepare_match( pub fn prepare_match(
mut commands: Commands, mut commands: Commands,
config: Res<MatchConfig>, config: Res<MatchConfig>,
mut input_assignments: ResMut<LocalInputAssignments>, input_assignments: Res<LocalInputAssignments>,
assets: Res<MatchAssets>, assets: Res<MatchAssets>,
gltfs: Res<Assets<Gltf>>, gltfs: Res<Assets<Gltf>>,
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
@@ -505,7 +505,10 @@ pub fn prepare_match(
} }
}; };
remap_local_input_assignments(&mut input_assignments, &config.players); commands.insert_resource(create_match_input_assignments(
&input_assignments,
&config.players,
));
let root = commands.spawn((Name::new("Match"), MatchRoot)).id(); let root = commands.spawn((Name::new("Match"), MatchRoot)).id();
@@ -566,18 +569,17 @@ fn player_count_for_handles(players: &[MatchPlayer]) -> usize {
players.len() players.len()
} }
fn remap_local_input_assignments( fn create_match_input_assignments(
assignments: &mut LocalInputAssignments, assignments: &LocalInputAssignments,
players: &[MatchPlayer], players: &[MatchPlayer],
) { ) -> MatchInputAssignments {
for (assigned_port, _) in assignments.iter_mut() { let mut match_assignments = vec![None; players.len()];
if let Some(player) = players for player in players {
.iter() match_assignments[player.handle] = assignments
.find(|player| player.controller_port == *assigned_port) .get_from_slot(player.controller_port)
{ .copied();
*assigned_port = player.handle;
}
} }
MatchInputAssignments(match_assignments)
} }
pub fn cleanup_match( pub fn cleanup_match(
@@ -598,6 +600,7 @@ pub fn cleanup_match(
commands.remove_resource::<MatchAssets>(); commands.remove_resource::<MatchAssets>();
commands.remove_resource::<MatchConfig>(); commands.remove_resource::<MatchConfig>();
commands.remove_resource::<MatchState>(); commands.remove_resource::<MatchState>();
commands.remove_resource::<MatchInputAssignments>();
commands.remove_resource::<FighterRoster>(); commands.remove_resource::<FighterRoster>();
} }
@@ -701,7 +704,7 @@ mod tests {
} }
#[test] #[test]
fn input_assignments_are_remapped_from_ports_to_ggrs_handles() { fn match_input_assignments_map_ports_to_ggrs_handles_without_mutating_ports() {
let players = [ let players = [
MatchPlayer { MatchPlayer {
handle: 0, handle: 0,
@@ -714,15 +717,20 @@ mod tests {
fighter: FighterId::TestFighter, fighter: FighterId::TestFighter,
}, },
]; ];
let mut assignments = LocalInputAssignments(vec![ let assignments = LocalInputAssignments(vec![
(0, LocalInputSource::Keyboard), (0, LocalInputSource::Keyboard),
(3, LocalInputSource::Gamepad(Entity::PLACEHOLDER)), (3, LocalInputSource::Gamepad(Entity::PLACEHOLDER)),
]); ]);
remap_local_input_assignments(&mut assignments, &players); let match_assignments = create_match_input_assignments(&assignments, &players);
assert_eq!(match_assignments.get_for_handle(0), Some(LocalInputSource::Keyboard));
assert_eq!(
match_assignments.get_for_handle(1),
Some(LocalInputSource::Gamepad(Entity::PLACEHOLDER))
);
assert_eq!(assignments[0].0, 0); assert_eq!(assignments[0].0, 0);
assert_eq!(assignments[1].0, 1); assert_eq!(assignments[1].0, 3);
} }
#[test] #[test]
+4
View File
@@ -232,6 +232,10 @@ impl StageManifest {
return Err("no spawn points are configured".to_owned()); return Err("no spawn points are configured".to_owned());
} }
if self.respawn_points.is_empty() {
return Err("no respawn points are configured".to_owned());
}
if self.neutral_spawn_point_sets.is_empty() { if self.neutral_spawn_point_sets.is_empty() {
return Err("no neutral spawn layouts are configured".to_owned()); return Err("no neutral spawn layouts are configured".to_owned());
} }