Compare commits

...
Author SHA1 Message Date
EIREXE c849caf5b9 Try and fix CI
CI / build-and-test (pull_request) Successful in 24m33s
2026-09-14 21:34:35 +02:00
EIREXE 9f66d55dcc Add keyboard support and hold to step
CI / build-and-test (pull_request) Failing after 15m7s
2026-09-14 19:12:25 +02:00
EIREXE 578648d2ec Add debug frame stepping
CI / build-and-test (pull_request) Failing after 15m6s
2026-09-14 18:47:55 +02:00
EIREXE 0d96a8aed8 Make warnings an error
CI / build-and-test (push) Failing after 15m6s
2026-09-14 18:25:15 +02:00
EIREXE cebf1148c2 Persist Rust CI cache (#4)
CI / build-and-test (push) Successful in 11m1s
## Summary

- Pin the nightly Rust toolchain used by CI
- Cache Cargo dependencies and build artifacts between Gitea Actions runs
- Share the Linux nightly cache across branches

Reviewed-on: #4
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-14 16:16:48 +00:00
EIREXE 088d594e97 Merge pull request 'Make debug menu usable with mouse and keyboard' (#2) from debug-menu-input into master
CI / build-and-test (push) Successful in 11m29s
Reviewed-on: #2
2026-09-14 13:32:42 +00:00
EIREXE 868df95cd4 wording fixes
CI / build-and-test (pull_request) Successful in 11m31s
2026-09-14 15:20:07 +02:00
EIREXE 504f63be54 Add support for split .gltf and .bin files (#3)
CI / build-and-test (push) Canceled after 15s
Reviewed-on: #3
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-14 13:19:29 +00:00
4 changed files with 163 additions and 12 deletions
+9 -3
View File
@@ -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
View File
@@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = "nightly" channel = "nightly-2026-09-14"
+100 -5
View File
@@ -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,7 +490,9 @@ fn activate_selected(
} }
DebugMenuEntry::Toggle { flag, .. } => { DebugMenuEntry::Toggle { flag, .. } => {
settings.toggle_flag(flag); settings.toggle_flag(flag);
queue_settings_save(commands); if !matches!(flag, DebugFlag::FrameStepping) {
queue_settings_save(commands);
}
} }
DebugMenuEntry::Submenu { entries, .. } => { DebugMenuEntry::Submenu { entries, .. } => {
settings.push_page(DebugMenuPage::Static(entries)); settings.push_page(DebugMenuPage::Static(entries));
@@ -623,7 +707,9 @@ 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);
queue_settings_save(&mut commands); if !matches!(flag, DebugFlag::FrameStepping) {
queue_settings_save(&mut commands);
}
} }
} }
DebugMenuEntry::Submenu { label, .. } DebugMenuEntry::Submenu { label, .. }
@@ -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();
+53 -3
View File
@@ -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(