Add the ability to respawn and improve controller port mapping #72

Merged
EIREXE merged 2 commits from respawn into master 2026-09-27 11:54:09 +00:00
4 changed files with 52 additions and 29 deletions
Showing only changes of commit ccf2306d0e - Show all commits
+5 -1
View File
@@ -6,7 +6,7 @@ use crate::{
FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits,
FighterSolvedHurtboxes, FighterStaleMoveQueue,
}, 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)]
@@ -19,6 +19,7 @@ struct Checkpoint {
fighters: Vec<FighterCheckpoint>,
stage_collision: StageCollision,
pending_hits: FighterPendingHits,
match_state: MatchState,
}
#[derive(Clone)]
@@ -90,10 +91,12 @@ pub(super) fn save_or_restore(world: &mut World) {
checkpoint_fighters.push(checkpoint);
}
let pending_hits = world.resource::<FighterPendingHits>().clone();
let match_state = world.resource::<MatchState>().clone();
world.resource_mut::<DebugSaveState>().checkpoint = Some(Checkpoint {
fighters: checkpoint_fighters,
stage_collision,
pending_hits,
match_state,
});
info!("Saved debug game state");
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.stage_collision);
world.insert_resource(checkpoint.match_state);
for mut match_camera in world.query::<&mut MatchCamera>().iter_mut(world) {
match_camera.needs_snap = true;
}
+17 -10
View File
@@ -28,7 +28,7 @@ pub enum LocalInputSource {
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)]
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::math::int::FGi32;
use crate::netcode::GGRSCfg;
@@ -244,20 +254,17 @@ fn read_local_inputs(
input_map: Res<BaseInputMap>,
gc_ports: Res<gamecube::GcPorts>,
gamepads: Query<&Gamepad>,
assignments: Res<LocalInputAssignments>,
assignments: Res<MatchInputAssignments>,
local_players: Res<LocalPlayers>,
game_settings: Res<GameSettings>,
) {
let mut local_inputs = HashMap::new();
for handle in &local_players.0 {
let assigned = assignments
.0
.iter()
.find(|(player_handle, _)| player_handle == handle);
let assigned = assignments.get_for_handle(*handle);
let gamepad = match assigned {
Some((_, LocalInputSource::Gamepad(entity))) => gamepads.get(*entity).ok(),
Some((_, LocalInputSource::Keyboard)) => None,
Some(LocalInputSource::Gamepad(entity)) => gamepads.get(entity).ok(),
Some(LocalInputSource::Keyboard) => None,
None => gc_ports
.entities
.get(*handle)
@@ -267,8 +274,8 @@ fn read_local_inputs(
};
let mut input_frame = match assigned {
Some((_, LocalInputSource::Keyboard)) => keyboard::sample_keyboard(&key, &input_map),
Some((_, LocalInputSource::Gamepad(_))) => gamepad
Some(LocalInputSource::Keyboard) => keyboard::sample_keyboard(&key, &input_map),
Some(LocalInputSource::Gamepad(_)) => gamepad
.map(|pad| gamepad::sample_gamepad(pad, &input_map))
.unwrap_or_default(),
None => FighterInputFrame::default(),
+26 -18
View File
@@ -19,7 +19,7 @@ use crate::{
spawn_fighter,
visual::FighterAnimations,
},
input::{LocalInputAssignments, LocalInputSource},
input::{LocalInputAssignments, LocalInputSource, MatchInputAssignments},
match_runtime::MatchState,
netcode::{
GGRSCfg,
@@ -36,7 +36,7 @@ use crate::{
pub struct MatchPlayer {
/// Dense index used by GGRS and rollback-simulated player state.
pub handle: usize,
/// Physical controller port selected for a single-player match.
/// Physical controller port selected in the menu.
pub controller_port: usize,
pub fighter: FighterId,
}
@@ -282,7 +282,7 @@ pub fn initiate_default_match(
pub fn prepare_match(
mut commands: Commands,
config: Res<MatchConfig>,
mut input_assignments: ResMut<LocalInputAssignments>,
input_assignments: Res<LocalInputAssignments>,
assets: Res<MatchAssets>,
gltfs: Res<Assets<Gltf>>,
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();
@@ -566,18 +569,17 @@ fn player_count_for_handles(players: &[MatchPlayer]) -> usize {
players.len()
}
fn remap_local_input_assignments(
assignments: &mut LocalInputAssignments,
fn create_match_input_assignments(
assignments: &LocalInputAssignments,
players: &[MatchPlayer],
) {
for (assigned_port, _) in assignments.iter_mut() {
if let Some(player) = players
.iter()
.find(|player| player.controller_port == *assigned_port)
{
*assigned_port = player.handle;
}
) -> MatchInputAssignments {
let mut match_assignments = vec![None; players.len()];
for player in players {
match_assignments[player.handle] = assignments
.get_from_slot(player.controller_port)
.copied();
}
MatchInputAssignments(match_assignments)
}
pub fn cleanup_match(
@@ -598,6 +600,7 @@ pub fn cleanup_match(
commands.remove_resource::<MatchAssets>();
commands.remove_resource::<MatchConfig>();
commands.remove_resource::<MatchState>();
commands.remove_resource::<MatchInputAssignments>();
commands.remove_resource::<FighterRoster>();
}
@@ -701,7 +704,7 @@ mod tests {
}
#[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 = [
MatchPlayer {
handle: 0,
@@ -714,15 +717,20 @@ mod tests {
fighter: FighterId::TestFighter,
},
];
let mut assignments = LocalInputAssignments(vec![
let assignments = LocalInputAssignments(vec![
(0, LocalInputSource::Keyboard),
(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[1].0, 1);
assert_eq!(assignments[1].0, 3);
}
#[test]
+4
View File
@@ -232,6 +232,10 @@ impl StageManifest {
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() {
return Err("no neutral spawn layouts are configured".to_owned());
}