diff --git a/source/main/CMakeLists.txt b/source/main/CMakeLists.txt index 67bbd8f28c..48d200f1c5 100644 --- a/source/main/CMakeLists.txt +++ b/source/main/CMakeLists.txt @@ -206,6 +206,7 @@ set(SOURCE_FILES utils/ConfigFile.{h,cpp} utils/ErrorUtils.{h,cpp} utils/ForceFeedback.{h,cpp} + utils/GenericFileFormat.{h,cpp} utils/ImprovedConfigFile.h utils/InputEngine.{h,cpp} utils/InterThreadStoreVector.h diff --git a/source/main/resources/tobj_fileformat/TObjFileFormat.cpp b/source/main/resources/tobj_fileformat/TObjFileFormat.cpp index ebd326813c..876f6b9e16 100644 --- a/source/main/resources/tobj_fileformat/TObjFileFormat.cpp +++ b/source/main/resources/tobj_fileformat/TObjFileFormat.cpp @@ -19,6 +19,8 @@ #include "TObjFileFormat.h" +#include "Application.h" +#include "Console.h" #include "ProceduralRoad.h" #define LOGSTREAM Ogre::LogManager::getSingleton().stream() << "[RoR|TObj fileformat] " @@ -29,16 +31,6 @@ using namespace Ogre; // -------------------------------- // TObjEntry -TObjEntry::TObjEntry(Ogre::Vector3 pos, Ogre::Vector3 rot, const char* odef, TObj::SpecialObject spc, const char* ty, const char* nam): - position(pos), - rotation(rot), - special(spc) -{ - strcpy(type, ty); - strcpy(instance_name, nam); - strcpy(odef_name, odef); -} - bool TObjEntry::IsRoad() const { return (special >= TObj::SpecialObject::ROAD) && (special <= TObj::SpecialObject::ROAD_BRIDGE); @@ -52,71 +44,88 @@ bool TObjEntry::IsActor() const } // -------------------------------- -// Parser +// Document -void TObjParser::Prepare() +void TObjDocument::Load(Ogre::DataStreamPtr datastream, BitMask_t options) { - m_cur_line = nullptr; - m_line_number = 0; - m_in_procedural_road = false; - m_in_procedural_road = false; - m_road2_use_old_mode = false; - - m_def = std::shared_ptr(new TObjFile()); + // Tokenize + Document::Load(datastream, Document::OPTION_ALLOW_NAKED_STRINGS | Document::OPTION_ALLOW_SLASH_COMMENTS); + + TObjReader reader(*this); + reader.Read(); + } -bool TObjParser::ProcessLine(const char* line) +// -------------------------------- +// Parser + +void TObjReader::Read() { - bool result = true; - if ((line != nullptr) && (line[0] != 0) && (line[0] != '/') && (line[0] != ';')) + bool end_requested = false; + while (!this->EndOfFile()) { - m_cur_line = line; // No trimming by design. - result = this->ProcessCurrentLine(); + if (this->GetType() == TokenType::KEYWORD) + { + m_cur_line_keyword = this->GetArgKeyword(); + } + else + { + m_cur_line_keyword = ""; + } + end_requested = !this->ProcessCurrentLine(); + this->SeekNextLine(); + } + + // finish the last road + if (m_road2_use_old_mode != 0) + { + Vector3 pp_pos = m_road2_last_pos + m_road2_last_rot * Vector3(10.0f, 0.0f, 0.9f); + this->ImportProceduralPoint(pp_pos, m_road2_last_rot, TObj::SpecialObject::ROAD); + + m_tobj_doc.proc_objects.push_back(m_cur_procedural_obj); } - m_line_number++; - return result; } // retval true = continue processing (false = stop) -bool TObjParser::ProcessCurrentLine() +bool TObjReader::ProcessCurrentLine() { // ** Process keywords - if (!strcmp(m_cur_line, "end")) + if (!strcmp(m_cur_line_keyword, "end")) { return false; } - if (strncmp(m_cur_line, "collision-tris", 14) == 0) + if (strncmp(m_cur_line_keyword, "collision-tris", 14) == 0) { return true; // Obsolete - ignore it. } - if (strncmp(m_cur_line, "grid", 4) == 0) + if (strncmp(m_cur_line_keyword, "grid", 4) == 0) { this->ProcessGridLine(); return true; } - if (strncmp(m_cur_line, "trees", 5) == 0) + if (strncmp(m_cur_line_keyword, "trees", 5) == 0) { this->ProcessTreesLine(); return true; } - if (strncmp(m_cur_line, "grass", 5) == 0) + if (strncmp(m_cur_line_keyword, "grass", 5) == 0) // 'grass' or 'grass2' { this->ProcessGrassLine(); return true; } - if (strncmp("begin_procedural_roads", m_cur_line, 22) == 0) + if (strncmp("begin_procedural_roads", m_cur_line_keyword, 22) == 0) { m_cur_procedural_obj = ProceduralObject(); // Hard reset, discarding last "non-procedural" road strip. For backwards compatibility. ~ Petr Ohlidal, 08/2020 m_in_procedural_road = true; m_road2_use_old_mode = true; return true; } - else if (strncmp("end_procedural_roads", m_cur_line, 20) == 0) + else if (strncmp("end_procedural_roads", m_cur_line_keyword, 20) == 0) { if (m_road2_use_old_mode) { - m_def->proc_objects.push_back(m_cur_procedural_obj); + m_tobj_doc.proc_objects.push_back(m_cur_procedural_obj); m_cur_procedural_obj = ProceduralObject(); } m_in_procedural_road = false; @@ -147,52 +156,34 @@ bool TObjParser::ProcessCurrentLine() } else { - m_def->objects.push_back(object); + m_tobj_doc.objects.push_back(object); } } } return true; } -std::shared_ptr TObjParser::Finalize() -{ - // finish the last road - if (m_road2_use_old_mode != 0) - { - Vector3 pp_pos = m_road2_last_pos + m_road2_last_rot * Vector3(10.0f, 0.0f, 0.9f); - this->ImportProceduralPoint(pp_pos, m_road2_last_rot, TObj::SpecialObject::ROAD); - - m_def->proc_objects.push_back(m_cur_procedural_obj); - } - - std::shared_ptr tmp_def = m_def; - m_def.reset(); - return tmp_def; // Pass ownership -} - -void TObjParser::ProcessOgreStream(Ogre::DataStream* stream) -{ - char raw_line_buf[TObj::LINE_BUF_LEN]; - bool keep_reading = true; - while (keep_reading && !stream->eof()) - { - stream->readLine(raw_line_buf, TObj::LINE_BUF_LEN); - keep_reading = this->ProcessLine(raw_line_buf); - } -} // -------------------------------- // Processing -void TObjParser::ProcessProceduralLine() +void TObjReader::ProcessProceduralLine() { ProceduralPoint point; - Str<300> obj_name; Ogre::Vector3 rot = Ogre::Vector3::ZERO; - sscanf(m_cur_line, "%f, %f, %f, %f, %f, %f, %f, %f, %f, %s", - &point.position.x, &point.position.y, &point.position.z, - &rot.x, &rot.y, &rot.z, - &point.width, &point.bwidth, &point.bheight, obj_name.GetBuffer()); + std::string obj_name; + + size_t num_args = this->CountLineArgs(); + if (num_args > 0) { point.position.x = this->GetArgFloat(0); } + if (num_args > 1) { point.position.y = this->GetArgFloat(1); } + if (num_args > 2) { point.position.z = this->GetArgFloat(2); } + if (num_args > 3) { rot.x = this->GetArgFloat(3); } + if (num_args > 4) { rot.y = this->GetArgFloat(4); } + if (num_args > 5) { rot.z = this->GetArgFloat(5); } + if (num_args > 6) { point.width = this->GetArgFloat(6); } + if (num_args > 7) { point.bwidth = this->GetArgFloat(7); } + if (num_args > 8) { point.bheight = this->GetArgFloat(8); } + if (num_args > 9) { obj_name = this->GetArgString(9); } point.rotation = this->CalcRotation(rot); @@ -209,52 +200,69 @@ void TObjParser::ProcessProceduralLine() m_cur_procedural_obj.points.push_back(point); } -void TObjParser::ProcessGridLine() +void TObjReader::ProcessGridLine() { - Ogre::Vector3 & pos = m_def->grid_position; - sscanf(m_cur_line, "grid %f, %f, %f", &pos.x, &pos.y, &pos.z); // No error check by design - m_def->grid_enabled = true; + size_t num_args = this->CountLineArgs(); + // arg 0 is keyword 'grid' + if (num_args > 1) m_tobj_doc.grid_position.x = this->GetArgFloat(1); + if (num_args > 2) m_tobj_doc.grid_position.y = this->GetArgFloat(2); + if (num_args > 3) m_tobj_doc.grid_position.z = this->GetArgFloat(3); + m_tobj_doc.grid_enabled = true; } -void TObjParser::ProcessTreesLine() +void TObjReader::ProcessTreesLine() { TObjTree tree; - sscanf(m_cur_line, "trees %f, %f, %f, %f, %f, %f, %f, %s %s %s %f %s", - &tree.yaw_from, &tree.yaw_to, - &tree.scale_from, &tree.scale_to, - &tree.high_density, - &tree.min_distance, &tree.max_distance, - tree.tree_mesh, tree.color_map, tree.density_map, - &tree.grid_spacing, tree.collision_mesh); - - m_def->trees.push_back(tree); + size_t num_args = this->CountLineArgs(); + // arg 0 is keyword 'trees' + if (num_args > 1) tree.yaw_from = this->GetArgFloat(1); + if (num_args > 2) tree.yaw_to = this->GetArgFloat(2); + if (num_args > 3) tree.scale_from = this->GetArgFloat(3); + if (num_args > 4) tree.scale_to = this->GetArgFloat(4); + if (num_args > 5) tree.high_density = this->GetArgFloat(5); + if (num_args > 6) tree.min_distance = this->GetArgFloat(6); + if (num_args > 7) tree.max_distance = this->GetArgFloat(7); + if (num_args > 8) tree.tree_mesh = this->GetArgString(8); + if (num_args > 9) tree.color_map = this->GetArgString(9); + if (num_args > 10) tree.density_map = this->GetArgString(10); + if (num_args > 11) tree.grid_spacing = this->GetArgFloat(11); + if (num_args > 12) tree.collision_mesh = this->GetArgString(12); + + m_tobj_doc.trees.push_back(tree); } -void TObjParser::ProcessGrassLine() +void TObjReader::ProcessGrassLine() { TObjGrass grass; - if (strncmp(m_cur_line, "grass2", 6) == 0) + + size_t num_args = this->CountLineArgs(); + // arg 0 is keyword 'grass' or 'grass2' + if (num_args > 1) grass.range = (int)this->GetArgFloat(1); + if (num_args > 2) grass.sway_speed = this->GetArgFloat(2); + if (num_args > 3) grass.sway_length = this->GetArgFloat(3); + if (num_args > 4) grass.sway_distrib = this->GetArgFloat(4); + if (num_args > 5) grass.density = this->GetArgFloat(5); + if (num_args > 6) grass.min_x = this->GetArgFloat(6); + if (num_args > 7) grass.min_y = this->GetArgFloat(7); + if (num_args > 8) grass.max_x = this->GetArgFloat(8); + if (num_args > 9) grass.max_y = this->GetArgFloat(9); + if (num_args > 10) grass.grow_techniq = (int)this->GetArgFloat(10); + if (num_args > 11) grass.min_h = this->GetArgFloat(11); + if (num_args > 12) grass.max_h = this->GetArgFloat(12); + + std::string keyword = this->GetArgKeyword(); + if (keyword == "grass2") { - sscanf(m_cur_line, "grass2 %d, %f, %f, %f, %f, %f, %f, %f, %f, %d, %f, %f, %d, %s %s %s", - &grass.range, - &grass.sway_speed, &grass.sway_length, &grass.sway_distrib, &grass.density, - &grass.min_x, &grass.min_y, &grass.max_x, &grass.max_y, - &grass.grow_techniq, &grass.min_h, &grass.max_h, &grass.technique, - grass.material_name, - grass.color_map_filename, - grass.density_map_filename); + if (num_args > 13) grass.technique = (int)this->GetArgFloat(13); // extra parameter of grass2 + if (num_args > 14) grass.material_name = this->GetArgString(14); + if (num_args > 15) grass.color_map_filename = this->GetArgString(15); + if (num_args > 16) grass.density_map_filename = this->GetArgString(16); } else { - // Same as 'grass2', except without 'technique' parameter - sscanf(m_cur_line, "grass %d, %f, %f, %f, %f, %f, %f, %f, %f, %d, %f, %f, %s %s %s", - &grass.range, - &grass.sway_speed, &grass.sway_length, &grass.sway_distrib, &grass.density, - &grass.min_x, &grass.min_y, &grass.max_x, &grass.max_y, - &grass.grow_techniq, &grass.min_h, &grass.max_h, - grass.material_name, - grass.color_map_filename, - grass.density_map_filename); + if (num_args > 13) grass.material_name = this->GetArgString(13); + if (num_args > 14) grass.color_map_filename = this->GetArgString(14); + if (num_args > 15) grass.density_map_filename = this->GetArgString(15); } // 0: GRASSTECH_QUAD; // Grass constructed of randomly placed and rotated quads @@ -266,21 +274,21 @@ void TObjParser::ProcessGrassLine() grass.technique = 1; } - m_def->grass.push_back(grass); + m_tobj_doc.grass.push_back(grass); } -void TObjParser::ProcessActorObject(const TObjEntry& object) +void TObjReader::ProcessActorObject(const TObjEntry& object) { TObjVehicle v; v.position = object.position; v.rotation = this->CalcRotation(object.rotation); v.type = object.special; - strcpy(v.name, object.type); + v.name = object.type; - m_def->vehicles.push_back(v); + m_tobj_doc.vehicles.push_back(v); } -void TObjParser::ProcessRoadObject(const TObjEntry& object) +void TObjReader::ProcessRoadObject(const TObjEntry& object) { // ** Import road objects as procedural road @@ -293,7 +301,7 @@ void TObjParser::ProcessRoadObject(const TObjEntry& object) this->ImportProceduralPoint(pp_pos, m_road2_last_rot, object.special); // finish it and start new object - m_def->proc_objects.push_back(m_cur_procedural_obj); + m_tobj_doc.proc_objects.push_back(m_cur_procedural_obj); m_cur_procedural_obj = ProceduralObject(); } m_road2_use_old_mode = true; @@ -312,7 +320,7 @@ void TObjParser::ProcessRoadObject(const TObjEntry& object) // -------------------------------- // Helpers -void TObjParser::ImportProceduralPoint(Ogre::Vector3 const& pos, Ogre::Vector3 const& rot, TObj::SpecialObject special) +void TObjReader::ImportProceduralPoint(Ogre::Vector3 const& pos, Ogre::Vector3 const& rot, TObj::SpecialObject special) { ProceduralPoint pp; pp.bheight = 0.2; @@ -326,41 +334,43 @@ void TObjParser::ImportProceduralPoint(Ogre::Vector3 const& pos, Ogre::Vector3 c m_cur_procedural_obj.points.push_back(pp); } -Ogre::Quaternion TObjParser::CalcRotation(Ogre::Vector3 const& rot) const +Ogre::Quaternion TObjReader::CalcRotation(Ogre::Vector3 const& rot) const { return Quaternion(Degree(rot.x), Vector3::UNIT_X) * Quaternion(Degree(rot.y), Vector3::UNIT_Y) * Quaternion(Degree(rot.z), Vector3::UNIT_Z); } -bool TObjParser::ParseObjectLine(TObjEntry& object) +bool TObjReader::ParseObjectLine(TObjEntry& object) { - Str<300> odef("generic"); - char type[100] = {}; - char name[100] = {}; - Ogre::Vector3 pos(Ogre::Vector3::ZERO); - Ogre::Vector3 rot(Ogre::Vector3::ZERO); - int r = sscanf(m_cur_line, "%f, %f, %f, %f, %f, %f, %s %s %s", - &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z, odef.GetBuffer(), type, name); - if (r < 6) - { + size_t num_args = this->CountLineArgs(); + if (num_args < 6) return false; - } - TObj::SpecialObject special = TObj::SpecialObject::NONE; - if (odef == "truck" ) { special = TObj::SpecialObject::TRUCK ; } - else if (odef == "load" ) { special = TObj::SpecialObject::LOAD ; } - else if (odef == "machine" ) { special = TObj::SpecialObject::MACHINE ; } - else if (odef == "boat" ) { special = TObj::SpecialObject::BOAT ; } - else if (odef == "truck2" ) { special = TObj::SpecialObject::TRUCK2 ; } - else if (odef == "grid" ) { special = TObj::SpecialObject::GRID ; } - else if (odef == "road" ) { special = TObj::SpecialObject::ROAD ; } - else if (odef == "roadborderleft" ) { special = TObj::SpecialObject::ROAD_BORDER_LEFT ; } - else if (odef == "roadborderright" ) { special = TObj::SpecialObject::ROAD_BORDER_RIGHT ; } - else if (odef == "roadborderboth" ) { special = TObj::SpecialObject::ROAD_BORDER_BOTH ; } - else if (odef == "roadbridgenopillar") { special = TObj::SpecialObject::ROAD_BRIDGE_NO_PILLARS; } - else if (odef == "roadbridge" ) { special = TObj::SpecialObject::ROAD_BRIDGE ; } - - object = TObjEntry(pos, rot, odef.ToCStr(), special, type, name); + object.odef_name = "generic"; + + if (num_args > 0) object.position.x = this->GetArgFloat(0); + if (num_args > 1) object.position.y = this->GetArgFloat(1); + if (num_args > 2) object.position.z = this->GetArgFloat(2); + if (num_args > 3) object.rotation.x = this->GetArgFloat(3); + if (num_args > 4) object.rotation.y = this->GetArgFloat(4); + if (num_args > 5) object.rotation.z = this->GetArgFloat(5); + if (num_args > 6) object.odef_name = this->GetArgString(6); + if (num_args > 7) object.type = this->GetArgString(7); + if (num_args > 8) object.instance_name = this->GetArgString(8); + + if (object.odef_name == "truck" ) { object.special = TObj::SpecialObject::TRUCK ; } + else if (object.odef_name == "load" ) { object.special = TObj::SpecialObject::LOAD ; } + else if (object.odef_name == "machine" ) { object.special = TObj::SpecialObject::MACHINE ; } + else if (object.odef_name == "boat" ) { object.special = TObj::SpecialObject::BOAT ; } + else if (object.odef_name == "truck2" ) { object.special = TObj::SpecialObject::TRUCK2 ; } + else if (object.odef_name == "grid" ) { object.special = TObj::SpecialObject::GRID ; } + else if (object.odef_name == "road" ) { object.special = TObj::SpecialObject::ROAD ; } + else if (object.odef_name == "roadborderleft" ) { object.special = TObj::SpecialObject::ROAD_BORDER_LEFT ; } + else if (object.odef_name == "roadborderright" ) { object.special = TObj::SpecialObject::ROAD_BORDER_RIGHT ; } + else if (object.odef_name == "roadborderboth" ) { object.special = TObj::SpecialObject::ROAD_BORDER_BOTH ; } + else if (object.odef_name == "roadbridgenopillar") { object.special = TObj::SpecialObject::ROAD_BRIDGE_NO_PILLARS; } + else if (object.odef_name == "roadbridge" ) { object.special = TObj::SpecialObject::ROAD_BRIDGE ; } + return true; } diff --git a/source/main/resources/tobj_fileformat/TObjFileFormat.h b/source/main/resources/tobj_fileformat/TObjFileFormat.h index 43b139f360..462edb7df1 100644 --- a/source/main/resources/tobj_fileformat/TObjFileFormat.h +++ b/source/main/resources/tobj_fileformat/TObjFileFormat.h @@ -25,16 +25,17 @@ #include "Collisions.h" #include "ProceduralManager.h" +#include "GenericFileFormat.h" #include #include +#include namespace RoR { namespace TObj { - const int STR_LEN = 300; const int LINE_BUF_LEN = 4000; enum class SpecialObject @@ -57,7 +58,6 @@ namespace TObj { } // namespace TObj -// ----------------------------------------------------------------------------- struct TObjTree { TObjTree(): @@ -67,10 +67,6 @@ struct TObjTree high_density(1.f), grid_spacing(0.f) { - tree_mesh [0] = '\0'; - color_map [0] = '\0'; - density_map [0] = '\0'; - collision_mesh[0] = '\0'; } float yaw_from, yaw_to; @@ -79,13 +75,12 @@ struct TObjTree float high_density; float grid_spacing; - char tree_mesh[TObj::STR_LEN]; - char color_map[TObj::STR_LEN]; - char density_map[TObj::STR_LEN]; - char collision_mesh[TObj::STR_LEN]; + std::string tree_mesh; + std::string color_map; + std::string density_map; + std::string collision_mesh; }; -// ----------------------------------------------------------------------------- /// Unified 'grass' and 'grass2' struct TObjGrass { @@ -100,9 +95,6 @@ struct TObjGrass min_x(0.2f), min_y(0.2f), min_h(-9999.f), max_x(1.0f), max_y(0.6f), max_h(+9999.f) { - material_name [0] = '\0'; - color_map_filename [0] = '\0'; - density_map_filename [0] = '\0'; } int range; @@ -116,66 +108,57 @@ struct TObjGrass float min_x, min_y, min_h; float max_x, max_y, max_h; - char material_name[TObj::STR_LEN]; - char color_map_filename[TObj::STR_LEN]; - char density_map_filename[TObj::STR_LEN]; + std::string material_name; + std::string color_map_filename; + std::string density_map_filename; }; -// ----------------------------------------------------------------------------- struct TObjVehicle { Ogre::Vector3 position; Ogre::Quaternion rotation; - char name[TObj::STR_LEN]; + std::string name; TObj::SpecialObject type; }; -// ----------------------------------------------------------------------------- struct TObjEntry { - TObjEntry() {}; - TObjEntry( - Ogre::Vector3 pos, Ogre::Vector3 rot, const char* instance_name, - TObj::SpecialObject special, const char* type, const char* name); - bool IsActor() const; bool IsRoad() const; Ogre::Vector3 position = Ogre::Vector3::ZERO; Ogre::Vector3 rotation = Ogre::Vector3::ZERO; TObj::SpecialObject special = TObj::SpecialObject::NONE; - char type[TObj::STR_LEN] = {}; - char instance_name[TObj::STR_LEN] = {}; - char odef_name[TObj::STR_LEN] = {}; + std::string type; + std::string instance_name; + std::string odef_name; }; -// ----------------------------------------------------------------------------- -struct TObjFile +struct TObjDocument: Document { - TObjFile(): - grid_position(), - grid_enabled(false) - {} + ~TObjDocument() {} - Ogre::Vector3 grid_position; - bool grid_enabled; + Ogre::Vector3 grid_position = Ogre::Vector3::ZERO; + bool grid_enabled = false; std::vector trees; std::vector grass; std::vector vehicles; std::vector objects; std::vector proc_objects; + + virtual void Load(Ogre::DataStreamPtr datastream, BitMask_t options = 0) override; }; -// ----------------------------------------------------------------------------- -class TObjParser +typedef std::shared_ptr TObjDocumentPtr; + +class TObjReader: public Reader { public: - void Prepare(); - bool ProcessLine(const char* line); - void ProcessOgreStream(Ogre::DataStream* stream); - std::shared_ptr Finalize(); //!< Passes ownership + TObjReader(TObjDocument& doc) : Reader(doc), m_tobj_doc(doc){} + ~TObjReader() {} + + void Read(); -private: // Processing: bool ProcessCurrentLine(); void ProcessProceduralLine(); @@ -189,12 +172,12 @@ class TObjParser void ImportProceduralPoint(Ogre::Vector3 const& pos, Ogre::Vector3 const& rot, TObj::SpecialObject special); Ogre::Quaternion CalcRotation(Ogre::Vector3 const& rot) const; bool ParseObjectLine(TObjEntry& object); - - std::shared_ptr m_def; - int m_line_number; - const char* m_cur_line; - bool m_in_procedural_road; // Old parser: 'bool proroad' - bool m_road2_use_old_mode; // Old parser: 'int r2oldmode' + + // Context: + TObjDocument& m_tobj_doc; + const char* m_cur_line_keyword = nullptr; + bool m_in_procedural_road = false; + bool m_road2_use_old_mode = false; Ogre::Vector3 m_road2_last_pos; Ogre::Vector3 m_road2_last_rot; ProceduralObject m_cur_procedural_obj; diff --git a/source/main/terrain/TerrainObjectManager.cpp b/source/main/terrain/TerrainObjectManager.cpp index aa0a2b95ff..7227396aee 100644 --- a/source/main/terrain/TerrainObjectManager.cpp +++ b/source/main/terrain/TerrainObjectManager.cpp @@ -153,15 +153,12 @@ void GenerateGridAndPutToScene(Ogre::Vector3 position) void TerrainObjectManager::LoadTObjFile(Ogre::String tobj_name) { - std::shared_ptr tobj; + TObjDocument tobj; try { - DataStreamPtr stream_ptr = ResourceGroupManager::getSingleton().openResource( + DataStreamPtr datastream = ResourceGroupManager::getSingleton().openResource( tobj_name, Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME); - TObjParser parser; - parser.Prepare(); - parser.ProcessOgreStream(stream_ptr.get()); - tobj = parser.Finalize(); + tobj.Load(datastream); } catch (Ogre::Exception& e) { @@ -178,15 +175,15 @@ void TerrainObjectManager::LoadTObjFile(Ogre::String tobj_name) int mapsizez = terrainManager->getGeometryManager()->getMaxTerrainSize().z; // Section 'grid' - if (tobj->grid_enabled) + if (tobj.grid_enabled) { - GenerateGridAndPutToScene(tobj->grid_position); + GenerateGridAndPutToScene(tobj.grid_position); } // Section 'trees' if (App::gfx_vegetation_mode->getEnum() != GfxVegetation::NONE) { - for (TObjTree tree : tobj->trees) + for (TObjTree tree : tobj.trees) { this->ProcessTree( tree.yaw_from, tree.yaw_to, @@ -200,7 +197,7 @@ void TerrainObjectManager::LoadTObjFile(Ogre::String tobj_name) // Section 'grass' / 'grass2' if (App::gfx_vegetation_mode->getEnum() != GfxVegetation::NONE) { - for (TObjGrass grass : tobj->grass) + for (TObjGrass grass : tobj.grass) { this->ProcessGrass( grass.sway_speed, grass.sway_length, grass.sway_distrib, grass.density, @@ -212,13 +209,13 @@ void TerrainObjectManager::LoadTObjFile(Ogre::String tobj_name) } // Procedural roads - for (ProceduralObject po : tobj->proc_objects) + for (ProceduralObject po : tobj.proc_objects) { m_procedural_mgr.addObject(po); } // Vehicles - for (TObjVehicle veh : tobj->vehicles) + for (TObjVehicle veh : tobj.vehicles) { if ((veh.type == TObj::SpecialObject::BOAT) && (terrainManager->getWater() == nullptr)) { @@ -245,7 +242,7 @@ void TerrainObjectManager::LoadTObjFile(Ogre::String tobj_name) } // Entries - for (TObjEntry entry : tobj->objects) + for (TObjEntry entry : tobj.objects) { this->LoadTerrainObject(entry.odef_name, entry.position, entry.rotation, m_staticgeometry_bake_node, entry.instance_name, entry.type); } @@ -259,17 +256,17 @@ void TerrainObjectManager::LoadTObjFile(Ogre::String tobj_name) void TerrainObjectManager::ProcessTree( float yawfrom, float yawto, float scalefrom, float scaleto, - char* ColorMap, char* DensityMap, char* treemesh, char* treeCollmesh, + std::string const& ColorMap, std::string const& DensityMap, std::string const& treemesh, std::string const& treeCollmesh, float gridspacing, float highdens, int minDist, int maxDist, int mapsizex, int mapsizez) { #ifdef USE_PAGED - if (strnlen(ColorMap, 3) == 0) + if (ColorMap == "") { LOG("tree ColorMap map zero!"); return; } - if (strnlen(DensityMap, 3) == 0) + if (DensityMap == "") { LOG("tree DensityMap zero!"); return; @@ -332,7 +329,7 @@ void TerrainObjectManager::ProcessTree( float scale = Math::RangeRandom(scalefrom, scaleto); Vector3 pos = Vector3(nx, 0, nz); treeLoader->addTree(curTree, pos, Degree(yaw), (Ogre::Real)scale); - if (strlen(treeCollmesh)) + if (treeCollmesh != "") { pos.y = terrainManager->GetHeightAt(pos.x, pos.z); scale *= 0.1f; @@ -366,7 +363,7 @@ void TerrainObjectManager::ProcessTree( float scale = Math::RangeRandom(scalefrom, scaleto); Vector3 pos = Vector3(nx, 0, nz); treeLoader->addTree(curTree, pos, Degree(yaw), (Ogre::Real)scale); - if (strlen(treeCollmesh)) + if (treeCollmesh != "") { pos.y = terrainManager->GetHeightAt(pos.x, pos.z); terrainManager->GetCollisions()->addCollisionMesh(treemesh, String(treeCollmesh),pos, Quaternion(Degree(yaw), Vector3::UNIT_Y), Vector3(scale, scale, scale)); @@ -382,7 +379,7 @@ void TerrainObjectManager::ProcessTree( void TerrainObjectManager::ProcessGrass( float SwaySpeed, float SwayLength, float SwayDistribution, float Density, float minx, float miny, float minH, float maxx, float maxy, float maxH, - char* grassmat, char* colorMapFilename, char* densityMapFilename, + std::string const& grassmat, std::string const& colorMapFilename, std::string const& densityMapFilename, int growtechnique, int techn, int range, int mapsizex, int mapsizez) { @@ -420,13 +417,13 @@ void TerrainObjectManager::ProcessGrass( grassLayer->setMapBounds(TBounds(0, 0, mapsizex, mapsizez)); - if (strcmp(colorMapFilename,"none") != 0) + if (colorMapFilename != "none") { grassLayer->setColorMap(colorMapFilename); grassLayer->setColorMapFilter(MAPFILTER_BILINEAR); } - if (strcmp(densityMapFilename,"none") != 0) + if (densityMapFilename != "none") { grassLayer->setDensityMap(densityMapFilename); grassLayer->setDensityMapFilter(MAPFILTER_BILINEAR); diff --git a/source/main/terrain/TerrainObjectManager.h b/source/main/terrain/TerrainObjectManager.h index 5d73470d1c..f48a6ec9f8 100644 --- a/source/main/terrain/TerrainObjectManager.h +++ b/source/main/terrain/TerrainObjectManager.h @@ -91,14 +91,14 @@ class TerrainObjectManager : public ZeroedMemoryAllocator void ProcessTree( float yawfrom, float yawto, float scalefrom, float scaleto, - char* ColorMap, char* DensityMap, char* treemesh, char* treeCollmesh, + std::string const& ColorMap, std::string const& DensityMap, std::string const& treemesh, std::string const& treeCollmesh, float gridspacing, float highdens, int minDist, int maxDist, int mapsizex, int mapsizez); void ProcessGrass( float SwaySpeed, float SwayLength, float SwayDistribution, float Density, float minx, float miny, float minH, float maxx, float maxy, float maxH, - char* grassmat, char* colorMapFilename, char* densityMapFilename, + std::string const& grassmat, std::string const& colorMapFilename, std::string const& densityMapFilename, int growtechnique, int techn, int range, int mapsizex, int mapsizez); struct localizer_t diff --git a/source/main/utils/GenericFileFormat.cpp b/source/main/utils/GenericFileFormat.cpp new file mode 100644 index 0000000000..6169a0a40f --- /dev/null +++ b/source/main/utils/GenericFileFormat.cpp @@ -0,0 +1,417 @@ +/* + This source file is part of Rigs of Rods + Copyright 2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods 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 Rigs of Rods. If not, see . +*/ + +#include "GenericFileFormat.h" + +#include "Application.h" +#include "Console.h" + +#include + +using namespace RoR; +using namespace Ogre; + +static void BeginToken(Document& doc, const BitMask_t options, std::vector& tok, RoR::TokenType& tok_type, bool& tok_number_dot, bool& tok_string_naked, Ogre::DataStreamPtr datastream, size_t line_num, char c) +{ + if (c == ';' || (c == '/' && (options & Document::OPTION_ALLOW_SLASH_COMMENTS))) + { + if (doc.tokens.size() == 0 || doc.tokens.back().type == TokenType::LINEBREAK) + tok_type = TokenType::COMMENT; + else + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: ignoring stray character '{}'", datastream->getName(), line_num, c)); + } + else if (c == '"') + { + tok_type = TokenType::STRING; + } + else if (c == '.') + { + tok.push_back(c); + tok_type = TokenType::NUMBER; + tok_number_dot = true; + } + else if (isdigit(c) || c == '-') + { + tok.push_back(c); + tok_type = TokenType::NUMBER; + } + else if (c == 't' || c == 'f') + { + tok.push_back(c); + tok_type = TokenType::BOOL; + } + else if (isalpha(c)) + { + if (doc.tokens.size() == 0 || doc.tokens.back().type == TokenType::LINEBREAK) + { + tok.push_back(c); + tok_type = TokenType::KEYWORD; + } + else if (BITMASK_IS_1(options, Document::OPTION_ALLOW_NAKED_STRINGS)) + { + tok.push_back(c); + tok_type = TokenType::STRING; + tok_string_naked = true; + } + else + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: ignoring stray character '{}'", datastream->getName(), line_num, c)); + } + } + else + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: ignoring stray character '{}'", datastream->getName(), line_num, c)); + } +} + +static void FlushToken(Document& doc, std::vector& tok, RoR::TokenType& tok_type, bool& tok_number_dot, bool& tok_string_naked, Ogre::DataStreamPtr datastream, size_t line_num) +{ + if (tok.size() > 0) + { + if (tok.back() != '\0') + tok.push_back('\0'); + + switch (tok_type) + { + case TokenType::STRING: + case TokenType::COMMENT: + case TokenType::KEYWORD: + doc.tokens.push_back({ tok_type, (float)doc.string_pool.size() }); + std::copy(tok.begin(), tok.end(), std::back_inserter(doc.string_pool)); + break; + + case TokenType::BOOL: + if (!std::strcmp(tok.data(), "true")) + doc.tokens.push_back({ tok_type, 1.f }); + else if (!std::strcmp(tok.data(), "false")) + doc.tokens.push_back({ tok_type, 0.f }); + else + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: ignoring garbage token '{}'", datastream->getName(), line_num, tok.data())); + break; + + case TokenType::NUMBER: + doc.tokens.push_back({ tok_type, (float)Ogre::StringConverter::parseReal(tok.data()) }); + break; + } + tok.clear(); + } + + tok_type = TokenType::NONE; + tok_number_dot = false; + tok_string_naked = false; +} + +static void ProcessNumber(Document& doc, std::vector& tok, RoR::TokenType& tok_type, bool& tok_number_dot, bool& tok_string_naked, Ogre::DataStreamPtr datastream, size_t line_num, char c) +{ + if (c == ' ' || c == ',' || c == '\t') + { + FlushToken(doc, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } + else if (c == '.') + { + if (!tok_number_dot) + { + tok.push_back(c); + tok_number_dot = true; + } + else + { + tok.push_back('\0'); + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: duplicate '.' in number, parsing as '{}'", datastream->getName(), line_num, c, tok.data())); + FlushToken(doc, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } + } + else if (isdigit(c)) + { + tok.push_back(c); + } + else + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: stray character '{}' in number, parsing as '{}'", datastream->getName(), line_num, c, tok.data())); + FlushToken(doc, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } +} + +static void ProcessBool(Document& doc, const BitMask_t options, std::vector& tok, RoR::TokenType& tok_type, bool& tok_number_dot, bool& tok_string_naked, Ogre::DataStreamPtr datastream, size_t line_num, char c) +{ + // Note: t (true) / f (false) are handled by BeginToken() + bool valid = false; + bool flush = false; + if (tok[0] == 't') + { + valid = ( + (tok.size() == 1 && c == 'r') || + (tok.size() == 2 && c == 'u') || + (tok.size() == 3 && c == 'e')); + flush = valid && tok.size() == 3; + } + else // (tok[0] == 'f') + { + valid = ( + (tok.size() == 1 && c == 'a') || + (tok.size() == 2 && c == 'l') || + (tok.size() == 3 && c == 's') || + (tok.size() == 4 && c == 'e')); + flush = valid && tok.size() == 4; + } + + if (valid) + { + tok.push_back(c); + if (flush) + FlushToken(doc, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } + else if (doc.tokens.size() == 0 || doc.tokens.back().type == TokenType::LINEBREAK) + { + tok.push_back(c); + tok_type = TokenType::KEYWORD; + } + else if (options & Document::OPTION_ALLOW_NAKED_STRINGS) + { + tok.push_back(c); + tok_type = TokenType::STRING; + tok_string_naked = true; + } + else + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: stray character '{}' in boolean", datastream->getName(), line_num, c)); + FlushToken(doc, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } +} + +void Document::Load(Ogre::DataStreamPtr datastream, BitMask_t options) +{ + // Reset the document + tokens.clear(); + string_pool.clear(); + + // Prepare context + const size_t BUF_MAX = 10 * 1024; // 10Kb + char buf[BUF_MAX]; + std::vector tok; + RoR::TokenType tok_type = TokenType::NONE; + bool tok_number_dot = false; + bool tok_string_naked = false; + size_t line_num = 0; + + // Parse the text + while (!datastream->eof()) + { + size_t buf_len = datastream->read(buf, BUF_MAX); + for (size_t i = 0; i < buf_len; i++) + { + const char c = buf[i]; + + if (c == '\r') // Carriage return character is ignored + continue; + + switch (tok_type) + { + case TokenType::NONE: + if (c == '\n') + { + tokens.push_back({ TokenType::LINEBREAK, 0.f }); + line_num++; + } + else if (c != ' ' && c != ',' && c != '\t') + { + BeginToken(*this, options, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num, c); + } + break; + + case TokenType::COMMENT: + if (c == '\n') + { + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + tokens.push_back({ TokenType::LINEBREAK, 0.f }); + line_num++; + } + else + { + tok.push_back(c); + } + break; + + case TokenType::STRING: + if (c == '\n') + { + tok.push_back('\0'); + if (BITMASK_IS_0(options, OPTION_ALLOW_NAKED_STRINGS)) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: accepting unclosed string '{}'", datastream->getName(), line_num, tok.data())); + } + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + + tokens.push_back({ TokenType::LINEBREAK, 0.f }); + line_num++; + } + else if (tok_string_naked && c == ' ' || c == ',' || c == '\t') + { + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } + else + { + tok.push_back(c); + } + break; + + case TokenType::NUMBER: + if (c == '\n') + { + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + tokens.push_back({ TokenType::LINEBREAK, 0.f }); + line_num++; + } + else + { + ProcessNumber(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num, c); + } + break; + + case TokenType::BOOL: + if (c == '\n') + { + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + tokens.push_back({ TokenType::LINEBREAK, 0.f }); + line_num++; + } + else + { + ProcessBool(*this, options, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num, c); + } + break; + + case TokenType::KEYWORD: + if (c == '\n') + { + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + tokens.push_back({ TokenType::LINEBREAK, 0.f }); + line_num++; + } + else if (c == ' ' || c == ',' || c == '\t') + { + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } + else if (isalnum(c)) + { + tok.push_back(c); + } + else + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}: stray character '{}' in keyword, parsing as '{}'", datastream->getName(), line_num, c, tok.data())); + FlushToken(*this, tok, tok_type, tok_number_dot, tok_string_naked, datastream, line_num); + } + break; + } + } + } +} + +#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 + const char* EOL_STR = "\r\n"; // CR+LF +#else + const char* EOL_STR = "\n"; // "LF" +#endif + +void Document::Save(Ogre::DataStreamPtr datastream) +{ + std::string separator; + const char* pool_str = nullptr; + const size_t BUF_MAX = 100; + char buf[BUF_MAX]; + + for (Token& tok : tokens) + { + switch (tok.type) + { + case TokenType::LINEBREAK: + datastream->write(EOL_STR, strlen(EOL_STR)); + separator = ""; + break; + + case TokenType::COMMENT: + datastream->write(";", 1); + pool_str = string_pool.data() + (size_t)tok.data; + datastream->write(pool_str, strlen(pool_str)); + break; + + case TokenType::STRING: + datastream->write(separator.data(), separator.size()); + pool_str = string_pool.data() + (size_t)tok.data; + datastream->write(pool_str, strlen(pool_str)); + separator = ","; + break; + + case TokenType::NUMBER: + datastream->write(separator.data(), separator.size()); + snprintf(buf, BUF_MAX, "%f", tok.data); + datastream->write(buf, strlen(buf)); + separator = ","; + break; + + case TokenType::BOOL: + datastream->write(separator.data(), separator.size()); + snprintf(buf, BUF_MAX, "%s", tok.data == 1.f ? "true" : "false"); + datastream->write(buf, strlen(buf)); + separator = ","; + break; + + case TokenType::KEYWORD: + pool_str = string_pool.data() + (size_t)tok.data; + datastream->write(pool_str, strlen(pool_str)); + separator = " "; + break; + } + } +} + +bool Reader::SeekNextLine() +{ + // Skip current line + while (!this->EndOfFile() && this->GetType() != TokenType::LINEBREAK) + { + this->MoveNext(); + } + + // Skip comments + while (!this->EndOfFile() && !this->IsArgString() && !this->IsArgFloat() && !this->IsArgBool() && !this->IsArgKeyword()) + { + this->MoveNext(); + } + + return this->EndOfFile(); +} + +size_t Reader::CountLineArgs() +{ + size_t count = 0; + while (!EndOfFile(count) && this->GetType(count) != TokenType::LINEBREAK) + count++; + return count; +} + diff --git a/source/main/utils/GenericFileFormat.h b/source/main/utils/GenericFileFormat.h new file mode 100644 index 0000000000..cf36024e9d --- /dev/null +++ b/source/main/utils/GenericFileFormat.h @@ -0,0 +1,102 @@ +/* + This source file is part of Rigs of Rods + Copyright 2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods 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 Rigs of Rods. If not, see . +*/ + +/// @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. + +#include +#include + +#include + +namespace RoR { + +enum 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. + NUMBER, // Float. + 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 +{ + TokenType type; + float data; +}; + +struct Document +{ + 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 `//`. + + virtual ~Document() {}; + + std::vector string_pool; // Data of COMMENT/KEYWORD/STRING tokens; NUL-terminated strings. + std::vector tokens; + + virtual void Load(Ogre::DataStreamPtr datastream, BitMask_t options = 0); + virtual void Save(Ogre::DataStreamPtr datastream); +}; + +struct Reader +{ + Reader(Document& d) : doc(d) {} + virtual ~Reader() {}; + + Document& doc; + size_t token_pos = 0; + size_t line_num = 0; + + bool MoveNext() { token_pos++; return EndOfFile(); } + bool SeekNextLine(); + size_t CountLineArgs(); + bool EndOfFile(size_t offset = 0) const { return token_pos + offset >= doc.tokens.size(); } + + TokenType GetType(size_t offset = 0) const { return !EndOfFile(offset) ? doc.tokens[token_pos + offset].type : TokenType::NONE; } + const char* GetStringData(size_t offset = 0) const { return !EndOfFile(offset) ? (doc.string_pool.data() + (size_t)doc.tokens[token_pos + offset].data) : nullptr; } + float GetFloatData(size_t offset = 0) const { return !EndOfFile(offset) ? doc.tokens[token_pos + offset].data : 0.f; } + + const char* GetArgString(size_t offset = 0) const { ROR_ASSERT(IsArgString(offset)); return GetStringData(offset); } + float GetArgFloat(size_t offset = 0) const { ROR_ASSERT(IsArgFloat(offset)); return GetFloatData(offset); } + bool GetArgBool(size_t offset = 0) const { ROR_ASSERT(IsArgBool(offset)); return GetFloatData(offset) == 1.f; } + const char* GetArgKeyword(size_t offset = 0) const { ROR_ASSERT(IsArgKeyword(offset)); return GetStringData(offset); } + + bool IsArgString(size_t offset = 0) const { return GetType(offset) == TokenType::STRING; }; + bool IsArgFloat(size_t offset = 0) const { return GetType(offset) == TokenType::NUMBER; }; + bool IsArgBool(size_t offset = 0) const { return GetType(offset) == TokenType::BOOL; }; + bool IsArgKeyword(size_t offset = 0) const { return GetType(offset) == TokenType::KEYWORD; }; + +}; + +} // namespace RoR \ No newline at end of file