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

feat(editor): layers window, scenery save and include descriptions

This commit is contained in:
2026-10-02 22:40:52 +02:00
parent 73f2f68aa0
commit 3199f3796e
18 changed files with 3045 additions and 123 deletions

View File

@@ -14,22 +14,44 @@ http://mozilla.org/MPL/2.0/.
#include "model/AnimModel.h"
#include "world/Track.h"
#include "world/Traction.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include <functional>
namespace scene
{
node_layers Layers;
// converts world space location to the one which produces it when loaded with this context in effect
glm::dvec3 layer_context::to_local(glm::dvec3 Location) const
{
// reverse of the transformation done by the scenery loader: rotation around the vertical axis, then scale, then offset
Location -= offset;
Location /= glm::dvec3{scale};
if (rotation != glm::vec3{0.f})
{
Location = glm::rotateY<double>(Location, -glm::radians(static_cast<double>(rotation.y)));
}
return Location;
}
bool layer_context::matches(layer_context const &Other) const
{
return glm::all(glm::epsilonEqual(offset, Other.offset, 1e-4)) && glm::all(glm::epsilonEqual(rotation, Other.rotation, 1e-4f)) && glm::all(glm::epsilonEqual(scale, Other.scale, 1e-5f));
}
std::size_t basic_layer::item_count() const
{
return std::accumulate(std::begin(items), std::end(items), std::size_t{0});
}
// indicates start of content of specified scenery file. returns: handle to the layer of the file
// indicates start of content of the scenario file. returns: handle to its layer
layer_handle node_layers::open(std::string const &Name)
{
// the same file can be included more than once, all its content goes to a single layer then
auto const lookup{std::find_if(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name; })};
auto const lookup{std::find_if(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })};
auto layerhandle{static_cast<layer_handle>(std::distance(std::begin(m_layers), lookup) + 1)};
if (lookup == std::end(m_layers))
{
@@ -38,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();
stat(layerhandle);
}
else
{
@@ -45,18 +68,39 @@ layer_handle node_layers::open(std::string const &Name)
layerhandle = handle();
}
}
if (valid(layerhandle) && layerhandle != handle())
{
layer(layerhandle).context_begin = m_context;
}
// NOTE: the stack receives an entry even if the layer wasn't created, to keep it in sync with close() calls
m_stack.emplace_back(layerhandle);
return layerhandle;
}
// indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file
layer_handle node_layers::open(std::string const &Name, include_site Site)
{
Site.parent = handle();
auto const layerhandle{open(Name)};
if (valid(layerhandle) && layerhandle != Site.parent)
{
layer(layerhandle).sites.emplace_back(Site);
}
return layerhandle;
}
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
layer_handle node_layers::close()
{
if (false == m_stack.empty())
{
auto const closing{m_stack.back()};
m_stack.pop_back();
if (valid(closing) && closing != handle())
{
layer(closing).context_end = m_context;
}
}
return handle();
}
@@ -66,22 +110,59 @@ void node_layers::clear()
m_layers.clear();
m_stack.clear();
m_active = null_handle;
m_context = layer_context();
m_sources.clear();
m_erased.clear();
m_templates.clear();
m_terraindirective = false;
}
// follows layer merges. returns: handle of the layer which currently holds content of specified layer
layer_handle node_layers::resolve(layer_handle Layer) const
{
// NOTE: merge() rules out cycles, the counter is only a safeguard
for (auto hops = 0; hops < 64 && valid(Layer) && layer(Layer).merged != null_handle; ++hops)
{
Layer = layer(Layer).merged;
}
return Layer;
}
int node_layers::depth(layer_handle Layer) const
{
auto depth{0};
// NOTE: parent is always created before its children, which rules out cycles
while (valid(Layer) && layer(Layer).parent != null_handle && layer(Layer).parent < Layer)
Layer = resolve(Layer);
while (valid(Layer) && layer(Layer).parent != null_handle && depth < 64)
{
Layer = layer(Layer).parent;
Layer = resolve(layer(Layer).parent);
++depth;
}
return depth;
}
void node_layers::count(layer_handle const Layer, layer_item const Item, int const Change)
std::vector<layer_handle> node_layers::tree() const
{
std::vector<layer_handle> order;
order.reserve(m_layers.size());
// NOTE: the layers are few, plain scans do
std::function<void(layer_handle)> const add = [&](layer_handle const Parent) {
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
if (listed(candidate) && resolve(layer(candidate).parent) == Parent && order.size() < m_layers.size())
{
order.emplace_back(candidate);
add(candidate);
}
}
};
add(null_handle);
return order;
}
void node_layers::count(layer_handle Layer, layer_item const Item, int const Change)
{
Layer = resolve(Layer);
if (false == valid(Layer))
{
return;
@@ -97,9 +178,10 @@ void node_layers::count(layer_handle const Layer, layer_item const Item, int con
}
}
void node_layers::active(layer_handle const Layer)
void node_layers::active(layer_handle Layer)
{
if (false == valid(Layer))
Layer = resolve(Layer);
if (false == valid(Layer) || layer(Layer).removed || false == writable(Layer))
{
return;
}
@@ -110,16 +192,17 @@ void node_layers::active(layer_handle const Layer)
}
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
void node_layers::visible(layer_handle const Layer, bool const Visible)
void node_layers::visible(layer_handle Layer, bool const Visible)
{
Layer = resolve(Layer);
if (false == valid(Layer) || layer(Layer).visible == Visible)
{
return;
}
layer(Layer).visible = Visible;
auto const update_node = [Layer, Visible](basic_node *Node) {
if (Node == nullptr || Node->layer() != Layer)
auto const update_node = [this, Layer, Visible](basic_node *Node) {
if (Node == nullptr || resolve(Node->layer()) != Layer)
{
return;
}
@@ -153,19 +236,101 @@ void node_layers::visible(layer_handle const Layer, bool const Visible)
}
}
void node_layers::locked(layer_handle const Layer, bool const Locked)
void node_layers::locked(layer_handle Layer, bool const Locked)
{
Layer = resolve(Layer);
if (valid(Layer))
{
layer(Layer).locked = Locked;
}
}
// true if nodes of specified layer can be selected and modified in the editor
bool node_layers::editable(layer_handle const Layer) const
// true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't
bool node_layers::writable(layer_handle Layer, std::string *Reason) const
{
Layer = resolve(Layer);
if (false == valid(Layer))
{
return false;
}
auto const &target{layer(Layer)};
char const *reason{nullptr};
if (target.binary)
{
reason = "the file is loaded from binary terrain cache";
}
else if (target.sites.size() > 1)
{
// one definition yields several nodes, there's no telling which of them should be written back
reason = "the file is included more than once";
}
else if (false == target.sites.empty() && target.sites.front().parameters)
{
reason = "the file is included with parameters";
}
if (reason != nullptr && Reason != nullptr)
{
*Reason = reason;
}
return reason == nullptr;
}
// true if nodes of specified layer can be selected and modified in the editor
bool node_layers::editable(layer_handle Layer) const
{
Layer = resolve(Layer);
// nodes without a layer (regular load, or created by the simulation) aren't restricted
return false == valid(Layer) || (layer(Layer).visible && false == layer(Layer).locked);
return false == valid(Layer) || (layer(Layer).visible && false == layer(Layer).locked && false == layer(Layer).removed && writable(Layer));
}
// true if specified node can be selected and modified in the editor. optionally explains why it can't
bool node_layers::editable(basic_node const *Node, std::string *Reason) const
{
if (Node == nullptr)
{
return false;
}
auto const nodelayer{resolve(Node->layer())};
if (false == valid(nodelayer))
{
return true;
}
std::string reason;
if (Node->m_template)
{
reason = "it's a part of an include template (.inc)";
}
else if (false == writable(nodelayer, &reason))
{
// reason was filled by the call
}
else if (layer(nodelayer).locked)
{
reason = "its layer is locked";
}
else if (false == layer(nodelayer).visible || layer(nodelayer).removed)
{
reason = "its layer is hidden";
}
if (false == reason.empty() && Reason != nullptr)
{
*Reason = reason;
}
return reason.empty();
}
// moves specified node to specified layer
void node_layers::move(basic_node *Node, layer_handle Layer)
{
Layer = resolve(Layer);
if (Node == nullptr || false == valid(Layer) || resolve(Node->layer()) == Layer)
{
return;
}
// NOTE: the only nodes the editor creates and moves between layers are model instances
count(Node->layer(), layer_item::model, -1);
Node->layer(Layer);
count(Layer, layer_item::model, 1);
}
std::vector<layer_handle> node_layers::hidden() const
@@ -185,10 +350,323 @@ void node_layers::show_all()
{
for (auto const hiddenlayer : hidden())
{
visible(hiddenlayer, true);
// layers dropped from the scenery stay out of sight
if (false == layer(hiddenlayer).removed)
{
visible(hiddenlayer, true);
}
}
}
// stores origin of a node defined directly in a layer file, to allow rewriting its definition on save
void node_layers::track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles, glm::vec3 const &Scale)
{
if (Node == nullptr || Node->m_template || false == valid(Node->layer()) || false == Span.valid())
{
return;
}
m_sources[Node] = {Node->layer(), Span, m_context, Node->location(), Angles, Scale};
}
// indicates specified node was removed from the scene
void node_layers::forget(basic_node const *Node)
{
auto const lookup{m_sources.find(Node)};
if (lookup == m_sources.end())
{
return;
}
m_erased.emplace_back(lookup->second.layer, lookup->second.span);
m_sources.erase(lookup);
}
bool node_layers::is_descendant(layer_handle Layer, layer_handle const Ancestor) const
{
// NOTE: content of a merged layer, includes of other files among it, belongs to the layer it was merged into
for (auto hops = 0; hops < 64 && valid(Layer); ++hops)
{
Layer = resolve(layer(Layer).parent);
if (Layer == Ancestor)
{
return true;
}
}
return false;
}
std::string node_layers::path(layer_handle const Layer) const
{
return Global.asCurrentSceneryPath + layer(Layer).name;
}
// stores current state of the layer file, to detect later whether it was changed behind our back
void node_layers::stat(layer_handle const Layer)
{
std::error_code error;
auto const filepath{std::filesystem::path(path(Layer))};
auto const filesize{std::filesystem::file_size(filepath, error)};
layer(Layer).filesize = error ? 0 : filesize;
auto const filetime{std::filesystem::last_write_time(filepath, error)};
layer(Layer).filetime = error ? std::filesystem::file_time_type{} : filetime;
}
// creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason
layer_handle node_layers::create(std::string Name, layer_handle Parent, std::string &Reason)
{
Parent = resolve(Parent);
if (false == valid(Parent) || layer(Parent).removed || layer(Parent).binary)
{
Reason = "the parent layer can't include other files";
return null_handle;
}
// file names in the scenery are processed in lower case, with forward slashes
Name = ToLower(Name);
replace_slashes(Name);
Name.erase(0, Name.find_first_not_of(" \t"));
Name.erase(Name.find_last_not_of(" \t") + 1);
if (Name.empty())
{
Reason = "the layer needs a file name";
return null_handle;
}
if (Name.find_first_of(" \t\";:*?<>|") != std::string::npos || Name.front() == '/' || Name.back() == '/' || contains(Name, ".."))
{
Reason = "the file name has to be a plain path inside the scenery directory, without spaces";
return null_handle;
}
if (std::filesystem::path(Name).extension().empty())
{
Name += ".scm";
}
if (Name.ends_with(".inc"))
{
Reason = ".inc files are include templates, not layers";
return null_handle;
}
if (m_layers.size() >= static_cast<std::size_t>(std::numeric_limits<layer_handle>::max()))
{
Reason = "too many layers";
return null_handle;
}
auto const taken{std::any_of(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })};
if (taken || FileExists(Global.asCurrentSceneryPath + Name))
{
Reason = "the file \"" + Name + "\" already exists";
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
auto anchor{null_handle};
std::streamoff anchorend{-1};
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const &sibling{m_layers[idx]};
if (sibling.dead || sibling.created || sibling.sites.size() != 1)
{
continue;
}
auto const &site{sibling.sites.front()};
if (site.parent == Parent && false == site.fixed && site.span.end > anchorend)
{
anchor = static_cast<layer_handle>(idx + 1);
anchorend = site.span.end;
}
}
m_layers.emplace_back();
auto const layerhandle{static_cast<layer_handle>(m_layers.size())};
auto &created{m_layers.back()};
created.name = Name;
created.parent = Parent;
created.created = true;
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_end = created.context_begin;
active(layerhandle);
return layerhandle;
}
bool node_layers::can_remove(layer_handle Layer, std::string &Reason) const
{
if (false == listed(Layer) || layer(Layer).removed)
{
Reason = "no such layer";
return false;
}
auto const &target{layer(Layer)};
if (target.created)
{
return true;
}
if (target.sites.empty())
{
Reason = "the scenario file can't be removed";
return false;
}
if (target.binary)
{
Reason = "the file is loaded from binary terrain cache";
return false;
}
if (target.sites.size() > 1)
{
Reason = "the file is included more than once";
return false;
}
if (target.sites.front().fixed)
{
Reason = "the file is included by an include template (.inc)";
return false;
}
return true;
}
// marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk
bool node_layers::remove(layer_handle Layer)
{
std::string reason;
if (false == can_remove(Layer, reason))
{
return false;
}
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
// layers merged elsewhere keep going, their content is no longer a part of the removed file
if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer)))
{
continue;
}
visible(candidate, false);
layer(candidate).removed = true;
if (m_active == candidate)
{
m_active = null_handle;
}
}
if (m_active == null_handle)
{
// fall back on the scenario file, which is always there
active(1);
}
return true;
}
// takes back remove() which wasn't saved yet
void node_layers::restore(layer_handle Layer)
{
if (false == valid(Layer) || false == layer(Layer).removed || layer(Layer).dead)
{
return;
}
// a layer can't be brought back without the file which includes it
auto const parent{resolve(layer(Layer).parent)};
if (valid(parent) && layer(parent).removed)
{
restore(parent);
}
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer)))
{
continue;
}
layer(candidate).removed = false;
visible(candidate, true);
}
}
bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const
{
if (false == listed(Source) || false == listed(Target) || layer(Source).removed || layer(Target).removed)
{
Reason = "no such layer";
return false;
}
if (Source == Target)
{
Reason = "pick another layer to merge into";
return false;
}
auto const &source{layer(Source)};
auto const &target{layer(Target)};
if (false == writable(Target, &Reason))
{
return false;
}
if (is_descendant(Target, Source))
{
// the merged text would include the file it was put in
Reason = "the target layer is included by the merged one";
return false;
}
if (source.created)
{
// nothing but the nodes made in the editor to move
return true;
}
if (source.sites.empty())
{
Reason = "the scenario file can't be merged into another layer";
return false;
}
if (false == writable(Source, &Reason))
{
return false;
}
if (source.sites.front().fixed)
{
Reason = "the file is included by an include template (.inc)";
return false;
}
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,
// 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))
{
Reason = "the layers are loaded with different origin, rotation or scale";
return false;
}
}
return true;
}
// moves content of the source layer to the target layer
bool node_layers::merge(layer_handle Source, layer_handle Target)
{
std::string reason;
if (false == can_merge(Source, Target, reason))
{
return false;
}
// start with the same visibility on both sides, as it's kept per node
visible(Source, true);
visible(Target, true);
for (std::size_t idx = 0; idx < layer(Source).items.size(); ++idx)
{
layer(Target).items[idx] += layer(Source).items[idx];
}
layer(Source).merged = Target;
if (m_active == Source)
{
active(Target);
}
return true;
}
// true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes
bool node_layers::pending() const
{
return std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); });
}
} // namespace scene
//---------------------------------------------------------------------------

View File

@@ -11,6 +11,9 @@ http://mozilla.org/MPL/2.0/.
#include "scene/scenenode.h"
#include <filesystem>
#include <set>
namespace scene
{
@@ -31,6 +34,39 @@ enum class layer_item
count_
};
// placement applied to scenery statements by the origin, rotate and scale directives in effect
struct layer_context
{
glm::dvec3 offset{0.0};
glm::vec3 rotation{0.f};
glm::vec3 scale{1.f};
// converts world space location to the one which produces it when loaded with this context in effect
glm::dvec3 to_local(glm::dvec3 Location) const;
bool matches(layer_context const &Other) const;
};
// location of a piece of text in a scenery file, as byte offsets
struct source_span
{
std::streamoff begin{-1};
std::streamoff end{-1};
bool valid() const
{
return begin >= 0 && end > begin;
}
};
// include directive bringing a scenery file into another one
struct include_site
{
layer_handle parent{null_handle}; // layer of the file holding the directive
source_span span; // location of the whole directive in that file
bool parameters{false}; // the file is included with parameters
bool fixed{false}; // the directive can't be rewritten: it's a part of an *.inc template, or its location is unknown
};
// scenery layer: content of a single scenery file (.scn, .scm, .ctr...) other than an .inc node template
struct basic_layer
{
@@ -40,18 +76,51 @@ struct basic_layer
bool locked{false}; // items of locked layer can't be selected in the editor
bool binary{false}; // the file wasn't parsed, its content came from binary terrain file
std::array<std::size_t, static_cast<std::size_t>(layer_item::count_)> items{}; // amount of items of each kind
// source data, used by the scenery save
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
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
bool created{false}; // layer was created in the editor, its file doesn't exist yet
bool editormade{false}; // the file was made by the editor during this session, there's no original content to keep a backup of
layer_handle anchor{null_handle}; // for created layer: sibling its include directive goes after
bool removed{false}; // layer is to be dropped from the scenery
layer_handle merged{null_handle}; // layer which took over the content of this one
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;
};
// origin of a node defined in a scenery layer file, with the placement it was loaded with
struct node_source
{
layer_handle layer{null_handle}; // layer of the file holding the node definition
source_span span; // location of the definition in that file
layer_context context; // placement in effect at the definition
glm::dvec3 location{0.0};
glm::vec3 angles{0.f};
glm::vec3 scale{1.f};
};
struct save_result
{
bool success{false};
std::vector<std::string> files; // names of saved files
std::string message; // summary, or cause of the failure
};
// holds scenery layers, established during load of scenery opened for editing (-edit)
// NOTE: regular load doesn't create any layers, all nodes are left with null_handle
class node_layers
{
public:
// methods
// indicates start of content of specified scenery file. returns: handle to the layer of the file
// indicates start of content of the scenario file. returns: handle to its layer
layer_handle open(std::string const &Name);
// indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file
layer_handle open(std::string const &Name, include_site Site);
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
layer_handle close();
// returns layer of the scenery file being loaded, or null_handle if there's none
@@ -82,32 +151,110 @@ class node_layers
{
return m_layers[Layer - 1];
}
// follows layer merges. returns: handle of the layer which currently holds content of specified layer
layer_handle resolve(layer_handle Layer) const;
// true if the layer is a part of the scenery and should be presented to the user
bool listed(layer_handle const Layer) const
{
return valid(Layer) && false == layer(Layer).dead && layer(Layer).merged == null_handle;
}
// number of layers between specified layer and the scenario file
int depth(layer_handle Layer) const;
// handles of the layers to present to the user, each followed by the layers it includes
std::vector<layer_handle> tree() const;
// adjusts item count of specified layer
void count(layer_handle const Layer, layer_item const Item, int const Change = 1);
void count(layer_handle Layer, layer_item const Item, int const Change = 1);
// layer receiving the items created in the editor, or null_handle if there's none
layer_handle active() const
{
return m_active;
}
// NOTE: active layer is made visible and unlocked
void active(layer_handle const Layer);
void active(layer_handle Layer);
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
void visible(layer_handle const Layer, bool const Visible);
void locked(layer_handle const Layer, bool const Locked);
void visible(layer_handle Layer, bool const Visible);
void locked(layer_handle Layer, bool const Locked);
// true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't
bool writable(layer_handle Layer, std::string *Reason = nullptr) const;
// true if nodes of specified layer can be selected and modified in the editor
bool editable(layer_handle const Layer) const;
bool editable(layer_handle Layer) const;
// true if specified node can be selected and modified in the editor. optionally explains why it can't
bool editable(basic_node const *Node, std::string *Reason = nullptr) const;
// moves specified node to specified layer
void move(basic_node *Node, layer_handle Layer);
// handles of layers which are currently hidden
std::vector<layer_handle> hidden() const;
// restores visibility of all layers
void show_all();
// scenery load bookkeeping
// registers *.inc template used by the scenery
void template_used(std::string const &Name)
{
m_templates.emplace(Name);
}
std::set<std::string> const &templates() const
{
return m_templates;
}
// updates placement in effect for the statements which follow
void context(layer_context const &Context)
{
m_context = Context;
}
// stores origin of a node defined directly in a layer file, to allow rewriting its definition on save
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);
// notes the scenery already holds the directive setting up terrain made in the editor
void terrain_directive(bool const Present)
{
m_terraindirective = Present;
}
bool terrain_directive() const
{
return m_terraindirective;
}
// layer management. changes are applied to the scenery files by save()
// creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason
layer_handle create(std::string Name, layer_handle Parent, std::string &Reason);
bool can_remove(layer_handle Layer, std::string &Reason) const;
// marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk
bool remove(layer_handle Layer);
// takes back remove() which wasn't saved yet
void restore(layer_handle Layer);
bool can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const;
// moves content of the source layer to the target layer
bool merge(layer_handle Source, layer_handle Target);
// true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes
bool pending() const;
// true if specified layer is included, directly or not, by the other one
bool is_descendant(layer_handle Layer, layer_handle const Ancestor) const;
// writes changes made in the editor to the scenery files: changed, created and deleted model instances and
// memory cells, and layer changes. the files are left intact unless the whole operation can be performed.
// Rootstatements: additional statements to place at the end of the scenario file
save_result save(std::vector<std::string> const &Rootstatements = {});
private:
// types
struct file_patch;
struct composition;
struct save_state;
// methods
std::string path(layer_handle const Layer) const;
void stat(layer_handle const Layer);
bool compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const;
// members
std::vector<basic_layer> m_layers; // layer handle is index in the vector + 1
std::vector<layer_handle> m_stack; // helper, layers of scenery files being loaded
layer_handle m_active{null_handle};
layer_context m_context; // placement in effect at the current point of scenery load
std::unordered_map<basic_node const *, node_source> m_sources; // origins of nodes which can be rewritten on save
std::vector<std::pair<layer_handle, source_span>> m_erased; // definitions of nodes deleted since the load or the last save
std::set<std::string> m_templates; // *.inc templates used by the scenery
bool m_terraindirective{false};
};
extern node_layers Layers;

972
scene/scenelayerssave.cpp Normal file
View File

@@ -0,0 +1,972 @@
/*
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/scenelayers.h"
#include "simulation/simulation.h"
#include "model/AnimModel.h"
#include "world/MemCell.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/utilities.h"
#include <map>
#include <optional>
#include <set>
// scenery save for sceneries opened for editing.
// the scenery files are patched rather than generated: text of a file is carried over as it is, except for the
// definitions of nodes changed in the editor and the include directives affected by layer changes. this keeps
// everything the editor doesn't understand or didn't touch, comments and formatting included.
namespace scene
{
namespace
{
struct text_token
{
std::size_t begin;
std::size_t end;
std::string text; // lower case
};
struct text_change
{
std::size_t begin;
std::size_t end;
std::string text;
};
bool is_separator(char const Char)
{
return Char == ' ' || Char == '\t' || Char == '\n' || Char == '\r' || Char == ';';
}
// splits scenery text into tokens the same way the scenery parser does: comments are skipped, quoted text is kept together
std::vector<text_token> tokenize(std::string const &Text)
{
std::vector<text_token> tokens;
auto const size{Text.size()};
std::size_t idx{0};
auto const starts = [&](char const *Mark) { return Text.compare(idx, 2, Mark) == 0; };
while (idx < size)
{
if (is_separator(Text[idx]))
{
++idx;
continue;
}
if (starts("//"))
{
idx = Text.find('\n', idx);
idx = (idx == std::string::npos ? size : idx + 1);
continue;
}
if (starts("/*"))
{
idx = Text.find("*/", idx + 2);
idx = (idx == std::string::npos ? size : idx + 2);
continue;
}
auto const begin{idx};
while (idx < size && false == is_separator(Text[idx]) && false == starts("//") && false == starts("/*"))
{
if (Text[idx] == '\"')
{
// quoted part runs up to the closing quote, whatever is inside
++idx;
while (idx < size && Text[idx] != '\"')
{
idx += (Text[idx] == '\\' ? 2 : 1);
}
}
++idx;
}
idx = std::min(idx, size);
tokens.push_back({begin, idx, ToLower(Text.substr(begin, idx - begin))});
}
return tokens;
}
std::string number(double const Value)
{
std::ostringstream converter;
converter.imbue(std::locale::classic());
converter << std::fixed << std::setprecision(3) << Value;
auto text{converter.str()};
return text == "-0.000" ? "0.000" : text;
}
std::string numbers(glm::dvec3 const &Values)
{
return number(Values.x) + ' ' + number(Values.y) + ' ' + number(Values.z);
}
void apply(std::string &Text, std::vector<text_change> Changes)
{
// from the end, so the locations of pending changes stay valid
std::sort(std::begin(Changes), std::end(Changes), [](text_change const &Left, text_change const &Right) { return Left.begin > Right.begin; });
for (auto const &change : Changes)
{
Text.replace(change.begin, change.end - change.begin, change.text);
}
}
// placement to write into definition of a model instance. empty parts are left as they are
struct model_placement
{
std::optional<glm::dvec3> location; // in the coordinates used by the definition
std::optional<glm::vec3> angles; // complete rotation of the instance
glm::vec3 rotation{0.f}; // rotation the loader adds to the angle specified in the definition
std::optional<glm::vec3> scale; // scale factor to put in the definition
};
bool about_equal(float const Left, float const Right)
{
return std::abs(Left - Right) < 1e-3f;
}
// definition layout: node <max> <min> <name> model <x> <y> <z> <angle> <model file> <texture> [options] endmodel
bool patch_model(std::string &Text, model_placement const &Placement, std::string &Error)
{
auto const tokens{tokenize(Text)};
if (tokens.size() < 12 || tokens[0].text != "node" || tokens[4].text != "model" || tokens.back().text != "endmodel")
{
Error = "unexpected layout of model definition";
return false;
}
std::vector<text_change> changes;
auto const replace = [&](std::size_t const Index, std::string const &Value) { changes.push_back({tokens[Index].begin, tokens[Index].end, Value}); };
// optional blocks of three values, located past the texture
auto const last{tokens.size() - 1};
auto const find_block = [&](char const *Keyword) {
for (std::size_t idx = 11; idx + 3 < last; ++idx)
{
if (tokens[idx].text == Keyword)
{
return idx;
}
}
return std::size_t{0};
};
std::string additions; // blocks to insert ahead of the closing keyword
if (Placement.location)
{
replace(5, number(Placement.location->x));
replace(6, number(Placement.location->y));
replace(7, number(Placement.location->z));
}
if (Placement.angles)
{
auto const &angles{*Placement.angles};
auto const block{find_block("angles")};
if (block != 0)
{
// the block specifies complete rotation of the instance, regardless of anything else
replace(block + 1, number(angles.x));
replace(block + 2, number(angles.y));
replace(block + 3, number(angles.z));
}
else if (about_equal(angles.x, Placement.rotation.x) && about_equal(angles.z, Placement.rotation.z))
{
// rotation around the vertical axis alone fits in the basic angle parameter
replace(8, number(angles.y - Placement.rotation.y));
}
else
{
additions += "angles " + numbers(angles) + ' ';
}
}
if (Placement.scale)
{
auto const &scale{*Placement.scale};
auto const unit{about_equal(scale.x, 1.f) && about_equal(scale.y, 1.f) && about_equal(scale.z, 1.f)};
auto const block{find_block("scale")};
if (block != 0)
{
if (unit)
{
// drop the block along with the whitespace separating it from the next token
changes.push_back({tokens[block].begin, tokens[block + 4].begin, ""});
}
else
{
replace(block + 1, number(scale.x));
replace(block + 2, number(scale.y));
replace(block + 3, number(scale.z));
}
}
else if (false == unit)
{
additions += "scale " + numbers(scale) + ' ';
}
}
if (false == additions.empty())
{
changes.push_back({tokens[last].begin, tokens[last].begin, additions});
}
apply(Text, changes);
return true;
}
// definition layout: node <max> <min> <name> memcell <x> <y> <z> <text> <value 1> <value 2> <track> endmemcell
bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &Error)
{
auto const tokens{tokenize(Text)};
if (tokens.size() < 13 || tokens[0].text != "node" || tokens[4].text != "memcell" || tokens.back().text != "endmemcell")
{
Error = "unexpected layout of memory cell definition";
return false;
}
apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}});
return true;
}
std::string detect_eol(std::string const &Text, std::string const &Fallback)
{
auto const linebreak{Text.find('\n')};
if (linebreak == std::string::npos)
{
return Fallback;
}
return (linebreak > 0 && Text[linebreak - 1] == '\r') ? "\r\n" : "\n";
}
void ensure_newline(std::string &Text, std::string const &Eol)
{
if (false == Text.empty() && Text.back() != '\n')
{
Text += Eol;
}
}
bool read_file(std::string const &Path, std::string &Content)
{
std::ifstream input{Path, std::ios_base::binary};
if (false == input.is_open())
{
return false;
}
Content.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
return true;
}
} // namespace
// changes to make in the text of a single scenery file
struct node_layers::file_patch
{
struct edit
{
std::streamoff begin{0}; // replaced range of the original text. empty range inserts, negative location stands for the end of the text
std::streamoff end{0};
std::string text; // replacement
std::size_t lead{0}; // size of the part of replacement preceding the definition it carries
std::size_t length{0}; // size of that definition
basic_node const *node{nullptr}; // node defined by the replacement
layer_handle site{null_handle}; // layer included by the directive in the replacement
layer_handle inlined{null_handle}; // layer whose content replaces the range
};
std::vector<edit> edits;
};
// text of a scenery file after the changes, with the new locations of what it defines
struct node_layers::composition
{
struct node_place
{
basic_node const *node;
source_span span;
bool rewritten;
};
struct site_place
{
layer_handle layer;
source_span span;
};
std::string text;
std::vector<node_place> nodes;
std::vector<site_place> sites;
// 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;
for (auto place : Other.nodes)
{
place.span.begin += offset;
place.span.end += offset;
nodes.emplace_back(place);
}
for (auto place : Other.sites)
{
place.span.begin += offset;
place.span.end += offset;
sites.emplace_back(place);
}
}
};
struct node_layers::save_state
{
std::map<layer_handle, std::string> content; // original text of the files
std::map<layer_handle, file_patch> patches;
std::map<layer_handle, std::vector<basic_node const *>> nodes; // unchanged nodes defined in each file
std::map<layer_handle, std::vector<layer_handle>> includes; // unchanged include directives in each file, by included layer
std::string eol{"\r\n"};
std::string error;
};
// produces text of specified file with the changes applied
bool node_layers::compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const
{
if (Depth > 32)
{
State.error = "layers include each other in a loop";
return false;
}
static std::string const empty;
auto const contentlookup{State.content.find(Layer)};
auto const &base{contentlookup != State.content.end() ? contentlookup->second : empty};
auto const basesize{static_cast<std::streamoff>(base.size())};
auto const eol{detect_eol(base, State.eol)};
std::vector<file_patch::edit> edits;
if (auto const lookup{State.patches.find(Layer)}; lookup != State.patches.end())
{
edits = lookup->second.edits;
}
for (auto &edit : edits)
{
if (edit.begin < 0)
{
edit.begin = edit.end = basesize;
}
}
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};
for (auto const &edit : edits)
{
if (edit.begin < cursor || edit.end < edit.begin || edit.end > basesize)
{
State.error = "conflicting changes in file \"" + layer(Layer).name + "\"";
return false;
}
cursor = edit.end;
}
// locations in the original text past which everything is shifted by given amount
std::vector<std::pair<std::streamoff, std::streamoff>> shifts;
cursor = 0;
for (std::size_t idx = 0; idx < edits.size(); ++idx)
{
auto const &edit{edits[idx]};
auto begin{edit.begin};
auto end{edit.end};
if (edit.text.empty() && edit.inlined == null_handle && end > begin)
{
// a definition is removed. if it had a line for itself take the whole line out, instead of leaving it blank
auto const limit{idx + 1 < edits.size() ? edits[idx + 1].begin : basesize};
auto linebegin{begin};
while (linebegin > cursor && (base[linebegin - 1] == ' ' || base[linebegin - 1] == '\t'))
{
--linebegin;
}
auto lineend{end};
while (lineend < limit && (base[lineend] == ' ' || base[lineend] == '\t'))
{
++lineend;
}
auto const startsline{linebegin == 0 || base[linebegin - 1] == '\n'};
auto endsline{lineend == basesize};
if (lineend + 1 < limit && base[lineend] == '\r' && base[lineend + 1] == '\n')
{
lineend += 2;
endsline = true;
}
else if (lineend < limit && base[lineend] == '\n')
{
lineend += 1;
endsline = true;
}
if (startsline && endsline)
{
begin = linebegin;
end = lineend;
}
}
Output.text.append(base, static_cast<std::size_t>(cursor), static_cast<std::size_t>(begin - cursor));
if (edit.inlined != null_handle)
{
// content of the included file takes place of the include directive
composition included;
if (false == compose(State, edit.inlined, included, Depth + 1))
{
return false;
}
if (end < basesize && (base[end] == '\r' || base[end] == '\n'))
{
// the line break which followed the directive ends the included text now, a second one would leave a blank line
if (false == included.text.empty() && included.text.back() == '\n')
{
included.text.pop_back();
if (false == included.text.empty() && included.text.back() == '\r')
{
included.text.pop_back();
}
}
}
else
{
ensure_newline(included.text, eol);
}
Output.append(included);
}
else
{
// the line break leading the replacement isn't needed if the text ahead happens to end with one
auto const lead{(Output.text.empty() || Output.text.back() == '\n') ? std::size_t{0} : edit.lead};
auto const offset{static_cast<std::streamoff>(Output.text.size() + lead)};
auto const span{source_span{offset, offset + static_cast<std::streamoff>(edit.length)}};
if (edit.node != nullptr)
{
Output.nodes.push_back({edit.node, span, true});
}
if (edit.site != null_handle)
{
Output.sites.push_back({edit.site, span});
}
Output.text.append(edit.text, edit.lead - lead, std::string::npos);
}
cursor = end;
shifts.emplace_back(end, static_cast<std::streamoff>(Output.text.size()) - end);
}
Output.text.append(base, static_cast<std::size_t>(cursor), std::string::npos);
// what the changes didn't touch only moves along with the text around it
auto const shift = [&](source_span Span) {
auto const lookup{std::upper_bound(std::begin(shifts), std::end(shifts), Span.begin,
[](std::streamoff const Location, std::pair<std::streamoff, std::streamoff> const &Shift) { return Location < Shift.first; })};
auto const offset{lookup == std::begin(shifts) ? 0 : std::prev(lookup)->second};
Span.begin += offset;
Span.end += offset;
return Span;
};
if (auto const lookup{State.nodes.find(Layer)}; lookup != State.nodes.end())
{
for (auto const *node : lookup->second)
{
Output.nodes.push_back({node, shift(m_sources.at(node).span), false});
}
}
if (auto const lookup{State.includes.find(Layer)}; lookup != State.includes.end())
{
for (auto const included : lookup->second)
{
Output.sites.push_back({included, shift(layer(included).sites.front().span)});
}
}
return true;
}
// writes changes made in the editor to the scenery files
save_result node_layers::save(std::vector<std::string> const &Rootstatements)
{
save_result result;
if (m_layers.empty())
{
result.message = "The scenery wasn't opened for editing, there are no layers to save.";
return result;
}
auto const root{layer_handle{1}};
save_state state;
auto const fail = [&](std::string const &Message) {
result.message = "Scenery not saved: " + Message;
ErrorLog(result.message);
return result;
};
// loads text of specified layer file, if it wasn't loaded yet
auto const load = [&](layer_handle const Layer) {
if (layer(Layer).created || state.content.count(Layer) != 0)
{
return true;
}
std::error_code error;
auto const filepath{std::filesystem::path(path(Layer))};
auto const filesize{std::filesystem::file_size(filepath, error)};
auto const filetime{error ? std::filesystem::file_time_type{} : std::filesystem::last_write_time(filepath, error)};
std::string content;
if (error || false == read_file(path(Layer), content))
{
state.error = "can't read file \"" + layer(Layer).name + "\"";
return false;
}
if (filesize != layer(Layer).filesize || filetime != layer(Layer).filetime || content.size() != filesize)
{
// locations of the definitions are no longer valid
state.error = "file \"" + layer(Layer).name + "\" was changed outside of the editor since the scenery was loaded";
return false;
}
state.content.emplace(Layer, std::move(content));
return true;
};
// a layer is saved if it's still a part of the scenery, as a file of its own
auto const is_output = [&](layer_handle const Layer) { return listed(Layer) && false == layer(Layer).removed && false == layer(Layer).binary; };
if (false == load(root))
{
return fail(state.error);
}
state.eol = detect_eol(state.content[root], "\r\n");
// changed and deleted nodes
std::set<basic_node const *> rewritten;
std::map<layer_handle, std::vector<std::pair<basic_node const *, std::string>>> created; // new definitions, by target layer
auto const process = [&](basic_node const *Node, TAnimModel const *Model) {
auto const nodelayer{resolve(Node->layer())};
if (false == is_output(nodelayer))
{
return true;
}
auto const location{Node->location()};
auto const angles{Model != nullptr ? Model->Angles() : glm::vec3{0.f}};
auto const scale{Model != nullptr ? Model->Scale() : glm::vec3{1.f}};
auto const lookup{m_sources.find(Node)};
std::string error;
if (lookup != m_sources.end())
{
// defined in a scenery file. rewrite the placement in its definition if it's no longer the one it was loaded with
auto const &source{lookup->second};
if (false == writable(source.layer))
{
// the editor doesn't let these be changed, as one definition stands for more than one node
return true;
}
auto const moved{glm::distance(location, source.location) > 1e-4};
auto const rotated{false == glm::all(glm::epsilonEqual(angles, source.angles, 1e-3f))};
auto const scaled{false == glm::all(glm::epsilonEqual(scale, source.scale, 1e-4f))};
if (false == (moved || rotated || scaled))
{
return true;
}
if (false == load(source.layer))
{
return false;
}
auto const &content{state.content[source.layer]};
if (source.span.end > static_cast<std::streamoff>(content.size()))
{
state.error = "definition of \"" + Node->name() + "\" is out of bounds of file \"" + layer(source.layer).name + "\"";
return false;
}
auto text{content.substr(static_cast<std::size_t>(source.span.begin), static_cast<std::size_t>(source.span.end - source.span.begin))};
auto patched{false};
if (Model != nullptr)
{
model_placement placement;
placement.rotation = source.context.rotation;
if (moved)
{
placement.location = source.context.to_local(location);
}
if (rotated)
{
placement.angles = angles;
}
if (scaled)
{
placement.scale = scale / source.context.scale;
}
patched = patch_model(text, placement, error);
}
else
{
patched = patch_memcell(text, source.context.to_local(location), error);
}
if (false == patched)
{
state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\"";
return false;
}
file_patch::edit edit;
edit.begin = source.span.begin;
edit.end = source.span.end;
edit.length = text.size();
edit.text = std::move(text);
edit.node = Node;
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(Node);
}
else if (Model != nullptr && false == Node->m_template)
{
// created in the editor. the definition is placed at the end of the layer file, with the placement in effect there
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};
if (false == context.matches(layer_context()))
{
model_placement placement;
placement.rotation = context.rotation;
placement.location = context.to_local(location);
placement.angles = angles;
placement.scale = scale / context.scale;
if (false == patch_model(text, placement, error))
{
state.error = error + " of \"" + Node->name() + "\"";
return false;
}
}
created[nodelayer].emplace_back(Node, std::move(text));
}
return true;
};
for (auto const *instance : simulation::Instances.sequence())
{
if (instance != nullptr && false == process(instance, instance))
{
return fail(state.error);
}
}
for (auto const *memorycell : simulation::Memory.sequence())
{
if (memorycell != nullptr && false == process(memorycell, nullptr))
{
return fail(state.error);
}
}
for (auto const &erased : m_erased)
{
file_patch::edit edit;
edit.begin = erased.second.begin;
edit.end = erased.second.end;
state.patches[erased.first].edits.emplace_back(std::move(edit));
}
// layer changes
std::map<layer_handle, std::vector<layer_handle>> appended; // merged layers placed at the end of their target
std::set<layer_handle> absorbed; // merged layers which cease to exist as files
std::set<layer_handle> detached; // layers whose include directives are replaced
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
auto const &source{layer(candidate)};
if (source.dead)
{
continue;
}
auto const hassite{false == source.created && source.sites.size() == 1 && false == source.sites.front().fixed && source.sites.front().span.valid()};
if (source.merged != null_handle)
{
auto const target{resolve(candidate)};
absorbed.emplace(candidate);
if (hassite)
{
file_patch::edit edit;
edit.begin = source.sites.front().span.begin;
edit.end = source.sites.front().span.end;
if (resolve(source.sites.front().parent) == target)
{
edit.inlined = candidate;
}
else
{
appended[target].emplace_back(candidate);
}
state.patches[source.sites.front().parent].edits.emplace_back(std::move(edit));
detached.emplace(candidate);
}
else
{
appended[target].emplace_back(candidate);
}
}
else if (source.removed)
{
if (hassite)
{
file_patch::edit edit;
edit.begin = source.sites.front().span.begin;
edit.end = source.sites.front().span.end;
state.patches[source.sites.front().parent].edits.emplace_back(std::move(edit));
detached.emplace(candidate);
}
}
else if (source.created)
{
// include directive for the new file, on a line of its own
auto const parent{source.parent};
auto const directive{"include " + source.name + " end"};
file_patch::edit edit;
edit.site = candidate;
edit.length = directive.size();
if (valid(source.anchor) && layer(source.anchor).sites.size() == 1 && layer(source.anchor).sites.front().parent == parent && layer(source.anchor).sites.front().span.valid())
{
edit.begin = edit.end = layer(source.anchor).sites.front().span.end;
edit.text = state.eol + directive;
edit.lead = state.eol.size();
}
else
{
edit.begin = edit.end = -1;
if (false == layer(parent).created)
{
if (false == load(parent))
{
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();
}
}
edit.text += directive + state.eol;
}
state.patches[parent].edits.emplace_back(std::move(edit));
}
}
// files to write
std::vector<layer_handle> outputs;
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
if (false == is_output(candidate))
{
continue;
}
if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 ||
(candidate == root && false == Rootstatements.empty()))
{
outputs.emplace_back(candidate);
}
}
// text of the merged layers travels with the file they end up in, but changes made to it are still theirs
std::vector<layer_handle> composed{outputs};
composed.insert(std::end(composed), std::begin(absorbed), std::end(absorbed));
for (auto const candidate : composed)
{
if (false == load(candidate))
{
return fail(state.error);
}
}
// what is defined in these files and isn't rewritten moves along with the text
for (auto const &source : m_sources)
{
if (state.content.count(source.second.layer) != 0 && rewritten.count(source.first) == 0)
{
state.nodes[source.second.layer].emplace_back(source.first);
}
}
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
auto const &included{layer(candidate)};
if (included.dead || included.created || detached.count(candidate) != 0 || included.sites.size() != 1 || included.sites.front().fixed || false == included.sites.front().span.valid())
{
continue;
}
state.includes[included.sites.front().parent].emplace_back(candidate);
}
std::map<layer_handle, composition> texts;
for (auto const output : outputs)
{
auto &text{texts[output]};
auto const eol{detect_eol(state.content.count(output) != 0 ? state.content[output] : std::string{}, state.eol)};
if (layer(output).created)
{
// NOTE: the parser treats an empty file as a missing one
text.text = "// scenery layer created in the editor" + eol;
}
composition body;
if (false == compose(state, output, body, 0))
{
return fail(state.error);
}
text.append(body);
if (auto const lookup{appended.find(output)}; lookup != appended.end())
{
for (auto const merged : lookup->second)
{
composition mergedtext;
if (false == compose(state, merged, mergedtext, 0))
{
return fail(state.error);
}
ensure_newline(text.text, eol);
text.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;
}
}
if (output == root)
{
for (auto const &statement : Rootstatements)
{
ensure_newline(text.text, eol);
text.text += statement + eol;
}
}
}
// with everything worked out it's safe to touch the files. new content goes to temporary files first,
// so a failure half way through doesn't leave the scenery with only a part of the changes
std::error_code error;
std::vector<layer_handle> written;
for (auto const output : outputs)
{
auto const lookup{state.content.find(output)};
if (lookup != state.content.end() && lookup->second == texts[output].text)
{
continue;
}
auto const filepath{std::filesystem::path(path(output))};
if (filepath.has_parent_path())
{
std::filesystem::create_directories(filepath.parent_path(), error);
}
std::ofstream file{path(output) + ".tmp", std::ios_base::binary | std::ios_base::trunc};
file << texts[output].text;
file.close();
if (file.fail())
{
for (auto const leftover : written)
{
std::filesystem::remove(path(leftover) + ".tmp", error);
}
std::filesystem::remove(path(output) + ".tmp", error);
return fail("can't write file \"" + layer(output).name + "\"");
}
written.emplace_back(output);
}
for (auto const output : written)
{
auto const filepath{path(output)};
// the first save of a file leaves its original content next to it
if (false == layer(output).editormade && std::filesystem::exists(filepath, error) && false == std::filesystem::exists(filepath + ".bak", error))
{
std::filesystem::copy_file(filepath, filepath + ".bak", error);
}
std::filesystem::rename(filepath + ".tmp", filepath, error);
if (error)
{
return fail("can't replace file \"" + layer(output).name + "\": " + error.message());
}
result.files.emplace_back(layer(output).name);
WriteLog("Scenery save: file \"" + layer(output).name + "\" updated");
}
// the scenery files now match the scene, update the bookkeeping to reflect it
auto removedterrain{false};
for (auto const output : outputs)
{
for (auto const &place : texts[output].nodes)
{
auto &source{m_sources[place.node]};
auto const isnew{source.layer == null_handle};
source.layer = output;
source.span = place.span;
if (isnew)
{
source.context = layer(output).context_end;
}
if (isnew || place.rewritten)
{
auto const *model{dynamic_cast<TAnimModel const *>(place.node)};
source.location = place.node->location();
source.angles = (model != nullptr ? model->Angles() : glm::vec3{0.f});
source.scale = (model != nullptr ? model->Scale() : glm::vec3{1.f});
}
}
for (auto const &place : texts[output].sites)
{
auto &included{layer(place.layer)};
if (included.sites.empty())
{
// directive of a layer created in the editor
included.sites.emplace_back();
}
included.sites.front().parent = output;
included.sites.front().span = place.span;
included.parent = output;
}
}
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
auto &saved{layer(candidate)};
if (saved.dead)
{
continue;
}
if (saved.merged != null_handle || saved.removed)
{
removedterrain |= saved.removed && (saved.items[static_cast<std::size_t>(layer_item::shape)] + saved.items[static_cast<std::size_t>(layer_item::lines)] > 0);
saved.dead = true;
saved.sites.clear();
}
saved.created = false;
saved.anchor = null_handle;
}
for (auto const output : written)
{
stat(output);
}
m_erased.clear();
if (removedterrain)
{
// binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load
auto terrainfile{layer(root).name};
while (false == terrainfile.empty() && terrainfile.front() == '$')
{
terrainfile.erase(0, 1);
}
erase_extension(terrainfile);
if (std::filesystem::remove(Global.asCurrentSceneryPath + terrainfile + ".sbt", error))
{
WriteLog("Scenery save: binary terrain file removed, it'll be generated again on the next load");
}
}
result.success = true;
if (result.files.empty())
{
result.message = "No changes to save.";
}
else
{
result.message = "Saved: ";
for (auto const &file : result.files)
{
result.message += file + (&file != &result.files.back() ? ", " : "");
}
}
return result;
}
} // namespace scene
//---------------------------------------------------------------------------

View File

@@ -718,6 +718,7 @@ memory_node::deserialize( cParser &Input, node_data const &Nodedata ) {
basic_node::basic_node( scene::node_data const &Nodedata ) :
m_name( Nodedata.name ),
m_template( Nodedata.is_template ),
m_layer( Nodedata.layer )
{
uuid = UID::random();

View File

@@ -71,6 +71,7 @@ struct node_data {
std::string name;
std::string type;
layer_handle layer { null_handle }; // scenery layer the node is defined in, if any
bool is_template { false }; // the node is defined by an *.inc template rather than directly by a scenery layer file
};
// holds unique piece of geometry, covered with single material
@@ -381,6 +382,7 @@ public:
std::string m_name;
bool m_dirty { false };
bool m_layerhidden { false }; // the node was visible until its scenery layer was hidden
bool m_template { false }; // the node is defined by an *.inc template, which the editor can't rewrite. set only for scenery opened for editing
scene::layer_handle m_layer { null_handle }; // scenery layer this node belongs to, if any
UID uuid;