diff --git a/CMakeLists.txt b/CMakeLists.txt
index b71bd32e9..a0c973c36 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,13 +4,18 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
-# Options
+# Options (General)
option(SOURCEPP_USE_DMXPP "Build dmxpp library" ON)
+option(SOURCEPP_USE_FGDPP "Build fgdpp library" ON)
option(SOURCEPP_USE_STUDIOMODELPP "Build studiomodelpp library" ON)
option(SOURCEPP_USE_VMFPP "Build vmfpp library" ON)
option(SOURCEPP_BUILD_TESTS "Build tests for enabled libraries" OFF)
+# Options (Library)
+option(FGDPP_ENABLE_SPEN_FGD_SUPPORT "Enable support for FGD alterations (https://github.com/TeamSpen210/HammerAddons/wiki/Unified-FGD) made by TeamSpen's HammerAddons. Fully backwards compatible with Valve's FGD standard." OFF)
+
+
# BufferStream
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/src/thirdparty/bufferstream")
@@ -66,6 +71,18 @@ if(SOURCEPP_USE_DMXPP)
endif()
+# fgdpp
+if(SOURCEPP_USE_FGDPP)
+ add_pretty_parser(fgdpp SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/fgdpp/structs/entityproperties.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/fgdpp/fgdpp.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/fgdpp/fgdpp.cpp")
+ if(FGDPP_ENABLE_SPEN_FGD_SUPPORT)
+ target_compile_definitions(fgdpp PUBLIC FGDPP_UNIFIED_FGD)
+ endif()
+endif()
+
+
# studiomodelpp
if(SOURCEPP_USE_STUDIOMODELPP)
add_pretty_parser(studiomodelpp SOURCES
diff --git a/README.md b/README.md
index 549e3ef48..adc9e858c 100644
--- a/README.md
+++ b/README.md
@@ -4,27 +4,11 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
Looking for more parsers in this vein? Try my other project which relies on this one,
[VPKEdit](https://github.com/craftablescience/VPKEdit)! It's a library too, not just a GUI.
-## Supported Formats
-This repository contains the following parsers:
-
-### dmxpp
-A parser for KV2/DMX files.
-
-Currently supports:
-- Binary
- - v1
- - v2
- - v3
- - v4
- - v5
-
-### studiomodelpp
-A parser for the various Source engine model formats.
-
-Currently supports:
-- MDL v44-v49
-- VTX v7
-- VVD v4
-
-### vmfpp
-A parser for uncompiled Source 1 map files. Supports any VMF.
+### Supported Formats
+
+| Library Name | Supports | Read | Write | Author(s) |
+|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----:|:-----:|----------------------------|
+| dmxpp | [DMX](https://developer.valvesoftware.com/wiki/DMX) Binary v1-5 | ✅ | ❌ | @craftablescience |
+| fgdpp | • [Valve FGD syntax](https://developer.valvesoftware.com/wiki/FGD)
• [TeamSpen's Unified FGD syntax](https://github.com/TeamSpen210/HammerAddons/wiki/Unified-FGD) | ✅ | ❌ | @Trico-Everfire |
+| studiomodelpp | • [MDL](https://developer.valvesoftware.com/wiki/MDL_(Source)) v44-49
• [VTX](https://developer.valvesoftware.com/wiki/VTX) v7
• [VVD](https://developer.valvesoftware.com/wiki/VVD) v4 | ✅ | ❌ | @craftablescience |
+| vmfpp | Any [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)) | ✅ | ❌ | @Galaco, @craftablescience |
diff --git a/include/fgdpp/fgdpp.h b/include/fgdpp/fgdpp.h
new file mode 100644
index 000000000..4b99b84aa
--- /dev/null
+++ b/include/fgdpp/fgdpp.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include "structs/entityproperties.h"
+
+namespace fgdpp {
+
+class FGD {
+ std::string rawFGDFile;
+
+ //TOKENIZER
+public:
+ enum TokenType {
+ COMMENT = 0, // //
+ DEFINITION, // @something
+ EQUALS, // =
+ OPEN_BRACE, // {
+ CLOSE_BRACE, // }
+ OPEN_BRACKET, // [
+ CLOSE_BRACKET, // ]
+ OPEN_PARENTHESIS, // (
+ CLOSE_PARENTHESIS, // )
+ COMMA, // ,
+ STRING, // "something"
+ PLUS, // +
+ LITERAL, // anything that isn't any of the other tokens.
+ COLUMN, // :
+ NUMBER, // numbers -200000 ... 0 ... 2000000
+ };
+
+ struct Token {
+ TokenType type;
+ Range range;
+ std::string_view string;
+ int line;
+ ParseError associatedError;
+ };
+
+ std::vector tokenList;
+
+public:
+ FGD(std::string_view path, bool parseIncludes);
+
+private:
+ bool TokenizeFile();
+
+ //PARSER.
+public:
+ FGDFile FGDFileContents;
+ ParsingError parseError{ParseError::NO_ERROR, 0, {0, 0}};
+
+ bool ParseFile();
+
+#ifdef FGDPP_UNIFIED_FGD
+ bool TagListDelimiter(std::vector::const_iterator& iter, TagList& tagList);
+#endif
+};
+
+} // namespace fgdpp
diff --git a/include/fgdpp/structs/entityproperties.h b/include/fgdpp/structs/entityproperties.h
new file mode 100644
index 000000000..0888ca67b
--- /dev/null
+++ b/include/fgdpp/structs/entityproperties.h
@@ -0,0 +1,159 @@
+#pragma once
+
+#include
+#include
+
+namespace fgdpp {
+
+enum class ParseError {
+ NO_ERROR = 0,
+ TOKENIZATION_ERROR,
+ INVALID_DEFINITION,
+ INVALID_EQUALS,
+ INVALID_OPEN_BRACE,
+ INVALID_CLOSE_BRACE,
+ INVALID_OPEN_BRACKET,
+ INVALID_CLOSE_BRACKET,
+ INVALID_OPEN_PARENTHESIS,
+ INVALID_CLOSE_PARENTHESIS,
+ INVALID_COMMA,
+ INVALID_STRING,
+ INVALID_PLUS,
+ INVALID_LITERAL,
+ INVALID_COLUMN,
+ INVALID_NUMBER,
+ FAILED_TO_OPEN,
+ PREMATURE_EOF,
+};
+
+struct Range {
+ int start;
+ int end;
+};
+
+struct ParsingError {
+ ParseError err;
+ int line;
+ Range span;
+};
+
+#ifdef FGDPP_UNIFIED_FGD
+struct TagList {
+ std::vector tags;
+};
+#endif
+
+struct ClassProperty {
+ std::vector properties;
+};
+
+struct ClassProperties {
+ std::string_view name;
+ std::vector classProperties;
+};
+
+enum class EntityIOPropertyType {
+ t_string = 0,
+ t_integer,
+ t_float,
+ t_bool,
+ t_void,
+ t_script,
+ t_vector,
+ t_target_destination,
+ t_color255,
+ t_custom,
+};
+
+struct Choice {
+ std::string_view value;
+ std::string_view displayName;
+#ifdef FGDPP_UNIFIED_FGD
+ TagList tagList;
+#endif
+};
+
+struct Flag {
+ int value;
+ bool checked;
+ std::string_view displayName;
+#ifdef FGDPP_UNIFIED_FGD
+ TagList tagList;
+#endif
+};
+
+struct EntityProperties {
+ std::string_view propertyName;
+ std::string_view type;
+ std::string_view displayName; // The following 3 are optional and may be empty as a result.
+ std::string_view defaultValue;
+ std::vector propertyDescription;
+ bool readOnly;
+ bool reportable;
+
+#ifdef FGDPP_UNIFIED_FGD
+ TagList tagList;
+#endif
+
+ int choiceCount; // This is a special case if the EntityPropertyType is t_choices
+ std::vector choices;
+
+ int flagCount; // This is a special case if the EntityPropertyType is t_flags
+ std::vector flags;
+};
+
+enum class IO {
+ INPUT = 0,
+ OUTPUT,
+};
+
+struct InputOutput {
+ std::string_view name;
+ std::vector description;
+ IO putType;
+ std::string_view stringType;
+ EntityIOPropertyType type;
+#ifdef FGDPP_UNIFIED_FGD
+ TagList tagList;
+#endif
+};
+
+#ifdef FGDPP_UNIFIED_FGD
+struct EntityResource {
+ std::string_view key;
+ std::string_view value;
+ TagList tagList;
+};
+#endif
+
+struct Entity {
+ std::string_view type;
+ std::vector classProperties;
+ std::string_view entityName;
+ std::vector entityDescription;
+ std::vector entityProperties;
+ std::vector inputOutput;
+#ifdef FGDPP_UNIFIED_FGD
+ std::vector resources;
+#endif
+};
+
+struct AutoVisGroupChild {
+ std::string_view name;
+ std::vector children;
+};
+
+struct AutoVisGroup {
+ std::string_view name;
+ struct std::vector children;
+};
+
+struct FGDFile {
+ Range mapSize{0,0};
+ std::vector entities;
+ std::vector materialExclusions;
+ std::vector includes;
+ std::vector autoVisGroups;
+};
+
+} // namespace fgdpp
diff --git a/src/fgdpp/fgdpp.cpp b/src/fgdpp/fgdpp.cpp
new file mode 100644
index 000000000..20a11d3cc
--- /dev/null
+++ b/src/fgdpp/fgdpp.cpp
@@ -0,0 +1,954 @@
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace fgdpp;
+
+constexpr int FGDPP_MAX_STR_CHUNK_LENGTH = 1024;
+
+constexpr char singleTokens[] = "{}[](),:=+";
+constexpr FGD::TokenType valueTokens[] = {FGD::OPEN_BRACE, FGD::CLOSE_BRACE, FGD::OPEN_BRACKET, FGD::CLOSE_BRACKET, FGD::OPEN_PARENTHESIS, FGD::CLOSE_PARENTHESIS, FGD::COMMA, FGD::COLUMN, FGD::EQUALS, FGD::PLUS};
+constexpr enum ParseError tokenErrors[] = {ParseError::INVALID_OPEN_BRACE, ParseError::INVALID_CLOSE_BRACE, ParseError::INVALID_OPEN_BRACKET, ParseError::INVALID_CLOSE_BRACKET, ParseError::INVALID_OPEN_PARENTHESIS, ParseError::INVALID_CLOSE_PARENTHESIS, ParseError::INVALID_COMMA, ParseError::INVALID_COLUMN, ParseError::INVALID_EQUALS, ParseError::INVALID_PLUS};
+
+std::string_view typeStrings[9] = {"string", "integer", "float", "bool", "void", "script", "vector", "target_destination", "color255"};
+EntityIOPropertyType typeList[9] = {EntityIOPropertyType::t_string, EntityIOPropertyType::t_integer, EntityIOPropertyType::t_float, EntityIOPropertyType::t_bool, EntityIOPropertyType::t_void, EntityIOPropertyType::t_script, EntityIOPropertyType::t_vector, EntityIOPropertyType::t_target_destination, EntityIOPropertyType::t_color255};
+
+namespace {
+
+bool ichar_equals(char a, char b) {
+ return std::tolower(static_cast(a)) == std::tolower(static_cast(b));
+}
+
+bool iequals(std::string_view lhs, std::string_view rhs) {
+ return std::ranges::equal(lhs, rhs, ichar_equals);
+}
+
+} // namespace
+
+bool FGD::TokenizeFile() {
+ if (this->rawFGDFile.empty())
+ return false;
+
+ int pos = 1, ln = 1, i = 0;
+
+ for (auto iterator = this->rawFGDFile.cbegin(); iterator != this->rawFGDFile.cend(); iterator++, i++, pos++) {
+ char c = *iterator;
+
+ if (c == '\t')
+ continue;
+
+ if (c == '\r')
+ continue;
+
+ if (c == '\n') {
+ ln++;
+ pos = 1;
+ continue;
+ }
+
+ if (c == '\"') {
+ int currentLine = ln;
+ int currentLength = i;
+ int currentPos = pos;
+ auto currentIteration = iterator;
+
+ c = '\t'; // We can get away with this to trick the while loop :)
+ while (c != '\"') {
+ iterator++;
+ pos++;
+ c = *iterator;
+ i++;
+ if (c == '\n')
+ ln++;
+ }
+
+ iterator++;
+ i++;
+ pos++;
+ Token token{};
+ token.line = currentLine;
+ token.type = STRING;
+ token.associatedError = ParseError::INVALID_STRING;
+
+ token.string = {currentIteration, iterator};
+
+ int subtractFromRange = static_cast(i - currentLength - token.string.length());
+ token.range = {currentPos, pos - (currentPos - subtractFromRange)};
+
+ this->tokenList.push_back(token);
+ iterator--;
+ i--;
+ pos--;
+ continue;
+ }
+
+ if (c == '/' && *std::next(iterator) == '/') {
+ int currentLength = i;
+ int currentPos = pos;
+ auto currentIteration = iterator;
+
+ while (c != '\n') {
+ c = *iterator;
+ pos++;
+ i++;
+ iterator++;
+ }
+ iterator--;
+ i--;
+ pos--;
+
+ Token token{};
+ token.line = ln;
+ token.type = COMMENT;
+
+ token.string = {currentIteration, iterator};
+
+ int subtractFromRange = static_cast(i - currentLength - token.string.length());
+ token.range = {currentPos, pos - (currentPos - subtractFromRange)};
+
+ this->tokenList.push_back(token);
+
+ iterator--;
+ i--;
+ pos--;
+ continue;
+ }
+
+ if (c == '@') {
+ int currentLength = i;
+ auto currentIteration = iterator;
+ int currentPos = pos;
+
+ while (c != '\n' && c != '\t' && c != '\r' && c != ' ' && c != '(') {
+ c = *iterator;
+ pos++;
+ i++;
+ iterator++;
+ }
+ iterator--;
+ i--;
+ pos--;
+
+ if (c == '\n')
+ ln++;
+ Token token;
+ token.line = ln;
+ token.type = DEFINITION;
+ token.associatedError = ParseError::INVALID_DEFINITION;
+
+ int newStrLength = 0;
+ token.string = {currentIteration, iterator};
+
+ int subtractFromRange = (i - currentLength - newStrLength);
+ token.range = {currentPos, pos - (currentPos - subtractFromRange)};
+
+ this->tokenList.push_back(token);
+
+ iterator--;
+ i--;
+ pos--;
+ continue;
+ }
+
+ if (std::isdigit(c) || (c == '-' && std::isdigit(*std::next(iterator)))) {
+ auto currentIteration = iterator;
+ int currentPos = pos;
+
+ if (c == '-') {
+ iterator++;
+ pos++;
+ i++;
+ c = *iterator;
+ }
+
+#ifdef FGDPP_UNIFIED_FGD
+ while (std::isdigit(c) || c == '.')
+#else
+ while (std::isdigit(c))
+#endif
+ {
+ c = *iterator;
+ i++;
+ pos++;
+ iterator++;
+ }
+
+ iterator--;
+ i--;
+ pos--;
+
+ Token token;
+ token.line = ln;
+ token.type = NUMBER;
+ token.associatedError = ParseError::INVALID_NUMBER;
+
+ token.string = {currentIteration, iterator};
+
+ token.range = {currentPos, pos};
+
+ this->tokenList.push_back(token);
+
+ iterator--;
+ i--;
+ pos--;
+ continue;
+ }
+
+ if (const char* valueKey = std::strchr(singleTokens, c)) {
+ int spaces = (int)((int)((char*) valueKey - (char*) singleTokens) / sizeof(char)); // char should be 1, but I am sanity checking it anyway.
+ TokenType tType = valueTokens[spaces];
+ enum ParseError tParseError = tokenErrors[spaces];
+ Token token;
+ token.line = ln;
+ token.type = tType;
+ token.associatedError = tParseError;
+
+ token.string = {iterator, std::next(iterator)};
+
+ token.range = {pos, pos + 1};
+
+ this->tokenList.push_back(token);
+
+ continue;
+ }
+
+ if (c != ' ') {
+ int currentLength = i;
+ auto currentIteration = iterator;
+ int currentPos = pos;
+
+ while (c != '\n' && c != ' ' && c != '\t' && c != '\r' && !std::strchr(singleTokens, c)) {
+ iterator++;
+ pos++;
+ c = *iterator;
+ i++;
+ }
+
+ Token token;
+ token.line = ln;
+ token.type = LITERAL;
+ token.associatedError = ParseError::INVALID_LITERAL;
+
+ token.string = {currentIteration, iterator};
+
+ int subtractFromRange = static_cast(i - currentLength - token.string.length());
+ token.range = {currentPos, pos - (currentPos - subtractFromRange)};
+
+ this->tokenList.push_back(token);
+
+ iterator--;
+ i--;
+ pos--;
+ continue;
+ }
+ }
+
+ return true;
+}
+
+FGD::FGD(std::string_view path, bool parseIncludes) {
+ std::ifstream file{path.data()};
+ if (!file.is_open()) {
+ this->parseError = {ParseError::FAILED_TO_OPEN, 0, {0, 0}};
+ return;
+ }
+
+ auto fileSize = static_cast(std::filesystem::file_size(path));
+ this->rawFGDFile = std::string(fileSize, ' ');
+ file.read(this->rawFGDFile.data(), fileSize);
+ file.close();
+
+ if (parseIncludes) {
+ std::vector exclusionList;
+ exclusionList.emplace_back(path.data());
+
+ std::string_view dirPath = path.substr(0, path.find_last_of('/'));
+ std::smatch match;
+ std::regex exr{"@include+ \"(.*)\""};
+
+ while (std::regex_search(this->rawFGDFile, match, exr)) {
+ std::regex thisInclude("@include+ \"" + match[1].str() + "\"");
+
+ std::string currentPath = dirPath.data() + match[1].str();
+
+ if (std::find_if(exclusionList.begin(), exclusionList.end(), [currentPath](const std::string& v) {
+ return v == currentPath;
+ }) != exclusionList.end()) {
+ this->rawFGDFile = std::regex_replace(this->rawFGDFile, thisInclude, "");
+ continue;
+ }
+
+ exclusionList.push_back(currentPath);
+
+ auto includeFilePath = std::string{dirPath} + '/' + match[1].str();
+ file.open(includeFilePath);
+ if (!file.is_open()) continue;
+
+ auto includeSize = static_cast(std::filesystem::file_size(includeFilePath));
+ std::string includeFileContents(includeSize, ' ');
+ file.read(includeFileContents.data(), includeSize);
+ file.close();
+
+ this->rawFGDFile = std::regex_replace(this->rawFGDFile, thisInclude, includeFileContents, std::regex_constants::format_first_only);
+ }
+ }
+
+ std::erase(this->rawFGDFile, '\r');
+
+ if (!this->TokenizeFile()) {
+ this->parseError = {ParseError::FAILED_TO_OPEN, 0, {0, 0}};
+ return;
+ }
+
+ if (!this->ParseFile()) {
+ this->parseError = {ParseError::FAILED_TO_OPEN, 0, {0, 0}};
+ return;
+ }
+}
+
+#define ErrorHandle(iter) this->parseError = (iter == this->tokenList.cend()) ? (ParsingError{ParseError::PREMATURE_EOF, lastLine, {0, 0}}) : (ParsingError{iter->associatedError, iter->line, {iter->range.start, iter->range.end}}); return false
+
+#define Forward(iterator, failureResult) \
+ do { \
+ iterator++; \
+ if (iterator == this->tokenList.end()) \
+ failureResult; \
+ while (iterator->type == COMMENT) { \
+ iterator++; \
+ if (iterator == this->tokenList.cend()) \
+ failureResult; \
+ } \
+ } while (0)
+
+
+#ifdef FGDPP_UNIFIED_FGD
+bool FGD::TagListDelimiter(std::vector::const_iterator& iter, TagList& tagList) {
+ std::vector fields;
+
+ int i = 0;
+ bool hasPlus = false;
+ while (iter->type == LITERAL || iter->type == PLUS || iter->type == COMMA || iter->type == STRING || iter->type == NUMBER) {
+ if (iter->type == PLUS) {
+ hasPlus = true;
+ Forward(iter, return false);
+ continue;
+ }
+
+ if (iter->type == COMMA) {
+ for (int j = 0; j < i; j++) {
+ tagList.tags.resize(j+1);
+ tagList.tags[j] = fields[j];
+ }
+
+ i = 0;
+ Forward(iter, return false);
+ continue;
+ }
+
+ if (!hasPlus) {
+ fields.resize(i+1);
+ fields[i] = iter->string;
+ i++;
+ } else {
+ //std::string t{"+"};
+ //t.append(( iter )->string);
+ //TODO: We ned to identify +, trust me, we do.
+ fields.resize(i+1);
+ fields[i] = iter->string;
+ i++;
+ }
+
+ Forward(iter, return false);
+ }
+
+ if (i > 0) {
+ for (int j = 0; j < i; j++) {
+ tagList.tags.resize(j+1);
+ tagList.tags[j] = fields[j];
+ }
+ }
+
+ if (iter->type != CLOSE_BRACKET)
+ return false;
+
+ return true;
+}
+#endif
+
+bool FGD::ParseFile() {
+ if (this->tokenList.empty())
+ return false;
+
+ int lastLine = this->tokenList[this->tokenList.size() - 1].line;
+ for (auto iter = this->tokenList.cbegin(); iter != this->tokenList.cend(); iter++) {
+ if (iter->type != DEFINITION)
+ continue;
+
+ if (iequals(iter->string, "@mapsize")) {
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != OPEN_PARENTHESIS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != NUMBER) {
+ ErrorHandle(iter);
+ }
+
+ this->FGDFileContents.mapSize.start = std::stoi(iter->string.data());
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != COMMA) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != NUMBER) {
+ ErrorHandle(iter);
+ }
+
+ this->FGDFileContents.mapSize.end = std::stoi(iter->string.data());
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != CLOSE_PARENTHESIS) {
+ ErrorHandle(iter);
+ }
+
+ continue;
+ }
+
+ if (iequals(iter->string, "@AutoVisgroup")) {
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != EQUALS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ AutoVisGroup visGroup;
+ visGroup.name = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != OPEN_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != STRING && iter->type != CLOSE_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ while (iter->type == STRING) {
+ AutoVisGroupChild visGroupChild;
+ visGroupChild.name = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != OPEN_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != STRING && iter->type != CLOSE_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ while (iter->type == STRING) {
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ visGroupChild.children.push_back(iter->string);
+
+ visGroup.children.push_back(visGroupChild);
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ if (iter->type != CLOSE_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ if (iter->type != CLOSE_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ this->FGDFileContents.autoVisGroups.push_back(visGroup);
+ continue;
+ }
+
+ if (iequals(iter->string, "@include")) {
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ this->FGDFileContents.includes.push_back(iter->string);
+ continue;
+ }
+
+ if (iequals(iter->string, "@MaterialExclusion")) {
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != OPEN_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ while (iter->type == STRING) {
+ this->FGDFileContents.materialExclusions.push_back( iter->string );
+
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ if (iter->type != CLOSE_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ continue;
+ }
+
+ if (iter->string.ends_with("Class")) {
+ Entity entity;
+ entity.type = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ while (iter->type == LITERAL) {
+ ClassProperties classProperties;
+ classProperties.name = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type == OPEN_PARENTHESIS) {
+ // if there are more than 64 non comma separated parameters, you're doing something wrong.
+ // The value is already so high in case anyone adds new fgd class parameters in the future that require them.
+ static constexpr int MAX_FIELDS = 64;
+
+ std::string_view fields[MAX_FIELDS];
+ int i = 0;
+
+ Forward(iter, { ErrorHandle(iter); });
+ while (iter->type == LITERAL || iter->type == COMMA || iter->type == STRING || iter->type == NUMBER) {
+ if (i > MAX_FIELDS) {
+ // wtf happened?
+ ErrorHandle(iter);
+ }
+
+ if (iter->type == COMMA) {
+ ClassProperty property;
+ for (int j = 0; j < i; j++) {
+ property.properties.push_back(fields[j]);
+ }
+
+ i = 0;
+ Forward(iter, { ErrorHandle(iter); });
+ classProperties.classProperties.push_back(property);
+ continue;
+ }
+
+ fields[i] = iter->string;
+ i++;
+
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ if (i > 0) {
+ ClassProperty property;
+ for (int j = 0; j < i; j++) {
+ property.properties.push_back(fields[j]);
+ }
+
+ i = 0;
+ Forward(iter, { ErrorHandle(iter); });
+ classProperties.classProperties.push_back(property);
+ entity.classProperties.push_back(classProperties);
+ continue;
+ }
+
+ if (iter->type != CLOSE_PARENTHESIS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ entity.classProperties.push_back(classProperties);
+ }
+
+ if (iter->type != EQUALS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != LITERAL) {
+ ErrorHandle(iter);
+ }
+
+ entity.entityName = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type == COLUMN) {
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ while (iter->type == STRING) {
+#ifndef FGDPP_UNIFIED_FGD
+ if (iter->string.length() > FGDPP_MAX_STR_CHUNK_LENGTH) {
+ ErrorHandle(iter);
+ }
+#endif
+ entity.entityDescription.push_back(iter->string);
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type == PLUS) {
+ Forward(iter, { ErrorHandle(iter); });
+ }
+ }
+ }
+
+ if (iter->type != OPEN_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ while (iter->type != CLOSE_BRACKET) {
+#ifdef FGDPP_UNIFIED_FGD
+ if (iequals(iter->string, "@resources")) {
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != OPEN_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ while (iter->type != CLOSE_BRACKET) {
+ if (iter->type != LITERAL) {
+ ErrorHandle(iter);
+ }
+
+ EntityResource resource;
+ resource.key = ( iter->string );
+
+ Forward(iter, { ErrorHandle(iter); });
+ resource.value = ( iter->string );
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type == OPEN_BRACKET) {
+ Forward(iter, { ErrorHandle(iter); });
+ if (!TagListDelimiter(iter, resource.tagList)) {
+ ErrorHandle(iter);
+ }
+ Forward(iter, { ErrorHandle(iter); });
+ }
+ }
+
+ if (iter->type != CLOSE_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ continue;
+ }
+#endif
+
+ if (iter->type != LITERAL) {
+ ErrorHandle(iter);
+ }
+
+ if (iequals(iter->string, "input") || iequals(iter->string, "output")) {
+ InputOutput inputOutput;
+ inputOutput.putType = iequals(iter->string, "input") == 0 ? IO::INPUT : IO::OUTPUT;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ inputOutput.name = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+#ifdef FGDPP_UNIFIED_FGD
+ if (iter->type == OPEN_BRACKET) {
+ Forward(iter, { ErrorHandle(iter); });
+ if (!TagListDelimiter(iter, inputOutput.tagList)) {
+ ErrorHandle(iter);
+ }
+ Forward(iter, { ErrorHandle(iter); });
+ }
+#endif
+
+ if (iter->type != OPEN_PARENTHESIS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != LITERAL) {
+ ErrorHandle(iter);
+ }
+
+ int index = 0;
+ while (index < 9) {
+ if (iequals(typeStrings[index], iter->string))
+ break;
+ index++;
+ }
+ if (index == 9)
+ inputOutput.type = EntityIOPropertyType::t_custom;
+ else
+ inputOutput.type = typeList[index];
+
+ inputOutput.stringType = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != CLOSE_PARENTHESIS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type == COLUMN) {
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ while (iter->type == STRING) {
+#ifndef FGDPP_UNIFIED_FGD
+ if (iter->string.length() > FGDPP_MAX_STR_CHUNK_LENGTH) {
+ ErrorHandle(iter);
+ }
+#endif
+ inputOutput.description.push_back(iter->string);
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type == PLUS) {
+ Forward(iter, { ErrorHandle(iter); });
+ }
+ }
+ }
+ entity.inputOutput.push_back(inputOutput);
+ continue;
+ } else {
+ EntityProperties entityProperties;
+ entityProperties.flagCount = 0;
+ entityProperties.choiceCount = 0;
+ entityProperties.readOnly = false;
+ entityProperties.reportable = false;
+ entityProperties.propertyName = iter->string;
+ Forward(iter, { ErrorHandle(iter); });
+
+#ifdef FGDPP_UNIFIED_FGD
+ if (iter->type == OPEN_BRACKET) {
+ Forward(iter, { ErrorHandle(iter); });
+ if (!TagListDelimiter(iter, entityProperties.tagList)) {
+ ErrorHandle(iter);
+ }
+ Forward(iter, { ErrorHandle(iter); });
+ }
+#endif
+
+ if (iter->type != OPEN_PARENTHESIS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != LITERAL) {
+ ErrorHandle(iter);
+ }
+
+ entityProperties.type = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != CLOSE_PARENTHESIS) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iequals(iter->string, "readonly")) {
+ entityProperties.readOnly = true;
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ if (iequals(iter->string, "*") || iequals(iter->string, "report")) {
+ entityProperties.reportable = true;
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ if (iter->type == EQUALS) {
+ goto isFOC;
+ }
+
+ if (iter->type != COLUMN)
+ continue;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ entityProperties.displayName = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type == EQUALS) {
+ goto isFOC;
+ }
+
+ if (iter->type != COLUMN)
+ continue;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != COLUMN) {
+ entityProperties.defaultValue = iter->string;
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ if (iter->type == EQUALS) {
+ goto isFOC;
+ }
+
+ if (iter->type != COLUMN)
+ continue;
+
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ while (iter->type == STRING) {
+#ifndef FGDPP_UNIFIED_FGD
+ if (iter->string.length() > FGDPP_MAX_STR_CHUNK_LENGTH) {
+ ErrorHandle(iter);
+ }
+#endif
+ entityProperties.propertyDescription.push_back(iter->string);
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type == PLUS) {
+ Forward(iter, { ErrorHandle(iter); });
+ }
+ }
+
+ /*
+ if (!ProcessFGDStrings(iter, &entityProperties->propertyDescription)) {
+ ErrorHandle(iter);
+ }
+ */
+
+ if (iter->type == EQUALS) {
+ goto isFOC;
+ }
+
+ continue;
+
+ isFOC:
+ {
+ bool isFlags = iequals(entityProperties.type, "flags");
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != OPEN_BRACKET) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ while (iter->type != CLOSE_BRACKET) {
+ if (isFlags && iter->type != NUMBER) {
+ ErrorHandle(iter);
+ }
+
+ if (isFlags) {
+ Flag flags;
+ flags.value = std::stoi(iter->string.data());
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != COLUMN) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if (iter->type != STRING) {
+ ErrorHandle(iter);
+ }
+
+ flags.displayName = iter->string;
+
+ if (std::next(iter)->type == COLUMN) {
+ Forward(iter, { ErrorHandle(iter); });
+
+ Forward(iter, { ErrorHandle(iter); });
+ if ( iter->type != NUMBER ) {
+ ErrorHandle(iter);
+ }
+ flags.checked = iter->string == "1";
+
+#ifdef FGDPP_UNIFIED_FGD
+ if (std::next(iter)->type == OPEN_BRACKET) {
+ Forward(iter, { ErrorHandle(iter); });
+ Forward(iter, { ErrorHandle(iter); });
+ if (!TagListDelimiter(iter, flags.tagList)) {
+ ErrorHandle(iter);
+ }
+ }
+#endif
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ } else {
+ Choice choice;
+ choice.value = iter->string;
+
+ Forward(iter, { ErrorHandle(iter); });
+ if ( iter->type != COLUMN ) {
+ ErrorHandle(iter);
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ if ( iter->type != STRING ) {
+ ErrorHandle(iter);
+ }
+
+ choice.displayName = iter->string;
+
+#ifdef FGDPP_UNIFIED_FGD
+ if (std::next(iter)->type == OPEN_BRACKET) {
+ Forward(iter, { ErrorHandle(iter); });
+ Forward(iter, { ErrorHandle(iter); });
+
+ if (!TagListDelimiter(iter, choice.tagList)) {
+ ErrorHandle(iter);
+ }
+ }
+#endif
+
+ Forward(iter, { ErrorHandle(iter); });
+ }
+ }
+ }
+ }
+
+ Forward(iter, { ErrorHandle(iter); });
+ }
+
+ this->FGDFileContents.entities.push_back(entity);
+ }
+ }
+
+ return true;
+}
diff --git a/src/thirdparty/bufferstream b/src/thirdparty/bufferstream
index 825aa17b3..34e044f1c 160000
--- a/src/thirdparty/bufferstream
+++ b/src/thirdparty/bufferstream
@@ -1 +1 @@
-Subproject commit 825aa17b3f53bb2bad56d1cf25e022e5d47bb298
+Subproject commit 34e044f1cfdf0dcf7df6f69f48c8266ebe92e9b6
diff --git a/test/fgdpp.cpp b/test/fgdpp.cpp
new file mode 100644
index 000000000..fc9cea7c3
--- /dev/null
+++ b/test/fgdpp.cpp
@@ -0,0 +1,19 @@
+#include
+
+#include
+
+#include "Helpers.h"
+
+using namespace fgdpp;
+
+TEST(fgdpp, parseBasePortal2) {
+ FGD fgd{ASSET_ROOT "fgdpp/portal2.fgd", true};
+ ASSERT_EQ(fgd.parseError.err, ParseError::NO_ERROR);
+}
+
+#ifdef FGDPP_UNIFIED_FGD
+TEST(fgdpp, parseUnifiedFGD) {
+ FGD fgd{ASSET_ROOT "fgdpp/unified/game_ui.fgd", true};
+ ASSERT_EQ(fgd.parseError.err, ParseError::NO_ERROR);
+}
+#endif
diff --git a/test/res/fgdpp/base.fgd b/test/res/fgdpp/base.fgd
new file mode 100755
index 000000000..7cbbd047a
--- /dev/null
+++ b/test/res/fgdpp/base.fgd
@@ -0,0 +1,6996 @@
+//====== Copyright 1996-2005, Valve Corporation, All rights reserved. =======
+//
+// Purpose: General game definition file (.fgd)
+//
+//=============================================================================
+
+@mapsize(-16384, 16384)
+
+
+//-------------------------------------------------------------------------
+//
+// Base Classes
+//
+//-------------------------------------------------------------------------
+
+@BaseClass = PaintableBrush
+[
+ // Inputs
+ input RemovePaint(void) : "Remove paint from the brush entity."
+]
+
+@BaseClass = Angles
+[
+ angles(angle) : "Pitch Yaw Roll (Y Z X)" : "0 0 0" : "This entity's orientation in the world. Pitch is rotation around the Y axis, " +
+ "yaw is the rotation around the Z axis, roll is the rotation around the X axis."
+]
+
+@BaseClass = Origin
+[
+ origin(origin) : "Origin (X Y Z)" : : "The position of this entity's center in the world. Rotating entities typically rotate around their origin."
+]
+
+@BaseClass = Reflection
+[
+ drawinfastreflection(boolean) : "Render in Fast Reflections" : 0 : "If enabled, causes this entity/prop to to render in fast water reflections (i.e. when a water material specifies $reflectonlymarkedentities) and in the world impostor pass."
+
+ input DisableDrawInFastReflection(void) : "Turns off rendering of this entity in reflections when using $reflectonlymarkedentities in water material."
+ input EnableDrawInFastReflection(void) : "Turn on rendering of this entity in reflections when using $reflectonlymarkedentities in water material."
+]
+
+@BaseClass = ToggleDraw
+[
+ input DisableDraw(void) : "Add the EF_NODRAW flag to this entity. Some entities manage this on their own so be aware you can override that value."
+ input EnableDraw(void) : "Remove the EF_NODRAW flag to this entity. Some entities manage this on their own so be aware you can override that value."
+]
+
+@BaseClass = Shadow
+[
+ disableshadows(boolean) : "Disable shadows" : 0
+ disableshadowdepth(boolean) : "Disable ShadowDepth" : 0 : "Used to disable rendering into shadow depth (for flashlight) for this entity."
+ shadowdepthnocache(choices) : "Projected Texture Cache" : 0 : "Used to hint projected texture system whether it is sufficient to cache shadow volume of this entity or to force render it every frame instead." =
+ [
+ 0 : "Default"
+ 1 : "No cache = render every frame"
+ 2 : "Cache it = render only once"
+ ]
+
+ disableflashlight(boolean) : "Disable flashlight" : 0 : "Used to disable flashlight (env_projectedtexture) lighting and shadows on this entity."
+
+ input DisableShadow(void) : "Turn shadow off."
+ input EnableShadow(void) : "Turn shadow on."
+
+ input DisableReceivingFlashlight(void) : "This object will not recieve light or shadows from projected textures (flashlights)."
+ input EnableReceivingFlashlight(void) : "This object may recieve light or shadows from projected textures (flashlights)."
+]
+
+@BaseClass base(Reflection, ToggleDraw, Shadow) = Studiomodel
+[
+ model(studio) report : "World Model"
+ skin(integer) : "Skin" : 0 : "Some models have multiple versions of their textures, called skins. Set this to a number other than 0 to use that skin instead of the default."
+
+ // Inputs
+ input Skin(integer) : "Changes the model skin to the specified number."
+ input DisableShadow(void) : "Turn shadow off."
+ input EnableShadow(void) : "Turn shadow on."
+ input AlternativeSorting(bool) : "Used to attempt to fix sorting problems when rendering. True activates, false deactivates"
+
+ // Outputs
+ output OnIgnite(void) : "Fired when this object catches fire."
+]
+
+@BaseClass = BasePlat
+[
+ input Toggle(void) : "Toggles the platform's state."
+ input GoUp(void) : "Tells the platform to go up."
+ input GoDown(void) : "Tells the platform to go down."
+]
+
+@BaseClass = Targetname
+[
+ targetname(target_source) : "Name" : : "The name that other entities refer to this entity by."
+
+ vscripts(scriptlist) : "Entity Scripts" : "" : "Name(s) of script files that are executed after all entities have spawned."
+ thinkfunction(string) : "Script think function" : "" : "Name of a function in this entity's script scope which will be called automatically."
+
+ input RunScriptFile(script) : "Execute a game script file from disk"
+ input RunScriptCode(string) : "Execute a string of script source code"
+
+ // Inputs
+ input Kill(void) : "Removes this entity from the world."
+ input KillHierarchy(void) : "Removes this entity and all its children from the world."
+ input AddOutput(string) : "Adds an entity I/O connection to this entity. Format: