Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c849caf5b9 | ||
|
|
9f66d55dcc | ||
|
|
578648d2ec | ||
|
|
0d96a8aed8 | ||
|
|
cebf1148c2 | ||
|
|
088d594e97 | ||
|
|
868df95cd4 | ||
|
|
504f63be54 |
@@ -10,7 +10,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Check out source
|
- name: Check out source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -20,7 +20,13 @@ jobs:
|
|||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||||
sh -s -- -y --profile minimal
|
sh -s -- -y --profile minimal
|
||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
"$HOME/.cargo/bin/rustup" toolchain install nightly --profile minimal
|
"$HOME/.cargo/bin/rustup" toolchain install nightly-2026-09-14 --profile minimal
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies and build artifacts
|
||||||
|
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||||
|
with:
|
||||||
|
shared-key: linux-nightly
|
||||||
|
cache-bin: false
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -39,7 +45,7 @@ jobs:
|
|||||||
pkg-config
|
pkg-config
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked -j 4
|
run: RUSTFLAGS="-D warnings" cargo build --locked -j 4
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: cargo test --locked --lib -j 4
|
run: cargo test --locked --lib -j 4
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "nightly-2026-09-14"
|
||||||
|
|||||||
+98
-3
@@ -3,11 +3,12 @@
|
|||||||
use std::{collections::HashMap, time::Duration};
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
|
input::InputSystems,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup},
|
settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup},
|
||||||
};
|
};
|
||||||
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
||||||
use bevy_ggrs::RollbackFrameRate;
|
use bevy_ggrs::{RollbackFrameRate, RunGgrsSystems};
|
||||||
use enum_cycling::EnumCycle;
|
use enum_cycling::EnumCycle;
|
||||||
use enum_cycling_derive::EnumCycle;
|
use enum_cycling_derive::EnumCycle;
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ enum DebugFlag {
|
|||||||
Hurtboxes,
|
Hurtboxes,
|
||||||
Camera,
|
Camera,
|
||||||
DrawThroughGeometry,
|
DrawThroughGeometry,
|
||||||
|
FrameStepping,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -174,6 +176,10 @@ const WORLD_MENU: &[DebugMenuEntry] = &[
|
|||||||
DebugMenuEntry::GameSpeed {
|
DebugMenuEntry::GameSpeed {
|
||||||
label: "Game speed",
|
label: "Game speed",
|
||||||
},
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "Frame stepping",
|
||||||
|
flag: DebugFlag::FrameStepping,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
||||||
@@ -233,6 +239,8 @@ pub struct DebugSettings {
|
|||||||
selections: Vec<usize>,
|
selections: Vec<usize>,
|
||||||
#[reflect(ignore)]
|
#[reflect(ignore)]
|
||||||
pub player_rows: HashMap<usize, [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]>,
|
pub player_rows: HashMap<usize, [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]>,
|
||||||
|
#[reflect(ignore)]
|
||||||
|
pub frame_stepping: bool,
|
||||||
pub network: bool,
|
pub network: bool,
|
||||||
pub ecb: bool,
|
pub ecb: bool,
|
||||||
pub stage_collision: bool,
|
pub stage_collision: bool,
|
||||||
@@ -253,6 +261,7 @@ impl Default for DebugSettings {
|
|||||||
pages: vec![DebugMenuPage::Static(ROOT_MENU)],
|
pages: vec![DebugMenuPage::Static(ROOT_MENU)],
|
||||||
selections: vec![0],
|
selections: vec![0],
|
||||||
player_rows: HashMap::new(),
|
player_rows: HashMap::new(),
|
||||||
|
frame_stepping: false,
|
||||||
network: false,
|
network: false,
|
||||||
ecb: false,
|
ecb: false,
|
||||||
stage_collision: false,
|
stage_collision: false,
|
||||||
@@ -325,6 +334,7 @@ impl DebugSettings {
|
|||||||
DebugFlag::Hurtboxes => self.hurtboxes,
|
DebugFlag::Hurtboxes => self.hurtboxes,
|
||||||
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
||||||
DebugFlag::Camera => self.camera,
|
DebugFlag::Camera => self.camera,
|
||||||
|
DebugFlag::FrameStepping => self.frame_stepping,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +350,7 @@ impl DebugSettings {
|
|||||||
self.draw_through_geometry = !self.draw_through_geometry
|
self.draw_through_geometry = !self.draw_through_geometry
|
||||||
}
|
}
|
||||||
DebugFlag::Camera => self.camera = !self.camera,
|
DebugFlag::Camera => self.camera = !self.camera,
|
||||||
|
DebugFlag::FrameStepping => self.frame_stepping = !self.frame_stepping,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,9 +367,25 @@ impl DebugSettings {
|
|||||||
|
|
||||||
pub struct DebugToolsPlugin;
|
pub struct DebugToolsPlugin;
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct FrameStepState {
|
||||||
|
requested: bool,
|
||||||
|
time_backup: Option<Time>,
|
||||||
|
last_step_time: Time
|
||||||
|
}
|
||||||
|
|
||||||
impl Plugin for DebugToolsPlugin {
|
impl Plugin for DebugToolsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<DebugSettings>()
|
app.init_resource::<DebugSettings>()
|
||||||
|
.init_resource::<FrameStepState>()
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
prepare_frame_step
|
||||||
|
.after(InputSystems)
|
||||||
|
.before(RunGgrsSystems)
|
||||||
|
.run_if(in_state(AppState::InMatch)),
|
||||||
|
)
|
||||||
|
.add_systems(PreUpdate, restore_frame_step_time.after(RunGgrsSystems))
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(handle_palette_input, sync_gizmo_depth_bias)
|
(handle_palette_input, sync_gizmo_depth_bias)
|
||||||
@@ -391,6 +418,61 @@ fn queue_settings_save(commands: &mut Commands) {
|
|||||||
commands.queue(SaveSettingsDeferred(Duration::from_secs_f32(0.1)));
|
commands.queue(SaveSettingsDeferred(Duration::from_secs_f32(0.1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn frame_step_duration(frame_rate: usize) -> Duration {
|
||||||
|
Duration::from_nanos(1_000_000_000u64 / frame_rate as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_frame_step(
|
||||||
|
settings: Res<DebugSettings>,
|
||||||
|
gamepads: Query<&Gamepad>,
|
||||||
|
key: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut state: ResMut<FrameStepState>,
|
||||||
|
mut time: ResMut<Time>,
|
||||||
|
frame_rate: Res<RollbackFrameRate>,
|
||||||
|
) {
|
||||||
|
if !settings.frame_stepping {
|
||||||
|
state.requested = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pressed = gamepads
|
||||||
|
.iter()
|
||||||
|
.any(|gamepad| gamepad.pressed(GamepadButton::DPadDown));
|
||||||
|
|
||||||
|
let just_pressed = gamepads
|
||||||
|
.iter()
|
||||||
|
.any(|gamepad| gamepad.just_pressed(GamepadButton::DPadDown));
|
||||||
|
|
||||||
|
let just_pressed = key.just_pressed(KeyCode::F4) || just_pressed;
|
||||||
|
let pressed = key.pressed(KeyCode::F4) || pressed;
|
||||||
|
|
||||||
|
if just_pressed {
|
||||||
|
state.last_step_time = time.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let can_repress = pressed && (time.elapsed() - state.last_step_time.elapsed()).as_millis() > 500;
|
||||||
|
|
||||||
|
if !settings.menu_open && (just_pressed || can_repress)
|
||||||
|
{
|
||||||
|
state.requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.time_backup = Some(*time);
|
||||||
|
let delta = if state.requested {
|
||||||
|
frame_step_duration(**frame_rate)
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
};
|
||||||
|
time.advance_by(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_frame_step_time(mut state: ResMut<FrameStepState>, mut time: ResMut<Time>) {
|
||||||
|
if let Some(backup) = state.time_backup.take() {
|
||||||
|
*time = backup;
|
||||||
|
state.requested = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_selected(
|
fn activate_selected(
|
||||||
settings: &mut DebugSettings,
|
settings: &mut DebugSettings,
|
||||||
page_len: usize,
|
page_len: usize,
|
||||||
@@ -408,8 +490,10 @@ fn activate_selected(
|
|||||||
}
|
}
|
||||||
DebugMenuEntry::Toggle { flag, .. } => {
|
DebugMenuEntry::Toggle { flag, .. } => {
|
||||||
settings.toggle_flag(flag);
|
settings.toggle_flag(flag);
|
||||||
|
if !matches!(flag, DebugFlag::FrameStepping) {
|
||||||
queue_settings_save(commands);
|
queue_settings_save(commands);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
DebugMenuEntry::Submenu { entries, .. } => {
|
DebugMenuEntry::Submenu { entries, .. } => {
|
||||||
settings.push_page(DebugMenuPage::Static(entries));
|
settings.push_page(DebugMenuPage::Static(entries));
|
||||||
}
|
}
|
||||||
@@ -623,9 +707,11 @@ fn draw_palette(
|
|||||||
let mut value = settings.flag(*flag);
|
let mut value = settings.flag(*flag);
|
||||||
if ui.checkbox(&mut value, *label).changed() {
|
if ui.checkbox(&mut value, *label).changed() {
|
||||||
settings.toggle_flag(*flag);
|
settings.toggle_flag(*flag);
|
||||||
|
if !matches!(flag, DebugFlag::FrameStepping) {
|
||||||
queue_settings_save(&mut commands);
|
queue_settings_save(&mut commands);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
DebugMenuEntry::Submenu { label, .. }
|
DebugMenuEntry::Submenu { label, .. }
|
||||||
| DebugMenuEntry::PlayerSlots { label } => {
|
| DebugMenuEntry::PlayerSlots { label } => {
|
||||||
if ui.button(format!("{label} >")).clicked() {
|
if ui.button(format!("{label} >")).clicked() {
|
||||||
@@ -739,9 +825,9 @@ fn draw_palette(
|
|||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
let controls = if matches!(page, DebugMenuPage::PlayerRows(_)) {
|
let controls = if matches!(page, DebugMenuPage::PlayerRows(_)) {
|
||||||
"Mouse: choose Arrows/WASD: select/change Escape/B: back"
|
"Arrows/WASD: select/change Escape/B: back"
|
||||||
} else {
|
} else {
|
||||||
"Mouse: activate Arrows/WASD: select Enter/A: activate Escape/B: back"
|
"Arrows/WASD: select Enter/A: activate Escape/B: back"
|
||||||
};
|
};
|
||||||
ui.label(controls);
|
ui.label(controls);
|
||||||
});
|
});
|
||||||
@@ -839,6 +925,15 @@ mod tests {
|
|||||||
assert_eq!(GameSpeedDebug::Quarter.frame_rate(), 15);
|
assert_eq!(GameSpeedDebug::Quarter.frame_rate(), 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_stepping_is_disabled_by_default_and_uses_game_speed_timestep() {
|
||||||
|
let settings = DebugSettings::default();
|
||||||
|
|
||||||
|
assert!(!settings.frame_stepping);
|
||||||
|
assert_eq!(frame_step_duration(60), Duration::from_nanos(16_666_666));
|
||||||
|
assert_eq!(frame_step_duration(30), Duration::from_nanos(33_333_333));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_player_list_does_not_activate() {
|
fn empty_player_list_does_not_activate() {
|
||||||
let mut settings = DebugSettings::default();
|
let mut settings = DebugSettings::default();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
io,
|
io,
|
||||||
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
@@ -517,6 +518,7 @@ struct FighterBakeDeclaration {
|
|||||||
#[derive(Asset, TypePath)]
|
#[derive(Asset, TypePath)]
|
||||||
struct FighterBakeSource {
|
struct FighterBakeSource {
|
||||||
model_bytes: Vec<u8>,
|
model_bytes: Vec<u8>,
|
||||||
|
buffers: Vec<Vec<u8>>,
|
||||||
animations: HashMap<AnimKind, String>,
|
animations: HashMap<AnimKind, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,8 +552,31 @@ impl AssetLoader for FighterBakeSourceLoader {
|
|||||||
.read_asset_bytes(&manifest.model_path)
|
.read_asset_bytes(&manifest.model_path)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||||
|
|
||||||
|
let gltf = gltf::Gltf::from_slice(&model_bytes)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
|
let mut buffers = Vec::with_capacity(gltf.buffers().len());
|
||||||
|
for buffer in gltf.buffers() {
|
||||||
|
let bytes = match buffer.source() {
|
||||||
|
gltf::buffer::Source::Uri(uri) => load_context
|
||||||
|
.read_asset_bytes(AssetPath::from_path_buf(
|
||||||
|
Path::new(&manifest.model_path)
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new(""))
|
||||||
|
.join(uri),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.map_err(|error| io::Error::other(error.to_string()))?,
|
||||||
|
gltf::buffer::Source::Bin => gltf.blob.clone().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, "missing glTF binary blob")
|
||||||
|
})?,
|
||||||
|
};
|
||||||
|
buffers.push(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(FighterBakeSource {
|
Ok(FighterBakeSource {
|
||||||
model_bytes,
|
model_bytes,
|
||||||
|
buffers,
|
||||||
animations: manifest.animations,
|
animations: manifest.animations,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -600,7 +625,7 @@ impl AssetTransformer for FighterBakeTransformer {
|
|||||||
source: TransformedAsset<Self::AssetInput>,
|
source: TransformedAsset<Self::AssetInput>,
|
||||||
_: &'a Self::Settings,
|
_: &'a Self::Settings,
|
||||||
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
|
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
|
||||||
let baked = bake_gltf(&source.model_bytes, &source.animations)?;
|
let baked = bake_gltf(&source.model_bytes, &source.buffers, &source.animations)?;
|
||||||
Ok(source.replace_asset(baked))
|
Ok(source.replace_asset(baked))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -656,11 +681,36 @@ impl Plugin for BakedAnimationPlugin {
|
|||||||
|
|
||||||
fn bake_gltf(
|
fn bake_gltf(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
|
buffer_bytes: &[Vec<u8>],
|
||||||
animations: &HashMap<AnimKind, String>,
|
animations: &HashMap<AnimKind, String>,
|
||||||
) -> io::Result<BakedFighterAnimations> {
|
) -> io::Result<BakedFighterAnimations> {
|
||||||
let (document, buffers, _) = gltf::import_slice(bytes)
|
let gltf = gltf::Gltf::from_slice(bytes)
|
||||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
bake_gltf_document(document, buffers, animations)
|
let buffers = gltf
|
||||||
|
.buffers()
|
||||||
|
.zip(buffer_bytes)
|
||||||
|
.map(|(buffer, bytes)| {
|
||||||
|
if bytes.len() < buffer.length() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"glTF buffer {} is too short: expected {}, got {}",
|
||||||
|
buffer.index(),
|
||||||
|
buffer.length(),
|
||||||
|
bytes.len()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(gltf::buffer::Data(bytes.clone()))
|
||||||
|
})
|
||||||
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
if buffers.len() != gltf.buffers().len() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"glTF buffer data is incomplete",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
bake_gltf_document(gltf.document, buffers, animations)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bake_gltf_document(
|
fn bake_gltf_document(
|
||||||
|
|||||||
Reference in New Issue
Block a user