hltas 0.9.0

Reading and writing Half-Life TAS scripts (`.hltas`).
Documentation
#define CATCH_CONFIG_MAIN
#include "catch.hpp"

#include <array>
#include <string>
#include <utility>

#include "hltas.hpp"

const std::array<const char*, 23> parse_files = {
	"bhop_20fps.hltas",
	"bhop.hltas",
	"bkz_goldbhop.hltas",
	"blolly.hltas",
	"change.hltas",
	"cs_estate.hltas",
	"destructo-hops.hltas",
	"extra-letters.hltas",
	"goldbhop.hltas",
	"halflife.hltas",
	"kz_synergy_x.hltas",
	"load_command.hltas",
	"mirror.hltas",
	"nuker.hltas",
	"render_yaw_override.hltas",
	"rng.hltas",
	"strafe.hltas",
	"target_yaw_override.hltas",
	"tas-hazard course-1.32,669.hltas",
	"tas-kz_summercliff2-1.14.060.hltas",
	"triggertest.hltas",
	"tripminetest.hltas",
	"vectorial.hltas"
};

TEST_CASE("Parse") {
	HLTAS::Input input;

	for (const auto file : parse_files) {
		SECTION(file) {
			const auto path = std::string("../test-data/parse/") + file;
			const auto err = input.Open(path);
			CHECK(err.Code == HLTAS::ErrorCode::OK);
		}
	}
}

TEST_CASE("Parse, write, parse") {
	HLTAS::Input input;

	for (const auto file : parse_files) {
		SECTION(file) {
			auto path = std::string("../test-data/parse/") + file;
			REQUIRE(input.Open(path).Code == HLTAS::ErrorCode::OK);
			path = std::string("../test-data/write-output/") + file;
			REQUIRE(input.Save(path).Code == HLTAS::ErrorCode::OK);
			REQUIRE(input.Open(path).Code == HLTAS::ErrorCode::OK);
		}
	}
}

TEST_CASE("Error") {
	const std::array<std::pair<const char*, HLTAS::ErrorCode>, 14> files = {
		std::make_pair("does-not-exist.hltas", HLTAS::ErrorCode::FAILOPEN),
		std::make_pair("no-version.hltas", HLTAS::ErrorCode::FAILVER),
		std::make_pair("too-high-version.hltas", HLTAS::ErrorCode::NOTSUPPORTED),
		std::make_pair("no-save-name.hltas", HLTAS::ErrorCode::NOSAVENAME),
		std::make_pair("too-few-dashes-field-0.hltas", HLTAS::ErrorCode::FAILFRAME),
		std::make_pair("no-seed.hltas", HLTAS::ErrorCode::NOSEED),
		std::make_pair("no-yaw.hltas", HLTAS::ErrorCode::NOYAW),
		std::make_pair("no-buttons.hltas", HLTAS::ErrorCode::NOBUTTONS),
		std::make_pair("both-j-d.hltas", HLTAS::ErrorCode::BOTHAJDT),
		std::make_pair("no-lgagst-action.hltas", HLTAS::ErrorCode::NOLGAGSTACTION),
		std::make_pair("no-lgagst-min-speed.hltas", HLTAS::ErrorCode::NOLGAGSTMINSPEED),
		std::make_pair("lgagst-action-times.hltas", HLTAS::ErrorCode::LGAGSTACTIONTIMES),
		std::make_pair("no-reset-seed.hltas", HLTAS::ErrorCode::NORESETSEED),
		std::make_pair("no-plus-minus-before-tolerance.hltas", HLTAS::ErrorCode::NO_PM_IN_TOLERANCE),
	};

	HLTAS::Input input;

	for (const auto& test : files) {
		const auto file = test.first;
		const auto code = test.second;

		SECTION(file) {
			const auto path = std::string("../test-data/error/") + file;
			const auto err = input.Open(path);
			CHECK(err.Code == code);
		}
	}
}

void validate(const HLTAS::Input& input) {
	CHECK(input.GetVersion() == 1);

	const auto& properties = input.GetProperties();
	CHECK(properties.size() == 3);
	CHECK(properties.at("demo") == "bhop");
	CHECK(properties.at("frametime0ms") == "0.0000001");
	CHECK(properties.at("hlstrafe_version") == "1");

	const auto& frames = input.GetFrames();
	REQUIRE(frames.size() == 7);

	SECTION("Frame 0") {
		const auto& frame = frames[0];
		CHECK(frame.Frametime == "0.001");
		CHECK(frame.GetRepeats() == 1);
		CHECK(frame.Commands == "sensitivity 0;bxt_timer_reset;bxt_taslog");
	}
	SECTION("Frame 1") {
		const auto& frame = frames[1];
		CHECK(frame.Frametime == "0.001");
		CHECK(frame.GetRepeats() == 5);
	}
	SECTION("Frame 2") {
		const auto& frame = frames[2];
		CHECK(frame.GetType() == HLTAS::StrafeType::MAXACCEL);
		CHECK(frame.GetDir() == HLTAS::StrafeDir::YAW);
		CHECK(frame.GetYaw() == 170);
		CHECK(frame.GetPitch() == 0);
		CHECK(frame.Frametime == "0.001");
		CHECK(frame.GetRepeats() == 400);
	}
	SECTION("Frame 3") {
		const auto& frame = frames[3];
		CHECK(frame.Frametime == "0.001");
		CHECK(frame.GetRepeats() == 2951);
	}
	SECTION("Frame 4") {
		const auto& frame = frames[4];
		CHECK(frame.GetType() == HLTAS::StrafeType::MAXACCEL);
		CHECK(frame.GetDir() == HLTAS::StrafeDir::YAW);
		CHECK(frame.GetYaw() == 90);
		CHECK(frame.Frametime == "0.001");
		CHECK(frame.GetRepeats() == 1);
		CHECK(frame.Commands == "bxt_timer_start");
	}
	SECTION("Frame 5") {
		const auto& frame = frames[5];
		CHECK(frame.GetType() == HLTAS::StrafeType::MAXACCEL);
		CHECK(frame.GetDir() == HLTAS::StrafeDir::YAW);
		CHECK(frame.Lgagst == true);
		CHECK(frame.GetDucktap0ms() == true);
		CHECK(frame.GetYaw() == 90);
		CHECK(frame.Frametime == "0.001");
		CHECK(frame.GetRepeats() == 5315);
		CHECK(frame.Comments == " More frames because some of them get converted to 0ms\n");
	}
	SECTION("Frame 6") {
		const auto& frame = frames[6];
		CHECK(frame.Frametime == "0.001");
		CHECK(frame.GetRepeats() == 1);
		CHECK(frame.Commands == "stop;bxt_timer_stop;pause;sensitivity 1;_bxt_taslog 0;bxt_taslog;//condebug");
	}
}

TEST_CASE("Parse and validate") {
	HLTAS::Input input;
	REQUIRE(input.Open("../test-data/parse/bhop.hltas").Code == HLTAS::ErrorCode::OK);

	validate(input);
}

TEST_CASE("Parse, write, parse and validate") {
	HLTAS::Input input;
	REQUIRE(input.Open("../test-data/parse/bhop.hltas").Code == HLTAS::ErrorCode::OK);
	REQUIRE(input.Save("../test-data/write-output/bhop.hltas").Code == HLTAS::ErrorCode::OK);
	REQUIRE(input.Open("../test-data/write-output/bhop.hltas").Code == HLTAS::ErrorCode::OK);

	validate(input);
}

TEST_CASE("Parse from string and validate") {
	const char *script = "version 1\n\
demo bhop\n\
frametime0ms 0.0000001\n\
hlstrafe_version 1\n\
frames\n\
----------|------|------|0.001|-|-|1|sensitivity 0;bxt_timer_reset;bxt_taslog\n\
----------|------|------|0.001|-|-|5\n\
\n\
s03-------|------|------|0.001|170|0|400\n\
----------|------|------|0.001|-|-|2951\n\
\n\
s03-------|------|------|0.001|90|-|1|bxt_timer_start\n\
\n\
// More frames because some of them get converted to 0ms\n\
s03l-D----|------|------|0.001|90|-|5315\n\
\n\
----------|------|------|0.001|-|-|1|stop;bxt_timer_stop;pause;sensitivity 1;_bxt_taslog 0;bxt_taslog;//condebug\n\
";

	HLTAS::Input input;
	REQUIRE(input.FromString(script).Code == HLTAS::ErrorCode::OK);

	validate(input);
}

TEST_CASE("Parse, write, parse and validate using strings") {
	const char *script = "version 1\n\
demo bhop\n\
frametime0ms 0.0000001\n\
hlstrafe_version 1\n\
frames\n\
----------|------|------|0.001|-|-|1|sensitivity 0;bxt_timer_reset;bxt_taslog\n\
----------|------|------|0.001|-|-|5\n\
\n\
s03-------|------|------|0.001|170|0|400\n\
----------|------|------|0.001|-|-|2951\n\
\n\
s03-------|------|------|0.001|90|-|1|bxt_timer_start\n\
\n\
// More frames because some of them get converted to 0ms\n\
s03l-D----|------|------|0.001|90|-|5315\n\
\n\
----------|------|------|0.001|-|-|1|stop;bxt_timer_stop;pause;sensitivity 1;_bxt_taslog 0;bxt_taslog;//condebug\n\
";

	char buffer[1024] = {};
	memset(buffer, 1, 1024); // Fill with ones to test NULL termination.

	HLTAS::Input input;
	REQUIRE(input.FromString(script).Code == HLTAS::ErrorCode::OK);
	REQUIRE(input.ToString(buffer, sizeof(buffer)).Code == HLTAS::ErrorCode::OK);
	REQUIRE(input.FromString(buffer).Code == HLTAS::ErrorCode::OK);

	validate(input);
}

TEST_CASE("Max accel yaw offset yaw strafing") {
	const char *script = "version 1\n\
hlstrafe_version 5\n\
frames\n\
s53-j-----|------|------|0.010000001|177 -1 2 3|-|158\n\
";

	char buffer[1024] = {};
	memset(buffer, 1, 1024); // Fill with ones to test NULL termination.

	HLTAS::Input input;
	REQUIRE(input.FromString(script).Code == HLTAS::ErrorCode::OK);

	const auto& frames = input.GetFrames();
	REQUIRE(frames.size() == 1);

	SECTION("Frame 3") {
		const auto& frame = frames[0];
		CHECK(frame.Frametime == "0.010000001");
		CHECK(frame.GetRepeats() == 158);
		CHECK(frame.GetYawPresent());
		CHECK(frame.GetYaw() == 177);
		CHECK(frame.GetMaxAccelYawOffsetStart() == -1);
		CHECK(frame.GetMaxAccelYawOffsetTarget() == 2);
		CHECK(frame.GetMaxAccelYawOffsetAccel() == 3);
	}
}