diff --git a/application/editormode.cpp b/application/editormode.cpp index ce22371d..6edf40e4 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1740,19 +1740,19 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, return; } - // the directive is written at the end of the file of the active layer, and has to make sense with the placement in effect there + // the directive is added to the file of the active layer, and has to make sense with the placement in effect where it goes auto const layer = scene::Layers.resolve(scene::Layers.active()); - if (std::string reason; false == scene::Layers.valid(layer) || scene::Layers.layer(layer).removed || false == scene::Layers.writable(layer, &reason)) + if (std::string reason; false == scene::Layers.accepts(layer, &reason)) { - ui()->set_status("Template \"" + File + "\" can't be placed: " + (reason.empty() ? "there's no active layer to put it in" : "the active layer can't be saved, " + reason), true); + ui()->set_status("Template \"" + File + "\" can't be placed: " + (reason.empty() ? "there's no active layer to put it in" : "the active layer can't take it, " + reason), true); return; } - auto const context = scene::Layers.layer(layer).context_end; + auto const context = scene::Layers.layer(layer).context_insert(); if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f}) { // a template can pass its parameters to an origin directive, which isn't affected by these, or straight to // the nodes, which are. there's no single set of parameter values which puts both at the cursor - ui()->set_status("Template \"" + File + "\" can't be placed: the file of the active layer ends with a rotation or a scale in effect. Pick another layer.", true); + ui()->set_status("Template \"" + File + "\" can't be placed: the file of the active layer would receive it with a rotation or a scale in effect. Pick another layer.", true); return; } glm::dvec3 const location = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index fee74f2f..4e9d423e 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -1404,9 +1404,9 @@ void layers_panel::render_list() ImGui::SameLine(); if (ImGui::RadioButton("##active", isactive) && false == isactive) { - if (isremoved || false == iswritable) + if (std::string reason; false == scene::Layers.accepts(handle, &reason)) { - report("Layer \"" + layer_name(handle) + "\" can't take new nodes" + (isremoved ? "" : ": " + readonly), true); + report("Layer \"" + layer_name(handle) + "\" can't take new nodes" + (reason.empty() ? "" : ": " + reason), true); } else { @@ -1431,6 +1431,10 @@ void layers_panel::render_list() { label += " [read-only]"; } + else if (layer.late) + { + label += " [after FirstInit]"; + } if (ImGui::Selectable(label.c_str(), handle == m_selected)) { m_selected = handle; @@ -1446,6 +1450,10 @@ void layers_panel::render_list() { content += "\nread-only: " + readonly; } + if (layer.late) + { + content += "\nloaded after the scenario initialization (FirstInit): takes no new nodes, only vehicles belong there"; + } if (auto const items{layer_content(layer)}; false == items.empty()) { content += "\n" + items; diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 99015442..0c9a1c5d 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -60,6 +60,7 @@ layer_handle node_layers::open(std::string const &Name) m_layers.emplace_back(); m_layers.back().name = Name; m_layers.back().parent = handle(); + m_layers.back().late = m_initialized; stat(layerhandle); } else @@ -116,9 +117,83 @@ void node_layers::clear() m_templates.clear(); m_instances.clear(); m_instance = 0; + m_initialized = false; m_terraindirective = false; } +// indicates the scenario initialization (FirstInit) is being performed +void node_layers::initialization(source_span const &Span, bool const Infile) +{ + if (m_initialized) + { + return; + } + m_initialized = true; + if (m_stack.empty() || false == valid(m_stack.back())) + { + return; + } + // the layer file being loaded holds the directive, or includes the template which does + auto &holder{layer(m_stack.back())}; + if (Infile) + { + // NOTE: scenario without the directive is initialized at its end, with nothing in the file to point at + std::string text(Span.valid() ? static_cast(Span.end - Span.begin) : 0, '\0'); + std::ifstream input{path(m_stack.back()), std::ios_base::binary}; + if (text.empty() || false == input.is_open() || false == input.seekg(Span.begin).good() || false == input.read(text.data(), static_cast(text.size())).good() || + ToLower(text) != "firstinit") + { + return; + } + holder.init = Span; + } + else if (tracked(m_instance) && instance(m_instance).layer == m_stack.back()) + { + holder.init = instance(m_instance).span; + } + else + { + return; + } + holder.context_init = m_context; + // for the files it was included through the initialization takes place at their include directives + for (auto level{m_stack.size() - 1}; level > 0; --level) + { + auto const child{m_stack[level]}; + auto const parent{m_stack[level - 1]}; + if (false == valid(parent) || parent == child || layer(child).sites.empty() || layer(child).sites.back().parent != parent || layer(child).sites.back().fixed || + false == layer(child).sites.back().span.valid()) + { + break; + } + layer(parent).init = layer(child).sites.back().span; + layer(parent).context_init = layer(child).context_begin; + } +} + +// true if specified layer can receive content added in the editor +bool node_layers::accepts(layer_handle Layer, std::string *Reason) const +{ + Layer = resolve(Layer); + if (false == valid(Layer) || layer(Layer).removed) + { + return false; + } + if (false == writable(Layer, Reason)) + { + return false; + } + if (layer(Layer).late) + { + if (Reason != nullptr) + { + *Reason = "the file is loaded after the scenario initialization (FirstInit), which only vehicles should follow"; + } + return false; + } + return true; +} + // follows layer merges. returns: handle of the layer which currently holds content of specified layer layer_handle node_layers::resolve(layer_handle Layer) const { @@ -183,7 +258,7 @@ void node_layers::count(layer_handle Layer, layer_item const Item, int const Cha void node_layers::active(layer_handle Layer) { Layer = resolve(Layer); - if (false == valid(Layer) || layer(Layer).removed || false == writable(Layer)) + if (false == accepts(Layer)) { return; } @@ -422,6 +497,11 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str Reason = "the parent layer can't include other files"; return null_handle; } + if (layer(Parent).late) + { + Reason = "the parent layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow"; + return null_handle; + } // file names in the scenery are processed in lower case, with forward slashes Name = ToLower(Name); replace_slashes(Name); @@ -458,10 +538,11 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str return null_handle; } - // the include directive goes after the last file included by the parent, so the scenery files stay ahead of - // what may follow them in a scenario, like the vehicles + // the include directive goes after the last file included by the parent ahead of the scenario initialization, + // so the scenery files stay ahead of what follows them in a scenario, like the vehicles auto anchor{null_handle}; std::streamoff anchorend{-1}; + auto const &init{layer(Parent).init}; for (std::size_t idx = 0; idx < m_layers.size(); ++idx) { auto const &sibling{m_layers[idx]}; @@ -470,7 +551,7 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str continue; } auto const &site{sibling.sites.front()}; - if (site.parent == Parent && false == site.fixed && site.span.end > anchorend) + if (site.parent == Parent && false == site.fixed && site.span.end > anchorend && (false == init.valid() || site.span.end <= init.begin)) { anchor = static_cast(idx + 1); anchorend = site.span.end; @@ -486,7 +567,7 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str created.editormade = true; created.anchor = anchor; // placement the new file is going to be loaded with - created.context_begin = valid(anchor) ? layer(anchor).context_end : layer(Parent).context_end; + created.context_begin = valid(anchor) ? layer(anchor).context_end : layer(Parent).context_insert(); created.context_end = created.context_begin; active(layerhandle); @@ -609,9 +690,16 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin Reason = "the target layer is included by the merged one"; return false; } - if (false == source.context_end.matches(target.context_end)) + if (target.late != source.late && (source.created || source.sites.empty() || resolve(source.sites.front().parent) != Target)) { - // directives of includes placed in the editor are prepared for the end of their layer file + // the content would change sides of the scenario initialization + Reason = (target.late ? "the target layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow" : + "the merged layer is loaded after the scenario initialization (FirstInit), its content has to stay there"); + return false; + } + if (false == source.context_insert().matches(target.context_insert())) + { + // directives of includes placed in the editor are prepared for the place their layer file receives them at auto const unsaved{std::any_of(std::begin(m_instances), std::end(m_instances), [&](include_instance const &Instance) { return false == Instance.dead && false == Instance.removed && false == Instance.span.valid() && resolve(Instance.layer) == Source; })}; @@ -642,9 +730,9 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin } if (resolve(source.sites.front().parent) != Target) { - // the text is moved to the end of another file. its statements have to keep their meaning there, + // the text is moved to another file. its statements have to keep their meaning there, // and can't leave anything behind for what used to follow it - if (false == source.context_begin.matches(target.context_end) || false == source.context_end.matches(source.context_begin)) + if (false == source.context_begin.matches(target.context_insert()) || false == source.context_end.matches(source.context_begin)) { Reason = "the layers are loaded with different origin, rotation or scale"; return false; @@ -779,7 +867,7 @@ void node_layers::removed(instance_handle Instance, bool Removed) instance_handle node_layers::place(layer_handle Layer, std::string const &File, std::string const &Directive) { Layer = resolve(Layer); - if (false == valid(Layer) || m_instances.size() + 1 >= untracked_instance) + if (false == accepts(Layer) || m_instances.size() + 1 >= untracked_instance) { return 0; } @@ -788,7 +876,7 @@ instance_handle node_layers::place(layer_handle Layer, std::string const &File, included.layer = Layer; included.file = &(*m_templates.emplace(File).first); included.directive = Directive; - included.context = layer(Layer).context_end; + included.context = layer(Layer).context_insert(); return static_cast(m_instances.size()); } diff --git a/scene/scenelayers.h b/scene/scenelayers.h index eacc6421..211de91f 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -81,6 +81,12 @@ struct basic_layer std::vector sites; // include directives bringing the file in; empty for the scenario file layer_context context_begin; // placement in effect at the start of the file... layer_context context_end; // ...and right after its end + // location in the file of what performs the scenario initialization, if the file takes a part in it: the FirstInit + // directive, or the directive including the file which holds it. only vehicles are meant to follow the + // initialization, so what the editor adds to the file goes ahead of this point rather than at the end + source_span init; + layer_context context_init; // placement in effect at that point + bool late{false}; // the file is loaded after the scenario initialization std::uintmax_t filesize{0}; // state of the file when it was loaded or saved the last time std::filesystem::file_time_type filetime{}; // pending changes, applied by the scenery save @@ -92,6 +98,11 @@ struct basic_layer bool dead{false}; // removal or merge of the layer was saved, it's no longer a part of the scenery std::size_t item_count() const; + // placement in effect at the point of the file which receives the content added by the editor + layer_context const &context_insert() const + { + return init.valid() ? context_init : context_end; + } }; // include directive bringing an *.inc template into a scenery layer file @@ -235,6 +246,11 @@ class node_layers void track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles = glm::vec3{0.f}, glm::vec3 const &Scale = glm::vec3{1.f}); // indicates specified node was removed from the scene void forget(basic_node const *Node); + // indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive. + // Infile: the directive comes straight from the layer file being loaded + void initialization(source_span const &Span, bool Infile); + // true if specified layer can receive content added in the editor. optionally explains why it can't + bool accepts(layer_handle Layer, std::string *Reason = nullptr) const; // notes the scenery already holds the directive setting up terrain made in the editor void terrain_directive(bool const Present) { @@ -317,6 +333,7 @@ class node_layers std::set m_templates; // *.inc templates used by the scenery std::vector m_instances; // includes of the templates; instance handle is index in the vector + 1 instance_handle m_instance{0}; // helper, include whose template is being loaded + bool m_initialized{false}; // helper, the scenario initialization was performed already bool m_terraindirective{false}; }; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 3a4071a2..ade1b884 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -304,28 +304,61 @@ struct node_layers::composition std::vector nodes; std::vector sites; std::vector instances; + source_span init; // location of what performs the scenario initialization, see basic_layer::init // adds another piece of text at the end void append(composition const &Other) { - auto const offset{static_cast(text.size())}; - text += Other.text; + insert(static_cast(text.size()), Other); + } + // adds another piece of text at specified location + void insert(std::streamoff const Location, composition const &Other) + { + auto const size{static_cast(Other.text.size())}; + auto const move = [=](source_span &Span) { + if (Span.begin >= Location) + { + Span.begin += size; + Span.end += size; + } + }; + for (auto &place : nodes) + { + move(place.span); + } + for (auto &place : sites) + { + move(place.span); + } + for (auto &place : instances) + { + move(place.span); + } + if (init.valid()) + { + move(init); + } + else if (Other.init.valid()) + { + init = {Other.init.begin + Location, Other.init.end + Location}; + } + text.insert(static_cast(Location), Other.text); for (auto place : Other.nodes) { - place.span.begin += offset; - place.span.end += offset; + place.span.begin += Location; + place.span.end += Location; nodes.emplace_back(place); } for (auto place : Other.sites) { - place.span.begin += offset; - place.span.end += offset; + place.span.begin += Location; + place.span.end += Location; sites.emplace_back(place); } for (auto place : Other.instances) { - place.span.begin += offset; - place.span.end += offset; + place.span.begin += Location; + place.span.end += Location; instances.emplace_back(place); } } @@ -361,12 +394,32 @@ bool node_layers::compose(save_state &State, layer_handle const Layer, compositi { edits = lookup->second.edits; } + // what's added to the file goes ahead of the scenario initialization, if the file takes a part in it + auto const &init{layer(Layer).init}; + auto const hasinit{init.valid() && init.end <= basesize}; + auto addition{basesize}; + if (hasinit) + { + addition = init.begin; + // on a line of its own, where the initialization has one + auto linebegin{addition}; + while (linebegin > 0 && (base[linebegin - 1] == ' ' || base[linebegin - 1] == '\t')) + { + --linebegin; + } + if (linebegin == 0 || base[linebegin - 1] == '\n') + { + addition = linebegin; + } + } + auto initedited{false}; // the directive standing for the initialization is replaced or removed for (auto &edit : edits) { if (edit.begin < 0) { - edit.begin = edit.end = basesize; + edit.begin = edit.end = addition; } + initedited |= hasinit && edit.begin == init.begin && edit.end == init.end; } std::stable_sort(std::begin(edits), std::end(edits), [](file_patch::edit const &Left, file_patch::edit const &Right) { return Left.begin < Right.begin; }); std::streamoff cursor{0}; @@ -502,6 +555,11 @@ bool node_layers::compose(save_state &State, layer_handle const Layer, compositi Output.instances.push_back({included, shift(instance(included).span)}); } } + // NOTE: with the file holding the initialization inlined, its own mark was carried over along with its text + if (hasinit && false == initedited && false == Output.init.valid()) + { + Output.init = shift(init); + } return true; } @@ -635,11 +693,11 @@ save_result node_layers::save(std::vector const &Rootstatements) } else if (Model != nullptr && false == Node->from_template()) { - // created in the editor. the definition is placed at the end of the layer file, with the placement in effect there + // created in the editor. the definition is added to the layer file, with the placement in effect at the place it goes to std::string text; Node->export_as_text(text); text.erase(text.find_last_not_of(" \t\r\n") + 1); - auto const &context{layer(nodelayer).context_end}; + auto const &context{layer(nodelayer).context_insert()}; if (false == context.matches(layer_context())) { model_placement placement; @@ -716,10 +774,10 @@ save_result node_layers::save(std::vector const &Rootstatements) { continue; } - if (false == included.context.matches(layer(target).context_end)) + if (false == included.context.matches(layer(target).context_insert())) { // shouldn't happen, merge of layers is refused when it'd lead to this - return fail("include \"" + *included.file + "\" was placed for different origin, rotation or scale than its layer ends with"); + return fail("include \"" + *included.file + "\" was placed for different origin, rotation or scale than its layer file receives it with"); } placed[target].emplace_back(static_cast(idx + 1)); } @@ -790,6 +848,7 @@ save_result node_layers::save(std::vector const &Rootstatements) } else { + // NOTE: negative location stands for the place the file receives additions at edit.begin = edit.end = -1; if (false == layer(parent).created) { @@ -797,12 +856,9 @@ save_result node_layers::save(std::vector const &Rootstatements) { return fail(state.error); } - auto const &content{state.content[parent]}; - if (false == content.empty() && content.back() != '\n') - { - edit.text = state.eol; - edit.lead = state.eol.size(); - } + // dropped by compose() if the text ahead ends with a line break already + edit.text = state.eol; + edit.lead = state.eol.size(); } edit.text += directive + state.eol; } @@ -878,6 +934,8 @@ save_result node_layers::save(std::vector const &Rootstatements) return fail(state.error); } text.append(body); + // what the editor adds to the file + composition added; if (auto const lookup{appended.find(output)}; lookup != appended.end()) { for (auto const merged : lookup->second) @@ -887,18 +945,18 @@ save_result node_layers::save(std::vector const &Rootstatements) { return fail(state.error); } - ensure_newline(text.text, eol); - text.append(mergedtext); + ensure_newline(added.text, eol); + added.append(mergedtext); } } if (auto const lookup{created.find(output)}; lookup != created.end()) { for (auto const &definition : lookup->second) { - ensure_newline(text.text, eol); - auto const offset{static_cast(text.text.size())}; - text.nodes.push_back({definition.first, {offset, offset + static_cast(definition.second.size())}, true}); - text.text += definition.second + eol; + ensure_newline(added.text, eol); + auto const offset{static_cast(added.text.size())}; + added.nodes.push_back({definition.first, {offset, offset + static_cast(definition.second.size())}, true}); + added.text += definition.second + eol; } } if (auto const lookup{placed.find(output)}; lookup != placed.end()) @@ -906,20 +964,49 @@ save_result node_layers::save(std::vector const &Rootstatements) for (auto const included : lookup->second) { auto const &directive{instance(included).directive}; - ensure_newline(text.text, eol); - auto const offset{static_cast(text.text.size())}; - text.instances.push_back({included, {offset, offset + static_cast(directive.size())}}); - text.text += directive + eol; + ensure_newline(added.text, eol); + auto const offset{static_cast(added.text.size())}; + added.instances.push_back({included, {offset, offset + static_cast(directive.size())}}); + added.text += directive + eol; } } if (output == root) { for (auto const &statement : Rootstatements) { - ensure_newline(text.text, eol); - text.text += statement + eol; + ensure_newline(added.text, eol); + added.text += statement + eol; } } + if (added.text.empty()) + { + continue; + } + if (false == text.init.valid()) + { + ensure_newline(text.text, eol); + text.append(added); + continue; + } + // the scenario initialization is meant to be followed by nothing but the vehicles, so the additions go ahead + // of it. they take whole lines, with the initialization left at the start of a line the way it was + ensure_newline(added.text, eol); + auto location{text.init.begin}; + while (location > 0 && (text.text[location - 1] == ' ' || text.text[location - 1] == '\t')) + { + --location; + } + if (location == 0 || text.text[location - 1] == '\n') + { + text.insert(location, added); + } + else + { + composition separated; + separated.text = eol; + separated.append(added); + text.insert(text.init.begin, separated); + } } // with everything worked out it's safe to touch the files. new content goes to temporary files first, @@ -1007,7 +1094,7 @@ save_result node_layers::save(std::vector const &Rootstatements) source.span = place.span; if (isnew) { - source.context = layer(output).context_end; + source.context = layer(output).context_insert(); } if (isnew || place.rewritten) { @@ -1036,6 +1123,19 @@ save_result node_layers::save(std::vector const &Rootstatements) included.sites.front().span = place.span; included.parent = output; } + // the mark of the scenario initialization moved along with the text around it, or came with the text of a merged layer + auto &saved{layer(output)}; + if (false == saved.init.valid() && texts[output].init.valid()) + { + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + if (m_layers[idx].init.valid() && m_layers[idx].merged != null_handle && resolve(static_cast(idx + 1)) == output) + { + saved.context_init = m_layers[idx].context_init; + } + } + } + saved.init = texts[output].init; } for (std::size_t idx = 0; idx < m_layers.size(); ++idx) { diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 459e7755..9e90941f 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -379,6 +379,9 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc if( true == Scratchpad.initialized ) { return; } + // scenery opened for editing: what the editor adds to the scenery files is kept ahead of the initialization + scene::Layers.initialization( { Input.TokenBegin(), Input.TokenEnd() }, Input.InLayerFile() ); + if( true == Scratchpad.binary.terrain ) { // at this stage it should be safe to import terrain from the binary scene file // TBD: postpone loading furter and only load required blocks during the simulation?