17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02:00

feat(editor): keep content added by the editor ahead of FirstInit

This commit is contained in:
2026-10-03 01:59:13 +02:00
parent ced26d171f
commit 521c4f9ebb
6 changed files with 266 additions and 50 deletions

View File

@@ -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<std::size_t>(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<std::streamsize>(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<layer_handle>(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<instance_handle>(m_instances.size());
}

View File

@@ -81,6 +81,12 @@ struct basic_layer
std::vector<include_site> 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<std::string> m_templates; // *.inc templates used by the scenery
std::vector<include_instance> 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};
};

View File

@@ -304,28 +304,61 @@ struct node_layers::composition
std::vector<node_place> nodes;
std::vector<site_place> sites;
std::vector<instance_place> 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<std::streamoff>(text.size())};
text += Other.text;
insert(static_cast<std::streamoff>(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<std::streamoff>(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<std::size_t>(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<std::string> 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<std::string> 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<instance_handle>(idx + 1));
}
@@ -790,6 +848,7 @@ save_result node_layers::save(std::vector<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::streamoff>(text.text.size())};
text.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(definition.second.size())}, true});
text.text += definition.second + eol;
ensure_newline(added.text, eol);
auto const offset{static_cast<std::streamoff>(added.text.size())};
added.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(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<std::string> const &Rootstatements)
for (auto const included : lookup->second)
{
auto const &directive{instance(included).directive};
ensure_newline(text.text, eol);
auto const offset{static_cast<std::streamoff>(text.text.size())};
text.instances.push_back({included, {offset, offset + static_cast<std::streamoff>(directive.size())}});
text.text += directive + eol;
ensure_newline(added.text, eol);
auto const offset{static_cast<std::streamoff>(added.text.size())};
added.instances.push_back({included, {offset, offset + static_cast<std::streamoff>(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<std::string> 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<std::string> 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<layer_handle>(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)
{