Compare commits

..
2 Commits
Author SHA1 Message Date
EIREXE 7271d02e29 Fix warnings and increase recursion
CI / production-build (pull_request) Successful in 11m5s
CI / build-and-test (pull_request) Successful in 1m58s
2026-09-26 19:37:18 +02:00
EIREXE 866a9c7500 Implement filled gizmos 2026-09-26 19:32:09 +02:00
7 changed files with 33 additions and 206 deletions
-13
View File
@@ -109,7 +109,6 @@ impl FighterDebugRowKind {
enum DebugFlag { enum DebugFlag {
Network, Network,
Ecb, Ecb,
BlastZones,
StageCollision, StageCollision,
AnimationBones, AnimationBones,
AttackHitboxes, AttackHitboxes,
@@ -151,10 +150,6 @@ const FIGHTER_MENU: &[DebugMenuEntry] = &[
label: "ECB", label: "ECB",
flag: DebugFlag::Ecb, flag: DebugFlag::Ecb,
}, },
DebugMenuEntry::Toggle {
label: "Blast Zones",
flag: DebugFlag::BlastZones,
},
DebugMenuEntry::Toggle { DebugMenuEntry::Toggle {
label: "Animation bones", label: "Animation bones",
flag: DebugFlag::AnimationBones, flag: DebugFlag::AnimationBones,
@@ -248,7 +243,6 @@ pub struct DebugSettings {
pub frame_stepping: bool, pub frame_stepping: bool,
pub network: bool, pub network: bool,
pub ecb: bool, pub ecb: bool,
pub blast_zones: bool,
pub stage_collision: bool, pub stage_collision: bool,
pub animation_bones: bool, pub animation_bones: bool,
pub attack_hitboxes: bool, pub attack_hitboxes: bool,
@@ -270,7 +264,6 @@ impl Default for DebugSettings {
frame_stepping: false, frame_stepping: false,
network: false, network: false,
ecb: false, ecb: false,
blast_zones: false,
stage_collision: false, stage_collision: false,
animation_bones: false, animation_bones: false,
attack_hitboxes: false, attack_hitboxes: false,
@@ -335,7 +328,6 @@ impl DebugSettings {
match flag { match flag {
DebugFlag::Network => self.network, DebugFlag::Network => self.network,
DebugFlag::Ecb => self.ecb, DebugFlag::Ecb => self.ecb,
DebugFlag::BlastZones => self.blast_zones,
DebugFlag::StageCollision => self.stage_collision, DebugFlag::StageCollision => self.stage_collision,
DebugFlag::AnimationBones => self.animation_bones, DebugFlag::AnimationBones => self.animation_bones,
DebugFlag::AttackHitboxes => self.attack_hitboxes, DebugFlag::AttackHitboxes => self.attack_hitboxes,
@@ -350,7 +342,6 @@ impl DebugSettings {
match flag { match flag {
DebugFlag::Network => self.network = !self.network, DebugFlag::Network => self.network = !self.network,
DebugFlag::Ecb => self.ecb = !self.ecb, DebugFlag::Ecb => self.ecb = !self.ecb,
DebugFlag::BlastZones => self.blast_zones = !self.blast_zones,
DebugFlag::StageCollision => self.stage_collision = !self.stage_collision, DebugFlag::StageCollision => self.stage_collision = !self.stage_collision,
DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones, DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones,
DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes, DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes,
@@ -893,10 +884,6 @@ pub fn stage_collision_enabled(settings: Res<DebugSettings>) -> bool {
settings.stage_collision settings.stage_collision
} }
pub fn blast_zones_enabled(settings: Res<DebugSettings>) -> bool {
settings.blast_zones
}
pub fn animation_bones_enabled(settings: Res<DebugSettings>) -> bool { pub fn animation_bones_enabled(settings: Res<DebugSettings>) -> bool {
settings.animation_bones settings.animation_bones
} }
+17 -53
View File
@@ -21,6 +21,7 @@ use crate::{
}, },
input::{LocalInputAssignments, LocalInputSource}, input::{LocalInputAssignments, LocalInputSource},
match_runtime::MatchState, match_runtime::MatchState,
math::{int::FGi32, vec::FGVec2},
netcode::{ netcode::{
GGRSCfg, GGRSCfg,
session::{ConnectionStatus, SessionConfig, create_session}, session::{ConnectionStatus, SessionConfig, create_session},
@@ -276,7 +277,7 @@ pub fn prepare_match(
if !valid_player_handles(&config.players) { if !valid_player_handles(&config.players) {
fail_match( fail_match(
&mut next_state, &mut next_state,
"player handles must be unique and non-empty", "player handles must be unique and cover 0..player_count",
); );
return; return;
} }
@@ -286,20 +287,6 @@ pub fn prepare_match(
return; return;
} }
let stage_handle = stage_registry
.get(config.stage)
.expect("Initialization check should ensure all stages exist");
let stage_manifest = stage_manifests
.get(&stage_handle)
.expect("Initialization check should ensure stage manifests are kept alive");
let spawn_positions = match stage_manifest.neutral_spawn_positions(config.players.len()) {
Ok(positions) => positions,
Err(error) => {
fail_match(&mut next_state, &error);
return;
}
};
if assets.fighters.len() != config.players.len() { if assets.fighters.len() != config.players.len() {
fail_match( fail_match(
&mut next_state, &mut next_state,
@@ -462,8 +449,7 @@ pub fn prepare_match(
)); ));
} }
let session_player_count = player_count_for_handles(&config.players); let session_config = match SessionConfig::from_args(&args, config.players.len()) {
let session_config = match SessionConfig::from_args(&args, session_player_count) {
Ok(config) => config, Ok(config) => config,
Err(error) => { Err(error) => {
fail_match( fail_match(
@@ -473,7 +459,7 @@ pub fn prepare_match(
return; return;
} }
}; };
let session = match create_session(&args, session_player_count) { let session = match create_session(&args, config.players.len()) {
Ok(session) => session, Ok(session) => session,
Err(error) => { Err(error) => {
fail_match( fail_match(
@@ -486,6 +472,12 @@ pub fn prepare_match(
let root = commands.spawn((Name::new("Match"), MatchRoot)).id(); let root = commands.spawn((Name::new("Match"), MatchRoot)).id();
let stage_handle = stage_registry
.get(config.stage)
.expect("Initialization check should ensure all stages exist");
let stage_manifest = stage_manifests
.get(&stage_handle)
.expect("Initialization check should ensure stage manifests are kept alive");
stage::spawn_stage( stage::spawn_stage(
&mut commands, &mut commands,
root, root,
@@ -495,15 +487,14 @@ pub fn prepare_match(
); );
let mut fighter_roster = Vec::with_capacity(config.players.len()); let mut fighter_roster = Vec::with_capacity(config.players.len());
for (index, (player, (scene, animations, attack_scripts, manifest_handle, model_scale, ecb_bone_indices))) in
for (spawn_index, (player, (scene, animations, attack_scripts, manifest_handle, model_scale, ecb_bone_indices))) in
config.players.iter().zip(prepared).enumerate() config.players.iter().zip(prepared).enumerate()
{ {
let spawn_point = spawn_positions[spawn_index]; let spawn_x = (index as i32 * 2 + 1 - config.players.len() as i32) * 5;
fighter_roster.push(spawn_fighter( fighter_roster.push(spawn_fighter(
&mut commands, &mut commands,
player.handle, player.handle,
spawn_point, FGVec2::new(FGi32::from_num(spawn_x), FGi32::lit("12.5")),
manifest_handle.clone(), manifest_handle.clone(),
model_scale, model_scale,
animations, animations,
@@ -527,22 +518,12 @@ fn fail_match(next_state: &mut NextState<AppState>, message: &str) {
} }
fn valid_player_handles(players: &[MatchPlayer]) -> bool { fn valid_player_handles(players: &[MatchPlayer]) -> bool {
if players.is_empty() { let mut handles = players
return false;
}
let mut handles = players.iter().map(|player| player.handle).collect::<Vec<_>>();
handles.sort_unstable();
handles.windows(2).all(|handles| handles[0] != handles[1])
&& handles.last().is_none_or(|handle| *handle != usize::MAX)
}
fn player_count_for_handles(players: &[MatchPlayer]) -> usize {
players
.iter() .iter()
.map(|player| player.handle) .map(|player| player.handle)
.max() .collect::<Vec<_>>();
.map_or(0, |handle| handle + 1) handles.sort_unstable();
handles == (0..players.len()).collect::<Vec<_>>()
} }
pub fn cleanup_match( pub fn cleanup_match(
@@ -609,23 +590,6 @@ mod tests {
assert!(!valid_player_handles(&players)); assert!(!valid_player_handles(&players));
} }
#[test]
fn player_handles_may_have_gaps() {
let players = [
MatchPlayer {
handle: 0,
fighter: FighterId::TestFighter,
},
MatchPlayer {
handle: 2,
fighter: FighterId::TestFighter,
},
];
assert!(valid_player_handles(&players));
assert_eq!(player_count_for_handles(&players), 3);
}
#[test] #[test]
fn match_rules_reject_zero_limits() { fn match_rules_reject_zero_limits() {
assert!( assert!(
+1 -6
View File
@@ -40,12 +40,7 @@ impl MatchState {
Self { Self {
elapsed_frames: 0, elapsed_frames: 0,
players: (0..config players: (0..config.players.len())
.players
.iter()
.map(|player| player.handle)
.max()
.map_or(0, |handle| handle + 1))
.map(|_| PlayerMatchState { .map(|_| PlayerMatchState {
stocks_remaining, stocks_remaining,
score: 0, score: 0,
+3 -14
View File
@@ -140,7 +140,7 @@ fn validate(replay: &Replay) -> Result<(), String> {
)); ));
} }
if replay.players_are_invalid() { if replay.players_are_invalid() {
return Err("replay player handles must be unique".into()); return Err("replay player handles must cover 0..player_count".into());
} }
if replay.frames.is_empty() { if replay.frames.is_empty() {
return Err("replay must contain at least one frame".into()); return Err("replay must contain at least one frame".into());
@@ -169,16 +169,7 @@ fn validate(replay: &Replay) -> Result<(), String> {
if replay if replay
.frames .frames
.iter() .iter()
.any(|frame| { .any(|frame| frame.inputs.len() != replay.match_config.players.len())
frame.inputs.len()
!= replay
.match_config
.players
.iter()
.map(|player| player.handle)
.max()
.map_or(0, |handle| handle + 1)
})
{ {
return Err("replay frame input count does not match player count".into()); return Err("replay frame input count does not match player count".into());
} }
@@ -194,9 +185,7 @@ impl Replay {
.map(|player| player.handle) .map(|player| player.handle)
.collect(); .collect();
handles.sort_unstable(); handles.sort_unstable();
handles.is_empty() handles != (0..handles.len()).collect::<Vec<_>>()
|| handles.windows(2).any(|handles| handles[0] == handles[1])
|| handles.last() == Some(&usize::MAX)
} }
fn frame(&self, frame: i32) -> Option<&ReplayFrame> { fn frame(&self, frame: i32) -> Option<&ReplayFrame> {
+1 -29
View File
@@ -1,12 +1,6 @@
use bevy::{color::palettes::css::RED, prelude::*}; use bevy::{color::palettes::css::RED, prelude::*};
use crate::{ use crate::stage::line::StageCollision;
match_loading::MatchConfig,
stage::{
line::StageCollision,
manifest::{StageManifest, StageManifestRegistry},
},
};
pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) { pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) {
for poly in stage_collision.stage_polys.iter() { for poly in stage_collision.stage_polys.iter() {
@@ -22,25 +16,3 @@ pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>
} }
} }
} }
pub fn debug_draw_blast_zones(
mut gizmos: Gizmos,
stage_registry: Res<StageManifestRegistry>,
stage_manifests: Res<Assets<StageManifest>>,
match_config: Res<MatchConfig>,
) {
let Some(stage_manifest_handle) = stage_registry.get(match_config.stage) else {
return;
};
let Some(stage_manifest) = stage_manifests.get(&stage_manifest_handle) else {
return;
};
let top_left = stage_manifest.blast_zone_top_left.to_vec2();
let bottom_right = stage_manifest.blast_zone_bottom_right.to_vec2();
let center = (top_left + bottom_right) / 2.0;
let size = (bottom_right - top_left).abs();
gizmos.rect(Vec3::new(center.x, center.y, 0.0), size, bevy::color::palettes::css::BLUE);
}
+7 -82
View File
@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use bevy_asset_loader::asset_collection::AssetCollection; use bevy_asset_loader::asset_collection::AssetCollection;
use ron_asset_manager::prelude::RonAsset; use ron_asset_manager::prelude::RonAsset;
@@ -7,10 +7,14 @@ use strum::IntoEnumIterator;
use strum_macros::EnumIter; use strum_macros::EnumIter;
use crate::{ use crate::{
AppState, game_settings::GameSettings, math::{vec::FGVec2}, stage::{ AppState,
game_settings::GameSettings,
math::vec::FGVec2,
stage::{
StagePoly, StagePoly,
line::{StageCollision, StagePolyLineSegmentType, StagePolyType}, line::{StageCollision, StagePolyLineSegmentType, StagePolyType},
}, texture_reference::TextureReference, },
texture_reference::TextureReference,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -83,15 +87,6 @@ pub struct StageManifest {
pub icon: TextureReference, pub icon: TextureReference,
pub camera: StageCameraProfile, pub camera: StageCameraProfile,
pub polygons: Vec<StagePolyDefinition>, pub polygons: Vec<StagePolyDefinition>,
#[reflect(ignore)]
pub blast_zone_top_left: FGVec2,
#[reflect(ignore)]
pub blast_zone_bottom_right: FGVec2,
#[reflect(ignore)]
pub spawn_points: Vec<FGVec2>,
pub neutral_spawn_point_sets: Vec<Vec<usize>>
} }
#[derive(Resource)] #[derive(Resource)]
@@ -167,13 +162,6 @@ pub fn prepare_stage_manifests(
); );
return; return;
} }
if let Err(error) = manifest.validate_spawn_points() {
fail(
&mut next_state,
&format!("{:?} has invalid spawn points: {error}", manifest.id),
);
return;
}
if registry.insert(manifest.id, handle.clone()).is_some() { if registry.insert(manifest.id, handle.clone()).is_some() {
fail( fail(
&mut next_state, &mut next_state,
@@ -198,69 +186,6 @@ pub fn prepare_stage_manifests(
} }
impl StageManifest { impl StageManifest {
pub fn neutral_spawn_positions(&self, player_count: usize) -> Result<Vec<FGVec2>, String> {
if player_count == 0 {
return Err("a match must contain at least one player".to_owned());
}
let layout = self
.neutral_spawn_point_sets
.get(player_count - 1)
.ok_or_else(|| format!("no spawn layout for {player_count} players"))?;
if layout.len() != player_count {
return Err(format!(
"the {player_count}-player layout contains {} positions",
layout.len()
));
}
layout
.iter()
.map(|&index| {
self.spawn_points.get(index).copied().ok_or_else(|| {
format!("the {player_count}-player layout references spawn point {index}")
})
})
.collect()
}
fn validate_spawn_points(&self) -> Result<(), String> {
if self.spawn_points.is_empty() {
return Err("no spawn points are configured".to_owned());
}
if self.neutral_spawn_point_sets.is_empty() {
return Err("no neutral spawn layouts are configured".to_owned());
}
for (layout_index, layout) in self.neutral_spawn_point_sets.iter().enumerate() {
let player_count = layout_index + 1;
if layout.len() != player_count {
return Err(format!(
"the {player_count}-player layout contains {} positions",
layout.len()
));
}
let mut unique_indices = HashSet::with_capacity(layout.len());
for &spawn_point_index in layout {
if spawn_point_index >= self.spawn_points.len() {
return Err(format!(
"the {player_count}-player layout references spawn point {spawn_point_index}"
));
}
if !unique_indices.insert(spawn_point_index) {
return Err(format!(
"the {player_count}-player layout uses spawn point {spawn_point_index} more than once"
));
}
}
}
Ok(())
}
pub fn to_collision(&self) -> StageCollision { pub fn to_collision(&self) -> StageCollision {
let stage_polys: Vec<StagePoly> = self let stage_polys: Vec<StagePoly> = self
.polygons .polygons
+4 -9
View File
@@ -10,7 +10,7 @@ pub mod scene;
pub use line::{StagePlaneIntersectResult, StagePoly}; pub use line::{StagePlaneIntersectResult, StagePoly};
use crate::{match_loading::MatchConfig, stage::{line::StageCollision, manifest::StageCameraProfile}}; use crate::stage::{line::StageCollision, manifest::StageCameraProfile};
/// Owns the stage geometry: which scene gets spawned and how it is drawn. /// Owns the stage geometry: which scene gets spawned and how it is drawn.
#[derive(Default)] #[derive(Default)]
@@ -20,14 +20,9 @@ impl Plugin for StagePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems( app.add_systems(
Update, Update,
( debug::debug_draw_scene
(debug::debug_draw_scene .run_if(resource_exists::<line::StageCollision>)
.run_if(resource_exists::<line::StageCollision>) .run_if(crate::debug_tools::stage_collision_enabled),
.run_if(crate::debug_tools::stage_collision_enabled)),
(debug::debug_draw_blast_zones
.run_if(resource_exists::<MatchConfig>)
.run_if(crate::debug_tools::blast_zones_enabled),),
),
); );
} }
} }