From c8bb966ace1f6e79b63bbce646f5136b50b12b16 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Mon, 3 Aug 2026 03:32:23 +0200 Subject: [PATCH 1/2] :angel:Script: Added GenericDocument API (for race system) Turns out the '.asdata' file format is different for client (INI-style) and server (custom BEGIN_/END_ clauses), so it cannot be used for the multiplayer race system. We need something that client can export from existing terrains and server script can read. Note that server script can be actual rorserver or newly also the client, see https://github.com/RigsOfRods/rigs-of-rods/pull/3393 --- source/protocol/rornet.h | 1 + source/server/GenericFileFormat.cpp | 1326 +++++++++++++++++++++++++++ source/server/GenericFileFormat.h | 185 ++++ source/server/ScriptEngine.cpp | 4 + source/server/listener.cpp.orig | 164 ++++ source/server/sequencer.cpp | 6 +- 6 files changed, 1683 insertions(+), 3 deletions(-) create mode 100644 source/server/GenericFileFormat.cpp create mode 100644 source/server/GenericFileFormat.h create mode 100644 source/server/listener.cpp.orig diff --git a/source/protocol/rornet.h b/source/protocol/rornet.h index d67921b0..f42707bd 100644 --- a/source/protocol/rornet.h +++ b/source/protocol/rornet.h @@ -21,6 +21,7 @@ #include +typedef uint32_t BitMask_t; #define BITMASK(x) (1 << (x-1)) namespace RoRnet { diff --git a/source/server/GenericFileFormat.cpp b/source/server/GenericFileFormat.cpp new file mode 100644 index 00000000..d5f12b15 --- /dev/null +++ b/source/server/GenericFileFormat.cpp @@ -0,0 +1,1326 @@ +/* +This file is part of "Rigs of Rods Server" (Relay mode) + +Copyright 2007 Pierre-Michel Ricordel +Copyright 2014+ Rigs of Rods Community + +"Rigs of Rods Server" is free software: you can redistribute it +and/or modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, either version 3 +of the License, or (at your option) any later version. + +"Rigs of Rods Server" is distributed in the hope that it will +be useful, but WITHOUT ANY WARRANTY; without even the implied +warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see . +*/ + +// Ported from Rigs of Rods (/source/main/utils/) at revision a95d75df8c7c376e36ff9e24df62e7006d6246df by ohlidalp, 2026 + +#include "GenericFileFormat.h" +#include "logger.h" + +#include +#include +#include +#include + +// < snatched from OgreStringConverter.cpp > +// A quick define to overcome different names for the same function +// A quick define to overcome different names for the same function +#if defined(_WIN32) +# define strtod_l _strtod_l +# define strtoul_l _strtoul_l +# define strtol_l _strtol_l +# define strtoull_l _strtoull_l +# define strtoll_l _strtoll_l +#else +# define strtod_l(ptr, end, l) strtod(ptr, end) +# define strtoul_l(ptr, end, base, l) strtoul(ptr, end, base) +# define strtol_l(ptr, end, base, l) strtol(ptr, end, base) +# define strtoull_l(ptr, end, base, l) strtoull(ptr, end, base) +# define strtoll_l(ptr, end, base, l) strtoll(ptr, end, base) +#endif + +#if (defined(_WIN32)) && !defined(__MINGW32__) +# define LC_NUMERIC_MASK LC_NUMERIC +# define newlocale(cat, loc, base) _create_locale(cat, loc) +#else +# define newlocale(cat, loc, base) 0 +#endif + +#ifdef __MINGW32__ +#define _strtoull_l _strtoul_l +#define _strtoll_l _strtol_l +#endif + +// < snatched from OgreStringConverter.h > +#ifdef _WIN32 +# define locale_t _locale_t +#else +# define locale_t int +#endif + +locale_t _numLocale = newlocale(LC_NUMERIC_MASK, "C", NULL); + +enum class PartialToken +{ + NONE, + COMMENT_SEMICOLON, // Comment starting with ';' + COMMENT_SLASH, // Comment starting with '//' + COMMENT_HASH, + STRING_QUOTED, // String starting/ending with '"' + STRING_NAKED, // String without '"' on either end + STRING_NAKED_CAPTURING_SPACES, // Only for OPTION_PARENTHESES_CAPTURE_SPACES - A naked string seeking the closing ')'. + TITLE_STRING, // A whole-line string, with spaces + NUMBER_STUB_MINUS, // Sole '-' character, may start a number or a naked string. + NUMBER_INTEGER, // Just digits and optionally leading '-' + NUMBER_DECIMAL, // Like INTEGER but already containing '.' + NUMBER_SCIENTIFIC_STUB, // Like DECIMAL, already containing 'e' or 'E' but not the exponent value. + NUMBER_SCIENTIFIC_STUB_MINUS, // Like SCIENTIFIC_STUB but with only '-' in exponent. + NUMBER_SCIENTIFIC, // Valid decimal number in scientific notation. + KEYWORD, // Unqoted string at the start of line. Accepted characters: alphanumeric and underscore + KEYWORD_BRACED, // Like KEYWORD but starting with '[' and ending with ']' + BOOL_TRUE, // Partial 'true' + BOOL_FALSE, // Partial 'false' + GARBAGE, // Text not fitting any above category, will be discarded +}; + +struct DocumentParser +{ + DocumentParser(GenericDocument& d, const BitMask_t opt, FILE* f) + : doc(d), options(opt), file(f) {} + + // Config + GenericDocument& doc; + const BitMask_t options; + FILE* file; + + // State + std::vector tok; + size_t line_num = 0; + size_t line_pos = 0; + PartialToken partial_tok_type = PartialToken::NONE; + bool title_found = false; // Only for OPTION_FIRST_LINE_IS_TITLE + + void ProcessChar(const char c); + void ProcessEOF(); + void ProcessSeparatorWithinBool(); + + void BeginToken(const char c); + void UpdateComment(const char c); + void UpdateString(const char c); + void UpdateNumber(const char c); + void UpdateBool(const char c); + void UpdateKeyword(const char c); + void UpdateTitle(const char c); // Only for OPTION_FIRST_LINE_IS_TITLE + void UpdateGarbage(const char c); + + void DiscontinueBool(); + void DiscontinueNumber(); + void DiscontinueKeyword(); + void FlushStringishToken(GDocTokenType type); + void FlushNumericToken(); +}; + +void DocumentParser::BeginToken(const char c) +{ + switch (c) + { + case '\r': + break; + + case ' ': + case ',': + case '\t': + line_pos++; + break; + + case ':': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_COLON) + { + line_pos++; + } + else + { + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; + tok.push_back(c); + line_pos++; + } + break; + + case '=': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_EQUALS) + { + line_pos++; + } + else + { + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; + tok.push_back(c); + line_pos++; + } + break; + + case '\n': + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + line_num++; + line_pos = 0; + break; + + case ';': + partial_tok_type = PartialToken::COMMENT_SEMICOLON; + line_pos++; + break; + + case '/': + if (options & GenericDocument::OPTION_ALLOW_SLASH_COMMENTS) + { + partial_tok_type = PartialToken::COMMENT_SLASH; + } + else + { + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; + tok.push_back(c); + } + line_pos++; + break; + + case '#': + if (options & GenericDocument::OPTION_ALLOW_HASH_COMMENTS) + { + partial_tok_type = PartialToken::COMMENT_HASH; + } + else + { + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; + tok.push_back(c); + } + line_pos++; + break; + + case '[': + if (options & GenericDocument::OPTION_ALLOW_BRACED_KEYWORDS) + { + partial_tok_type = PartialToken::KEYWORD_BRACED; + } + else + { + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; + } + tok.push_back(c); + line_pos++; + break; + + case '"': + partial_tok_type = PartialToken::STRING_QUOTED; + line_pos++; + break; + + case '.': + tok.push_back(c); + partial_tok_type = PartialToken::NUMBER_DECIMAL; + line_pos++; + break; + + case 't': + tok.push_back(c); + partial_tok_type = PartialToken::BOOL_TRUE; + line_pos++; + break; + + case 'f': + tok.push_back(c); + partial_tok_type = PartialToken::BOOL_FALSE; + line_pos++; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + partial_tok_type = PartialToken::NUMBER_INTEGER; + tok.push_back(c); + line_pos++; + break; + + case '-': + partial_tok_type = PartialToken::NUMBER_STUB_MINUS; + tok.push_back(c); + line_pos++; + break; + + default: + if (isalpha(c) && + (doc.tokens.size() == 0 || doc.tokens.back().type == GDocTokenType::LINEBREAK)) // on line start? + { + tok.push_back(c); + partial_tok_type = PartialToken::KEYWORD; + } + else if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + { + tok.push_back(c); + partial_tok_type = PartialToken::STRING_NAKED; + } + else + { + partial_tok_type = PartialToken::GARBAGE; + tok.push_back(c); + } + line_pos++; + break; + } + + if (options & GenericDocument::OPTION_FIRST_LINE_IS_TITLE + && !title_found + && (doc.tokens.size() == 0 || doc.tokens.back().type == GDocTokenType::LINEBREAK) + && partial_tok_type != PartialToken::NONE + && partial_tok_type != PartialToken::COMMENT_SEMICOLON + && partial_tok_type != PartialToken::COMMENT_SLASH) + { + title_found = true; + partial_tok_type = PartialToken::TITLE_STRING; + } + + if (partial_tok_type == PartialToken::GARBAGE) + { + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: stray character '%c'", line_num, line_pos, c); + } +} + +void DocumentParser::UpdateComment(const char c) +{ + switch (c) + { + case '\r': + break; + + case '\n': + this->FlushStringishToken(GDocTokenType::COMMENT); + // Break line + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + line_num++; + line_pos = 0; + break; + + case '/': + if (partial_tok_type != PartialToken::COMMENT_SLASH || tok.size() > 0) // With COMMENT_SLASH, skip any number of leading '/' + { + tok.push_back(c); + } + line_pos++; + break; + + default: + tok.push_back(c); + line_pos++; + break; + } +} + +void DocumentParser::UpdateString(const char c) +{ + switch (c) + { + case '\r': + break; + + case ' ': + if (partial_tok_type == PartialToken::STRING_QUOTED + || partial_tok_type == PartialToken::STRING_NAKED_CAPTURING_SPACES) + { + tok.push_back(c); + } + else // (partial_tok_type == PartialToken::STRING_NAKED) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + line_pos++; + break; + + case ',': + case '\t': + if (partial_tok_type == PartialToken::STRING_QUOTED) + { + tok.push_back(c); + } + else // (partial_tok_type == PartialToken::STRING_NAKED) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + line_pos++; + break; + + case '\n': + if (partial_tok_type == PartialToken::STRING_QUOTED) + { + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: quoted string interrupted by newline", line_num, line_pos); + } + this->FlushStringishToken(GDocTokenType::STRING); + // Break line + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + line_num++; + line_pos = 0; + break; + + case ':': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_COLON + && (partial_tok_type == PartialToken::STRING_NAKED || partial_tok_type == PartialToken::STRING_NAKED_CAPTURING_SPACES)) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + else + { + tok.push_back(c); + } + line_pos++; + break; + + case '=': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_EQUALS + && (partial_tok_type == PartialToken::STRING_NAKED || partial_tok_type == PartialToken::STRING_NAKED_CAPTURING_SPACES)) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + else + { + tok.push_back(c); + } + line_pos++; + break; + + case '"': + if (partial_tok_type == PartialToken::STRING_QUOTED) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + else // (partial_tok_type == PartialToken::STRING_NAKED) + { + partial_tok_type = PartialToken::GARBAGE; + tok.push_back(c); + } + line_pos++; + break; + + case '(': + if (partial_tok_type == PartialToken::STRING_NAKED + && options & GenericDocument::OPTION_PARENTHESES_CAPTURE_SPACES) + { + partial_tok_type = PartialToken::STRING_NAKED_CAPTURING_SPACES; + } + tok.push_back(c); + line_pos++; + break; + + case ')': + if (partial_tok_type == PartialToken::STRING_NAKED_CAPTURING_SPACES) + { + partial_tok_type = PartialToken::STRING_NAKED; + } + tok.push_back(c); + line_pos++; + break; + + default: + tok.push_back(c); + line_pos++; + break; + } + + if (partial_tok_type == PartialToken::GARBAGE) + { + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: stray character '%c'", line_num, line_pos, c); + } +} + +void DocumentParser::UpdateNumber(const char c) +{ + switch (c) + { + case '\r': + break; + + case ' ': + case ',': + case '\t': + if (partial_tok_type == PartialToken::NUMBER_STUB_MINUS + && options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + else + { + this->FlushNumericToken(); + } + line_pos++; + break; + + case '\n': + if (partial_tok_type == PartialToken::NUMBER_STUB_MINUS + && options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + else + { + this->FlushNumericToken(); + } + // Break line + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + line_num++; + line_pos = 0; + break; + + case ':': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_COLON) + { + if (partial_tok_type == PartialToken::NUMBER_STUB_MINUS + && options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + else + { + this->FlushNumericToken(); + } + } + else + { + this->DiscontinueNumber(); + tok.push_back(c); + } + line_pos++; + break; + + case '=': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_EQUALS) + { + if (partial_tok_type == PartialToken::NUMBER_STUB_MINUS + && options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + { + this->FlushStringishToken(GDocTokenType::STRING); + } + else + { + this->FlushNumericToken(); + } + } + else + { + this->DiscontinueNumber(); + tok.push_back(c); + } + line_pos++; + break; + + case '.': + if (partial_tok_type == PartialToken::NUMBER_INTEGER + || partial_tok_type == PartialToken::NUMBER_STUB_MINUS) + { + partial_tok_type = PartialToken::NUMBER_DECIMAL; + } + else + { + this->DiscontinueNumber(); + } + tok.push_back(c); + line_pos++; + break; + + case 'e': + case 'E': + if (partial_tok_type == PartialToken::NUMBER_DECIMAL + || partial_tok_type == PartialToken::NUMBER_INTEGER) + { + partial_tok_type = PartialToken::NUMBER_SCIENTIFIC_STUB; + } + else + { + this->DiscontinueNumber(); + } + tok.push_back(c); + line_pos++; + break; + + case '-': + if (partial_tok_type == PartialToken::NUMBER_SCIENTIFIC_STUB) + { + partial_tok_type = PartialToken::NUMBER_SCIENTIFIC_STUB_MINUS; + } + else + { + this->DiscontinueNumber(); + } + tok.push_back(c); + line_pos++; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (partial_tok_type == PartialToken::NUMBER_SCIENTIFIC_STUB + || partial_tok_type == PartialToken::NUMBER_SCIENTIFIC_STUB_MINUS) + { + partial_tok_type = PartialToken::NUMBER_SCIENTIFIC; + } + else if (partial_tok_type == PartialToken::NUMBER_STUB_MINUS) + { + partial_tok_type = PartialToken::NUMBER_INTEGER; + } + tok.push_back(c); + line_pos++; + break; + + default: + this->DiscontinueNumber(); + tok.push_back(c); + line_pos++; + break; + + } + + if (partial_tok_type == PartialToken::GARBAGE) + { + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: stray character '%c' in number", line_num, line_pos, c); + } +} + +void DocumentParser::ProcessSeparatorWithinBool() +{ + this->DiscontinueBool(); + switch (partial_tok_type) + { + case PartialToken::KEYWORD: + this->FlushStringishToken(GDocTokenType::KEYWORD); + break; + case PartialToken::STRING_NAKED: + this->FlushStringishToken(GDocTokenType::STRING); + break; + default: + // Discard token + tok.push_back('\0'); + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: discarding incomplete boolean token '%s'", line_num, line_pos, tok.data()); + tok.clear(); + partial_tok_type = PartialToken::NONE; + break; + } +} + +void DocumentParser::UpdateBool(const char c) +{ + switch (c) + { + case '\r': + break; + + case ' ': + case ',': + case '\t': + this->ProcessSeparatorWithinBool(); + line_pos++; + break; + + case '\n': + this->ProcessSeparatorWithinBool(); + // Break line + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + line_num++; + line_pos = 0; + break; + + case ':': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_COLON) + { + this->ProcessSeparatorWithinBool(); + } + else + { + this->DiscontinueBool(); + tok.push_back(c); + } + line_pos++; + break; + + case '=': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_EQUALS) + { + this->ProcessSeparatorWithinBool(); + } + else + { + this->DiscontinueBool(); + tok.push_back(c); + } + line_pos++; + break; + + case 'r': + if (partial_tok_type != PartialToken::BOOL_TRUE || tok.size() != 1) + { + this->DiscontinueBool(); + } + tok.push_back(c); + line_pos++; + break; + + case 'u': + if (partial_tok_type != PartialToken::BOOL_TRUE || tok.size() != 2) + { + this->DiscontinueBool(); + } + tok.push_back(c); + line_pos++; + break; + + case 'a': + if (partial_tok_type != PartialToken::BOOL_FALSE || tok.size() != 1) + { + this->DiscontinueBool(); + } + tok.push_back(c); + line_pos++; + break; + + case 'l': + if (partial_tok_type != PartialToken::BOOL_FALSE || tok.size() != 2) + { + this->DiscontinueBool(); + } + tok.push_back(c); + line_pos++; + break; + + case 's': + if (partial_tok_type != PartialToken::BOOL_FALSE || tok.size() != 3) + { + this->DiscontinueBool(); + } + tok.push_back(c); + line_pos++; + break; + + case 'e': + if (partial_tok_type == PartialToken::BOOL_TRUE && tok.size() == 3) + { + doc.tokens.push_back({ GDocTokenType::BOOL, 1.f }); + tok.clear(); + partial_tok_type = PartialToken::NONE; + } + else if (partial_tok_type == PartialToken::BOOL_FALSE && tok.size() == 4) + { + doc.tokens.push_back({ GDocTokenType::BOOL, 0.f }); + tok.clear(); + partial_tok_type = PartialToken::NONE; + } + else + { + this->DiscontinueBool(); + tok.push_back(c); + } + line_pos++; + break; + + default: + this->DiscontinueBool(); + tok.push_back(c); + line_pos++; + break; + } + + if (partial_tok_type == PartialToken::GARBAGE) + { + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: stray character '%c' in boolean", line_num, line_pos, c); + } +} + +void DocumentParser::DiscontinueBool() +{ + if (doc.tokens.size() == 0 || doc.tokens.back().type == GDocTokenType::LINEBREAK) + partial_tok_type = PartialToken::KEYWORD; + else if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; +} + +void DocumentParser::DiscontinueNumber() +{ + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; +} + +void DocumentParser::DiscontinueKeyword() +{ + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + partial_tok_type = PartialToken::STRING_NAKED; + else + partial_tok_type = PartialToken::GARBAGE; +} + +void DocumentParser::UpdateKeyword(const char c) +{ + switch (c) + { + case '\r': + break; + + case ' ': + case ',': + case '\t': + this->FlushStringishToken(GDocTokenType::KEYWORD); + line_pos++; + break; + + case '\n': + this->FlushStringishToken(GDocTokenType::KEYWORD); + // Break line + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + line_num++; + line_pos = 0; + break; + + case ':': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_COLON) + { + this->FlushStringishToken(GDocTokenType::KEYWORD); + } + else + { + this->DiscontinueKeyword(); + tok.push_back(c); + } + line_pos++; + break; + + case '=': + if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_EQUALS) + { + this->FlushStringishToken(GDocTokenType::KEYWORD); + } + else + { + this->DiscontinueKeyword(); + tok.push_back(c); + } + line_pos++; + break; + + case '_': + tok.push_back(c); + line_pos++; + break; + + case '(': + if (options & GenericDocument::OPTION_ALLOW_NAKED_STRINGS) + { + if (options & GenericDocument::OPTION_PARENTHESES_CAPTURE_SPACES) + partial_tok_type = PartialToken::STRING_NAKED_CAPTURING_SPACES; + else + partial_tok_type = PartialToken::STRING_NAKED; + } + else + { + partial_tok_type = PartialToken::GARBAGE; + } + tok.push_back(c); + line_pos++; + break; + + case ']': + if (partial_tok_type == PartialToken::KEYWORD_BRACED) + { + partial_tok_type = PartialToken::KEYWORD; // Do not allow any more ']'. + } + else + { + this->DiscontinueKeyword(); + } + tok.push_back(c); + line_pos++; + break; + + default: + if (!isalnum(c)) + { + this->DiscontinueKeyword(); + } + tok.push_back(c); + line_pos++; + break; + } + + if (partial_tok_type == PartialToken::GARBAGE) + { + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: stray character '%c' in keyword", line_num, line_pos, c); + } +} + +void DocumentParser::UpdateTitle(const char c) +{ + switch (c) + { + case '\r': + break; + + case '\n': + this->FlushStringishToken(GDocTokenType::STRING); + // Break line + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + line_num++; + line_pos = 0; + break; + + default: + tok.push_back(c); + line_pos++; + break; + } +} + +void DocumentParser::UpdateGarbage(const char c) +{ + switch (c) + { + case '\r': + break; + + case ' ': + case ',': + case '\t': + case '\n': + tok.push_back('\0'); + Logger::Log(LOG_WARN, "GenericFileFormat: line %zd, pos %zd: discarding garbage token '%s'", line_num, line_pos, tok.data()); + tok.clear(); + partial_tok_type = PartialToken::NONE; + line_pos++; + break; + + default: + tok.push_back(c); + line_pos++; + break; + } +} + +void DocumentParser::FlushStringishToken(GDocTokenType type) +{ + doc.tokens.push_back({ type, (float)doc.string_pool.size() }); + tok.push_back('\0'); + std::copy(tok.begin(), tok.end(), std::back_inserter(doc.string_pool)); + tok.clear(); + partial_tok_type = PartialToken::NONE; +} + +void DocumentParser::FlushNumericToken() +{ + tok.push_back('\0'); + if (partial_tok_type == PartialToken::NUMBER_INTEGER) + { + char* end; + long val = strtol_l(tok.data(), &end, 0, _numLocale); + if (tok.data() != end) + { + doc.tokens.push_back({ GDocTokenType::INT, (float)val }); + } + else + { + Logger::Log(LOG_ERROR, "GenericFileFormat: could not parse '%s' as INTEGER", tok.data()); + } + } + else + { + char* end; + double val = strtod_l(tok.data(), &end, _numLocale); + if (tok.data() != end) + { + doc.tokens.push_back({ GDocTokenType::FLOAT, (float)val }); + } + else + { + Logger::Log(LOG_ERROR, "GenericFileFormat: could not parse '%s' as FLOAT", tok.data()); + } + } + tok.clear(); + partial_tok_type = PartialToken::NONE; +} + +void DocumentParser::ProcessChar(const char c) +{ + switch (partial_tok_type) + { + case PartialToken::NONE: + this->BeginToken(c); + break; + + case PartialToken::COMMENT_SEMICOLON: + case PartialToken::COMMENT_SLASH: + case PartialToken::COMMENT_HASH: + this->UpdateComment(c); + break; + + case PartialToken::STRING_QUOTED: + case PartialToken::STRING_NAKED: + case PartialToken::STRING_NAKED_CAPTURING_SPACES: + this->UpdateString(c); + break; + + case PartialToken::NUMBER_INTEGER: + case PartialToken::NUMBER_STUB_MINUS: + case PartialToken::NUMBER_DECIMAL: + case PartialToken::NUMBER_SCIENTIFIC: + case PartialToken::NUMBER_SCIENTIFIC_STUB: + case PartialToken::NUMBER_SCIENTIFIC_STUB_MINUS: + this->UpdateNumber(c); + break; + + case PartialToken::BOOL_TRUE: + case PartialToken::BOOL_FALSE: + this->UpdateBool(c); + break; + + case PartialToken::KEYWORD: + case PartialToken::KEYWORD_BRACED: + this->UpdateKeyword(c); + break; + + case PartialToken::TITLE_STRING: + this->UpdateTitle(c); + break; + + case PartialToken::GARBAGE: + this->UpdateGarbage(c); + break; + } +} + +void DocumentParser::ProcessEOF() +{ + // Flush any partial token + switch (partial_tok_type) + { + case PartialToken::STRING_QUOTED: + case PartialToken::STRING_NAKED_CAPTURING_SPACES: + case PartialToken::TITLE_STRING: + this->FlushStringishToken(GDocTokenType::STRING); + break; + + case PartialToken::KEYWORD_BRACED: + this->FlushStringishToken(GDocTokenType::KEYWORD); + break; + + default: + this->ProcessChar(' '); // Pretend processing a separator to flush any partial whitespace-incompatible token. + break; + } + + // Ensure newline at end of file + if (doc.tokens.size() == 0 || doc.tokens.back().type != GDocTokenType::LINEBREAK) + { + doc.tokens.push_back({ GDocTokenType::LINEBREAK, 0.f }); + } +} + +bool GenericDocument::loadFromFile(const std::string& filename, const BitMask_t options) +{ + FILE* f = fopen(filename.c_str(), "r"); + if (!f) + { + Logger::Log(LOG_ERROR, "GenericDocument::loadFromFile(): Could not open '%s'", filename.c_str()); + return false; + } + + // Reset the document + tokens.clear(); + string_pool.clear(); + + // Prepare context + DocumentParser parser(*this, options, f); + + // Parse the text + while (!feof(f)) + { + const char c = (const char)fgetc(f); + parser.ProcessChar(c); + } + parser.ProcessEOF(); + return true; +} + +bool GenericDocument::saveToFile(const std::string& filename) +{ + FILE* f = fopen(filename.c_str(), "w"); + if (!f) + { + Logger::Log(LOG_ERROR, "GenericDocument::saveToFile(): Could not open '%s'", filename.c_str()); + return false; + } + + std::string separator; + const char* pool_str = nullptr; + + for (Token& tok : tokens) + { + switch (tok.type) + { + case GDocTokenType::LINEBREAK: + fprintf(f, "\n"); + separator = ""; + break; + + case GDocTokenType::COMMENT: + pool_str = string_pool.data() + (size_t)tok.data; + fprintf(f, ";%s", pool_str); + break; + + case GDocTokenType::STRING: + pool_str = string_pool.data() + (size_t)tok.data; + fprintf(f, "%s%s", separator.c_str(), pool_str); + separator = ", "; + break; + + case GDocTokenType::FLOAT: + fprintf(f, "%s%g", separator.c_str(), tok.data); + separator = ", "; + break; + + case GDocTokenType::INT: + fprintf(f, "%s%d", separator.c_str(), (int)tok.data); + separator = ", "; + break; + + case GDocTokenType::BOOL: + fprintf(f, "%s%s", separator.c_str(), tok.data == 1.f ? "true" : "false"); + separator = ", "; + break; + + case GDocTokenType::KEYWORD: + pool_str = string_pool.data() + (size_t)tok.data; + fprintf(f, "%s", pool_str); + separator = " "; + break; + } + } + return true; +} + +bool GenericDocContext::seekNextLine() +{ + // Skip current line + while (!this->endOfFile() && this->tokenType() != GDocTokenType::LINEBREAK) + { + this->moveNext(); + } + this->moveNext(); + + // Skip comments and empty lines + while (!this->endOfFile() && (this->isTokComment(0) || this->isTokLineBreak(0))) + { + this->moveNext(); + } + + return this->endOfFile(); +} + +int GenericDocContext::countLineArgs() +{ + int count = 0; + while (!endOfFile(count) && this->tokenType(count) != GDocTokenType::LINEBREAK) + count++; + return count; +} + +// ----------------- +// Editing functions + +void GenericDocContext::appendTokens(int count) +{ + if (count <= 0) + return; + + token_pos = (int)doc->tokens.size(); + for (int i = 0; i < count; i++) + { + doc->tokens.push_back({ GDocTokenType::NONE, 0.f }); + } +} + +bool GenericDocContext::insertToken(int offset) +{ + if (endOfFile(offset)) + return false; + + doc->tokens.insert(doc->tokens.begin() + token_pos + offset, { GDocTokenType::NONE, 0.f }); + return true; +} + +bool GenericDocContext::eraseToken(int offset) +{ + if (endOfFile(offset)) + return false; + + // Just erase the token. + // We don't care about garbage in `string_pool` - the strings are usually just 1-6 characters long anyway. + + doc->tokens.erase(doc->tokens.begin() + token_pos + offset); + return true; +} + +bool GenericDocContext::setStringData(int offset, GDocTokenType type, const std::string& data) +{ + if (endOfFile(offset)) + return false; + + // Insert the string at the end of the string_pool + // We don't care about order - updating string offsets in tokens would be complicated and unlikely helpful. + + doc->tokens[token_pos + offset] = { type, (float)doc->string_pool.size() }; + std::copy(data.begin(), data.end(), std::back_inserter(doc->string_pool)); + doc->string_pool.push_back('\0'); + return true; +} + +bool GenericDocContext::setFloatData(int offset, GDocTokenType type, float data) +{ + if (endOfFile(offset)) + return false; + + doc->tokens[token_pos + offset] = { type, data }; + return true; +} + + // ----------------------------- registering AngelScript bindings --------------------------------- // + +// Factories +static GenericDocument* GenericDocumentFactory() +{ + return new GenericDocument(); +} + +static GenericDocContext* GenericDocContextFactory(GenericDocument* doc) +{ + return new GenericDocContext(doc); +} + +void RegisterGenericFileFormat(asIScriptEngine* engine) +{ + // enum TokenType + engine->RegisterEnum("TokenType"); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_NONE", (int)GDocTokenType::NONE); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_LINEBREAK", (int)GDocTokenType::LINEBREAK); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_COMMENT", (int)GDocTokenType::COMMENT); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_STRING", (int)GDocTokenType::STRING); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_FLOAT", (int)GDocTokenType::FLOAT); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_INT", (int)GDocTokenType::INT); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_BOOL", (int)GDocTokenType::BOOL); + engine->RegisterEnumValue("TokenType", "TOKEN_TYPE_KEYWORD", (int)GDocTokenType::KEYWORD); + + + // GenericDocument constants + engine->RegisterEnum("GenericDocumentOptions"); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS", GenericDocument::OPTION_ALLOW_NAKED_STRINGS); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS", GenericDocument::OPTION_ALLOW_SLASH_COMMENTS); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE", GenericDocument::OPTION_FIRST_LINE_IS_TITLE); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON", GenericDocument::OPTION_ALLOW_SEPARATOR_COLON); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES", GenericDocument::OPTION_PARENTHESES_CAPTURE_SPACES); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_ALLOW_BRACED_KEYWORDS", GenericDocument::OPTION_ALLOW_BRACED_KEYWORDS); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_EQUALS", GenericDocument::OPTION_ALLOW_SEPARATOR_EQUALS); + engine->RegisterEnumValue("GenericDocumentOptions", "GENERIC_DOCUMENT_OPTION_ALLOW_HASH_COMMENTS", GenericDocument::OPTION_ALLOW_HASH_COMMENTS); + + + // class GenericDocument + engine->RegisterObjectType("GenericDocumentClass", sizeof(GenericDocument), asOBJ_REF); + engine->RegisterObjectBehaviour("GenericDocumentClass", asBEHAVE_FACTORY, "GenericDocumentClass@ f()", asFUNCTION(GenericDocumentFactory), asCALL_CDECL); + // Registering the addref/release behaviours + engine->RegisterObjectBehaviour("GenericDocumentClass", asBEHAVE_ADDREF, "void f()", asMETHOD(GenericDocument, AddRef), asCALL_THISCALL); + engine->RegisterObjectBehaviour("GenericDocumentClass", asBEHAVE_RELEASE, "void f()", asMETHOD(GenericDocument, Release), asCALL_THISCALL); + + // RORSERVER: This is the only difference from client's `GenericDocumentClass` + engine->RegisterObjectMethod("GenericDocumentClass", "bool loadFromFile(string const&in,int)", asMETHOD(GenericDocument, loadFromFile), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocumentClass", "bool saveToFile(string const&in)", asMETHOD(GenericDocument, saveToFile), asCALL_THISCALL); + + + // class GenericDocContext + // (Please maintain the same order as in 'GenericFileFormat.h' and 'doc/*/GenericDocContextClass.h') + engine->RegisterObjectType("GenericDocContextClass", sizeof(GenericDocument), asOBJ_REF); + engine->RegisterObjectBehaviour("GenericDocContextClass", asBEHAVE_FACTORY, "GenericDocContextClass@ f()", asFUNCTION(GenericDocContextFactory), asCALL_CDECL); + // Registering the addref/release behaviours + engine->RegisterObjectBehaviour("GenericDocContextClass", asBEHAVE_ADDREF, "void f()", asMETHOD(GenericDocument, AddRef), asCALL_THISCALL); + engine->RegisterObjectBehaviour("GenericDocContextClass", asBEHAVE_RELEASE, "void f()", asMETHOD(GenericDocument, Release), asCALL_THISCALL); + + engine->RegisterObjectMethod("GenericDocContextClass", "bool moveNext()", asMETHOD(GenericDocContext, moveNext), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "uint getPos()", asMETHOD(GenericDocContext, getPos), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool seekNextLine()", asMETHOD(GenericDocContext, seekNextLine), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "uint countLineArgs()", asMETHOD(GenericDocContext, countLineArgs), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool endOfFile(int offset = 0)", asMETHOD(GenericDocContext, endOfFile), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "GDocTokenType tokenType(int offset = 0)", asMETHOD(GenericDocContext, tokenType), asCALL_THISCALL); + + engine->RegisterObjectMethod("GenericDocContextClass", "string getTokString(int offset = 0)", asMETHOD(GenericDocContext, getTokString), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "float getTokFloat(int offset = 0)", asMETHOD(GenericDocContext, getTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "int getTokInt(int offset = 0)", asMETHOD(GenericDocContext, getTokInt), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool getTokBool(int offset = 0)", asMETHOD(GenericDocContext, getTokBool), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "string getTokKeyword(int offset = 0)", asMETHOD(GenericDocContext, getTokKeyword), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "string getTokComment(int offset = 0)", asMETHOD(GenericDocContext, getTokComment), asCALL_THISCALL); + + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokString(int offset = 0)", asMETHOD(GenericDocContext, isTokString), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokFloat(int offset = 0)", asMETHOD(GenericDocContext, isTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokInt(int offset = 0)", asMETHOD(GenericDocContext, isTokInt), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokBool(int offset = 0)", asMETHOD(GenericDocContext, isTokBool), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokKeyword(int offset = 0)", asMETHOD(GenericDocContext, isTokKeyword), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokComment(int offset = 0)", asMETHOD(GenericDocContext, isTokComment), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool isTokLineBreak(int offset = 0)", asMETHOD(GenericDocContext, isTokLineBreak), asCALL_THISCALL); + + // > Editing functions: + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokens(int count)", asMETHOD(GenericDocContext, appendTokens), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool insertToken(int offset = 0)", asMETHOD(GenericDocContext, insertToken), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool eraseToken(int offset = 0)", asMETHOD(GenericDocContext, eraseToken), asCALL_THISCALL); + + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokString(const string &in)", asMETHOD(GenericDocContext, appendTokString), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokFloat(float)", asMETHOD(GenericDocContext, appendTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokInt(int)", asMETHOD(GenericDocContext, appendTokInt), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokBool(bool)", asMETHOD(GenericDocContext, appendTokBool), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokKeyword(const string &in)", asMETHOD(GenericDocContext, appendTokKeyword), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokComment(const string &in)", asMETHOD(GenericDocContext, appendTokComment), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "void appendTokLineBreak()", asMETHOD(GenericDocContext, appendTokLineBreak), asCALL_THISCALL); + + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokString(int offset, const string &in)", asMETHOD(GenericDocContext, setTokString), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokFloat(int offset, float)", asMETHOD(GenericDocContext, setTokFloat), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokInt(int offset, int)", asMETHOD(GenericDocContext, setTokInt), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokBool(int offset, bool)", asMETHOD(GenericDocContext, setTokBool), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokKeyword(int offset, const string &in)", asMETHOD(GenericDocContext, setTokKeyword), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokComment(int offset, const string &in)", asMETHOD(GenericDocContext, setTokComment), asCALL_THISCALL); + engine->RegisterObjectMethod("GenericDocContextClass", "bool setTokLineBreak(int offset)", asMETHOD(GenericDocContext, setTokLineBreak), asCALL_THISCALL); + +} diff --git a/source/server/GenericFileFormat.h b/source/server/GenericFileFormat.h new file mode 100644 index 00000000..f011bafe --- /dev/null +++ b/source/server/GenericFileFormat.h @@ -0,0 +1,185 @@ +/* +This file is part of "Rigs of Rods Server" (Relay mode) + +Copyright 2007 Pierre-Michel Ricordel +Copyright 2014+ Rigs of Rods Community + +"Rigs of Rods Server" is free software: you can redistribute it +and/or modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, either version 3 +of the License, or (at your option) any later version. + +"Rigs of Rods Server" is distributed in the hope that it will +be useful, but WITHOUT ANY WARRANTY; without even the implied +warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see . +*/ + +// Ported from Rigs of Rods (/source/main/utils/) at revision a95d75df8c7c376e36ff9e24df62e7006d6246df by ohlidalp, 2026 + +/// @file +/// @brief Generic text file parser +/// +/// Syntax: +/// - Lines starting with semicolon (;) (ignoring leading whitespace) are comments +/// - Separators are space, tabulator or comma (,) +/// - All strings must be in double quotes (except if OPTION_ALLOW_NAKED_STRINGS is used). +/// - If the first argument on line is an unqoted string, it's considered KEYWORD token type. +/// - Reserved keywords are 'true' and 'false' for the BOOL token type. +/// +/// Remarks: +/// - Strings cannot be multiline. Linebreak within string ends the string. +/// - KEYWORD tokens cannot start with a digit or special character. + +#pragma once + +#include "rornet.h" + +#include +#include +#include +#include + +enum class GDocTokenType // RORSERVER: This is just `RoR::TokenType` on client, but here it conflicts with `_TOKEN_INFORMATION_CLASS::TokenType` +{ + NONE, + LINEBREAK, // Input: LF (CR is ignored); Output: platform-specific. + COMMENT, // Line starting with ; (skipping whitespace). Data: offset in string pool. + STRING, // Quoted string. Data: offset in string pool. + FLOAT, // Numbers with or without a decimal point. + INT, // Only numbers without decimal point. + BOOL, // Lowercase 'true'/'false'. Data: 1.0 for true, 0.0 for false. + KEYWORD, // Unquoted string at start of line (skipping whitespace). Data: offset in string pool. +}; + +struct Token +{ + GDocTokenType type; + float data; +}; + +struct GenericDocument +{ + static const BitMask_t OPTION_ALLOW_NAKED_STRINGS = BITMASK(1); //!< Allow strings without quotes, for backwards compatibility. + static const BitMask_t OPTION_ALLOW_SLASH_COMMENTS = BITMASK(2); //!< Allow comments starting with `//`. + static const BitMask_t OPTION_FIRST_LINE_IS_TITLE = BITMASK(3); //!< First non-empty & non-comment line is a naked string with spaces. + static const BitMask_t OPTION_ALLOW_SEPARATOR_COLON = BITMASK(4); //!< Allow ':' as separator between tokens. + static const BitMask_t OPTION_PARENTHESES_CAPTURE_SPACES = BITMASK(5); //!< If non-empty NAKED string encounters '(', following spaces will be captured until matching ')' is found. + static const BitMask_t OPTION_ALLOW_BRACED_KEYWORDS = BITMASK(6); //!< Allow INI-like '[keyword]' tokens. + static const BitMask_t OPTION_ALLOW_SEPARATOR_EQUALS = BITMASK(7); //!< Allow '=' as separator between tokens. + static const BitMask_t OPTION_ALLOW_HASH_COMMENTS = BITMASK(8); //!< Allow comments starting with `#`. + + virtual ~GenericDocument() {}; + + std::vector string_pool; // Data of COMMENT/KEYWORD/STRING tokens; NUL-terminated strings. + std::vector tokens; + + virtual bool loadFromFile(const std::string& filename, BitMask_t options = 0); //!< Loaded from dedicated 'storage' folder specified in server config. + virtual bool saveToFile(const std::string& filename); //!< Stored to dedicated 'storage' folder specified in server config. + + // AngelScript reference counting + void AddRef() + { + // Increase the reference counter + refCount++; + } + void Release() + { + // Decrease ref count and delete if it reaches 0 + if( --refCount == 0 ) + delete this; + } +private: + int refCount = 1; +}; + +struct GenericDocContext +{ + GenericDocContext(GenericDocument* d) : doc(d) + { + assert(doc != nullptr); + if (doc == nullptr && asGetActiveContext() != nullptr) + { + asGetActiveContext()->SetException("Cannot create GenericDocContextClass from null GenericDocument!"); + } + } + virtual ~GenericDocContext() {}; + + GenericDocument* doc; + uint32_t token_pos = 0; + + // PLEASE maintain the same order as in 'bindings/GenericFileFormatAngelscript.cpp' and 'doc/*/GenericDocContextClass.h' + + bool moveNext() { token_pos++; return endOfFile(); } + uint32_t getPos() const { return token_pos; } + bool seekNextLine(); + int countLineArgs(); + bool endOfFile(int offset = 0) const { return token_pos + offset >= doc->tokens.size(); } + GDocTokenType tokenType(int offset = 0) const { return !endOfFile(offset) ? doc->tokens[token_pos + offset].type : GDocTokenType::NONE; } + + std::string getTokString(int offset = 0) const { assert(isTokString(offset)); return getStringData(offset); } + float getTokFloat(int offset = 0) const { assert(isTokFloat(offset)); return getFloatData(offset); } + int getTokInt(int offset = 0) const { assert(isTokInt(offset)); return (int)getFloatData(offset); } + float getTokNumeric(int offset = 0) const { assert(isTokNumeric(offset)); return getFloatData(offset); } + bool getTokBool(int offset = 0) const { assert(isTokBool(offset)); return getFloatData(offset) == 1.f; } + std::string getTokKeyword(int offset = 0) const { assert(isTokKeyword(offset)); return getStringData(offset); } + std::string getTokComment(int offset = 0) const { assert(isTokComment(offset)); return getStringData(offset); } + + bool isTokString(int offset = 0) const { return tokenType(offset) == GDocTokenType::STRING; } + bool isTokFloat(int offset = 0) const { return tokenType(offset) == GDocTokenType::FLOAT || tokenType(offset) == GDocTokenType::INT; } + bool isTokInt(int offset = 0) const { return tokenType(offset) == GDocTokenType::INT; } + bool isTokBool(int offset = 0) const { return tokenType(offset) == GDocTokenType::BOOL; } + bool isTokKeyword(int offset = 0) const { return tokenType(offset) == GDocTokenType::KEYWORD; } + bool isTokComment(int offset = 0) const { return tokenType(offset) == GDocTokenType::COMMENT; } + bool isTokLineBreak(int offset = 0) const { return tokenType(offset) == GDocTokenType::LINEBREAK; } + bool isTokNumeric(int offset = 0) const { return isTokInt(offset) || isTokFloat(offset); } + + // Editing functions: + + void appendTokens(int count); //!< Appends a series of `GDocTokenType::NONE` and sets Pos at the first one added; use `setTok*` functions to fill them. + bool insertToken(int offset = 0); //!< Inserts `GDocTokenType::NONE`; @return false if offset is beyond EOF + bool eraseToken(int offset = 0); //!< @return false if offset is beyond EOF + + void appendTokString(const std::string& str) { appendTokens(1); setTokString(0, str); } + void appendTokFloat(float val) { appendTokens(1); setTokFloat(0, val); } + void appendTokInt(int val) { appendTokens(1); setTokInt(0, val); } + void appendTokBool(bool val) { appendTokens(1); setTokBool(0, val); } + void appendTokKeyword(const std::string& str) { appendTokens(1); setTokKeyword(0, str); } + void appendTokComment(const std::string& str) { appendTokens(1); setTokComment(0, str); } + void appendTokLineBreak() { appendTokens(1); setTokLineBreak(0); } + + bool setTokString(int offset, const std::string& str) { return setStringData(offset, GDocTokenType::STRING, str); } + bool setTokFloat(int offset, float val) { return setFloatData(offset, GDocTokenType::FLOAT, val); } + bool setTokInt(int offset, int val) { return setFloatData(offset, GDocTokenType::INT, (float)val); } + bool setTokBool(int offset, bool val) { return setFloatData(offset, GDocTokenType::BOOL, val); } + bool setTokKeyword(int offset, const std::string& str) { return setStringData(offset, GDocTokenType::KEYWORD, str); } + bool setTokComment(int offset, const std::string& str) { return setStringData(offset, GDocTokenType::COMMENT, str); } + bool setTokLineBreak(int offset) { return setFloatData(offset, GDocTokenType::LINEBREAK, 0.f); } + + // Not exported to script: + + const char* getStringData(int offset = 0) const { return !endOfFile(offset) ? (doc->string_pool.data() + (uint32_t)doc->tokens[token_pos + offset].data) : ""; } + float getFloatData(int offset = 0) const { return !endOfFile(offset) ? doc->tokens[token_pos + offset].data : 0.f; } + bool setStringData(int offset, GDocTokenType type, const std::string& data); //!< @return false if offset is beyond EOF + bool setFloatData(int offset, GDocTokenType type, float data); //!< @return false if offset is beyond EOF + + // AngelScript reference counting + void AddRef() + { + // Increase the reference counter + refCount++; + } + void Release() + { + // Decrease ref count and delete if it reaches 0 + if( --refCount == 0 ) + delete this; + } +private: + int refCount = 1; +}; + +void RegisterGenericFileFormat(asIScriptEngine* engine); diff --git a/source/server/ScriptEngine.cpp b/source/server/ScriptEngine.cpp index 3244cb65..023a115f 100644 --- a/source/server/ScriptEngine.cpp +++ b/source/server/ScriptEngine.cpp @@ -27,6 +27,7 @@ along with Foobar. If not, see . #include "config.h" #include "messaging.h" #include "CurlHelpers.h" +#include "GenericFileFormat.h" #include "scriptstdstring/scriptstdstring.h" // angelscript addon #include "scriptmath/scriptmath.h" // angelscript addon #include "scriptmath3d/scriptmath3d.h" // angelscript addon @@ -570,6 +571,9 @@ void ScriptEngine::init() { result = engine->RegisterGlobalProperty("const int TO_ALL", (void *) &TO_ALL); assert_net(result >= 0); + Logger::Log(LOG_INFO, "ScriptEngine: Registering the generic document parser..."); + + RegisterGenericFileFormat(engine); // Defined in 'GenericFileFormat.cpp' Logger::Log(LOG_INFO, "ScriptEngine: Registration done"); } diff --git a/source/server/listener.cpp.orig b/source/server/listener.cpp.orig new file mode 100644 index 00000000..50265a62 --- /dev/null +++ b/source/server/listener.cpp.orig @@ -0,0 +1,164 @@ +/* +This file is part of "Rigs of Rods Server" (Relay mode) + +Copyright 2007 Pierre-Michel Ricordel +Copyright 2014+ Rigs of Rods Community + +"Rigs of Rods Server" is free software: you can redistribute it +and/or modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, either version 3 +of the License, or (at your option) any later version. + +"Rigs of Rods Server" is distributed in the hope that it will +be useful, but WITHOUT ANY WARRANTY; without even the implied +warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see . +*/ + +#include "listener.h" + +#include "rornet.h" +#include "messaging.h" +#include "sequencer.h" +#include "SocketW.h" +#include "logger.h" +#include "config.h" +#include "UnicodeStrings.h" +#include "utils.h" + +#include +#include +#include + +#ifdef __GNUC__ + +#include + +#endif + + +Listener::Listener(Sequencer *sequencer) : + m_sequencer(sequencer) { +} + +bool Listener::Initialize() { + // Make sure it's not started twice + std::lock_guard lock(m_mutex); + if (m_thread_state != ThreadState::NOT_RUNNING) + { + return true; + } + + // Start listening on the socket + SWBaseSocket::SWBaseError error; + m_listen_socket.bind(Config::getListenPort(), &error); + if (error != SWBaseSocket::ok) { + Logger::Log(LOG_ERROR, "FATAL Listerer: %s", error.get_error().c_str()); + return false; + } + m_listen_socket.listen(); + + // Start the thread + m_thread = std::thread(&Listener::ThreadMain, this); + m_thread_state = ThreadState::RUNNING; + + return true; +} + +void Listener::Shutdown() { + // Make sure it's not shut down twice + std::lock_guard lock(m_mutex); + if (m_thread_state != ThreadState::RUNNING) + { + return; + } + + Logger::Log(LOG_VERBOSE, "Stopping listener thread..."); + m_thread_state = ThreadState::STOP_REQUESTED; + m_thread.join(); + Logger::Log(LOG_VERBOSE, "Listener thread stopped"); +} + +void Listener::ThreadMain() { + Logger::Log(LOG_DEBUG, "Listerer thread starting"); + SWBaseSocket::SWBaseError error; + + //await connections + while (GetThreadState() == ThreadState::RUNNING) { + Logger::Log(LOG_VERBOSE, "Listener awaiting connections"); + SWInetSocket *ts = (SWInetSocket *) m_listen_socket.accept(&error); + if (error != SWBaseSocket::ok) { + if (GetThreadState() == ThreadState::STOP_REQUESTED) { + Logger::Log(LOG_ERROR, "INFO Listener shutting down"); + } else { + Logger::Log(LOG_ERROR, "ERROR Listener: %s", error.get_error().c_str()); + } + } + + Logger::Log(LOG_VERBOSE, "Listener got a new connection"); + + ts->set_timeout(5, 0); + + //receive a magic + int type; + int source; + unsigned int len; + unsigned int streamid; + char buffer[RORNET_MAX_MESSAGE_LENGTH]; + + try { + // this is the start of it all, it all starts with a simple hello + if (Messaging::SWReceiveMessage(ts, &type, &source, &streamid, &len, + buffer, RORNET_MAX_MESSAGE_LENGTH)) + throw std::runtime_error("ERROR Listener: receiving first message"); + + // make sure our first message is a hello message + if (type != RoRnet::MSG2_HELLO) { + Messaging::SWSendMessage(ts, RoRnet::MSG2_WRONG_VER, 0, 0, 0, 0); + throw std::runtime_error("ERROR Listener: protocol error"); + } + + // check client version + if (source == 5000 && (std::string(buffer) == "MasterServer")) { + Logger::Log(LOG_VERBOSE, "Master Server knocked ..."); + // send back some information, then close socket + char tmp[2048] = ""; + sprintf(tmp, "protocol:%s\nrev:%s\nbuild_on:%s_%s\n", RORNET_VERSION, VERSION, __DATE__, __TIME__); + if (Messaging::SWSendMessage(ts, RoRnet::MSG2_MASTERINFO, 0, 0, (unsigned int) strlen(tmp), tmp)) { + throw std::runtime_error("ERROR Listener: sending master info"); + } + // close socket + ts->disconnect(&error); + delete ts; + continue; + } + + // compare the versions if they are compatible + if (strncmp(buffer, RORNET_VERSION, strlen(RORNET_VERSION))) { + // not compatible + Messaging::SWSendMessage(ts, RoRnet::MSG2_WRONG_VER, 0, 0, 0, 0); + throw std::runtime_error("ERROR Listener: bad version: " + std::string(buffer) + ". rejecting ..."); + } + + // compatible version - tell client to reconnect using ENet + Messaging::SWSendMessage(ts, RoRnet::MSG2_VERSION, 0, 0, 0, 0); + // close socket + ts->disconnect(&error); + delete ts; + } + catch (std::runtime_error &e) { + Logger::Log(LOG_ERROR, e.what()); + ts->disconnect(&error); + delete ts; + } + } +} + +Listener::ThreadState Listener::GetThreadState() +{ + std::lock_guard lock(m_mutex); + return m_thread_state; +} diff --git a/source/server/sequencer.cpp b/source/server/sequencer.cpp index 2345e01a..33348da3 100644 --- a/source/server/sequencer.cpp +++ b/source/server/sequencer.cpp @@ -979,10 +979,10 @@ void Sequencer::queueMessage(int uid, int type, unsigned int streamid, char *dat // special case if the user has exactly 1 vehicle if (client->streams.size() == NON_VEHICLE_STREAMS + 1) sprintf(sayMsg, "You now have 1 vehicle. The vehicle limit on this server is set to %d.", - Config::getMaxVehicles()); + (int)Config::getMaxVehicles()); else - sprintf(sayMsg, "You now have %lu vehicles. The vehicle limit on this server is set to %d.", - (client->streams.size() - NON_VEHICLE_STREAMS), Config::getMaxVehicles()); + sprintf(sayMsg, "You now have %d vehicles. The vehicle limit on this server is set to %d.", + (int)(client->streams.size() - NON_VEHICLE_STREAMS), (int)Config::getMaxVehicles()); serverSay(sayMsg, client->user.uniqueid, FROM_SERVER); } From 0ef8335052a7d6e609e3227263f453b251a50a98 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Mon, 3 Aug 2026 16:49:23 +0200 Subject: [PATCH 2/2] :angel:Script: added GenericDocument to 'example-script.as' --- contrib/example-race.racetrack | 18 ++++ contrib/example-script.as | 176 +++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 contrib/example-race.racetrack diff --git a/contrib/example-race.racetrack b/contrib/example-race.racetrack new file mode 100644 index 00000000..4177caba --- /dev/null +++ b/contrib/example-race.racetrack @@ -0,0 +1,18 @@ +; ~~ New '.racetrack' format (file extension: .racetrack). ~~ +; Each file specifies a single race track. +; In .terrn2 file, list the race files under new section [Races] +; Filenames must include extension and end with = (like scripts do) +; Race system supports branching/joining paths! +; Checkpoint format: checkpointNum(1+), altpathNum(1+), x, y, z, rotX, rotY, rotZ, objName(override, optional) +; By convention, the checkpoint meshes are oriented sideways (facing X axis) + +racetrack_name "1 kilometer drag" +racetrack_laps 0 +racetrack_checkpoint_object 31-checkpoint +racetrack_start_object 31-checkpoint +racetrack_finish_object 31-checkpoint + +begin_checkpoints +1, 1, 1010, 9, 505, 0, 90, 0 +2, 1, 10, 9, 505, 0, 90, 0 +end_checkpoints diff --git a/contrib/example-script.as b/contrib/example-script.as index 500375c0..0d324d24 100644 --- a/contrib/example-script.as +++ b/contrib/example-script.as @@ -51,6 +51,80 @@ enum curlStatusType // Used by `curlStatus()` callback. TO_ALL = -1 // constant for functions that receive an uid for sending something +The GenericDocument tokenizer/parser API +---------------------------------------- + +Adopted from the game (client) to have a common data format that the game can edit/export and server can load. +Primary motivation is races, see https://github.com/RigsOfRods/rigs-of-rods/pull/3395. +Usage: start by creating empty GenericDocumentClass. You can create document by hand or load existing. +To traverse/edit tokens, you need to create GenericDocContextClass with the document as parameter. + +enum TokenType +{ + TOKEN_TYPE_NONE, + TOKEN_TYPE_LINEBREAK, //!< Input: LF (CR is ignored); Output: platform-specific. + TOKEN_TYPE_COMMENT, //!< Line starting with ; (skipping whitespace). + TOKEN_TYPE_STRING, //!< Quoted string. + TOKEN_TYPE_FLOAT, //!< Numbers with or without a decimal point. + TOKEN_TYPE_INT, //!< Only numbers without decimal point. + TOKEN_TYPE_BOOL, //!< Lowercase 'true'/'false'. + TOKEN_TYPE_KEYWORD, //!< Unquoted string at start of line (skipping whitespace). +}; + +enum GenericDocumentOptions +{ + GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS, //!< Allow strings without quotes, for backwards compatibility. + GENERIC_DOCUMENT_OPTION_ALLOW_SLASH_COMMENTS, //!< Allow comments starting with `//`. + GENERIC_DOCUMENT_OPTION_FIRST_LINE_IS_TITLE, //!< First non-empty & non-comment line is a naked string with spaces. + GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_COLON, //!< Allow ':' as separator between tokens. + GENERIC_DOCUMENT_OPTION_PARENTHESES_CAPTURE_SPACES, //!< If non-empty NAKED string encounters '(', following spaces will be captured until matching ')' is found. + GENERIC_DOCUMENT_OPTION_ALLOW_BRACED_KEYWORDS, //!< Allow INI-like '[keyword]' tokens. + GENERIC_DOCUMENT_OPTION_ALLOW_SEPARATOR_EQUALS, //!< Allow '=' as separator between tokens. + GENERIC_DOCUMENT_OPTION_ALLOW_HASH_COMMENTS //!< Allow comments starting with `#`. +}; + +class GenericDocumentClass +{ + bool loadFromFile(string filename, int options = 0); // Loads and parses a document from dedicated server script directory. + bool saveToFile(string filename); // Saves the document to dedicated server script directory. +}; + +class GenericDocContextClass +{ + GenericDocContext(GenericDocument@ d); + + // Traversal + bool moveNext(); + uint getPos(); + bool seekNextLine(); + int countLineArgs(); + bool endOfFile(int offset = 0); + TokenType tokenType(int offset = 0); + + // Token getter functions: + // * DATATYPE string ~ TOKENTYPE String, Keyword, Comment + // * DATATYPE float ~ TOKENTYPE Float, Int + // * DATATYPE bool ~ TOKENTYPE Bool + // * no DATATYPE ~ TOKENTYPE LineBreak + + DATATYPE getTokTOKENTYPE(int offset = 0); + bool isTokTOKENTYPE(int offset = 0); + + // Token getter functions: + // * DATATYPE const string&in ~ TOKENTYPE String, Keyword, Comment + // * DATATYPE float ~ TOKENTYPE Float, Int + // * DATATYPE bool ~ TOKENTYPE Bool + // * no DATATYPE ~ TOKENTYPE LineBreak + + void appendTokens(int count); //!< Appends a series of `TokenType::NONE` and sets Pos at the first one added; use `setTok*` functions to fill them. + bool insertToken(int offset = 0); //!< Inserts `TokenType::NONE`; @return false if offset is beyond EOF + bool eraseToken(int offset = 0); //!< @return false if offset is beyond EOF + + void appendTokTOKENTYPE(DATATYPE val); + + bool setTokTOKENTYPE(int offset, DATATYPE str); +}; + */ // ============================================================================ @@ -71,6 +145,9 @@ void main() server.setCallback("playerChat", "myChatMessageCallback", null); // CAUTION! This replaces the previous callback! server.setCallback("gameCmd", "myCommandCallback", null); // CAUTION! This replaces the previous callback! + // Showcase the GenericDocument API + loadExampleRacetrackFile("example-race.racetrack"); + server.Log("Example server script loaded!"); } @@ -222,4 +299,103 @@ void myCommandCallback(int uid, const string &in cmd) { server.say("Example server script: myCommandCallback(): UID: " + uid + ", cmd: '" + cmd + "'.", TO_ALL, FROM_SERVER); } + +// ============================================================================ +// GenericDocument (.racetrack) parsing example +// ============================================================================ + +void loadExampleRacetrackFile(string filename) +{ + GenericDocumentClass doc; + if (!doc.loadFromFile(filename, GENERIC_DOCUMENT_OPTION_ALLOW_NAKED_STRINGS)) + { + server.say("Example server script: could not load file '"+filename + +"' - you need to move it from '/contrib' dir to '/storage' dir.", TO_ALL, FROM_SERVER); + return; + } + + GenericDocContextClass ctx(doc); + + server.say("Example server script: reading GenericDocument file '"+filename+"'", TO_ALL, FROM_SERVER); + + // BEGIN copypasta from game's 'races.as' file, function `racesManager::addRaceFromDefinitionFile()` + + bool inCheckpoints = false; + /* RORSERVER: we ignore procedural roads for this example + bool inProceduralRoad = false; + */ + array checkpointTokPositions; // We must pre-count checkpoints to pick finish-obj correctly. + int highestCheckpointNum = 0; // Multiple finish lines are supported! + while (!ctx.endOfFile()) + { + //game.log("DBG addRaceFromDefinitionFile() token "+genericdoc_utils::tokenTypeStr(ctx.tokenType())+" at pos "+ctx.getPos()); + + if (ctx.isTokKeyword(0)) + { + if (ctx.isTokString(1) && ctx.getTokKeyword() == "racetrack_name") + { + server.say(" * Race name: "+ctx.getTokString(1), TO_ALL, FROM_SERVER); + } + if (ctx.isTokInt(1) && ctx.getTokKeyword() == "racetrack_laps") + { + server.say(" * Race laps: "+ctx.getTokInt(1), TO_ALL, FROM_SERVER); + } + else if (ctx.isTokString(1) && ctx.getTokKeyword() == "racetrack_checkpoint_object") + { + server.say(" * Race checkpoint-object: "+ctx.getTokString(1), TO_ALL, FROM_SERVER); + } + else if (ctx.isTokString(1) && ctx.getTokKeyword() == "racetrack_start_object") + { + server.say(" * Race start-object: "+ctx.getTokString(1), TO_ALL, FROM_SERVER); + } + else if (ctx.isTokString(1) && ctx.getTokKeyword() == "racetrack_finish_object") + { + server.say(" * Race finish-object: "+ctx.getTokString(1), TO_ALL, FROM_SERVER); + } + else if (ctx.getTokKeyword() == "begin_checkpoints") + { + inCheckpoints = true; + server.say(" * Race checkpoints...", TO_ALL, FROM_SERVER); + } + else if (ctx.getTokKeyword() == "end_checkpoints") + { + inCheckpoints = false; + } + /* RORSERVER: we ignore procedural roads for this example + else if (ctx.getTokKeyword() == "begin_procedural_roads") + { + inProceduralRoad = true; + } + else if (ctx.getTokKeyword() == "end_procedural_roads") + { + inProceduralRoad = false; + } + } + else if (inProceduralRoad) + { + ProceduralObjectClass@ road = road_utils::ParseProceduralRoadFromFile(ctx); + if (@road != null) // Errors already logged + { + this.raceList[raceID].proceduralRoads.insertLast(road); + } + */ + } + else if (inCheckpoints) + { + if (ctx.isTokInt(0) && ctx.isTokInt(1) // chkpNum, altpathNum + && ctx.isTokFloat(2) && ctx.isTokFloat(3) && ctx.isTokFloat(4) // Pos XYZ + && ctx.isTokFloat(5) && ctx.isTokFloat(6) && ctx.isTokFloat(7)) // Rot XYZ + { + highestCheckpointNum = (ctx.getTokInt() > highestCheckpointNum) ? ctx.getTokInt() : highestCheckpointNum; + server.say(" ** Checkpoint: chkpNum="+ctx.getTokInt(0) +", altpathNum="+ctx.getTokInt(1) // chkpNum, altpathNum + +", posX="+ ctx.getTokFloat(2) +", posY="+ ctx.getTokFloat(3) +", posZ="+ ctx.getTokFloat(4) // Pos XYZ + +", rotX="+ ctx.getTokFloat(5) +", rotY="+ ctx.getTokFloat(6) +", rotZ="+ ctx.getTokFloat(7), // Rot XYZ + TO_ALL, FROM_SERVER); + } + } + ctx.seekNextLine(); + } + + // END copypasta +} \ No newline at end of file