This commit is contained in:
2023-06-05 13:53:35 +02:00
commit dc0759ee63
25 changed files with 2125 additions and 0 deletions

248
src/component/console.cpp Normal file
View File

@ -0,0 +1,248 @@
#include "std_include.hpp"
#include "console.hpp"
#ifdef _WIN32
#define COLOR_LOG_INFO 11
#define COLOR_LOG_WARN 14
#define COLOR_LOG_ERROR 12
#define COLOR_LOG_DEBUG 15
#else
#define COLOR_LOG_INFO "\033[0;36m"
#define COLOR_LOG_WARN "\033[0;33m"
#define COLOR_LOG_ERROR "\033[0;31m"
#define COLOR_LOG_DEBUG "\033[0m"
#endif
namespace console
{
namespace
{
std::mutex signal_mutex;
std::function<void()> signal_callback;
#ifdef _WIN32
#define COLOR(win, posix) win
using color_type = WORD;
#else
#define COLOR(win, posix) posix
using color_type = const char*;
#endif
const color_type color_array[] =
{
COLOR(0x8, "\033[0;90m"), // 0 - black
COLOR(0xC, "\033[0;91m"), // 1 - red
COLOR(0xA, "\033[0;92m"), // 2 - green
COLOR(0xE, "\033[0;93m"), // 3 - yellow
COLOR(0x9, "\033[0;94m"), // 4 - blue
COLOR(0xB, "\033[0;96m"), // 5 - cyan
COLOR(0xD, "\033[0;95m"), // 6 - pink
COLOR(0xF, "\033[0;97m"), // 7 - white
};
#ifdef _WIN32
BOOL WINAPI handler(const DWORD signal)
{
if (signal == CTRL_C_EVENT && signal_callback)
{
signal_callback();
}
return TRUE;
}
#else
void handler(int signal)
{
if (signal == SIGINT && signal_callback)
{
signal_callback();
}
}
#endif
std::string format(va_list* ap, const char* message)
{
static thread_local char buffer[0x1000];
#ifdef _WIN32
const int count = vsnprintf_s(buffer, _TRUNCATE, message, *ap);
#else
const int count = vsnprintf(buffer, sizeof(buffer), message, *ap);
#endif
if (count < 0) return {};
return {buffer, static_cast<size_t>(count)};
}
#ifdef _WIN32
HANDLE get_console_handle()
{
return GetStdHandle(STD_OUTPUT_HANDLE);
}
#endif
void set_color(const color_type color)
{
#ifdef _WIN32
SetConsoleTextAttribute(get_console_handle(), color);
#else
printf("%s", color);
#endif
}
bool apply_color(const std::string& data, const size_t index, const color_type base_color)
{
if (data[index] != '^' || (index + 1) >= data.size())
{
return false;
}
auto code = data[index + 1] - '0';
if (code < 0 || code > 11)
{
return false;
}
code = std::min(code, 7); // Everything above white is white
if (code == 7)
{
set_color(base_color);
}
else
{
set_color(color_array[code]);
}
return true;
}
void print_colored(const std::string& line, const color_type base_color)
{
lock _{};
set_color(base_color);
for (std::size_t i = 0; i < line.size(); ++i)
{
if (apply_color(line, i, base_color))
{
++i;
continue;
}
std::putchar(line[i]);
}
reset_color();
}
}
lock::lock()
{
#ifdef _WIN32
_lock_file(stdout);
#else
flockfile(stdout);
#endif
}
lock::~lock()
{
#ifdef _WIN32
_unlock_file(stdout);
#else
funlockfile(stdout);
#endif
}
void reset_color()
{
lock _{};
#ifdef _WIN32
SetConsoleTextAttribute(get_console_handle(), 7);
#else
printf("\033[0m");
#endif
fflush(stdout);
}
void info(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[+] " + data + "\n", COLOR_LOG_INFO);
va_end(ap);
}
void warn(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[!] " + data + "\n", COLOR_LOG_WARN);
va_end(ap);
}
void error(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[-] " + data + "\n", COLOR_LOG_ERROR);
va_end(ap);
}
void log(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[*] " + data + "\n", COLOR_LOG_DEBUG);
va_end(ap);
}
void set_title(const std::string& title)
{
lock _{};
#ifdef _WIN32
SetConsoleTitleA(title.data());
#else
printf("\033]0;%s\007", title.data());
fflush(stdout);
#endif
}
signal_handler::signal_handler(std::function<void()> callback)
: std::lock_guard<std::mutex>(signal_mutex)
{
signal_callback = std::move(callback);
#ifdef _WIN32
SetConsoleCtrlHandler(handler, TRUE);
#else
signal(SIGINT, handler);
#endif
}
signal_handler::~signal_handler()
{
#ifdef _WIN32
SetConsoleCtrlHandler(handler, FALSE);
#else
signal(SIGINT, SIG_DFL);
#endif
signal_callback = {};
}
}

32
src/component/console.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
namespace console
{
class lock
{
public:
lock();
~lock();
lock(lock&&) = delete;
lock(const lock&) = delete;
lock& operator=(lock&&) = delete;
lock& operator=(const lock&) = delete;
};
void reset_color();
void info(const char* message, ...);
void warn(const char* message, ...);
void error(const char* message, ...);
void log(const char* message, ...);
void set_title(const std::string& title);
class signal_handler : std::lock_guard<std::mutex>
{
public:
signal_handler(std::function<void()> callback);
~signal_handler();
};
}

View File

@ -0,0 +1,72 @@
#include <std_include.hpp>
#include <utils/string.hpp>
#include "map_rotation.hpp"
using namespace std::literals;
namespace map_rotation
{
rotation_data::rotation_data()
: index_(0)
{
}
void rotation_data::randomize()
{
std::random_device rd;
std::mt19937 gen(rd());
std::ranges::shuffle(this->rotation_entries_, gen);
}
void rotation_data::add_entry(const std::string& key, const std::string& value)
{
this->rotation_entries_.emplace_back(std::make_pair(key, value));
}
bool rotation_data::contains(const std::string& key, const std::string& value) const
{
return std::ranges::any_of(this->rotation_entries_, [&](const auto& entry)
{
return entry.first == key && entry.second == value;
});
}
bool rotation_data::empty() const noexcept
{
return this->rotation_entries_.empty();
}
std::size_t rotation_data::get_entries_size() const noexcept
{
return this->rotation_entries_.size();
}
rotation_data::rotation_entry& rotation_data::get_next_entry()
{
const auto index = this->index_;
++this->index_ %= this->rotation_entries_.size();
return this->rotation_entries_.at(index);
}
void rotation_data::parse(const std::string& data)
{
const auto tokens = utils::string::split(data, ' ');
for (std::size_t i = 0; !tokens.empty() && i < (tokens.size() - 1); i += 2)
{
const auto& key = tokens[i];
const auto& value = tokens[i + 1];
if (key == "map"s || key == "gametype"s)
{
this->add_entry(key, value);
}
else
{
throw map_rotation_parse_error();
}
}
}
}

View File

@ -0,0 +1,32 @@
#pragma once
namespace map_rotation
{
struct map_rotation_parse_error : public std::exception
{
[[nodiscard]] const char* what() const noexcept override { return "Map Rotation Parse Error"; }
};
class rotation_data
{
public:
using rotation_entry = std::pair<std::string, std::string>;
rotation_data();
void randomize();
void add_entry(const std::string& key, const std::string& value);
[[nodiscard]] bool contains(const std::string& key, const std::string& value) const;
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] std::size_t get_entries_size() const noexcept;
[[nodiscard]] rotation_entry& get_next_entry();
void parse(const std::string& data);
private:
std::vector<rotation_entry> rotation_entries_;
std::size_t index_;
};
}

View File

@ -0,0 +1,339 @@
#include <std_include.hpp>
#include "component/console.hpp"
#include "cg_client_side_effects_mp.hpp"
#include "q_shared.hpp"
#define MAX_CLIENT_ENT_SOUNDS 1024
#define SKIP_LINE(buffer, text) \
if (match_line_starting_with((buffer), (text))) \
{ \
(buffer) = skip_line_starting_with((buffer), (text)); \
}
#define SKIP_TEXT(buffer, text) \
(buffer) = skip_text((buffer), (text)); \
if (!(buffer)) \
{ \
return false; \
}
#define SKIP_TEXT_PTR(buffer, text) \
(buffer) = skip_text((buffer), (text)); \
if (!(buffer)) \
{ \
return nullptr; \
}
#define SKIP_WHITE_SPACE(buffer) \
(buffer) = skip_white_space((buffer)); \
if (!(buffer)) \
{ \
return false; \
}
#define SKIP_WHITE_SPACE_PTR(buffer) \
(buffer) = skip_white_space((buffer)); \
if (!(buffer)) \
{ \
return nullptr; \
}
#define PARSE_STRING_PTR(buffer, out) \
(buffer) = parse_string_finish((buffer), (out), sizeof(out)); \
if (!(buffer)) \
{ \
return nullptr; \
}
#define PARSE_VEC3_PTR(buffer, out) \
(buffer) = parse_vec3_finish((buffer), (out)); \
if (!(buffer)) \
{ \
return nullptr; \
}
namespace game
{
auto client_ent_sound_count = 0;
// Just report back to the user if there are too many
void add_client_ent_sound(const float* origin, const char* sound_alias)
{
if (client_ent_sound_count == MAX_CLIENT_ENT_SOUNDS)
{
console::error("Unable to add %s at [%.2f, %.2f, %.2f]-> Too many client ent sounds. Reduce sounds or increase MAX_CLIENT_ENT_SOUNDS (%d).\n",
sound_alias, origin[0], origin[1], origin[2], MAX_CLIENT_ENT_SOUNDS);
return;
}
++client_ent_sound_count;
}
const char* skip_text(const char* line, const char* skip_text)
{
char error_text[128]{};
if (std::strncmp(skip_text, line, std::strlen(skip_text)) != 0)
{
I_strncpyz(error_text, line, sizeof(error_text));
console::error("Unexpected text '%s' when trying to find '%s' in map's effect file\n", error_text, skip_text);
return nullptr;
}
return &line[std::strlen(skip_text)];
}
const char* skip_white_space(const char* line)
{
while (std::isspace(static_cast<unsigned char>(*line)))
{
++line;
}
return line;
}
const char* skip_line_starting_with(const char* line, const char* skip_line)
{
const auto* result = skip_text(line, skip_line);
if (!result)
{
return nullptr;
}
for (auto i = *result; i != '\n'; i = *++result)
{
if (i == '\r' || i == '\0')
{
break;
}
}
return skip_white_space(result);
}
bool match_line_starting_with(const char* line, const char* start_line)
{
return std::strncmp(start_line, line, std::strlen(start_line)) == 0;
}
const char* parse_string(const char* line, char* text, unsigned int buffer_size)
{
char error_text[128]{};
if (*line != '"')
{
I_strncpyz(error_text, line, sizeof(error_text));
console::error("Expected a quoted string instead of '%s'\n");
return nullptr;
}
auto c = line[1];
unsigned int i;
for (i = 0; ((c != '\"' && (c != '\0')) && (i < buffer_size)); ++i)
{
text[i] = c;
c = line[i + 2];
}
if (i == buffer_size)
{
I_strncpyz(error_text, line, sizeof(error_text));
console::error("String was longer than expected '%s'\n", error_text);
return nullptr;
}
text[i] = '\0';
return &line[i + 2];
}
const char* parse_string_finish(const char* line, char* text, unsigned int buffer_size)
{
line = parse_string(line, text, buffer_size);
if (!line)
{
return nullptr;
}
for (auto i = *line; i != '\n'; i = *++line)
{
if (i == '\r' || i == '\0')
{
break;
}
}
return skip_white_space(line);
}
const char* parse_float_finish(const char* line, float* value)
{
char error_text[128]{};
if (std::sscanf(line, "%f", value) != 1)
{
I_strncpyz(error_text, line, sizeof(error_text));
console::error("Expected a float instead of '%s'\n", error_text);
return nullptr;
}
for (auto i = *line; i != '\n'; i = *++line)
{
if (i == '\r' || i == '\0')
{
break;
}
}
return skip_white_space(line);
}
const char* parse_vec3_finish(const char* line, float* origin)
{
char error_text[128]{};
if (std::sscanf(line, "%f, %f, %f", origin, origin + 1, origin + 2) != 3)
{
I_strncpyz(error_text, line, sizeof(error_text));
console::error("Expected 3 floats instead of '%s'\n", error_text);
return nullptr;
}
for (auto i = *line; i != '\n'; i = *++line)
{
if (i == '\r' || i == '\0')
{
break;
}
}
return skip_white_space(line);
}
const char* parse_sound(const char* line)
{
float origin[3], angles[3];
char sound_alias[256]{};
SKIP_TEXT_PTR(line, "ent = createLoopSound();");
SKIP_WHITE_SPACE_PTR(line);
SKIP_TEXT_PTR(line, "ent.v[ \"origin\" ] = (");
PARSE_VEC3_PTR(line, origin);
SKIP_TEXT_PTR(line, "ent.v[ \"angles\" ] = (");
PARSE_VEC3_PTR(line, angles);
SKIP_TEXT_PTR(line, "ent.v[ \"soundalias\" ] = ");
PARSE_STRING_PTR(line, sound_alias);
add_client_ent_sound(origin, sound_alias);
return line;
}
const char* parse_effect(const char* line)
{
float delay;
float origin[3], angles[3];
char sound_alias[256]{};
char fx_def_name[256]{};
SKIP_TEXT_PTR(line, "ent = createOneshotEffect( ");
PARSE_STRING_PTR(line, fx_def_name);
SKIP_TEXT_PTR(line, "ent.v[ \"origin\" ] = (");
PARSE_VEC3_PTR(line, origin);
SKIP_TEXT_PTR(line, "ent.v[ \"angles\" ] = (");
PARSE_VEC3_PTR(line, angles);
SKIP_TEXT_PTR(line, "ent.v[ \"fxid\" ] = ");
PARSE_STRING_PTR(line, fx_def_name);
SKIP_TEXT_PTR(line, "ent.v[ \"delay\" ] = ");
line = parse_float_finish(line, &delay);
if (!line)
{
return nullptr;
}
if (match_line_starting_with(line, "ent.v[ \"soundalias\" ] = "))
{
SKIP_TEXT_PTR(line, "ent.v[ \"soundalias\" ] = ");
PARSE_STRING_PTR(line, sound_alias);
add_client_ent_sound(origin, sound_alias);
}
// Skip FX registration code
return line;
}
bool parse_client_effects(const char* buffer)
{
char error_text[128]{};
// Header section
SKIP_TEXT(buffer, "//_createfx generated. Do not touch!!");
SKIP_WHITE_SPACE(buffer);
SKIP_TEXT(buffer, "#include common_scripts\\utility;");
SKIP_WHITE_SPACE(buffer);
SKIP_TEXT(buffer, "#include common_scripts\\_createfx;");
SKIP_WHITE_SPACE(buffer);
// Fake GSC section
SKIP_TEXT(buffer, "main()");
SKIP_WHITE_SPACE(buffer);
SKIP_TEXT(buffer, "{");
SKIP_WHITE_SPACE(buffer);
SKIP_LINE(buffer, "//");
if (buffer)
{
while (*buffer != '}' && *buffer)
{
if (match_line_starting_with(buffer, "ent = createLoopSound();"))
{
buffer = parse_sound(buffer);
if (!buffer)
{
return false;
}
}
else if (match_line_starting_with(buffer, "ent = createOneshotEffect( "))
{
buffer = parse_effect(buffer);
if (!buffer)
{
return false;
}
}
else
{
I_strncpyz(error_text, buffer, sizeof(error_text));
console::error("Expected 'ent = createLoopSound();' or 'ent = createOneshotEffect' instead of '%s' in map's effect file\n", error_text);
return false;
}
}
}
buffer = skip_line_starting_with(buffer, "}");
if (buffer && *buffer)
{
I_strncpyz(error_text, buffer, sizeof(error_text));
console::error("Unexpected data after parsing '%s' map's effect file\n", error_text);
return false;
}
return true;
}
}

View File

@ -0,0 +1,6 @@
#pragma once
namespace game
{
bool parse_client_effects(const char* buffer);
}

39
src/game/q_shared.cpp Normal file
View File

@ -0,0 +1,39 @@
#include <std_include.hpp>
#include "q_shared.hpp"
namespace game
{
void I_strncpyz(char* dest, const char* src, std::size_t dest_size)
{
assert(src);
assert(dest);
if (!dest)
{
return;
}
if (!src)
{
*dest = '\0';
}
else
{
auto* p = reinterpret_cast<const unsigned char*>(src - 1);
auto* q = reinterpret_cast<unsigned char*>(dest - 1);
auto n = dest_size + 1;
do
{
if (!--n)
{
break;
}
*++q = *++p;
} while (*q);
dest[dest_size - 1] = '\0';
}
}
}

6
src/game/q_shared.hpp Normal file
View File

@ -0,0 +1,6 @@
#pragma once
namespace game
{
void I_strncpyz(char* dest, const char* src, std::size_t dest_size);
}

125
src/main.cpp Normal file
View File

@ -0,0 +1,125 @@
#include "std_include.hpp"
#include "component/console.hpp"
#include "component/map_rotation.hpp"
#include "game/cg_client_side_effects_mp.hpp"
#include <utils/io.hpp>
namespace
{
bool load_client_effects(const std::string& filename)
{
if (filename.empty())
{
console::error("filename parameter is empty\n");
return false;
}
const auto data = utils::io::read_file(filename);
if (data.empty())
{
console::error("'%s' is empty\n", filename.data());
return false;
}
return game::parse_client_effects(data.data());
}
bool load_map_rotation(const std::string& filename)
{
if (filename.empty())
{
console::error("filename parameter is empty\n");
return false;
}
const auto data = utils::io::read_file(filename);
if (data.empty())
{
console::error("'%s' is empty\n", filename.data());
return false;
}
try
{
map_rotation::rotation_data rotation_data;
rotation_data.parse(data);
}
catch (const std::exception& ex)
{
console::error("%s: '%s' contains invalid data!\n", ex.what(), filename.data());
return false;
}
console::info("Successfully parsed map rotation\n");
return true;
}
}
int unsafe_main(std::string&& prog, std::vector<std::string>&& args)
{
// Parse command-line flags (only increment i for matching flags)
for (auto i = args.begin(); i != args.end();)
{
if (*i == "-createfx")
{
++i;
const auto filename = i != args.end() ? *i++ : std::string();
console::info("Parsing createfx '%s'\n", filename.data());
if (!load_client_effects(filename))
{
return EXIT_FAILURE;
}
}
else if (*i == "-map-rotation")
{
++i;
const auto filename = i != args.end() ? *i++ : std::string();
console::info("Parsing map rotation '%s'\n", filename.data());
if (!load_map_rotation(filename))
{
return EXIT_FAILURE;
}
}
else
{
console::info("X Labs IW4x validator tool\n"
"Usage: %s OPTIONS\n"
" -createfx <filename>\n"
" -fx <filename>\n"
" -map-rotation <filename>\n",
prog.data()
);
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
int main(int argc, char* argv[])
{
console::set_title("X Labs IW4x-validator");
console::log("Starting X Labs IW4x-validator");
try
{
std::string prog(argv[0]);
std::vector<std::string> args;
args.reserve(argc - 1);
args.assign(argv + 1, argv + argc);
return unsafe_main(std::move(prog), std::move(args));
}
catch (const std::exception& ex)
{
console::error("Fatal error: %s\n", ex.what());
return EXIT_FAILURE;
}
}

1
src/std_include.cpp Normal file
View File

@ -0,0 +1 @@
#include <std_include.hpp>

41
src/std_include.hpp Normal file
View File

@ -0,0 +1,41 @@
#ifdef _WIN32
#pragma once
#define WIN32_LEAN_AND_MEAN
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#else
#include <sys/types.h>
#include <unistd.h>
#endif
// min and max is required by gdi, therefore NOMINMAX won't work
#ifdef max
#undef max
#endif
#ifdef min
#undef min
#endif
#include <algorithm>
#include <cassert>
#include <cctype>
#include <csignal>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <mutex>
#include <random>
#include <ranges>
#include <sstream>
#include <utility>
#include <vector>

92
src/utils/io.cpp Normal file
View File

@ -0,0 +1,92 @@
#include <std_include.hpp>
#include "io.hpp"
namespace utils
{
namespace io
{
bool remove_file(const std::string& file)
{
return std::remove(file.data()) == 0;
}
bool move_file(const std::string& src, const std::string& target)
{
return std::rename(src.data(), target.data()) == 0;
}
bool file_exists(const std::string& file)
{
return std::ifstream(file).good();
}
bool write_file(const std::string& file, const std::string& data, const bool append)
{
auto mode = std::ios::binary | std::ofstream::out;
if (append)
{
mode |= std::ofstream::app;
}
std::ofstream stream(file, mode);
if (stream.is_open())
{
stream.write(data.data(), static_cast<std::streamsize>(data.size()));
stream.close();
return true;
}
return false;
}
std::string read_file(const std::string& file)
{
std::string data;
read_file(file, &data);
return data;
}
bool read_file(const std::string& file, std::string* data)
{
if (!data) return false;
data->clear();
if (file_exists(file))
{
std::ifstream stream(file, std::ios::binary);
if (!stream.is_open()) return false;
stream.seekg(0, std::ios::end);
const std::streamsize size = stream.tellg();
stream.seekg(0, std::ios::beg);
if (size > -1)
{
data->resize(static_cast<std::uint32_t>(size));
stream.read(const_cast<char*>(data->data()), size);
stream.close();
return true;
}
}
return false;
}
std::size_t file_size(const std::string& file)
{
if (file_exists(file))
{
std::ifstream stream(file, std::ios::binary);
if (stream.good())
{
stream.seekg(0, std::ios::end);
return static_cast<std::size_t>(stream.tellg());
}
}
return 0;
}
}
}

17
src/utils/io.hpp Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <string>
namespace utils
{
namespace io
{
bool remove_file(const std::string& file);
bool move_file(const std::string& src, const std::string& target);
bool file_exists(const std::string& file);
bool write_file(const std::string& file, const std::string& data, bool append = false);
bool read_file(const std::string& file, std::string* data);
std::string read_file(const std::string& file);
std::size_t file_size(const std::string& file);
}
}

41
src/utils/string.cpp Normal file
View File

@ -0,0 +1,41 @@
#include <std_include.hpp>
#include "string.hpp"
namespace utils::string
{
std::vector<std::string> split(const std::string& s, const char delim)
{
std::stringstream ss(s);
std::string item;
std::vector<std::string> elems;
while (std::getline(ss, item, delim))
{
elems.push_back(item); // elems.push_back(std::move(item)); // if C++11
}
return elems;
}
std::string to_lower(const std::string& text)
{
std::string result;
std::ranges::transform(text, std::back_inserter(result), [](const unsigned char input)
{
return static_cast<char>(std::tolower(input));
});
return result;
}
std::string to_upper(const std::string& text)
{
std::string result;
std::ranges::transform(text, std::back_inserter(result), [](const unsigned char input)
{
return static_cast<char>(std::toupper(input));
});
return result;
}
}

9
src/utils/string.hpp Normal file
View File

@ -0,0 +1,9 @@
#pragma once
namespace utils::string
{
std::vector<std::string> split(const std::string& s, char delim);
std::string to_lower(const std::string& text);
std::string to_upper(const std::string& text);
}