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 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); }