use std::{io::Write, num::NonZeroU32};
use cookie_factory::GenError;
#[cfg(feature = "proptest1")]
use proptest::prelude::*;
#[cfg(feature = "proptest1")]
use proptest_derive::Arbitrary;
#[cfg(feature = "serde1")]
use serde::{Deserialize, Serialize};
use crate::{read, write};
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct HLTAS {
pub properties: Properties,
pub lines: Vec<Line>,
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct Properties {
#[cfg_attr(
feature = "proptest1",
proptest(strategy = "prop::option::of(arbitrary_property_value())")
)]
pub demo: Option<String>,
#[cfg_attr(
feature = "proptest1",
proptest(strategy = "prop::option::of(arbitrary_property_value())")
)]
pub save: Option<String>,
#[cfg_attr(
feature = "proptest1",
proptest(strategy = "prop::option::of(arbitrary_frame_time())")
)]
pub frametime_0ms: Option<String>,
pub seeds: Option<Seeds>,
#[cfg_attr(
feature = "proptest1",
proptest(strategy = "any::<u32>().prop_map(NonZeroU32::new)")
)]
pub hlstrafe_version: Option<NonZeroU32>,
#[cfg_attr(
feature = "proptest1",
proptest(strategy = "prop::option::of(arbitrary_property_value())")
)]
pub load_command: Option<String>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct Seeds {
pub shared: u32,
pub non_shared: i64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum Line {
FrameBulk(FrameBulk),
Save(#[cfg_attr(feature = "proptest1", proptest(regex = "\\S+"))] String),
SharedSeed(u32),
Buttons(Buttons),
LGAGSTMinSpeed(f32),
Reset {
non_shared_seed: i64,
},
Comment(String),
VectorialStrafing(bool),
VectorialStrafingConstraints(VectorialStrafingConstraints),
Change(Change),
TargetYawOverride(
#[cfg_attr(
feature = "proptest1",
proptest(strategy = "prop::collection::vec(any::<f32>(), 1..100)")
)]
Vec<f32>,
),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum Buttons {
Reset,
Set {
air_left: Button,
air_right: Button,
ground_left: Button,
ground_right: Button,
},
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum Button {
Forward,
ForwardLeft,
Left,
BackLeft,
Back,
BackRight,
Right,
ForwardRight,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct FrameBulk {
pub auto_actions: AutoActions,
pub movement_keys: MovementKeys,
pub action_keys: ActionKeys,
#[cfg_attr(feature = "proptest1", proptest(strategy = "arbitrary_frame_time()"))]
pub frame_time: String,
pub pitch: Option<f32>,
#[cfg_attr(feature = "proptest1", proptest(strategy = "arbitrary_non_zero_u32()"))]
pub frame_count: NonZeroU32,
pub console_command: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct AutoActions {
pub movement: Option<AutoMovement>,
pub leave_ground_action: Option<LeaveGroundAction>,
pub jump_bug: Option<JumpBug>,
pub duck_before_collision: Option<DuckBeforeCollision>,
pub duck_before_ground: Option<DuckBeforeGround>,
pub duck_when_jump: Option<DuckWhenJump>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum AutoMovement {
SetYaw(f32),
Strafe(StrafeSettings),
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct StrafeSettings {
pub type_: StrafeType,
pub dir: StrafeDir,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum StrafeType {
MaxAccel,
MaxAngle,
MaxDeccel,
ConstSpeed,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum StrafeDir {
Left,
Right,
Best,
Yaw(f32),
Point { x: f32, y: f32 },
Line {
yaw: f32,
},
LeftRight(
#[cfg_attr(feature = "proptest1", proptest(strategy = "arbitrary_non_zero_u32()"))]
NonZeroU32,
),
RightLeft(
#[cfg_attr(feature = "proptest1", proptest(strategy = "arbitrary_non_zero_u32()"))]
NonZeroU32,
),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum Times {
UnlimitedWithinFrameBulk,
Limited(
#[cfg_attr(feature = "proptest1", proptest(strategy = "arbitrary_non_zero_u32()"))]
NonZeroU32,
),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct LeaveGroundAction {
pub speed: LeaveGroundActionSpeed,
pub times: Times,
pub type_: LeaveGroundActionType,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum LeaveGroundActionSpeed {
Any,
Optimal,
OptimalWithFullMaxspeed,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum LeaveGroundActionType {
Jump,
DuckTap {
zero_ms: bool,
},
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct JumpBug {
pub times: Times,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct DuckBeforeCollision {
pub times: Times,
pub including_ceilings: bool,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct DuckBeforeGround {
pub times: Times,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct DuckWhenJump {
pub times: Times,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct MovementKeys {
pub forward: bool,
pub left: bool,
pub right: bool,
pub back: bool,
pub up: bool,
pub down: bool,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct ActionKeys {
pub jump: bool,
pub duck: bool,
pub use_: bool,
pub attack_1: bool,
pub attack_2: bool,
pub reload: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum VectorialStrafingConstraints {
VelocityYaw {
tolerance: f32,
},
AvgVelocityYaw {
tolerance: f32,
},
VelocityYawLocking {
tolerance: f32,
},
Yaw {
yaw: f32,
tolerance: f32,
},
YawRange {
from: f32,
to: f32,
},
LookAt {
#[cfg_attr(
feature = "proptest1",
proptest(strategy = "any::<u32>().prop_map(NonZeroU32::new)")
)]
entity: Option<NonZeroU32>,
x: f32,
y: f32,
z: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub struct Change {
pub target: ChangeTarget,
pub final_value: f32,
pub over: f32,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "proptest1", derive(Arbitrary))]
pub enum ChangeTarget {
Yaw,
Pitch,
VectorialStrafingYaw,
VectorialStrafingYawOffset,
}
#[cfg(feature = "proptest1")]
fn arbitrary_non_zero_u32() -> impl Strategy<Value = NonZeroU32> {
(1..=u32::MAX).prop_map(|x| NonZeroU32::new(x).unwrap())
}
#[cfg(feature = "proptest1")]
fn arbitrary_frame_time() -> impl Strategy<Value = String> {
any::<f64>().prop_map(|x| x.to_string())
}
#[cfg(feature = "proptest1")]
fn arbitrary_property_value() -> impl Strategy<Value = String> {
any_with::<String>("\\S|\\S\\PC*\\S".into())
}
impl HLTAS {
#[allow(clippy::should_implement_trait)] pub fn from_str(input: &str) -> Result<Self, read::Error> {
match read::hltas(input) {
Ok((_, hltas)) => Ok(hltas),
Err(nom::Err::Error(mut e)) | Err(nom::Err::Failure(mut e)) => {
e.whole_input = input;
Err(e)
}
Err(nom::Err::Incomplete(_)) => unreachable!(), }
}
pub fn to_writer<W: Write>(&self, writer: W) -> Result<(), GenError> {
write::hltas(writer, self)
}
}
impl FrameBulk {
#[inline]
pub fn with_frame_time(frame_time: String) -> Self {
Self {
auto_actions: Default::default(),
movement_keys: Default::default(),
action_keys: Default::default(),
frame_time,
pitch: None,
frame_count: NonZeroU32::new(1).unwrap(),
console_command: None,
}
}
}
impl From<u32> for Times {
#[inline]
fn from(x: u32) -> Self {
if x == 0 {
Times::UnlimitedWithinFrameBulk
} else {
Times::Limited(NonZeroU32::new(x).unwrap())
}
}
}
impl From<Times> for u32 {
#[inline]
fn from(x: Times) -> Self {
if let Times::Limited(t) = x {
t.get()
} else {
0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
fs::{read_dir, read_to_string},
str::from_utf8,
};
#[test]
fn parse() {
for entry in read_dir("test-data/parse")
.unwrap()
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_name()
.to_str()
.map(|name| name.ends_with(".hltas"))
.unwrap_or(false)
})
{
let contents = read_to_string(entry.path()).unwrap();
assert!(HLTAS::from_str(&contents).is_ok());
}
}
#[test]
fn parse_write_parse() {
for entry in read_dir("test-data/parse")
.unwrap()
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_name()
.to_str()
.map(|name| name.ends_with(".hltas"))
.unwrap_or(false)
})
{
let contents = read_to_string(entry.path()).unwrap();
let hltas = HLTAS::from_str(&contents).unwrap();
let mut output = Vec::new();
hltas.to_writer(&mut output).unwrap();
let hltas_2 = HLTAS::from_str(from_utf8(&output).unwrap()).unwrap();
assert_eq!(hltas, hltas_2);
}
}
fn bhop_gt() -> HLTAS {
HLTAS {
properties: Properties {
demo: Some("bhop".to_owned()),
frametime_0ms: Some("0.0000001".to_owned()),
save: None,
seeds: None,
hlstrafe_version: Some(NonZeroU32::new(1).unwrap()),
load_command: None,
},
lines: vec![
Line::FrameBulk(FrameBulk {
console_command: Some("sensitivity 0;bxt_timer_reset;bxt_taslog".to_owned()),
..FrameBulk::with_frame_time("0.001".to_owned())
}),
Line::FrameBulk(FrameBulk {
frame_count: NonZeroU32::new(5).unwrap(),
..FrameBulk::with_frame_time("0.001".to_owned())
}),
Line::FrameBulk(FrameBulk {
auto_actions: AutoActions {
movement: Some(AutoMovement::Strafe(StrafeSettings {
type_: StrafeType::MaxAccel,
dir: StrafeDir::Yaw(170.),
})),
..AutoActions::default()
},
frame_count: NonZeroU32::new(400).unwrap(),
pitch: Some(0.),
..FrameBulk::with_frame_time("0.001".to_owned())
}),
Line::FrameBulk(FrameBulk {
frame_count: NonZeroU32::new(2951).unwrap(),
..FrameBulk::with_frame_time("0.001".to_owned())
}),
Line::FrameBulk(FrameBulk {
auto_actions: AutoActions {
movement: Some(AutoMovement::Strafe(StrafeSettings {
type_: StrafeType::MaxAccel,
dir: StrafeDir::Yaw(90.),
})),
..AutoActions::default()
},
frame_count: NonZeroU32::new(1).unwrap(),
console_command: Some("bxt_timer_start".to_owned()),
..FrameBulk::with_frame_time("0.001".to_owned())
}),
Line::Comment(" More frames because some of them get converted to 0ms".to_owned()),
Line::FrameBulk(FrameBulk {
auto_actions: AutoActions {
movement: Some(AutoMovement::Strafe(StrafeSettings {
type_: StrafeType::MaxAccel,
dir: StrafeDir::Yaw(90.),
})),
leave_ground_action: Some(LeaveGroundAction {
speed: LeaveGroundActionSpeed::Optimal,
times: Times::UnlimitedWithinFrameBulk,
type_: LeaveGroundActionType::DuckTap { zero_ms: true },
}),
..AutoActions::default()
},
frame_count: NonZeroU32::new(5315).unwrap(),
..FrameBulk::with_frame_time("0.001".to_owned())
}),
Line::FrameBulk(FrameBulk {
console_command: Some(
"stop;bxt_timer_stop;pause;sensitivity 1;_bxt_taslog 0;bxt_taslog;\
//condebug"
.to_owned(),
),
..FrameBulk::with_frame_time("0.001".to_owned())
}),
],
}
}
#[test]
fn validate() {
let contents = read_to_string("test-data/parse/bhop.hltas").unwrap();
let hltas = HLTAS::from_str(&contents).unwrap();
let gt = bhop_gt();
assert_eq!(hltas, gt);
}
#[test]
fn parse_write_parse_validate() {
let contents = read_to_string("test-data/parse/bhop.hltas").unwrap();
let hltas = HLTAS::from_str(&contents).unwrap();
let mut output = Vec::new();
hltas.to_writer(&mut output).unwrap();
let hltas = HLTAS::from_str(from_utf8(&output).unwrap()).unwrap();
let gt = bhop_gt();
assert_eq!(hltas, gt);
}
#[test]
fn write_to_too_small_buffer() {
let contents = read_to_string("test-data/parse/bhop.hltas").unwrap();
let hltas = HLTAS::from_str(&contents).unwrap();
let mut buf = [0; 4];
assert!(matches!(
hltas.to_writer(&mut buf[..]),
Err(GenError::BufferTooSmall(_))
));
}
#[test]
fn write_to_big_enough_buffer() {
let contents = read_to_string("test-data/parse/bhop.hltas").unwrap();
let hltas = HLTAS::from_str(&contents).unwrap();
let mut buf = [0; 1024];
let mut buf = &mut buf[..]; hltas.to_writer(&mut buf).unwrap();
assert!(buf.len() < 1024);
}
macro_rules! test_error {
($test_name:ident, $filename:literal, $context:ident) => {
#[test]
fn $test_name() {
let contents =
read_to_string(concat!("test-data/error/", $filename, ".hltas")).unwrap();
let err = HLTAS::from_str(&contents).unwrap_err();
assert_eq!(err.context, Some(read::Context::$context));
}
};
}
test_error! { error_no_version, "no-version", ErrorReadingVersion }
test_error! { error_too_high_version, "too-high-version", VersionTooHigh }
test_error! { error_no_save_name, "no-save-name", NoSaveName }
test_error! { error_no_seed, "no-seed", NoSeed }
test_error! { error_no_buttons, "no-buttons", NoButtons }
test_error! { error_no_lgagst_min_speed, "no-lgagst-min-speed", NoLGAGSTMinSpeed }
test_error! { error_no_reset_seed, "no-reset-seed", NoResetSeed }
test_error! { error_both_autojump_ducktap, "both-j-d", BothAutoJumpAndDuckTap }
test_error! { error_no_yaw, "no-yaw", NoYaw }
test_error! { error_no_lgagst_action, "no-lgagst-action", NoLeaveGroundAction }
test_error! { error_lgagst_action_times, "lgagst-action-times", TimesOnLeaveGroundAction }
test_error! {
error_no_plus_minus_before_tolerance,
"no-plus-minus-before-tolerance",
NoPlusMinusBeforeTolerance
}
#[cfg(feature = "proptest1")]
proptest! {
#[test]
fn write_parse(hltas: HLTAS) {
let mut buffer = Vec::new();
hltas.to_writer(&mut buffer).unwrap();
let hltas_2 = HLTAS::from_str(from_utf8(&buffer).unwrap()).unwrap();
prop_assert_eq!(hltas, hltas_2);
}
}
}