Formatting
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@ use bevy_ggrs::RollbackFrameRate;
|
|||||||
use enum_cycling::EnumCycle;
|
use enum_cycling::EnumCycle;
|
||||||
use enum_cycling_derive::EnumCycle;
|
use enum_cycling_derive::EnumCycle;
|
||||||
|
|
||||||
use crate::{player::Player, replay::StopReplayRecording, AppState};
|
use crate::{AppState, player::Player, replay::StopReplayRecording};
|
||||||
|
|
||||||
pub const PLAYER_DEBUG_ROW_COUNT: usize = 6;
|
pub const PLAYER_DEBUG_ROW_COUNT: usize = 6;
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub fn apply_animation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect, EnumIter
|
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect, EnumIter,
|
||||||
)]
|
)]
|
||||||
pub enum AnimKind {
|
pub enum AnimKind {
|
||||||
Wait,
|
Wait,
|
||||||
|
|||||||
+30
-18
@@ -6,11 +6,18 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterTranslation, animation::{AnimKind, FighterAnimationFrame}, baked_animation::{FighterBoneMatrices, FixedMat4}, hurtbox::{FixedAffineCapsule, FixedCapsule}, manifest::FighterManifest,
|
Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterTranslation,
|
||||||
}, math::{
|
animation::{AnimKind, FighterAnimationFrame},
|
||||||
|
baked_animation::{FighterBoneMatrices, FixedMat4},
|
||||||
|
hurtbox::{FixedAffineCapsule, FixedCapsule},
|
||||||
|
manifest::FighterManifest,
|
||||||
|
},
|
||||||
|
math::{
|
||||||
int::{FGi32, FGi32Ext},
|
int::{FGi32, FGi32Ext},
|
||||||
vec3::FGVec3,
|
vec3::FGVec3,
|
||||||
}, schedule::GameplaySet, scripting::FighterAttackScript,
|
},
|
||||||
|
schedule::GameplaySet,
|
||||||
|
scripting::FighterAttackScript,
|
||||||
};
|
};
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct FighterDamage(FGi32);
|
pub struct FighterDamage(FGi32);
|
||||||
@@ -207,18 +214,23 @@ pub fn solve_hitboxes(
|
|||||||
&FighterTranslation,
|
&FighterTranslation,
|
||||||
&FighterFacingDirection,
|
&FighterFacingDirection,
|
||||||
&mut FighterHitboxes,
|
&mut FighterHitboxes,
|
||||||
&FighterAnimationFrame
|
&FighterAnimationFrame,
|
||||||
)>,
|
)>,
|
||||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||||
) {
|
) {
|
||||||
for (matrices, translation, facing_direction, mut hitboxes, animation_frame) in &mut query {
|
for (matrices, translation, facing_direction, mut hitboxes, animation_frame) in &mut query {
|
||||||
hitboxes.active_hitboxes_solved.clear();
|
hitboxes.active_hitboxes_solved.clear();
|
||||||
if let Some(ref attack_script) = hitboxes.attack_script {
|
if let Some(ref attack_script) = hitboxes.attack_script {
|
||||||
let attack_script = attack_scripts.get(attack_script).expect("Attack script should be valid");
|
let attack_script = attack_scripts
|
||||||
|
.get(attack_script)
|
||||||
|
.expect("Attack script should be valid");
|
||||||
let fighter_trf = translation.get_3d_transform(facing_direction);
|
let fighter_trf = translation.get_3d_transform(facing_direction);
|
||||||
let active_hitboxes = hitboxes.active_hitboxes.clone();
|
let active_hitboxes = hitboxes.active_hitboxes.clone();
|
||||||
for hitbox_idx in active_hitboxes {
|
for hitbox_idx in active_hitboxes {
|
||||||
let hitbox = attack_script.hitboxes.get(hitbox_idx).expect("Hitbox IDX should be valid");
|
let hitbox = attack_script
|
||||||
|
.hitboxes
|
||||||
|
.get(hitbox_idx)
|
||||||
|
.expect("Hitbox IDX should be valid");
|
||||||
let matrices = matrices.get(&hitbox.bone);
|
let matrices = matrices.get(&hitbox.bone);
|
||||||
|
|
||||||
if let None = matrices {
|
if let None = matrices {
|
||||||
@@ -233,23 +245,19 @@ pub fn solve_hitboxes(
|
|||||||
|
|
||||||
if hitbox.start_frame == animation_frame.frame {
|
if hitbox.start_frame == animation_frame.frame {
|
||||||
// Sphere only
|
// Sphere only
|
||||||
hitboxes.active_hitboxes_solved.push(
|
hitboxes.active_hitboxes_solved.push(FixedCapsule {
|
||||||
FixedCapsule {
|
|
||||||
start: curr_hbox_pos,
|
start: curr_hbox_pos,
|
||||||
end: curr_hbox_pos,
|
end: curr_hbox_pos,
|
||||||
radius: hitbox.radius
|
radius: hitbox.radius,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
let prev_hbox_matrix = fighter_trf.mul(prev_bone_matrix);
|
let prev_hbox_matrix = fighter_trf.mul(prev_bone_matrix);
|
||||||
let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset);
|
let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset);
|
||||||
hitboxes.active_hitboxes_solved.push(
|
hitboxes.active_hitboxes_solved.push(FixedCapsule {
|
||||||
FixedCapsule {
|
|
||||||
start: prev_hbox_pos,
|
start: prev_hbox_pos,
|
||||||
end: curr_hbox_pos,
|
end: curr_hbox_pos,
|
||||||
radius: hitbox.radius
|
radius: hitbox.radius,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,7 +311,10 @@ pub fn solve_hurtboxes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn intersect_attacks_with_hurtboxes(query: Query<(Entity, &FighterSolvedHurtboxes, &FighterHitboxes)>, attack_scripts: Res<Assets<FighterAttackScript>>) {
|
pub fn intersect_attacks_with_hurtboxes(
|
||||||
|
query: Query<(Entity, &FighterSolvedHurtboxes, &FighterHitboxes)>,
|
||||||
|
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||||
|
) {
|
||||||
/*for (entity_hurting, _, hitboxes) in query {
|
/*for (entity_hurting, _, hitboxes) in query {
|
||||||
if hitboxes.attack_script.is_none() {
|
if hitboxes.attack_script.is_none() {
|
||||||
continue;
|
continue;
|
||||||
@@ -338,7 +349,8 @@ impl Plugin for FighterAttackPlugin {
|
|||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
GgrsSchedule,
|
GgrsSchedule,
|
||||||
(solve_hurtboxes, solve_hitboxes).chain()
|
(solve_hurtboxes, solve_hitboxes)
|
||||||
|
.chain()
|
||||||
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
|
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
|
||||||
.in_set(GameplaySet::Animation),
|
.in_set(GameplaySet::Animation),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ pub fn air_collide_with_stage(
|
|||||||
|
|
||||||
for (poly_idx, poly) in state_context.stage_collision.stage_polys.iter().enumerate() {
|
for (poly_idx, poly) in state_context.stage_collision.stage_polys.iter().enumerate() {
|
||||||
if let Some(intersect_result) = poly.intersect_ray(ray_segment) {
|
if let Some(intersect_result) = poly.intersect_ray(ray_segment) {
|
||||||
intersect_results.push((AirCollisionWithStageResult {
|
intersect_results.push((
|
||||||
|
AirCollisionWithStageResult {
|
||||||
line_id: {
|
line_id: {
|
||||||
StageLineID {
|
StageLineID {
|
||||||
polygon: poly_idx,
|
polygon: poly_idx,
|
||||||
@@ -58,14 +59,17 @@ pub fn air_collide_with_stage(
|
|||||||
},
|
},
|
||||||
hit_position: intersect_result.position,
|
hit_position: intersect_result.position,
|
||||||
hit_normal: intersect_result.normal,
|
hit_normal: intersect_result.normal,
|
||||||
}, intersect_result.position.distance_squared(ray_segment.point2())));
|
},
|
||||||
|
intersect_result
|
||||||
|
.position
|
||||||
|
.distance_squared(ray_segment.point2()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
intersect_results.sort_by(| (_, a), (_, b) | {
|
intersect_results.sort_by(|(_, a), (_, b)| a.cmp(b));
|
||||||
a.cmp(b)
|
|
||||||
});
|
|
||||||
|
|
||||||
intersect_results.iter().last().map(|(val, _)| {
|
intersect_results
|
||||||
val.to_owned().clone()
|
.iter()
|
||||||
})
|
.last()
|
||||||
|
.map(|(val, _)| val.to_owned().clone())
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-11
@@ -73,7 +73,6 @@ impl FixedAffineCapsule {
|
|||||||
// current simplex to fixed-point precision, so conservatively collide.
|
// current simplex to fixed-point precision, so conservatively collide.
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trait SupportMap {
|
trait SupportMap {
|
||||||
@@ -243,18 +242,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn affine_scale_changes_capsule_intersection() {
|
fn affine_scale_changes_capsule_intersection() {
|
||||||
let sphere = FixedAffineCapsule::new(
|
let sphere = FixedAffineCapsule::new(FixedMat4::IDENTITY, FGi32::ZERO, FGi32::lit("1"));
|
||||||
FixedMat4::IDENTITY,
|
|
||||||
FGi32::ZERO,
|
|
||||||
FGi32::lit("1"),
|
|
||||||
);
|
|
||||||
let mut squashed_transform = FixedMat4::IDENTITY;
|
let mut squashed_transform = FixedMat4::IDENTITY;
|
||||||
squashed_transform.cols[1][1] = FGi32::lit("0.5");
|
squashed_transform.cols[1][1] = FGi32::lit("0.5");
|
||||||
let squashed = FixedAffineCapsule::new(
|
let squashed = FixedAffineCapsule::new(squashed_transform, FGi32::ZERO, FGi32::lit("1"));
|
||||||
squashed_transform,
|
|
||||||
FGi32::ZERO,
|
|
||||||
FGi32::lit("1"),
|
|
||||||
);
|
|
||||||
let target = FixedCapsule::new(
|
let target = FixedCapsule::new(
|
||||||
FGVec3::lit("0", "1.4", "0"),
|
FGVec3::lit("0", "1.4", "0"),
|
||||||
FGVec3::lit("0", "1.4", "0"),
|
FGVec3::lit("0", "1.4", "0"),
|
||||||
|
|||||||
+14
-3
@@ -30,8 +30,19 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes}, baked_animation::FighterBoneMatrices, hurtbox::FixedCapsule, manifest::FighterManifest, state::{FighterState, fall::FallState}, visual::FighterAnimations,
|
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes},
|
||||||
}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision,
|
baked_animation::FighterBoneMatrices,
|
||||||
|
hurtbox::FixedCapsule,
|
||||||
|
manifest::FighterManifest,
|
||||||
|
state::{FighterState, fall::FallState},
|
||||||
|
visual::FighterAnimations,
|
||||||
|
},
|
||||||
|
input::FighterInput,
|
||||||
|
math::{int::FGi32, vec::FGVec2},
|
||||||
|
player::Player,
|
||||||
|
schedule::GameplaySet,
|
||||||
|
scripting::FighterAttackScript,
|
||||||
|
stage::line::StageCollision,
|
||||||
};
|
};
|
||||||
use state::state_interrupt_system;
|
use state::state_interrupt_system;
|
||||||
|
|
||||||
@@ -221,7 +232,7 @@ pub fn spawn_fighter(
|
|||||||
FighterHitboxes {
|
FighterHitboxes {
|
||||||
attack_script: None,
|
attack_script: None,
|
||||||
active_hitboxes: vec![],
|
active_hitboxes: vec![],
|
||||||
active_hitboxes_solved: vec![]
|
active_hitboxes_solved: vec![],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ pub fn check_interrupt(
|
|||||||
.input
|
.input
|
||||||
.has_command(crate::input::FighterCommands::Attack)
|
.has_command(crate::input::FighterCommands::Attack)
|
||||||
{
|
{
|
||||||
let attack_kind = state_context.input.get_attack_command_type().to_attack_kind_grounded();
|
let attack_kind = state_context
|
||||||
|
.input
|
||||||
|
.get_attack_command_type()
|
||||||
|
.to_attack_kind_grounded();
|
||||||
Some(FighterState::GroundAttack(GroundAttackState {
|
Some(FighterState::GroundAttack(GroundAttackState {
|
||||||
attack_kind,
|
attack_kind,
|
||||||
grounded_common: grounded_common.clone(),
|
grounded_common: grounded_common.clone(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_asset_loader::asset_collection::AssetCollection;
|
use bevy_asset_loader::asset_collection::AssetCollection;
|
||||||
|
use ron_asset_manager::prelude::RonAsset;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{math::int::FGi32, menus::style::FGUiStyle};
|
use crate::{math::int::FGi32, menus::style::FGUiStyle};
|
||||||
@@ -23,7 +24,7 @@ pub struct InputSettingsCommon {
|
|||||||
pub input_buffer_size: u8,
|
pub input_buffer_size: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Reflect, Serialize, Deserialize, Resource, Asset, Clone)]
|
#[derive(Reflect, Serialize, Deserialize, Resource, Asset, Clone, RonAsset)]
|
||||||
pub struct GameSettings {
|
pub struct GameSettings {
|
||||||
pub fighter_common: FighterSettingsCommon,
|
pub fighter_common: FighterSettingsCommon,
|
||||||
pub input_common: InputSettingsCommon,
|
pub input_common: InputSettingsCommon,
|
||||||
|
|||||||
+12
-6
@@ -177,7 +177,8 @@ pub fn postprocess_input(
|
|||||||
{
|
{
|
||||||
input.frames_in_x_flick_deadzone = 1;
|
input.frames_in_x_flick_deadzone = 1;
|
||||||
} else {
|
} else {
|
||||||
input.frames_in_x_flick_deadzone = input.frames_in_x_flick_deadzone.saturating_add(1);
|
input.frames_in_x_flick_deadzone =
|
||||||
|
input.frames_in_x_flick_deadzone.saturating_add(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +191,8 @@ pub fn postprocess_input(
|
|||||||
{
|
{
|
||||||
input.frames_in_y_flick_deadzone = 1;
|
input.frames_in_y_flick_deadzone = 1;
|
||||||
} else {
|
} else {
|
||||||
input.frames_in_y_flick_deadzone = input.frames_in_y_flick_deadzone.saturating_add(1);
|
input.frames_in_y_flick_deadzone =
|
||||||
|
input.frames_in_y_flick_deadzone.saturating_add(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +230,10 @@ pub fn postprocess_input(
|
|||||||
input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size);
|
input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut y_flick_detected = y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold;
|
let mut y_flick_detected =
|
||||||
let mut x_flick_detected = x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold;
|
y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold;
|
||||||
|
let mut x_flick_detected =
|
||||||
|
x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold;
|
||||||
|
|
||||||
// if both flicks are detected, only one wins
|
// if both flicks are detected, only one wins
|
||||||
|
|
||||||
@@ -270,11 +274,13 @@ pub fn postprocess_input(
|
|||||||
FighterAttackCommandType::Neutral
|
FighterAttackCommandType::Neutral
|
||||||
};
|
};
|
||||||
|
|
||||||
input.set_lifetime(FighterCommands::Attack, game_settings.input_common.input_buffer_size);
|
input.set_lifetime(
|
||||||
|
FighterCommands::Attack,
|
||||||
|
game_settings.input_common.input_buffer_size,
|
||||||
|
);
|
||||||
input.set_attack_command_type(attack_type);
|
input.set_attack_command_type(attack_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if x_flick_detected {
|
if x_flick_detected {
|
||||||
input.clear_command(FighterCommands::SmashMoveLeft);
|
input.clear_command(FighterCommands::SmashMoveLeft);
|
||||||
input.clear_command(FighterCommands::SmashMoveRight);
|
input.clear_command(FighterCommands::SmashMoveRight);
|
||||||
|
|||||||
@@ -149,19 +149,19 @@ pub fn gc_event_system(
|
|||||||
send_axis!(GamepadAxis::LeftStickY, now.left_stick.1);
|
send_axis!(GamepadAxis::LeftStickY, now.left_stick.1);
|
||||||
}
|
}
|
||||||
if prev.right_stick != now.right_stick {
|
if prev.right_stick != now.right_stick {
|
||||||
send_axis!(
|
send_axis!(GamepadAxis::RightStickX, now.right_stick.0);
|
||||||
GamepadAxis::RightStickX,
|
send_axis!(GamepadAxis::RightStickY, now.right_stick.1);
|
||||||
now.right_stick.0
|
|
||||||
);
|
|
||||||
send_axis!(
|
|
||||||
GamepadAxis::RightStickY,
|
|
||||||
now.right_stick.1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if prev.triggers != now.triggers {
|
if prev.triggers != now.triggers {
|
||||||
// The analog triggers are represented as button-axis values, as we have no GamepadAxis to use.
|
// The analog triggers are represented as button-axis values, as we have no GamepadAxis to use.
|
||||||
send_button!(GamepadButton::LeftTrigger2, unsigned_axis_float(now.triggers.0));
|
send_button!(
|
||||||
send_button!(GamepadButton::RightTrigger2, unsigned_axis_float(now.triggers.1));
|
GamepadButton::LeftTrigger2,
|
||||||
|
unsigned_axis_float(now.triggers.0)
|
||||||
|
);
|
||||||
|
send_button!(
|
||||||
|
GamepadButton::RightTrigger2,
|
||||||
|
unsigned_axis_float(now.triggers.1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! digital {
|
macro_rules! digital {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
pub mod bridge;
|
pub mod bridge;
|
||||||
pub mod poll;
|
pub mod poll;
|
||||||
|
|
||||||
pub use bridge::{GcPort, GcPorts, GC_PORT_COUNT};
|
pub use bridge::{GC_PORT_COUNT, GcPort, GcPorts};
|
||||||
pub use poll::{GcAdapterSnapshot, GcPortState};
|
pub use poll::{GcAdapterSnapshot, GcPortState};
|
||||||
|
|
||||||
use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system};
|
|
||||||
use bevy::input::InputSystems;
|
use bevy::input::InputSystems;
|
||||||
|
use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|||||||
@@ -98,12 +98,9 @@ pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
|||||||
.name("gc-adapter-poll".into())
|
.name("gc-adapter-poll".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
// Flush whatever the adapter had buffered before we started
|
// Flush whatever the adapter had buffered before we started
|
||||||
if let Err(payload) =
|
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| adapter.refresh_inputs()))
|
||||||
panic::catch_unwind(AssertUnwindSafe(|| adapter.refresh_inputs()))
|
|
||||||
{
|
{
|
||||||
error!(
|
error!("GameCube Controller Adapter USB refresh panicked on startup: {payload:?}");
|
||||||
"GameCube Controller Adapter USB refresh panicked on startup: {payload:?}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -112,8 +109,7 @@ pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
|||||||
let mut rate_window_count: u32 = 0;
|
let mut rate_window_count: u32 = 0;
|
||||||
|
|
||||||
while !thread_shutdown.load(Ordering::Relaxed) {
|
while !thread_shutdown.load(Ordering::Relaxed) {
|
||||||
let result =
|
let result = panic::catch_unwind(AssertUnwindSafe(|| adapter.read_controllers()));
|
||||||
panic::catch_unwind(AssertUnwindSafe(|| adapter.read_controllers()));
|
|
||||||
|
|
||||||
let controllers = match result {
|
let controllers = match result {
|
||||||
Ok(Ok(controllers)) => controllers,
|
Ok(Ok(controllers)) => controllers,
|
||||||
@@ -142,7 +138,8 @@ pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
|||||||
|
|
||||||
let mut states = [GcPortState::default(); 4];
|
let mut states = [GcPortState::default(); 4];
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
states[i] = GcPortState::make_port_state(&controllers[i], adapter.calibration(i));
|
states[i] =
|
||||||
|
GcPortState::make_port_state(&controllers[i], adapter.calibration(i));
|
||||||
}
|
}
|
||||||
*thread_ports.lock().unwrap_or_else(|e| e.into_inner()) = states;
|
*thread_ports.lock().unwrap_or_else(|e| e.into_inner()) = states;
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -21,8 +21,16 @@ pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap) -> FighterInputFr
|
|||||||
// Digital buttons driving movement isn't used by the
|
// Digital buttons driving movement isn't used by the
|
||||||
// default bindings (the stick covers that), but stay
|
// default bindings (the stick covers that), but stay
|
||||||
// consistent with the keyboard backend if one is added.
|
// consistent with the keyboard backend if one is added.
|
||||||
InputMapAction::MovementXDir(sign) => if pressed { movement.x += sign as f32 },
|
InputMapAction::MovementXDir(sign) => {
|
||||||
InputMapAction::MovementYDir(sign) => if pressed { movement.y += sign as f32 },
|
if pressed {
|
||||||
|
movement.x += sign as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputMapAction::MovementYDir(sign) => {
|
||||||
|
if pressed {
|
||||||
|
movement.y += sign as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// gamepad does not have a separate axis for left and right, so sign here should be 1
|
// gamepad does not have a separate axis for left and right, so sign here should be 1
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ pub fn sample_keyboard(key: &ButtonInput<KeyCode>, input_map: &BaseInputMap) ->
|
|||||||
InputMapAction::Jump => input_frame.jump = action_strength != FGi32::ZERO,
|
InputMapAction::Jump => input_frame.jump = action_strength != FGi32::ZERO,
|
||||||
InputMapAction::Shield => input_frame.shield = action_strength != FGi32::ZERO,
|
InputMapAction::Shield => input_frame.shield = action_strength != FGi32::ZERO,
|
||||||
InputMapAction::Attack => input_frame.attack = action_strength != FGi32::ZERO,
|
InputMapAction::Attack => input_frame.attack = action_strength != FGi32::ZERO,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+95
-26
@@ -41,6 +41,10 @@ impl LocalInputAssignments {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_from_slot(&self, slot: usize) -> Option<&LocalInputSource> {
|
||||||
|
self.0.iter().find(|(c_slot, _)| slot == *c_slot).map(|(_, source)| source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
@@ -92,8 +96,6 @@ impl Plugin for FighterInputPlugin {
|
|||||||
key: KeyCode::KeyJ,
|
key: KeyCode::KeyJ,
|
||||||
action: InputMapAction::Attack,
|
action: InputMapAction::Attack,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// Menu
|
// Menu
|
||||||
],
|
],
|
||||||
gamepad: vec![
|
gamepad: vec![
|
||||||
@@ -125,33 +127,92 @@ impl Plugin for FighterInputPlugin {
|
|||||||
binding: GamepadBinding::Button(GamepadButton::East),
|
binding: GamepadBinding::Button(GamepadButton::East),
|
||||||
action: InputMapAction::Attack,
|
action: InputMapAction::Attack,
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.insert_resource(MenuInputMap {
|
.insert_resource(MenuInputMap {
|
||||||
keyboard: vec![
|
keyboard: vec![
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::ArrowUp, action: MenuInputMapAction::MovementYDir(1) },
|
MenuKeyboardInputMapElement {
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::ArrowDown, action: MenuInputMapAction::MovementYDir(-1) },
|
key: KeyCode::ArrowUp,
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::ArrowLeft, action: MenuInputMapAction::MovementXDir(-1) },
|
action: MenuInputMapAction::MovementYDir(1),
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::ArrowRight, action: MenuInputMapAction::MovementXDir(1) },
|
},
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::Enter, action: MenuInputMapAction::Accept },
|
MenuKeyboardInputMapElement {
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::Escape, action: MenuInputMapAction::Back },
|
key: KeyCode::ArrowDown,
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::Space, action: MenuInputMapAction::Start },
|
action: MenuInputMapAction::MovementYDir(-1),
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::KeyW, action: MenuInputMapAction::MovementYDir(1) },
|
},
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::KeyS, action: MenuInputMapAction::MovementYDir(-1) },
|
MenuKeyboardInputMapElement {
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::KeyA, action: MenuInputMapAction::MovementXDir(-1) },
|
key: KeyCode::ArrowLeft,
|
||||||
MenuKeyboardInputMapElement { key: KeyCode::KeyD, action: MenuInputMapAction::MovementXDir(1) },
|
action: MenuInputMapAction::MovementXDir(-1),
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::ArrowRight,
|
||||||
|
action: MenuInputMapAction::MovementXDir(1),
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::Enter,
|
||||||
|
action: MenuInputMapAction::Accept,
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::Escape,
|
||||||
|
action: MenuInputMapAction::Back,
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::Space,
|
||||||
|
action: MenuInputMapAction::Start,
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::KeyW,
|
||||||
|
action: MenuInputMapAction::MovementYDir(1),
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::KeyS,
|
||||||
|
action: MenuInputMapAction::MovementYDir(-1),
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::KeyA,
|
||||||
|
action: MenuInputMapAction::MovementXDir(-1),
|
||||||
|
},
|
||||||
|
MenuKeyboardInputMapElement {
|
||||||
|
key: KeyCode::KeyD,
|
||||||
|
action: MenuInputMapAction::MovementXDir(1),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
gamepad: vec![
|
gamepad: vec![
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Button(GamepadButton::DPadUp), action: MenuInputMapAction::MovementYDir(1) },
|
MenuGamepadInputMapElement {
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Button(GamepadButton::DPadDown), action: MenuInputMapAction::MovementYDir(-1) },
|
binding: GamepadBinding::Button(GamepadButton::DPadUp),
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Button(GamepadButton::DPadLeft), action: MenuInputMapAction::MovementXDir(-1) },
|
action: MenuInputMapAction::MovementYDir(1),
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Button(GamepadButton::DPadRight), action: MenuInputMapAction::MovementXDir(1) },
|
},
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Axis(GamepadAxis::LeftStickX, 1), action: MenuInputMapAction::MovementXDir(1) },
|
MenuGamepadInputMapElement {
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Axis(GamepadAxis::LeftStickY, 1), action: MenuInputMapAction::MovementYDir(1) },
|
binding: GamepadBinding::Button(GamepadButton::DPadDown),
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Button(GamepadButton::South), action: MenuInputMapAction::Accept },
|
action: MenuInputMapAction::MovementYDir(-1),
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Button(GamepadButton::West), action: MenuInputMapAction::Back },
|
},
|
||||||
MenuGamepadInputMapElement { binding: GamepadBinding::Button(GamepadButton::Start), action: MenuInputMapAction::Start },
|
MenuGamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Button(GamepadButton::DPadLeft),
|
||||||
|
action: MenuInputMapAction::MovementXDir(-1),
|
||||||
|
},
|
||||||
|
MenuGamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Button(GamepadButton::DPadRight),
|
||||||
|
action: MenuInputMapAction::MovementXDir(1),
|
||||||
|
},
|
||||||
|
MenuGamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Axis(GamepadAxis::LeftStickX, 1),
|
||||||
|
action: MenuInputMapAction::MovementXDir(1),
|
||||||
|
},
|
||||||
|
MenuGamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Axis(GamepadAxis::LeftStickY, 1),
|
||||||
|
action: MenuInputMapAction::MovementYDir(1),
|
||||||
|
},
|
||||||
|
MenuGamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Button(GamepadButton::South),
|
||||||
|
action: MenuInputMapAction::Accept,
|
||||||
|
},
|
||||||
|
MenuGamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Button(GamepadButton::West),
|
||||||
|
action: MenuInputMapAction::Back,
|
||||||
|
},
|
||||||
|
MenuGamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Button(GamepadButton::Start),
|
||||||
|
action: MenuInputMapAction::Start,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.init_resource::<LocalInputAssignments>()
|
.init_resource::<LocalInputAssignments>()
|
||||||
@@ -183,16 +244,24 @@ fn read_local_inputs(
|
|||||||
gamepads: Query<&Gamepad>,
|
gamepads: Query<&Gamepad>,
|
||||||
assignments: Res<LocalInputAssignments>,
|
assignments: Res<LocalInputAssignments>,
|
||||||
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.0.iter().find(|(player_handle, _)| player_handle == handle);
|
let assigned = assignments
|
||||||
|
.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.entities.get(*handle).copied().flatten().and_then(|entity| gamepads.get(entity).ok()),
|
None => gc_ports
|
||||||
|
.entities
|
||||||
|
.get(*handle)
|
||||||
|
.copied()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|entity| gamepads.get(entity).ok()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut input_frame = match assigned {
|
let mut input_frame = match assigned {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::math::{
|
use crate::math::{
|
||||||
|
|||||||
+156
-35
@@ -1,13 +1,24 @@
|
|||||||
//! Deterministic input recording and playback.
|
//! Deterministic input recording and playback.
|
||||||
|
|
||||||
use std::{collections::BTreeMap, fs, path::{Path, PathBuf}};
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_ggrs::{prelude::*, PlayerInputs, RollbackFrameCount, SaveWorld};
|
use bevy_ggrs::{PlayerInputs, RollbackFrameCount, SaveWorld, prelude::*};
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{args::Args, input::FighterInputFrame, match_loading::{MatchPlayer, PendingMatch}, netcode::GGRSCfg, schedule::GameplaySet, AppState};
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
args::Args,
|
||||||
|
input::FighterInputFrame,
|
||||||
|
match_loading::{MatchPlayer, PendingMatch},
|
||||||
|
netcode::GGRSCfg,
|
||||||
|
schedule::GameplaySet,
|
||||||
|
};
|
||||||
|
|
||||||
const MAGIC: &[u8; 8] = b"SPRPLY01";
|
const MAGIC: &[u8; 8] = b"SPRPLY01";
|
||||||
pub const REPLAY_VERSION: u32 = 1;
|
pub const REPLAY_VERSION: u32 = 1;
|
||||||
@@ -79,7 +90,12 @@ impl Plugin for ReplayPlugin {
|
|||||||
.in_set(GameplaySet::Input)
|
.in_set(GameplaySet::Input)
|
||||||
.before(crate::input::buffer::postprocess_input),
|
.before(crate::input::buffer::postprocess_input),
|
||||||
)
|
)
|
||||||
.add_systems(SaveWorld, record_checksum.after(SaveWorldSystems::Checksum).before(SaveWorldSystems::Snapshot))
|
.add_systems(
|
||||||
|
SaveWorld,
|
||||||
|
record_checksum
|
||||||
|
.after(SaveWorldSystems::Checksum)
|
||||||
|
.before(SaveWorldSystems::Snapshot),
|
||||||
|
)
|
||||||
.add_systems(OnExit(AppState::InMatch), finish_recording)
|
.add_systems(OnExit(AppState::InMatch), finish_recording)
|
||||||
.add_systems(Update, stop_recording.run_if(in_state(AppState::InMatch)))
|
.add_systems(Update, stop_recording.run_if(in_state(AppState::InMatch)))
|
||||||
.add_systems(Update, finish_playback.run_if(in_state(AppState::InMatch)));
|
.add_systems(Update, finish_playback.run_if(in_state(AppState::InMatch)));
|
||||||
@@ -87,7 +103,8 @@ impl Plugin for ReplayPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(path: &Path) -> Result<Replay, String> {
|
pub fn load(path: &Path) -> Result<Replay, String> {
|
||||||
let bytes = fs::read(path).map_err(|error| format!("cannot read replay {}: {error}", path.display()))?;
|
let bytes = fs::read(path)
|
||||||
|
.map_err(|error| format!("cannot read replay {}: {error}", path.display()))?;
|
||||||
let replay = if bytes.starts_with(MAGIC) {
|
let replay = if bytes.starts_with(MAGIC) {
|
||||||
if bytes.len() < MAGIC.len() + 4 {
|
if bytes.len() < MAGIC.len() + 4 {
|
||||||
return Err("replay binary header is truncated".into());
|
return Err("replay binary header is truncated".into());
|
||||||
@@ -96,7 +113,8 @@ pub fn load(path: &Path) -> Result<Replay, String> {
|
|||||||
if version != REPLAY_VERSION {
|
if version != REPLAY_VERSION {
|
||||||
return Err(format!("unsupported replay version {version}"));
|
return Err(format!("unsupported replay version {version}"));
|
||||||
}
|
}
|
||||||
bincode::deserialize(&bytes[12..]).map_err(|error| format!("invalid binary replay: {error}"))?
|
bincode::deserialize(&bytes[12..])
|
||||||
|
.map_err(|error| format!("invalid binary replay: {error}"))?
|
||||||
} else {
|
} else {
|
||||||
serde_json::from_slice(&bytes).map_err(|error| format!("invalid JSON replay: {error}"))?
|
serde_json::from_slice(&bytes).map_err(|error| format!("invalid JSON replay: {error}"))?
|
||||||
};
|
};
|
||||||
@@ -110,10 +128,14 @@ pub fn save(path: &Path, format: ReplayFormat, replay: &Replay) -> Result<(), St
|
|||||||
ReplayFormat::Binary => {
|
ReplayFormat::Binary => {
|
||||||
let mut bytes = MAGIC.to_vec();
|
let mut bytes = MAGIC.to_vec();
|
||||||
bytes.extend(REPLAY_VERSION.to_le_bytes());
|
bytes.extend(REPLAY_VERSION.to_le_bytes());
|
||||||
bytes.extend(bincode::serialize(replay).map_err(|error| format!("cannot encode replay: {error}"))?);
|
bytes.extend(
|
||||||
|
bincode::serialize(replay)
|
||||||
|
.map_err(|error| format!("cannot encode replay: {error}"))?,
|
||||||
|
);
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
ReplayFormat::Json => serde_json::to_vec_pretty(replay).map_err(|error| format!("cannot encode replay JSON: {error}"))?,
|
ReplayFormat::Json => serde_json::to_vec_pretty(replay)
|
||||||
|
.map_err(|error| format!("cannot encode replay JSON: {error}"))?,
|
||||||
};
|
};
|
||||||
let temporary = path.with_extension("replay.tmp");
|
let temporary = path.with_extension("replay.tmp");
|
||||||
fs::write(&temporary, bytes).map_err(|error| format!("cannot write replay: {error}"))?;
|
fs::write(&temporary, bytes).map_err(|error| format!("cannot write replay: {error}"))?;
|
||||||
@@ -122,7 +144,10 @@ pub fn save(path: &Path, format: ReplayFormat, replay: &Replay) -> Result<(), St
|
|||||||
|
|
||||||
fn validate(replay: &Replay) -> Result<(), String> {
|
fn validate(replay: &Replay) -> Result<(), String> {
|
||||||
if replay.replay_version != REPLAY_VERSION {
|
if replay.replay_version != REPLAY_VERSION {
|
||||||
return Err(format!("unsupported replay version {}", replay.replay_version));
|
return Err(format!(
|
||||||
|
"unsupported replay version {}",
|
||||||
|
replay.replay_version
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if replay.players_are_invalid() {
|
if replay.players_are_invalid() {
|
||||||
return Err("replay player handles must cover 0..player_count".into());
|
return Err("replay player handles must cover 0..player_count".into());
|
||||||
@@ -130,10 +155,18 @@ fn validate(replay: &Replay) -> Result<(), String> {
|
|||||||
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());
|
||||||
}
|
}
|
||||||
if replay.frames.windows(2).any(|frames| frames[0].frame >= frames[1].frame) {
|
if replay
|
||||||
|
.frames
|
||||||
|
.windows(2)
|
||||||
|
.any(|frames| frames[0].frame >= frames[1].frame)
|
||||||
|
{
|
||||||
return Err("replay frames must be strictly ordered".into());
|
return Err("replay frames must be strictly ordered".into());
|
||||||
}
|
}
|
||||||
if replay.frames.iter().any(|frame| frame.inputs.len() != replay.match_config.players.len()) {
|
if replay
|
||||||
|
.frames
|
||||||
|
.iter()
|
||||||
|
.any(|frame| frame.inputs.len() != replay.match_config.players.len())
|
||||||
|
{
|
||||||
return Err("replay frame input count does not match player count".into());
|
return Err("replay frame input count does not match player count".into());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -141,7 +174,12 @@ fn validate(replay: &Replay) -> Result<(), String> {
|
|||||||
|
|
||||||
impl Replay {
|
impl Replay {
|
||||||
fn players_are_invalid(&self) -> bool {
|
fn players_are_invalid(&self) -> bool {
|
||||||
let mut handles: Vec<_> = self.match_config.players.iter().map(|player| player.handle).collect();
|
let mut handles: Vec<_> = self
|
||||||
|
.match_config
|
||||||
|
.players
|
||||||
|
.iter()
|
||||||
|
.map(|player| player.handle)
|
||||||
|
.collect();
|
||||||
handles.sort_unstable();
|
handles.sort_unstable();
|
||||||
handles != (0..handles.len()).collect::<Vec<_>>()
|
handles != (0..handles.len()).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
@@ -149,23 +187,44 @@ impl Replay {
|
|||||||
|
|
||||||
fn start_replay(mut commands: Commands, args: Res<Args>, loaded: Option<Res<LoadedReplay>>) {
|
fn start_replay(mut commands: Commands, args: Res<Args>, loaded: Option<Res<LoadedReplay>>) {
|
||||||
let mode = if let Some(replay) = loaded {
|
let mode = if let Some(replay) = loaded {
|
||||||
ReplayMode::Playback { replay: replay.0.clone(), mismatch: false }
|
ReplayMode::Playback {
|
||||||
|
replay: replay.0.clone(),
|
||||||
|
mismatch: false,
|
||||||
|
}
|
||||||
} else if let Some(path) = args.record_replay.clone() {
|
} else if let Some(path) = args.record_replay.clone() {
|
||||||
ReplayMode::Record { path, format: args.replay_format }
|
ReplayMode::Record {
|
||||||
|
path,
|
||||||
|
format: args.replay_format,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
commands.insert_resource(ReplayRuntime { mode, frames: BTreeMap::new(), checksums: BTreeMap::new(), finished: false });
|
commands.insert_resource(ReplayRuntime {
|
||||||
|
mode,
|
||||||
|
frames: BTreeMap::new(),
|
||||||
|
checksums: BTreeMap::new(),
|
||||||
|
finished: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replay_inputs(mut inputs: ResMut<PlayerInputs<GGRSCfg>>, frame: Res<RollbackFrameCount>, runtime: Option<ResMut<ReplayRuntime>>) {
|
fn replay_inputs(
|
||||||
|
mut inputs: ResMut<PlayerInputs<GGRSCfg>>,
|
||||||
|
frame: Res<RollbackFrameCount>,
|
||||||
|
runtime: Option<ResMut<ReplayRuntime>>,
|
||||||
|
) {
|
||||||
let Some(mut runtime) = runtime else { return };
|
let Some(mut runtime) = runtime else { return };
|
||||||
match &mut runtime.mode {
|
match &mut runtime.mode {
|
||||||
ReplayMode::Record { .. } => {
|
ReplayMode::Record { .. } => {
|
||||||
runtime.frames.insert(frame.0, inputs.iter().map(|(input, _)| *input).collect());
|
runtime
|
||||||
|
.frames
|
||||||
|
.insert(frame.0, inputs.iter().map(|(input, _)| *input).collect());
|
||||||
}
|
}
|
||||||
ReplayMode::Playback { replay, mismatch } if !*mismatch => {
|
ReplayMode::Playback { replay, mismatch } if !*mismatch => {
|
||||||
let Some(recorded) = replay.frames.iter().find(|recorded| recorded.frame == frame.0) else {
|
let Some(recorded) = replay
|
||||||
|
.frames
|
||||||
|
.iter()
|
||||||
|
.find(|recorded| recorded.frame == frame.0)
|
||||||
|
else {
|
||||||
error!("Replay has no input for frame {}", frame.0);
|
error!("Replay has no input for frame {}", frame.0);
|
||||||
*mismatch = true;
|
*mismatch = true;
|
||||||
return;
|
return;
|
||||||
@@ -178,14 +237,26 @@ fn replay_inputs(mut inputs: ResMut<PlayerInputs<GGRSCfg>>, frame: Res<RollbackF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_checksum(frame: Res<RollbackFrameCount>, checksum: Res<Checksum>, runtime: Option<ResMut<ReplayRuntime>>) {
|
fn record_checksum(
|
||||||
|
frame: Res<RollbackFrameCount>,
|
||||||
|
checksum: Res<Checksum>,
|
||||||
|
runtime: Option<ResMut<ReplayRuntime>>,
|
||||||
|
) {
|
||||||
let Some(mut runtime) = runtime else { return };
|
let Some(mut runtime) = runtime else { return };
|
||||||
if matches!(&runtime.mode, ReplayMode::Record { .. }) {
|
if matches!(&runtime.mode, ReplayMode::Record { .. }) {
|
||||||
runtime.checksums.insert(frame.0, checksum.0);
|
runtime.checksums.insert(frame.0, checksum.0);
|
||||||
} else if let ReplayMode::Playback { replay, mismatch } = &mut runtime.mode {
|
} else if let ReplayMode::Playback { replay, mismatch } = &mut runtime.mode {
|
||||||
if let Some(expected) = replay.checksums.iter().find(|value| value.frame == frame.0).map(|value| value.checksum) {
|
if let Some(expected) = replay
|
||||||
|
.checksums
|
||||||
|
.iter()
|
||||||
|
.find(|value| value.frame == frame.0)
|
||||||
|
.map(|value| value.checksum)
|
||||||
|
{
|
||||||
if expected != checksum.0 {
|
if expected != checksum.0 {
|
||||||
error!("Replay desync at frame {}: expected {:X}, got {:X}", frame.0, expected, checksum.0);
|
error!(
|
||||||
|
"Replay desync at frame {}: expected {:X}, got {:X}",
|
||||||
|
frame.0, expected, checksum.0
|
||||||
|
);
|
||||||
*mismatch = true;
|
*mismatch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +276,11 @@ fn stop_recording(
|
|||||||
finish_recording_inner(&mut commands, runtime, request);
|
finish_recording_inner(&mut commands, runtime, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_recording(mut commands: Commands, runtime: Option<ResMut<ReplayRuntime>>, request: Option<Res<PendingMatch>>) {
|
fn finish_recording(
|
||||||
|
mut commands: Commands,
|
||||||
|
runtime: Option<ResMut<ReplayRuntime>>,
|
||||||
|
request: Option<Res<PendingMatch>>,
|
||||||
|
) {
|
||||||
finish_recording_inner(&mut commands, runtime, request);
|
finish_recording_inner(&mut commands, runtime, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,27 +290,59 @@ fn finish_recording_inner(
|
|||||||
request: Option<Res<PendingMatch>>,
|
request: Option<Res<PendingMatch>>,
|
||||||
) {
|
) {
|
||||||
let Some(mut runtime) = runtime else { return };
|
let Some(mut runtime) = runtime else { return };
|
||||||
let ReplayMode::Record { path, format } = &runtime.mode else { return };
|
let ReplayMode::Record { path, format } = &runtime.mode else {
|
||||||
if runtime.finished || runtime.frames.is_empty() { return; }
|
return;
|
||||||
|
};
|
||||||
|
if runtime.finished || runtime.frames.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(request) = request else { return };
|
let Some(request) = request else { return };
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
let format = *format;
|
let format = *format;
|
||||||
let replay = Replay {
|
let replay = Replay {
|
||||||
replay_version: REPLAY_VERSION,
|
replay_version: REPLAY_VERSION,
|
||||||
game_version: env!("CARGO_PKG_VERSION").into(),
|
game_version: env!("CARGO_PKG_VERSION").into(),
|
||||||
match_config: ReplayMatch { stage: request.stage, players: request.players.clone() },
|
match_config: ReplayMatch {
|
||||||
frames: runtime.frames.iter().map(|(&frame, inputs)| ReplayFrame { frame, inputs: inputs.clone() }).collect(),
|
stage: request.stage,
|
||||||
checksums: runtime.checksums.iter().map(|(&frame, &checksum)| ReplayChecksum { frame, checksum }).collect(),
|
players: request.players.clone(),
|
||||||
|
},
|
||||||
|
frames: runtime
|
||||||
|
.frames
|
||||||
|
.iter()
|
||||||
|
.map(|(&frame, inputs)| ReplayFrame {
|
||||||
|
frame,
|
||||||
|
inputs: inputs.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
checksums: runtime
|
||||||
|
.checksums
|
||||||
|
.iter()
|
||||||
|
.map(|(&frame, &checksum)| ReplayChecksum { frame, checksum })
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
if let Err(error) = save(&path, format, &replay) { error!("Failed to save replay: {error}"); }
|
if let Err(error) = save(&path, format, &replay) {
|
||||||
|
error!("Failed to save replay: {error}");
|
||||||
|
}
|
||||||
runtime.finished = true;
|
runtime.finished = true;
|
||||||
commands.remove_resource::<ReplayRuntime>();
|
commands.remove_resource::<ReplayRuntime>();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_playback(mut commands: Commands, mut next_state: ResMut<NextState<AppState>>, frame: Res<RollbackFrameCount>, runtime: Option<Res<ReplayRuntime>>) {
|
fn finish_playback(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
frame: Res<RollbackFrameCount>,
|
||||||
|
runtime: Option<Res<ReplayRuntime>>,
|
||||||
|
) {
|
||||||
let Some(runtime) = runtime else { return };
|
let Some(runtime) = runtime else { return };
|
||||||
let ReplayMode::Playback { replay, mismatch } = &runtime.mode else { return };
|
let ReplayMode::Playback { replay, mismatch } = &runtime.mode else {
|
||||||
if *mismatch || replay.frames.last().is_some_and(|last| frame.0 >= last.frame) {
|
return;
|
||||||
|
};
|
||||||
|
if *mismatch
|
||||||
|
|| replay
|
||||||
|
.frames
|
||||||
|
.last()
|
||||||
|
.is_some_and(|last| frame.0 >= last.frame)
|
||||||
|
{
|
||||||
commands.remove_resource::<ReplayRuntime>();
|
commands.remove_resource::<ReplayRuntime>();
|
||||||
commands.remove_resource::<LoadedReplay>();
|
commands.remove_resource::<LoadedReplay>();
|
||||||
commands.insert_resource(ReplayComplete);
|
commands.insert_resource(ReplayComplete);
|
||||||
@@ -246,7 +353,12 @@ fn finish_playback(mut commands: Commands, mut next_state: ResMut<NextState<AppS
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{fighter::FighterId, math::{int::FGi32, vec::FGVec2}, match_loading::MatchPlayer, stage::manifest::StageId};
|
use crate::{
|
||||||
|
fighter::FighterId,
|
||||||
|
match_loading::MatchPlayer,
|
||||||
|
math::{int::FGi32, vec::FGVec2},
|
||||||
|
stage::manifest::StageId,
|
||||||
|
};
|
||||||
|
|
||||||
fn sample() -> Replay {
|
fn sample() -> Replay {
|
||||||
Replay {
|
Replay {
|
||||||
@@ -254,13 +366,22 @@ mod tests {
|
|||||||
game_version: "test".into(),
|
game_version: "test".into(),
|
||||||
match_config: ReplayMatch {
|
match_config: ReplayMatch {
|
||||||
stage: StageId::TestStage,
|
stage: StageId::TestStage,
|
||||||
players: vec![MatchPlayer { handle: 0, fighter: FighterId::TestFighter }],
|
players: vec![MatchPlayer {
|
||||||
|
handle: 0,
|
||||||
|
fighter: FighterId::TestFighter,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
frames: vec![ReplayFrame {
|
frames: vec![ReplayFrame {
|
||||||
frame: 1,
|
frame: 1,
|
||||||
inputs: vec![FighterInputFrame { movement: FGVec2::new(FGi32::from_num(1), FGi32::ZERO), ..default() }],
|
inputs: vec![FighterInputFrame {
|
||||||
|
movement: FGVec2::new(FGi32::from_num(1), FGi32::ZERO),
|
||||||
|
..default()
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
checksums: vec![ReplayChecksum {
|
||||||
|
frame: 1,
|
||||||
|
checksum: 42,
|
||||||
}],
|
}],
|
||||||
checksums: vec![ReplayChecksum { frame: 1, checksum: 42 }],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user