use std::{io::Write, num::NonZeroU32};
use cookie_factory::GenError;
use nom;
use crate::{read, write};
#[derive(Debug, Clone, PartialEq, Default)]
pub struct HLTAS<'a> {
pub properties: Properties<'a>,
pub lines: Vec<Line<'a>>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub struct Properties<'a> {
pub demo: Option<&'a str>,
pub save: Option<&'a str>,
pub frametime_0ms: Option<&'a str>,
pub seeds: Option<Seeds>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Seeds {
pub shared: u32,
pub non_shared: i64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Line<'a> {
FrameBulk(FrameBulk<'a>),
Save(&'a str),
SharedSeed(u32),
Buttons(Buttons),
LGAGSTMinSpeed(f32),
Reset {
non_shared_seed: i64,
},
Comment(&'a str),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Buttons {
Reset,
Set {
air_left: Button,
air_right: Button,
ground_left: Button,
ground_right: Button,
},
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Button {
Forward,
ForwardLeft,
Left,
BackLeft,
Back,
BackRight,
Right,
ForwardRight,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameBulk<'a> {
pub auto_actions: AutoActions,
pub movement_keys: MovementKeys,
pub action_keys: ActionKeys,
pub frame_time: &'a str,
pub pitch: Option<f32>,
pub frame_count: NonZeroU32,
pub console_command: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
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)]
pub enum AutoMovement {
SetYaw(f32),
Strafe(StrafeSettings),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StrafeSettings {
pub type_: StrafeType,
pub dir: StrafeDir,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum StrafeType {
MaxAccel,
MaxAngle,
MaxDeccel,
ConstSpeed,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StrafeDir {
Left,
Right,
Best,
Yaw(f32),
Point { x: f32, y: f32 },
Line {
yaw: f32,
},
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Times {
UnlimitedWithinFrameBulk,
Limited(NonZeroU32),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct LeaveGroundAction {
pub speed: LeaveGroundActionSpeed,
pub times: Times,
pub type_: LeaveGroundActionType,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum LeaveGroundActionSpeed {
Any,
Optimal,
OptimalWithFullMaxspeed,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum LeaveGroundActionType {
Jump,
DuckTap {
zero_ms: bool,
},
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct JumpBug {
pub times: Times,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct DuckBeforeCollision {
pub times: Times,
pub including_ceilings: bool,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct DuckBeforeGround {
pub times: Times,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct DuckWhenJump {
pub times: Times,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
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)]
pub struct ActionKeys {
pub jump: bool,
pub duck: bool,
pub use_: bool,
pub attack_1: bool,
pub attack_2: bool,
pub reload: bool,
}
impl<'a> HLTAS<'a> {
#[allow(clippy::should_implement_trait)]
pub fn from_str(input: &'a 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<'a> FrameBulk<'a> {
#[inline]
pub fn with_frame_time(frame_time: &'a str) -> 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<'static> {
HLTAS {
properties: Properties {
demo: Some("bhop"),
frametime_0ms: Some("0.0000001"),
save: None,
seeds: None,
},
lines: vec![
Line::FrameBulk(FrameBulk {
console_command: Some("sensitivity 0;bxt_timer_reset;bxt_taslog"),
..FrameBulk::with_frame_time("0.001")
}),
Line::FrameBulk(FrameBulk {
frame_count: NonZeroU32::new(5).unwrap(),
..FrameBulk::with_frame_time("0.001")
}),
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")
}),
Line::FrameBulk(FrameBulk {
frame_count: NonZeroU32::new(2951).unwrap(),
..FrameBulk::with_frame_time("0.001")
}),
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"),
..FrameBulk::with_frame_time("0.001")
}),
Line::Comment(" More frames because some of them get converted to 0ms"),
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")
}),
Line::FrameBulk(FrameBulk {
console_command: Some(
"stop;bxt_timer_stop;pause;sensitivity 1;_bxt_taslog 0;bxt_taslog;\
//condebug",
),
..FrameBulk::with_frame_time("0.001")
}),
],
}
}
#[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);
}
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 }
}