From 90c5420396fda3610197b7f758ec3e990c28e17d Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 6 Oct 2026 21:58:49 +0200 Subject: [PATCH] perf(scenery): runs of model instances read straight from the text by a reader made for them, shared between threads; anything the reader doesn't cover left to the parser; model and skin located once for a run of instances; lookup of nodes by uuid filled on load only for a scenery opened for editing --- CMakeLists.txt | 1 + model/AnimModel.cpp | 58 +++- model/AnimModel.h | 7 + scene/scenemodelentries.cpp | 401 +++++++++++++++++++++++ scene/scenemodelentries.h | 81 +++++ simulation/simulationstateserializer.cpp | 116 +++++-- simulation/simulationstateserializer.h | 4 + utilities/parser.cpp | 41 +++ utilities/parser.h | 16 +- 9 files changed, 696 insertions(+), 29 deletions(-) create mode 100644 scene/scenemodelentries.cpp create mode 100644 scene/scenemodelentries.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 30f6396c..37f5c4ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,6 +198,7 @@ set(SOURCES "scene/scenelayers.cpp" "scene/scenelayerssave.cpp" "scene/sceneterrain.cpp" +"scene/scenemodelentries.cpp" "simulation/simulationenvironment.cpp" "simulation/simulationstateserializer.cpp" "rendering/precipitation.cpp" diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 52917a66..cf6df321 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -291,12 +291,9 @@ TAnimModel::is_keyword( std::string const &Token ) const { || Token == "notransition"; } -bool TAnimModel::Load(cParser *parser, bool ter) -{ // rozpoznanie wpisu modelu i ustawienie świateł - std::string name = parser->getToken(); - std::string texture = parser->getToken(false); - replace_slashes( name ); - replace_slashes( texture ); +// assigns the model and the replacable skin to the instance, and binds light and variant submodels of the model +void TAnimModel::assign_model( std::string &name, std::string const &texture, bool const ter ) +{ if (!Init( name, texture )) { if (name != "notload") @@ -329,6 +326,15 @@ bool TAnimModel::Load(cParser *parser, bool ter) for (int i = 0; i < iMaxNumLights; ++i) if (LightsOn[i] || LightsOff[i]) // Ra: zlikwidowałem wymóg istnienia obu iNumLights = i + 1; +} + +bool TAnimModel::Load(cParser *parser, bool ter) +{ // rozpoznanie wpisu modelu i ustawienie świateł + std::string name = parser->getToken(); + std::string texture = parser->getToken(false); + replace_slashes( name ); + replace_slashes( texture ); + assign_model( name, texture, ter ); std::string token; do { @@ -400,6 +406,46 @@ bool TAnimModel::Load(cParser *parser, bool ter) return true; } +// sets up the instance from the values of a definition which was already taken apart, the way Load() does it from the text +void TAnimModel::Load( std::string const &Name, std::string const &Texture, TAnimModel const *Twin, glm::vec3 const *Angles, glm::vec3 const *Scale, bool const Transition ) +{ + if( Twin != nullptr + && Twin->pModel != nullptr + && ( Twin->m_materialdata.replacable_skins[ 1 ] != null_handle || Texture == "none" ) ) { + // the same model with the same skin was located for the twin already. + // NOTE: a model or a skin which couldn't be located is searched for and reported for each instance, like it always was + m_skintoken = Twin->m_skintoken; + m_materialdata = Twin->m_materialdata; + pModel = Twin->pModel; + LightsOn = Twin->LightsOn; + LightsOff = Twin->LightsOff; + sm_winter_variant = Twin->sm_winter_variant; + sm_spring_variant = Twin->sm_spring_variant; + sm_summer_variant = Twin->sm_summer_variant; + sm_autumn_variant = Twin->sm_autumn_variant; + iNumLights = Twin->iNumLights; + } + else { + auto name { Name }; + assign_model( name, Texture, false ); + } + // the optional blocks, each with the effect it has in Load() + if( Angles != nullptr ) { + vAngle = *Angles; + } + if( Scale != nullptr + && Scale->x > 0.0f && Scale->y > 0.0f && Scale->z > 0.0f ) { + m_scale.x *= Scale->x; + m_scale.y *= Scale->y; + m_scale.z *= Scale->z; + } + if( false == Transition ) { + m_transition = false; + } + + update_instanceable_flag(); +} + namespace { // returns true if this animation type mutates per-instance state on the shared // TSubModel tree, which would make batched rendering unsafe. Most animations diff --git a/model/AnimModel.h b/model/AnimModel.h index 8116f1bb..61b1ccb7 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -118,6 +118,11 @@ public: static void AnimUpdate( double dt ); bool Init(std::string const &asName, std::string const &asReplacableTexture); bool Load(cParser *parser, bool ter = false); + // sets up the instance from the values of a definition which was already taken apart, the way Load(cParser *) does it + // from the text. Name, Texture: as that method gets them ready, lower case model name and forward slashes in both. + // Twin: instance set up for the same model and texture, to take them after instead of locating them again; can be nullptr. + // Angles, Scale: content of the optional blocks of the definition, nullptr for a block which isn't there + void Load( std::string const &Name, std::string const &Texture, TAnimModel const *Twin, glm::vec3 const *Angles, glm::vec3 const *Scale, bool const Transition ); std::shared_ptr AddContainer(std::string const &Name); std::shared_ptr GetContainer(std::string const &Name = ""); void LightSet( int const n, float const v ); @@ -226,6 +231,8 @@ public: bool m_instanceable { false }; // helper: evaluates current state and updates m_instanceable accordingly. void update_instanceable_flag(); + // helper: assigns the model and the replacable skin to the instance, and binds light and variant submodels of the model + void assign_model( std::string &name, std::string const &texture, bool const ter ); // diagnostic counters (process-wide). Updated inside update_instanceable_flag() // so the renderer can surface load-time classification stats in the debug overlay. diff --git a/scene/scenemodelentries.cpp b/scene/scenemodelentries.cpp new file mode 100644 index 00000000..d11255e3 --- /dev/null +++ b/scene/scenemodelentries.cpp @@ -0,0 +1,401 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/scenemodelentries.h" + +#include "utilities/parser.h" + +#include +#include +#include +#include +#include +#include + +namespace scene +{ + +namespace +{ + +enum char_kind : unsigned char +{ + char_regular, + char_separator, // one of the separators the scenery parser splits the text on + char_special // can make the parser treat the token in a way of its own: a quote, or a part of a comment mark +}; + +constexpr std::array make_char_kinds() +{ + std::array kinds{}; + kinds['\n'] = char_separator; + kinds['\r'] = char_separator; + kinds['\t'] = char_separator; + kinds[' '] = char_separator; + kinds[';'] = char_separator; + kinds['"'] = char_special; + kinds['/'] = char_special; + return kinds; +} + +constexpr std::array char_kinds{make_char_kinds()}; + +char to_lower(char const Character) +{ + // only the ascii letters, like the parser does it + return (Character >= 'A' && Character <= 'Z') ? static_cast(Character - 'A' + 'a') : Character; +} + +// compares a token with a lower case keyword the way a token converted by the parser would compare +bool equals(std::string_view const Token, std::string_view const Keyword) +{ + if (Token.size() != Keyword.size()) + { + return false; + } + for (std::size_t idx = 0; idx < Token.size(); ++idx) + { + if (to_lower(Token[idx]) != Keyword[idx]) + { + return false; + } + } + return true; +} + +// reads the definitions held by one piece of a text +class entry_reader +{ + public: + explicit entry_reader(std::string_view const Text) : m_text(Text) {} + // reads definitions from specified place in the text, until the end of one reaches Limit or something else turns up. + // NOTE: line breaks are counted from the place the reading starts at + void read(std::size_t Begin, std::size_t Limit, bool Keywordread); + // members + model_entry_block block; + std::size_t cursor{0}; // end of the last definition read, or the place the reading started at + std::size_t linebreaks{0}; // line breaks ahead of the cursor + bool complete{false}; // the reading got as far as the limit + + private: + // moves past the separators and the line comments. returns: false at a block comment + bool skip(std::size_t &Position, std::size_t &Linebreaks) const; + // takes the token at specified place, which has to be past the separators. returns: false if the parser wouldn't take + // the token the way it's written: there's a quote in it, or a comment starts in its middle + bool token(std::size_t &Position, std::string_view &Token) const; + // takes the next token. returns: false if there isn't one which can be used as it's written + bool next(std::size_t &Position, std::size_t &Linebreaks, std::string_view &Token) const + { + return skip(Position, Linebreaks) && token(Position, Token) && false == Token.empty(); + } + // takes the next token as a number. returns: false unless the parser would convert the token without a stream + template bool number(std::size_t &Position, std::size_t &Linebreaks, Type_ &Value) const + { + std::string_view text; + return next(Position, Linebreaks, text) && cParser::parseNumber(text, Value); + } + // takes a complete definition. returns: false if what's there isn't one, or isn't one the reader covers + bool entry(std::size_t &Position, std::size_t &Linebreaks, bool Keywordread, model_entry &Entry); + std::uint32_t appearance(std::string_view Model, std::string_view Texture); + // members + std::string_view m_text; + std::unordered_map m_appearances; // by the part of the text which names the model and the texture + std::string_view m_recentkey; + std::uint32_t m_recentappearance{0}; +}; + +bool entry_reader::skip(std::size_t &Position, std::size_t &Linebreaks) const +{ + auto const *const data{m_text.data()}; + auto const size{m_text.size()}; + while (Position < size) + { + auto const character{data[Position]}; + if (char_kinds[static_cast(character)] == char_separator) + { + if (character == '\n') + { + ++Linebreaks; + } + ++Position; + continue; + } + if (character == '/' && Position + 1 < size) + { + if (data[Position + 1] == '/') + { + // the comment takes the rest of the line, along with the line break + auto const end{m_text.find('\n', Position + 2)}; + if (end == std::string_view::npos) + { + Position = size; + } + else + { + Position = end + 1; + ++Linebreaks; + } + continue; + } + if (data[Position + 1] == '*') + { + return false; + } + } + break; + } + return true; +} + +bool entry_reader::token(std::size_t &Position, std::string_view &Token) const +{ + auto const *const data{m_text.data()}; + auto const size{m_text.size()}; + auto const begin{Position}; + while (Position < size) + { + auto const kind{char_kinds[static_cast(data[Position])]}; + if (kind == char_separator) + { + break; + } + if (kind == char_special) + { + if (data[Position] == '"') + { + return false; + } + if (Position + 1 < size && (data[Position + 1] == '/' || data[Position + 1] == '*')) + { + return false; + } + } + ++Position; + } + Token = m_text.substr(begin, Position - begin); + return true; +} + +bool entry_reader::entry(std::size_t &Position, std::size_t &Linebreaks, bool const Keywordread, model_entry &Entry) +{ + std::string_view word; + if (false == Keywordread) + { + if (false == next(Position, Linebreaks, word) || false == equals(word, "node")) + { + return false; + } + Entry.begin = Position - word.size(); + // the parser takes a token along with the separator which ends it, and the loader notes the line right after + Entry.line = Linebreaks + ((Position < m_text.size() && m_text[Position] == '\n') ? 1 : 0); + } + // NOTE: negative minimal range marks the model as terrain, which is processed in a way of its own + if (false == number(Position, Linebreaks, Entry.range_max) || false == number(Position, Linebreaks, Entry.range_min) || Entry.range_min < 0.0) + { + return false; + } + // NOTE: the parser takes include for a directive wherever it shows up + if (false == next(Position, Linebreaks, Entry.name) || equals(Entry.name, "include")) + { + return false; + } + if (false == next(Position, Linebreaks, word) || false == equals(word, "model")) + { + return false; + } + if (false == number(Position, Linebreaks, Entry.location.x) || false == number(Position, Linebreaks, Entry.location.y) || false == number(Position, Linebreaks, Entry.location.z) || + false == number(Position, Linebreaks, Entry.angle)) + { + return false; + } + std::string_view model; + std::string_view texture; + if (false == next(Position, Linebreaks, model) || equals(model, "include") || false == next(Position, Linebreaks, texture) || equals(texture, "include")) + { + return false; + } + while (true) + { + if (false == next(Position, Linebreaks, word)) + { + return false; + } + if (equals(word, "endmodel")) + { + break; + } + if (equals(word, "angles")) + { + // NOTE: a block which shows up again is left to the parser, along with the question of what it means + if (Entry.has_angles || false == number(Position, Linebreaks, Entry.angles.x) || false == number(Position, Linebreaks, Entry.angles.y) || false == number(Position, Linebreaks, Entry.angles.z)) + { + return false; + } + Entry.has_angles = true; + } + else if (equals(word, "scale")) + { + if (Entry.has_scale || false == number(Position, Linebreaks, Entry.scale.x) || false == number(Position, Linebreaks, Entry.scale.y) || false == number(Position, Linebreaks, Entry.scale.z)) + { + return false; + } + Entry.has_scale = true; + } + else if (equals(word, "notransition")) + { + Entry.notransition = true; + } + else + { + return false; + } + } + Entry.end = Position; + Entry.appearance = appearance(model, texture); + return true; +} + +std::uint32_t entry_reader::appearance(std::string_view const Model, std::string_view const Texture) +{ + // the two tokens along with whatever is between them. the same pair written in another way gets an entry of its own + std::string_view const key{Model.data(), static_cast((Texture.data() + Texture.size()) - Model.data())}; + if (key == m_recentkey) + { + return m_recentappearance; + } + auto lookup{m_appearances.find(key)}; + if (lookup == m_appearances.end()) + { + model_appearance appearance; + appearance.model.assign(Model); + std::transform(std::begin(appearance.model), std::end(appearance.model), std::begin(appearance.model), [](char const Character) { return Character == '\\' ? '/' : to_lower(Character); }); + appearance.texture.assign(Texture); + std::replace(std::begin(appearance.texture), std::end(appearance.texture), '\\', '/'); + block.appearances.emplace_back(std::move(appearance)); + lookup = m_appearances.emplace(key, static_cast(block.appearances.size() - 1)).first; + } + m_recentkey = key; + m_recentappearance = lookup->second; + return lookup->second; +} + +void entry_reader::read(std::size_t const Begin, std::size_t const Limit, bool Keywordread) +{ + cursor = Begin; + linebreaks = 0; + while (cursor < Limit) + { + auto position{cursor}; + auto breaks{linebreaks}; + model_entry definition; + if (false == entry(position, breaks, Keywordread, definition)) + { + break; + } + Keywordread = false; + block.entries.emplace_back(definition); + cursor = position; + linebreaks = breaks; + } + complete = (cursor >= Limit); +} + +} // namespace + +model_entry_run read_model_entries(std::string_view const Text, bool const Keywordread, model_reader_setup const &Setup) +{ + model_entry_run run; + + entry_reader lead{Text}; + lead.read(0, std::min(Text.size(), Setup.lead), Keywordread); + if (lead.block.entries.empty()) + { + return run; + } + auto cursor{lead.cursor}; + auto linebreaks{lead.linebreaks}; + run.blocks.emplace_back(std::move(lead.block)); + + if (lead.complete && cursor < Text.size()) + { + // the run goes on: the next part of the text is divided between the threads. a thread needs a place to start at, + // which is found by looking for the keyword ending a definition. whether the place really is an end of a definition + // is known only when the thread reading the part ahead of it gets there, so the pieces are put together as long as + // each one ended right where the next one began. if that's not the case, e.g. the place found was inside a comment, + // the pieces past it are dropped and their text is read again by the next call, starting from a place which is certain + auto const limit{cursor + std::min(Text.size() - cursor, std::max(Setup.window, 1))}; + auto const threads{std::clamp(Setup.threads != 0 ? Setup.threads : std::thread::hardware_concurrency(), 1, 16)}; + auto const count{std::clamp((limit - cursor) / std::max(Setup.share, 1), 1, threads)}; + std::string_view const endmark{"endmodel"}; + std::vector bounds{cursor}; + for (std::size_t idx = 1; idx < count; ++idx) + { + auto const mark{Text.find(endmark, cursor + (limit - cursor) / count * idx)}; + if (mark == std::string_view::npos || mark + endmark.size() >= limit) + { + break; + } + if (mark + endmark.size() > bounds.back()) + { + bounds.emplace_back(mark + endmark.size()); + } + } + bounds.emplace_back(limit); + + std::vector readers(bounds.size() - 1, entry_reader{Text}); + { + std::vector> tasks; + for (std::size_t idx = 1; idx < readers.size(); ++idx) + { + auto const work{[&readers, &bounds, idx]() { readers[idx].read(bounds[idx], bounds[idx + 1], false); }}; + try + { + tasks.emplace_back(std::async(std::launch::async, work)); + } + catch (std::system_error const &) + { + // no thread to be had, the piece is read right here + work(); + } + } + readers.front().read(bounds[0], bounds[1], false); + for (auto &task : tasks) + { + task.get(); + } + } + for (std::size_t idx = 0; idx < readers.size(); ++idx) + { + auto &reader{readers[idx]}; + for (auto &definition : reader.block.entries) + { + definition.line += linebreaks; + } + cursor = reader.cursor; + linebreaks += reader.linebreaks; + if (false == reader.block.entries.empty()) + { + run.blocks.emplace_back(std::move(reader.block)); + } + if (reader.cursor != bounds[idx + 1]) + { + break; + } + } + } + run.length = cursor; + run.linebreaks = linebreaks; + + return run; +} + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/scene/scenemodelentries.h b/scene/scenemodelentries.h new file mode 100644 index 00000000..98b171ee --- /dev/null +++ b/scene/scenemodelentries.h @@ -0,0 +1,81 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace scene +{ + +// model file and replacable skin of a model instance, in the form TAnimModel::Load() gives the tokens which name them +struct model_appearance +{ + std::string model; // lower case, forward slashes + std::string texture; // forward slashes +}; + +// definition of a model instance read from scenery text: +// node model [angles ] [scale ] [notransition] endmodel +struct model_entry +{ + double range_max{0.0}; + double range_min{0.0}; + std::string_view name; // the way the text spells it. NOTE: refers to the text + glm::dvec3 location{0.0}; + float angle{0.f}; // rotation around the vertical axis + glm::vec3 angles{0.f}; // content of the angles block, if there's one + glm::vec3 scale{1.f}; // content of the scale block, if there's one + bool has_angles{false}; + bool has_scale{false}; + bool notransition{false}; + std::uint32_t appearance{0}; // model and texture of the instance, index into the appearances of the block holding the entry + std::size_t begin{0}; // location of the definition in the text: from the start of its node keyword... + std::size_t end{0}; // ...to the end of its closing keyword + std::size_t line{0}; // line breaks in the text ahead of the definition, counted the way the parser counts them for a node +}; + +// definitions read from one piece of the text +struct model_entry_block +{ + std::vector entries; + std::vector appearances; +}; + +// run of definitions a text starts with +struct model_entry_run +{ + std::vector blocks; // in the order of the text; no block is empty + std::size_t length{0}; // amount of the text taken by the definitions + std::size_t linebreaks{0}; // number of line breaks in that part of the text +}; + +// how the text is divided between the threads +struct model_reader_setup +{ + std::size_t lead{64 * 1024}; // amount of the text read before the threads get involved; most runs of definitions are short + std::size_t window{8 * 1024 * 1024}; // most of the text a single call goes through past the lead + std::size_t share{256 * 1024}; // least amount of the text worth a thread of its own + unsigned int threads{0}; // most threads put to work; 0: as many as the hardware runs at a time +}; + +// reads the definitions of model instances provided text starts with. the reading ends where the text holds something else +// than a definition the reader knows inside out: another kind of node or directive, a block comment, a definition of +// terrain, one with lights or anything else not listed above, with quoted text, an include or a comment glued to a token. +// that part is left for the scenery parser, so the outcome is the same as if the parser went through all of the text. +// Keywordread: the node keyword of the first definition was already taken off the text; begin and line of that entry aren't set +model_entry_run read_model_entries(std::string_view Text, bool Keywordread, model_reader_setup const &Setup = model_reader_setup()); + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 048af99e..8736e56d 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenenodegroups.h" #include "scene/scenelayers.h" #include "scene/sceneterrain.h" +#include "scene/scenemodelentries.h" #include "rendering/particles.h" #include "world/Event.h" #include "world/MemCell.h" @@ -484,6 +485,8 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch // defined by a template, or by something else which isn't a scenery layer file nodedata.instance = ( scene::Layers.instance() != 0 ? scene::Layers.instance() : scene::untracked_instance ); } + // a run of plain model instances is taken straight from the text, instead of token by token + if( true == deserialize_models( Input, Scratchpad, nodedata, inputline, sourcebegin ) ) { return; } // common data and node type indicator Input.getTokens( 4 ); Input @@ -667,25 +670,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch // model import can potentially fail if( instance == nullptr ) { return; } - if( instance->Model() != nullptr ) { - for( auto const &smokesource : instance->Model()->smoke_sources() ) { - Particles.insert( - smokesource.first, - instance, - smokesource.second ); - } - } - - if( false == simulation::Instances.insert( instance ) ) { - ErrorLog( "Bad scenario: duplicate 3d model instance name \"" + instance->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); - } - scene::Groups.insert( scene::Groups.handle(), instance ); - simulation::Region->insert( instance ); - scene::Layers.track( instance, { sourcebegin, Input.TokenEnd() }, instance->Angles(), instance->Scale() ); - scene::basic_node *hierarchy_node = instance; - if (hierarchy_node) - { scene::Hierarchy[hierarchy_node->uuid.to_string()] = hierarchy_node; - } + insert_model( instance, Input, inputline, { sourcebegin, Input.TokenEnd() } ); } } else if( nodedata.type == "triangles" @@ -825,6 +810,99 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } } +// makes model instance defined by a scenery file a part of the simulation +void +state_serializer::insert_model( TAnimModel *Instance, cParser const &Input, std::size_t const Line, scene::source_span const &Span ) { + + if( Instance->Model() != nullptr ) { + for( auto const &smokesource : Instance->Model()->smoke_sources() ) { + Particles.insert( + smokesource.first, + Instance, + smokesource.second ); + } + } + + if( false == simulation::Instances.insert( Instance ) ) { + ErrorLog( "Bad scenario: duplicate 3d model instance name \"" + Instance->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( Line ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), Instance ); + simulation::Region->insert( Instance ); + scene::Layers.track( Instance, Span, Instance->Angles(), Instance->Scale() ); + // the lookup by uuid serves the scenery editor. it's filled only for a scenery opened for editing, as with a lot + // of instances it takes more time than everything else done for an instance put together + if( true == Global.editor_session ) { + scene::Hierarchy[ Instance->uuid.to_string() ] = Instance; + } +} + +// loads a run of model instances straight from the text of the scenery file, starting with the instance whose node keyword +// was just read. the text is taken apart by a reader made for these definitions, with a few threads sharing the work; the +// instances are then created here one by one, in the order of the text. returns: true if any instances were loaded, false +// if the definition at hand has to go through the parser +bool +state_serializer::deserialize_models( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data &Nodedata, std::size_t const Line, std::streamoff const Sourcebegin ) { + + auto const text { Input.remainingText() }; + if( true == text.empty() ) { return false; } + + auto const run { scene::read_model_entries( text, true ) }; + if( true == run.blocks.empty() ) { return false; } + // NOTE: the parser moves on but the text stays where it is, the definitions refer to it + Input.skipText( run.length, run.linebreaks, std::string_view { "endmodel" }.size() ); + auto const textbegin { Input.TokenEnd() - static_cast( run.length ) }; + + Nodedata.type = "model"; + auto loaded { 0 }; + for( auto const &block : run.blocks ) { + // instances of the same model with the same skin are set up after the first one of them + std::vector twins( block.appearances.size(), nullptr ); + for( auto const &entry : block.entries ) { + + Nodedata.range_max = entry.range_max; + Nodedata.range_min = entry.range_min; + // the parser supplies the names in lower case, which goes only for the ascii letters + Nodedata.name.assign( entry.name ); + for( auto &character : Nodedata.name ) { + if( character >= 'A' && character <= 'Z' ) { + character = static_cast( character - 'A' + 'a' ); + } + } + if( Nodedata.name == "none" ) { Nodedata.name.clear(); } + + // what follows is what deserialize_model() does + auto *instance = new TAnimModel( Nodedata ); + instance->Angles( Scratchpad.location.rotation + glm::vec3 { 0.f, entry.angle, 0.f } ); + if( false == Scratchpad.location.scale.empty() ) { + instance->Scale( Scratchpad.location.scale.top() ); + } + auto const &appearance { block.appearances[ entry.appearance ] }; + auto &twin { twins[ entry.appearance ] }; + instance->Load( + appearance.model, appearance.texture, twin, + ( entry.has_angles ? &entry.angles : nullptr ), + ( entry.has_scale ? &entry.scale : nullptr ), + false == entry.notransition ); + if( twin == nullptr ) { twin = instance; } + instance->location( transform( entry.location, Scratchpad ) ); + + // location of the first definition is known from its node keyword, which went through the parser + auto const first { loaded == 0 }; + insert_model( + instance, Input, + ( first ? Line : Line + entry.line ), + { ( first ? Sourcebegin : textbegin + static_cast( entry.begin ) ), textbegin + static_cast( entry.end ) } ); + ++loaded; + } + } + if( Nodedata.layer != null_handle ) { + // scenery opened for editing: keep count of what the scenery file contains + scene::Layers.count( Nodedata.layer, scene::layer_item::model, loaded ); + } + + return true; +} + namespace { // passes the placement in effect to the layer bookkeeping of scenery opened for editing diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index 2a39c934..001f6ad3 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -86,6 +86,10 @@ private: TMemCell * deserialize_memorycell( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TEventLauncher * deserialize_eventlauncher( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TAnimModel * deserialize_model( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); + // loads a run of model instances straight from the text. returns: true if any were loaded, false if the definition at hand is left to the parser + bool deserialize_models( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data &Nodedata, std::size_t Line, std::streamoff Sourcebegin ); + // makes model instance defined by a scenery file a part of the simulation + void insert_model( TAnimModel *Instance, cParser const &Input, std::size_t Line, scene::source_span const &Span ); TDynamicObject * deserialize_dynamic( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); sound_source * deserialize_sound( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); void init_time(); diff --git a/utilities/parser.cpp b/utilities/parser.cpp index ff10c0b4..c8410e37 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -832,3 +832,44 @@ bool cParser::InLayerFile() const { return mIncludeParser ? mIncludeParser->InLayerFile() : (false == mFile.empty() && false == mIncFile); } + +std::string_view cParser::remainingText() const +{ + if (false == tokens.empty()) + { + return {}; + } + // each parser on the way gets to change the tokens of the file being read + auto const *source{this}; + while (true) + { + if (false == source->parameters.empty() || false == source->skipComments || source->mComments.size() != 2) + { + return {}; + } + if (source->mIncludeParser == nullptr) + { + break; + } + source = source->mIncludeParser.get(); + } + // NOTE: the first token of a file is left to the regular code, which deals with the byte order mark + if (source->mFirstToken || source->mEofBit || source->mFailBit || static_cast(source->mPosition) >= source->mBuffer.size()) + { + return {}; + } + return std::string_view{source->mBuffer}.substr(static_cast(source->mPosition)); +} + +void cParser::skipText(std::size_t const Count, std::size_t const Lines, std::size_t const Tokenlength) +{ + if (mIncludeParser) + { + mIncludeParser->skipText(Count, Lines, Tokenlength); + return; + } + mPosition = static_cast(std::min(static_cast(mPosition) + Count, mBuffer.size())); + mLine += Lines; + mTokenEnd = mPosition; + mTokenBegin = mPosition - static_cast(std::min(Tokenlength, static_cast(mPosition))); +} diff --git a/utilities/parser.h b/utilities/parser.h index 23ae0ef3..5c1efed4 100644 --- a/utilities/parser.h +++ b/utilities/parser.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include #include #include @@ -107,6 +108,16 @@ class cParser //: public std::stringstream std::streamoff TokenEnd() const; // true if the most recently read token came straight from a scenery file which isn't an *.inc node template bool InLayerFile() const; + // text which follows the most recently read token, for the readers which take entries of a known kind straight from + // the text instead of token by token. empty if the tokens can't be taken that way: some wait to be picked up, there + // are parameters of an include to put in them, or comment marks other than the standard ones are in use + std::string_view remainingText() const; + // moves past specified amount of the text returned by remainingText(): Lines is the number of line breaks in it, + // Tokenlength the length of the token it ends with + void skipText(std::size_t Count, std::size_t Lines, std::size_t Tokenlength); + // converts text holding a plain number without involving a stream. returns: true on success, false if the text needs regular treatment + template + static bool parseNumber( std::string_view Token, Type_ &Output ); bool expandIncludes = true; bool allowRandomIncludes = false; bool skipComments = true; @@ -158,9 +169,6 @@ class cParser //: public std::stringstream bool hasChar(); bool getChar( char &Char ); void updateCharClasses( char const *Break ); - // converts text holding a plain number without involving a stream. returns: true on success, false if the text needs regular treatment - template - static bool parseNumber( std::string const &Token, Type_ &Output ); // members: bool m_autoclear { true }; // unretrieved tokens are discarded when another read command is issued (legacy behaviour) bool LoadTraction { true }; // load traction? @@ -202,7 +210,7 @@ cParser::getToken( bool const ToLower, const char *Break ); template bool -cParser::parseNumber( std::string const &Token, Type_ &Output ) { +cParser::parseNumber( std::string_view const Token, Type_ &Output ) { using type = std::remove_cv_t; if constexpr(