mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 17:24:53 +02:00
Labels: - every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track, grade), sides told from directions, degrees and per mille as signs, messages built from one format string - plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header - lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped - roads: the notes and errors naming a road translated; the build options and the layout fold the texture length, kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the lanes start folded Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once: - fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way is one point of intersection, a curve with no room is as sharp as the room lets it be - the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run - apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the ends of the route are brought to its height - Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
1908 lines
60 KiB
C++
1908 lines
60 KiB
C++
/*
|
|
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 "world/Track.h"
|
|
#include "world/Road.h"
|
|
#include "world/Sweep.h"
|
|
#include "world/RoadPoint.h"
|
|
|
|
#include "simulation/simulation.h"
|
|
#include "model/AnimModel.h"
|
|
#include "scene/scenenodegroups.h"
|
|
#include "world/MemCell.h"
|
|
#include "world/Traction.h"
|
|
#include "world/EvLaunch.h"
|
|
#include "world/Event.h"
|
|
#include "utilities/Globals.h"
|
|
#include "utilities/Logs.h"
|
|
#include "utilities/utilities.h"
|
|
#include "utilities/translation.h"
|
|
#include "editor/editorFormat.hpp"
|
|
|
|
#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
|
|
std::optional<std::string> name; // name of the node
|
|
};
|
|
|
|
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.name)
|
|
{
|
|
replace(3, *Placement.name);
|
|
}
|
|
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, std::optional<glm::dvec3> const &Location, std::string const &Track, 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;
|
|
}
|
|
std::vector<text_change> changes;
|
|
if (Location)
|
|
changes.insert(changes.end(), {{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)}});
|
|
auto const &track{tokens[tokens.size() - 2]};
|
|
if (false == Track.empty() && track.text != Track)
|
|
changes.push_back({track.begin, track.end, Track});
|
|
apply(Text, changes);
|
|
return true;
|
|
}
|
|
|
|
// definition layout: node <max> <min> <name> traction <supply> <voltage> <current> <resistivity> <material> <thickness> <damage>
|
|
// <point 1> <point 2> <point 3> <point 4> <minimal height> <segment length> <wires> <offset> <vis> [parallel <name>] endtraction
|
|
bool patch_traction(std::string &Text, TTraction const &Traction, glm::dvec3 const &Offset, std::string &Error)
|
|
{
|
|
auto const tokens{tokenize(Text)};
|
|
if (tokens.size() < 30 || tokens[0].text != "node" || tokens[4].text != "traction" || tokens.back().text != "endtraction")
|
|
{
|
|
Error = "unexpected layout of traction definition";
|
|
return false;
|
|
}
|
|
std::vector<text_change> changes;
|
|
std::size_t index{12};
|
|
for (auto const &point : {Traction.pPoint1, Traction.pPoint2, Traction.pPoint3, Traction.pPoint4})
|
|
{
|
|
auto const local{point - Offset};
|
|
for (auto const value : {local.x, local.y, local.z})
|
|
{
|
|
changes.push_back({tokens[index].begin, tokens[index].end, number(value)});
|
|
++index;
|
|
}
|
|
}
|
|
auto const &p1{Traction.pPoint1};
|
|
auto const &p2{Traction.pPoint2};
|
|
auto const &p3{Traction.pPoint3};
|
|
auto const &p4{Traction.pPoint4};
|
|
changes.push_back({tokens[24].begin, tokens[24].end, number((p3.y - p1.y + p4.y - p2.y) * 0.5 - Traction.fHeightDifference)});
|
|
if (Traction.iNumSections > 0)
|
|
{
|
|
// the load truncates length / segment length, the margin keeps the rounding of the number from losing a section
|
|
changes.push_back({tokens[25].begin, tokens[25].end, number(glm::length(p1 - p2) / (Traction.iNumSections + 0.001))});
|
|
}
|
|
apply(Text, changes);
|
|
return true;
|
|
}
|
|
|
|
// definition layout: node <max> <min> <name> eventlauncher <x> <y> <z> <radius> ... end
|
|
bool patch_launcher(std::string &Text, glm::dvec3 const &Location, std::string &Error)
|
|
{
|
|
auto const tokens{tokenize(Text)};
|
|
if (tokens.size() < 9 || tokens[0].text != "node" || tokens[4].text != "eventlauncher")
|
|
{
|
|
Error = "unexpected layout of event launcher 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
|
|
instance_handle instance{0}; // include of a template made 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;
|
|
};
|
|
struct instance_place
|
|
{
|
|
instance_handle instance;
|
|
source_span span;
|
|
};
|
|
std::string text;
|
|
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)
|
|
{
|
|
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 += Location;
|
|
place.span.end += Location;
|
|
nodes.emplace_back(place);
|
|
}
|
|
for (auto place : Other.sites)
|
|
{
|
|
place.span.begin += Location;
|
|
place.span.end += Location;
|
|
sites.emplace_back(place);
|
|
}
|
|
for (auto place : Other.instances)
|
|
{
|
|
place.span.begin += Location;
|
|
place.span.end += Location;
|
|
instances.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::map<layer_handle, std::vector<instance_handle>> instances; // unchanged includes of templates in each file
|
|
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;
|
|
}
|
|
// 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 = 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};
|
|
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});
|
|
}
|
|
if (edit.instance != 0)
|
|
{
|
|
Output.instances.push_back({edit.instance, 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)});
|
|
}
|
|
}
|
|
if (auto const lookup{State.instances.find(Layer)}; lookup != State.instances.end())
|
|
{
|
|
for (auto const included : lookup->second)
|
|
{
|
|
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;
|
|
}
|
|
|
|
// writes changes made in the editor to the scenery files
|
|
save_result node_layers::save(std::vector<std::string> const &Rootstatements, std::vector<std::string> const &Trailingstatements)
|
|
{
|
|
save_result result;
|
|
if (m_layers.empty())
|
|
{
|
|
result.message = STR_C("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 = format(STR_C("Scenery not saved: %s"), Message.c_str());
|
|
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;
|
|
}
|
|
if (Model != nullptr && Groups.relocating(Node))
|
|
{
|
|
// written anew along with its group, below
|
|
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))};
|
|
auto const renamed{Model != nullptr && m_renamed.find(Node) != m_renamed.end()};
|
|
auto const *cell{Model == nullptr ? static_cast<TMemCell const *>(Node) : nullptr};
|
|
auto const track{cell != nullptr && cell->Track != nullptr ? cell->Track->name() : std::string{}};
|
|
if (false == (moved || rotated || scaled || renamed || (false == track.empty() && Node->dirty()))) // NOSONAR
|
|
{
|
|
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 + "\""; // NOSONAR
|
|
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;
|
|
}
|
|
if (renamed)
|
|
{
|
|
placement.name = (Node->name().empty() ? "none" : Node->name());
|
|
}
|
|
patched = patch_model(text, placement, error);
|
|
}
|
|
else
|
|
{
|
|
patched = patch_memcell(text, moved ? std::optional<glm::dvec3>{source.context.to_local(location)} : std::nullopt, track, error);
|
|
}
|
|
if (false == patched)
|
|
{
|
|
state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; // NOSONAR
|
|
return false;
|
|
}
|
|
if (text.size() == static_cast<std::size_t>(source.span.end - source.span.begin) && content.compare(static_cast<std::size_t>(source.span.begin), text.size(), text) == 0)
|
|
{
|
|
return true;
|
|
}
|
|
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->from_template())
|
|
{
|
|
// 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_insert()};
|
|
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);
|
|
}
|
|
}
|
|
// models grouped or ungrouped in the editor: their definitions are taken out of where they are and written anew, a group
|
|
// as one block between group and endgroup in the layer of its first model, a model of a dissolved group on its own
|
|
std::set<basic_node const *> relocated;
|
|
auto const relocate = [&](TAnimModel const *Model, layer_handle const Target, std::vector<std::pair<basic_node const *, std::string>> &Output) {
|
|
if (auto const lookup{m_sources.find(Model)}; lookup != m_sources.end())
|
|
{
|
|
auto const &source{lookup->second};
|
|
if (false == writable(source.layer))
|
|
{
|
|
// one definition stands for more than one node, it stays where it is
|
|
return true;
|
|
}
|
|
if (false == load(source.layer))
|
|
{
|
|
return false;
|
|
}
|
|
file_patch::edit edit;
|
|
edit.begin = source.span.begin;
|
|
edit.end = source.span.end;
|
|
state.patches[source.layer].edits.emplace_back(std::move(edit));
|
|
rewritten.emplace(Model);
|
|
}
|
|
std::string text;
|
|
Model->export_as_text(text);
|
|
text.erase(text.find_last_not_of(" \t\r\n") + 1);
|
|
auto const &context{layer(Target).context_insert()};
|
|
if (false == context.matches(layer_context()))
|
|
{
|
|
model_placement placement;
|
|
placement.rotation = context.rotation;
|
|
placement.location = context.to_local(Model->location());
|
|
placement.angles = Model->Angles();
|
|
placement.scale = Model->Scale() / context.scale;
|
|
std::string error;
|
|
if (false == patch_model(text, placement, error))
|
|
{
|
|
state.error = error + " of \"" + Model->name() + "\"";
|
|
return false;
|
|
}
|
|
}
|
|
Output.emplace_back(Model, std::move(text));
|
|
relocated.emplace(Model);
|
|
return true;
|
|
};
|
|
for (auto *node : Groups.loose())
|
|
{
|
|
auto const *model{dynamic_cast<TAnimModel const *>(node)};
|
|
if (model == nullptr || model->from_template() || false == is_output(resolve(node->layer())))
|
|
{
|
|
continue;
|
|
}
|
|
if (false == relocate(model, resolve(node->layer()), created[resolve(node->layer())]))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
}
|
|
for (auto const group : Groups.rewritten())
|
|
{
|
|
auto const &nodes{Groups.group(group).nodes};
|
|
if (nodes.empty() || false == is_output(resolve(nodes.front()->layer())))
|
|
{
|
|
continue;
|
|
}
|
|
auto const target{resolve(nodes.front()->layer())};
|
|
auto &output{created[target]};
|
|
// NOTE: lines with no node are written as they are, without a place in the bookkeeping
|
|
output.emplace_back(nullptr, "group");
|
|
for (auto const *node : nodes)
|
|
{
|
|
auto const *model{dynamic_cast<TAnimModel const *>(node)};
|
|
if (model == nullptr || model->from_template())
|
|
{
|
|
continue;
|
|
}
|
|
if (false == relocate(model, target, output))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
}
|
|
output.emplace_back(nullptr, "endgroup");
|
|
}
|
|
auto const push_edit = [&](layer_handle const Layer, source_span const &Span, std::string Text, basic_node const *Node) {
|
|
file_patch::edit edit;
|
|
edit.begin = Span.begin;
|
|
edit.end = Span.end;
|
|
edit.length = Text.size();
|
|
edit.text = std::move(Text);
|
|
edit.node = Node;
|
|
state.patches[Layer].edits.emplace_back(std::move(edit));
|
|
};
|
|
// changes which can't be written, as their definition stands for more than one node
|
|
std::vector<std::string> unwritten;
|
|
// traction and event launchers are rewritten when the editor moved them along with the track they belong to
|
|
std::vector<basic_node *> patchednodes;
|
|
auto const patch_node = [&](basic_node *Node, auto const &Patch) {
|
|
auto const lookup{m_sources.find(Node)};
|
|
if (lookup == m_sources.end() || false == Node->dirty())
|
|
{
|
|
return true;
|
|
}
|
|
auto const &source{lookup->second};
|
|
if (false == is_output(resolve(source.layer)))
|
|
{
|
|
return true;
|
|
}
|
|
if (false == writable(source.layer))
|
|
{
|
|
unwritten.push_back(Node->name());
|
|
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))};
|
|
std::string error;
|
|
if (false == Patch(text, source, error))
|
|
{
|
|
state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\"";
|
|
return false;
|
|
}
|
|
push_edit(source.layer, source.span, std::move(text), Node);
|
|
rewritten.emplace(Node);
|
|
patchednodes.push_back(Node);
|
|
return true;
|
|
};
|
|
for (auto *traction : simulation::Traction.sequence())
|
|
{
|
|
if (traction != nullptr &&
|
|
false == patch_node(traction, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_traction(Text, *traction, Source.context.offset, Error); }))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
}
|
|
for (auto *launcher : simulation::Events.launchers())
|
|
{
|
|
if (launcher != nullptr &&
|
|
false == patch_node(launcher, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_launcher(Text, Source.context.to_local(launcher->location()), Error); }))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
}
|
|
std::vector<TTrack *> savedpaths;
|
|
std::vector<TTrack const *> droppedpaths;
|
|
auto const path_text = [](TTrack &Path, glm::dvec3 const &Offset) {
|
|
auto const paths{Path.m_paths};
|
|
for (auto &path : Path.m_paths)
|
|
path.translate(-Offset);
|
|
std::string text;
|
|
Path.export_as_text(text);
|
|
Path.m_paths = paths;
|
|
text.erase(text.find_last_not_of(" \t\r\n") + 1);
|
|
return text;
|
|
};
|
|
for (auto *path : simulation::Paths.sequence())
|
|
{
|
|
if (path == nullptr || (path->iCategoryFlag & 0x80) != 0 || path->m_road != nullptr || path->from_template())
|
|
{
|
|
continue;
|
|
}
|
|
auto const nodelayer{resolve(path->layer())};
|
|
if (false == is_output(nodelayer))
|
|
{
|
|
continue;
|
|
}
|
|
auto const lookup{m_sources.find(path)};
|
|
if (lookup == m_sources.end())
|
|
{
|
|
if (path->m_editorremoved || false == path->dirty())
|
|
{
|
|
continue;
|
|
}
|
|
// a file included more than once would get a copy of the new path at every inclusion
|
|
auto const target{writable(nodelayer) ? nodelayer : root};
|
|
created[target].emplace_back(path, path_text(*path, layer(target).context_insert().offset));
|
|
savedpaths.push_back(path);
|
|
continue;
|
|
}
|
|
auto const &source{lookup->second};
|
|
if (false == path->m_editorremoved && false == path->dirty())
|
|
{
|
|
continue;
|
|
}
|
|
if (false == writable(source.layer))
|
|
{
|
|
unwritten.push_back(path->name().empty() ? std::string{STR_C("(no name)")} : path->name());
|
|
continue;
|
|
}
|
|
if (false == load(source.layer))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
if (path->m_editorremoved)
|
|
{
|
|
push_edit(source.layer, source.span, {}, nullptr);
|
|
droppedpaths.push_back(path);
|
|
}
|
|
else
|
|
{
|
|
push_edit(source.layer, source.span, path_text(*path, source.context.offset), path);
|
|
savedpaths.push_back(path);
|
|
}
|
|
rewritten.emplace(path);
|
|
}
|
|
// roads. the lanes aren't saved, they're generated from the definition of the road
|
|
std::vector<road_node *> savedroads;
|
|
std::vector<road_node const *> droppedroads;
|
|
auto const road_text = [](road_node &Road, glm::dvec3 const &Offset) {
|
|
auto const definition{Road.definition()};
|
|
auto local{definition};
|
|
local.axis.points[segment_data::point::start] -= Offset;
|
|
local.axis.points[segment_data::point::end] -= Offset;
|
|
Road.define(local);
|
|
std::string text;
|
|
Road.export_as_text(text);
|
|
Road.define(definition);
|
|
text.erase(text.find_last_not_of(" \t\r\n") + 1);
|
|
return text;
|
|
};
|
|
for (auto *road : simulation::Roads.sequence())
|
|
{
|
|
if (road == nullptr || road->from_template())
|
|
{
|
|
continue;
|
|
}
|
|
auto const nodelayer{resolve(road->layer())};
|
|
if (false == is_output(nodelayer))
|
|
{
|
|
continue;
|
|
}
|
|
auto const lookup{m_sources.find(road)};
|
|
if (lookup == m_sources.end())
|
|
{
|
|
if (road->m_editorremoved || false == road->dirty())
|
|
{
|
|
continue;
|
|
}
|
|
created[nodelayer].emplace_back(road, road_text(*road, layer(nodelayer).context_insert().offset));
|
|
savedroads.push_back(road);
|
|
continue;
|
|
}
|
|
auto const &source{lookup->second};
|
|
if ((false == road->m_editorremoved && false == road->dirty()) || false == writable(source.layer))
|
|
{
|
|
continue;
|
|
}
|
|
if (false == load(source.layer))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
file_patch::edit edit;
|
|
edit.begin = source.span.begin;
|
|
edit.end = source.span.end;
|
|
if (road->m_editorremoved)
|
|
{
|
|
droppedroads.push_back(road);
|
|
}
|
|
else
|
|
{
|
|
edit.text = road_text(*road, source.context.offset);
|
|
edit.length = edit.text.size();
|
|
edit.node = road;
|
|
savedroads.push_back(road);
|
|
}
|
|
state.patches[source.layer].edits.emplace_back(std::move(edit));
|
|
rewritten.emplace(road);
|
|
}
|
|
// models laid along curves, likewise
|
|
std::vector<sweep_node *> savedsweeps;
|
|
std::vector<sweep_node const *> droppedsweeps;
|
|
auto const sweep_text = [](sweep_node &Sweep, glm::dvec3 const &Offset) {
|
|
auto const definition{Sweep.definition()};
|
|
auto local{definition};
|
|
for (auto &piece : local.pieces)
|
|
{
|
|
piece.points[segment_data::point::start] -= Offset;
|
|
piece.points[segment_data::point::end] -= Offset;
|
|
}
|
|
Sweep.define(local);
|
|
std::string text;
|
|
Sweep.export_as_text(text);
|
|
Sweep.define(definition);
|
|
text.erase(text.find_last_not_of(" \t\r\n") + 1);
|
|
return text;
|
|
};
|
|
for (auto *sweep : simulation::Sweeps.sequence())
|
|
{
|
|
if (sweep == nullptr || sweep->from_template())
|
|
{
|
|
continue;
|
|
}
|
|
auto const nodelayer{resolve(sweep->layer())};
|
|
if (false == is_output(nodelayer))
|
|
{
|
|
continue;
|
|
}
|
|
auto const lookup{m_sources.find(sweep)};
|
|
if (lookup == m_sources.end())
|
|
{
|
|
if (sweep->m_editorremoved || false == sweep->dirty())
|
|
{
|
|
continue;
|
|
}
|
|
created[nodelayer].emplace_back(sweep, sweep_text(*sweep, layer(nodelayer).context_insert().offset));
|
|
savedsweeps.push_back(sweep);
|
|
continue;
|
|
}
|
|
auto const &source{lookup->second};
|
|
if ((false == sweep->m_editorremoved && false == sweep->dirty()) || false == writable(source.layer))
|
|
{
|
|
continue;
|
|
}
|
|
if (false == load(source.layer))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
file_patch::edit edit;
|
|
edit.begin = source.span.begin;
|
|
edit.end = source.span.end;
|
|
if (sweep->m_editorremoved)
|
|
{
|
|
droppedsweeps.push_back(sweep);
|
|
}
|
|
else
|
|
{
|
|
edit.text = sweep_text(*sweep, source.context.offset);
|
|
edit.length = edit.text.size();
|
|
edit.node = sweep;
|
|
savedsweeps.push_back(sweep);
|
|
}
|
|
state.patches[source.layer].edits.emplace_back(std::move(edit));
|
|
rewritten.emplace(sweep);
|
|
}
|
|
// road junctions, likewise
|
|
std::vector<junction_node *> savedjunctions;
|
|
std::vector<junction_node const *> droppedjunctions;
|
|
auto const junction_text = [](junction_node &Junction, glm::dvec3 const &Offset) {
|
|
auto const definition{Junction.definition()};
|
|
auto local{definition};
|
|
local.centre -= Offset;
|
|
for (auto &arm : local.arms)
|
|
{
|
|
arm.position -= Offset;
|
|
}
|
|
Junction.define(local);
|
|
std::string text;
|
|
Junction.export_as_text(text);
|
|
Junction.define(definition);
|
|
text.erase(text.find_last_not_of(" \t\r\n") + 1);
|
|
return text;
|
|
};
|
|
for (auto *junction : simulation::Junctions.sequence())
|
|
{
|
|
if (junction == nullptr || junction->from_template())
|
|
{
|
|
continue;
|
|
}
|
|
auto const nodelayer{resolve(junction->layer())};
|
|
if (false == is_output(nodelayer))
|
|
{
|
|
continue;
|
|
}
|
|
auto const lookup{m_sources.find(junction)};
|
|
if (lookup == m_sources.end())
|
|
{
|
|
if (junction->m_editorremoved || false == junction->dirty())
|
|
{
|
|
continue;
|
|
}
|
|
created[nodelayer].emplace_back(junction, junction_text(*junction, layer(nodelayer).context_insert().offset));
|
|
savedjunctions.push_back(junction);
|
|
continue;
|
|
}
|
|
auto const &source{lookup->second};
|
|
if ((false == junction->m_editorremoved && false == junction->dirty()) || false == writable(source.layer))
|
|
{
|
|
continue;
|
|
}
|
|
if (false == load(source.layer))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
file_patch::edit edit;
|
|
edit.begin = source.span.begin;
|
|
edit.end = source.span.end;
|
|
if (junction->m_editorremoved)
|
|
{
|
|
droppedjunctions.push_back(junction);
|
|
}
|
|
else
|
|
{
|
|
edit.text = junction_text(*junction, source.context.offset);
|
|
edit.length = edit.text.size();
|
|
edit.node = junction;
|
|
savedjunctions.push_back(junction);
|
|
}
|
|
state.patches[source.layer].edits.emplace_back(std::move(edit));
|
|
rewritten.emplace(junction);
|
|
}
|
|
// level crossings and traffic points of the roads
|
|
std::vector<roadpoint_node *> savedroadpoints;
|
|
std::vector<roadpoint_node const *> droppedroadpoints;
|
|
auto const roadpoint_text = [](roadpoint_node &Point, glm::dvec3 const &Offset) {
|
|
auto const definition{Point.definition()};
|
|
auto local{definition};
|
|
local.position -= Offset;
|
|
Point.define(local);
|
|
std::string text;
|
|
Point.export_as_text(text);
|
|
Point.define(definition);
|
|
text.erase(text.find_last_not_of(" \t\r\n") + 1);
|
|
return text;
|
|
};
|
|
for (auto *point : simulation::Roadpoints.sequence())
|
|
{
|
|
if (point == nullptr || point->from_template())
|
|
{
|
|
continue;
|
|
}
|
|
auto const nodelayer{resolve(point->layer())};
|
|
if (false == is_output(nodelayer))
|
|
{
|
|
continue;
|
|
}
|
|
auto const lookup{m_sources.find(point)};
|
|
if (lookup == m_sources.end())
|
|
{
|
|
if (point->m_editorremoved || false == point->dirty())
|
|
{
|
|
continue;
|
|
}
|
|
created[nodelayer].emplace_back(point, roadpoint_text(*point, layer(nodelayer).context_insert().offset));
|
|
savedroadpoints.push_back(point);
|
|
continue;
|
|
}
|
|
auto const &source{lookup->second};
|
|
if ((false == point->m_editorremoved && false == point->dirty()) || false == writable(source.layer))
|
|
{
|
|
continue;
|
|
}
|
|
if (false == load(source.layer))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
file_patch::edit edit;
|
|
edit.begin = source.span.begin;
|
|
edit.end = source.span.end;
|
|
if (point->m_editorremoved)
|
|
{
|
|
droppedroadpoints.push_back(point);
|
|
}
|
|
else
|
|
{
|
|
edit.text = roadpoint_text(*point, source.context.offset);
|
|
edit.length = edit.text.size();
|
|
edit.node = point;
|
|
savedroadpoints.push_back(point);
|
|
}
|
|
state.patches[source.layer].edits.emplace_back(std::move(edit));
|
|
rewritten.emplace(point);
|
|
}
|
|
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));
|
|
}
|
|
|
|
// editor data in the comment lines: the old lines go, the new ones are added to the file like new definitions
|
|
std::map<layer_handle, std::vector<std::string>> markedtext;
|
|
for (auto const &marking : m_marking)
|
|
{
|
|
auto const markedlayer{marking.first.first};
|
|
auto const &mark{marking.first.second};
|
|
if (auto const lookup{m_marked.find(markedlayer)}; lookup != m_marked.end())
|
|
{
|
|
for (auto const &line : lookup->second)
|
|
{
|
|
if (line.mark == mark)
|
|
{
|
|
push_edit(markedlayer, line.span, {}, nullptr);
|
|
}
|
|
}
|
|
}
|
|
auto const target{resolve(markedlayer)};
|
|
if (marking.second.empty() || false == is_output(target))
|
|
{
|
|
continue;
|
|
}
|
|
for (auto const &text : marking.second)
|
|
{
|
|
markedtext[target].push_back(mark + ' ' + text);
|
|
}
|
|
}
|
|
|
|
// includes of templates
|
|
std::map<layer_handle, std::vector<instance_handle>> placed; // directives made in the editor, by target layer
|
|
for (std::size_t idx = 0; idx < m_instances.size(); ++idx)
|
|
{
|
|
auto const &included{m_instances[idx]};
|
|
if (included.dead)
|
|
{
|
|
continue;
|
|
}
|
|
if (included.span.valid())
|
|
{
|
|
if (included.removed)
|
|
{
|
|
file_patch::edit edit;
|
|
edit.begin = included.span.begin;
|
|
edit.end = included.span.end;
|
|
state.patches[included.layer].edits.emplace_back(std::move(edit));
|
|
}
|
|
else if (false == included.directive.empty())
|
|
{
|
|
// changed in the editor, the new directive takes the place of the one in the file
|
|
file_patch::edit edit;
|
|
edit.begin = included.span.begin;
|
|
edit.end = included.span.end;
|
|
edit.length = included.directive.size();
|
|
edit.text = included.directive;
|
|
edit.instance = static_cast<instance_handle>(idx + 1);
|
|
state.patches[included.layer].edits.emplace_back(std::move(edit));
|
|
}
|
|
}
|
|
else if (false == included.removed && false == included.directive.empty())
|
|
{
|
|
auto const target{resolve(included.layer)};
|
|
if (false == is_output(target))
|
|
{
|
|
continue;
|
|
}
|
|
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(format(STR_C("include \"%s\" was placed for different origin, rotation or scale than its layer file receives it with"), included.file->c_str()));
|
|
}
|
|
placed[target].emplace_back(static_cast<instance_handle>(idx + 1));
|
|
}
|
|
}
|
|
|
|
// 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
|
|
{
|
|
// NOTE: negative location stands for the place the file receives additions at
|
|
edit.begin = edit.end = -1;
|
|
if (false == layer(parent).created)
|
|
{
|
|
if (false == load(parent))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
// 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;
|
|
}
|
|
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 || placed.count(candidate) != 0 ||
|
|
markedtext.count(candidate) != 0 || (candidate == root && (false == Rootstatements.empty() || false == Trailingstatements.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);
|
|
}
|
|
for (std::size_t idx = 0; idx < m_instances.size(); ++idx)
|
|
{
|
|
auto const &included{m_instances[idx]};
|
|
if (false == included.dead && false == included.removed && included.directive.empty() && included.span.valid() && state.content.count(included.layer) != 0)
|
|
{
|
|
state.instances[included.layer].emplace_back(static_cast<instance_handle>(idx + 1));
|
|
}
|
|
}
|
|
|
|
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);
|
|
// 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)
|
|
{
|
|
composition mergedtext;
|
|
if (false == compose(state, merged, mergedtext, 0))
|
|
{
|
|
return fail(state.error);
|
|
}
|
|
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(added.text, eol);
|
|
auto const offset{static_cast<std::streamoff>(added.text.size())};
|
|
if (definition.first != nullptr)
|
|
{
|
|
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())
|
|
{
|
|
for (auto const included : lookup->second)
|
|
{
|
|
auto const &directive{instance(included).directive};
|
|
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 (auto const lookup{markedtext.find(output)}; lookup != markedtext.end())
|
|
{
|
|
for (auto const &line : lookup->second)
|
|
{
|
|
ensure_newline(added.text, eol);
|
|
added.text += line + eol;
|
|
}
|
|
}
|
|
if (output == root)
|
|
{
|
|
for (auto const &statement : Rootstatements)
|
|
{
|
|
ensure_newline(added.text, eol);
|
|
added.text += statement + eol;
|
|
}
|
|
}
|
|
if (output == root && false == Trailingstatements.empty())
|
|
{
|
|
composition trailing;
|
|
for (auto const &statement : Trailingstatements)
|
|
trailing.text += statement + eol;
|
|
ensure_newline(text.text, eol);
|
|
text.append(trailing);
|
|
}
|
|
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,
|
|
// 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(format(STR_C("can't write file \"%s\""), layer(output).name.c_str()));
|
|
}
|
|
written.emplace_back(output);
|
|
}
|
|
// a file which loses its ground to the terrain of the editor keeps a copy of itself each time, whatever was saved before.
|
|
// without the copy nothing is replaced
|
|
{
|
|
auto const now{std::time(nullptr)};
|
|
char stamp[32];
|
|
std::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", std::localtime(&now));
|
|
for (auto const output : written)
|
|
{
|
|
auto const filepath{path(output)};
|
|
bool const losesground{std::any_of(std::begin(m_shapes), std::end(m_shapes), [&](shape_source const &Shape) { return Shape.erased && Shape.layer == output; })};
|
|
if (false == losesground || false == std::filesystem::exists(filepath, error))
|
|
{
|
|
continue;
|
|
}
|
|
auto const backup{filepath + ".bak-" + stamp + "-przed-terenem-edytora"};
|
|
std::error_code copyerror;
|
|
if (false == std::filesystem::copy_file(filepath, backup, std::filesystem::copy_options::overwrite_existing, copyerror))
|
|
{
|
|
for (auto const leftover : written)
|
|
{
|
|
std::filesystem::remove(path(leftover) + ".tmp", error);
|
|
}
|
|
return fail(format(STR_C("can't make a backup of file \"%s\": %s"), layer(output).name.c_str(), copyerror.message().c_str()));
|
|
}
|
|
WriteLog("Scenery save: the triangles of the ground go from \"" + layer(output).name + "\", its previous content is kept in \"" + backup + "\"");
|
|
result.backups.emplace_back(backup);
|
|
}
|
|
}
|
|
std::vector<layer_handle> replaced;
|
|
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)
|
|
{
|
|
// e.g. the file is held open by another program. take back what was done so far: the scenery files
|
|
// have to stay in step with each other, and with what we know about them
|
|
auto const cause{error.message()};
|
|
for (auto const restored : replaced)
|
|
{
|
|
auto const lookup{state.content.find(restored)};
|
|
if (lookup != state.content.end())
|
|
{
|
|
std::ofstream{path(restored), std::ios_base::binary | std::ios_base::trunc} << lookup->second;
|
|
stat(restored);
|
|
}
|
|
else
|
|
{
|
|
std::filesystem::remove(path(restored), error);
|
|
}
|
|
}
|
|
for (auto const leftover : written)
|
|
{
|
|
std::filesystem::remove(path(leftover) + ".tmp", error);
|
|
}
|
|
result.files.clear();
|
|
return fail(format(STR_C("can't replace file \"%s\": %s"), layer(output).name.c_str(), cause.c_str()));
|
|
}
|
|
replaced.emplace_back(output);
|
|
result.files.emplace_back(layer(output).name);
|
|
}
|
|
for (auto const &file : result.files)
|
|
{
|
|
WriteLog("Scenery save: file \"" + file + "\" updated");
|
|
}
|
|
|
|
// the scenery files now match the scene, update the bookkeeping to reflect it
|
|
Groups.saved();
|
|
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;
|
|
// a definition written anew elsewhere is in the placement of the place it went to, as a new one is
|
|
if (isnew || relocated.count(place.node) != 0)
|
|
{
|
|
source.context = layer(output).context_insert();
|
|
}
|
|
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].instances)
|
|
{
|
|
auto &included{m_instances[place.instance - 1]};
|
|
included.layer = output;
|
|
included.span = place.span;
|
|
included.directive.clear();
|
|
}
|
|
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;
|
|
}
|
|
// 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)
|
|
{
|
|
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);
|
|
scan(output);
|
|
}
|
|
for (auto const &marking : m_marking)
|
|
{
|
|
if (resolve(marking.first.first) != marking.first.first)
|
|
{
|
|
// the lines of the merged layer are now in the file of its target
|
|
m_marked.erase(marking.first.first);
|
|
}
|
|
}
|
|
m_marking.clear();
|
|
for (auto &included : m_instances)
|
|
{
|
|
// removed includes are gone from the files now
|
|
included.dead = included.dead || included.removed;
|
|
}
|
|
m_erased.clear();
|
|
m_renamed.clear();
|
|
// the shapes of the rewritten files are somewhere else in them now
|
|
m_shapes.erase(std::remove_if(std::begin(m_shapes), std::end(m_shapes), [&](shape_source const &Shape) { return std::find(std::begin(composed), std::end(composed), Shape.layer) != std::end(composed); }), std::end(m_shapes));
|
|
for (auto *path : savedpaths)
|
|
{
|
|
path->m_dirty = false;
|
|
}
|
|
for (auto *node : patchednodes)
|
|
{
|
|
node->m_dirty = false;
|
|
}
|
|
for (auto const *path : droppedpaths)
|
|
{
|
|
m_sources.erase(path);
|
|
}
|
|
for (auto *road : savedroads)
|
|
{
|
|
road->m_dirty = false;
|
|
}
|
|
for (auto const *road : droppedroads)
|
|
{
|
|
m_sources.erase(road);
|
|
}
|
|
for (auto *sweep : savedsweeps)
|
|
{
|
|
sweep->m_dirty = false;
|
|
}
|
|
for (auto const *sweep : droppedsweeps)
|
|
{
|
|
m_sources.erase(sweep);
|
|
}
|
|
for (auto *junction : savedjunctions)
|
|
{
|
|
junction->m_dirty = false;
|
|
}
|
|
for (auto const *junction : droppedjunctions)
|
|
{
|
|
m_sources.erase(junction);
|
|
}
|
|
for (auto *point : savedroadpoints)
|
|
{
|
|
point->m_dirty = false;
|
|
}
|
|
for (auto const *point : droppedroadpoints)
|
|
{
|
|
m_sources.erase(point);
|
|
}
|
|
removedterrain |= m_shapeserased;
|
|
m_shapeserased = false;
|
|
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 = STR_C("No changes to save");
|
|
}
|
|
else
|
|
{
|
|
std::string files;
|
|
for (auto const &file : result.files)
|
|
{
|
|
files += file + (&file != &result.files.back() ? ", " : "");
|
|
}
|
|
result.message = format(STR_C("Saved: %s"), files.c_str());
|
|
for (auto const &backup : result.backups)
|
|
{
|
|
result.message += format(STR_C(". Backup before the ground was taken out: %s"), std::filesystem::path(backup).filename().string().c_str());
|
|
}
|
|
}
|
|
if (false == unwritten.empty())
|
|
{
|
|
std::string names;
|
|
for (std::size_t i = 0; i < std::min<std::size_t>(unwritten.size(), 5); ++i)
|
|
{
|
|
names += (i > 0 ? ", " : "") + unwritten[i];
|
|
}
|
|
if (unwritten.size() > 5)
|
|
{
|
|
names += ", ...";
|
|
}
|
|
WriteLog("Scenery save: " + std::to_string(unwritten.size()) + " changed nodes not saved, their files are included with parameters or more than once: " + names, logtype::generic);
|
|
auto const warning{format(STR_C("Changed nodes left unsaved (their files are included with parameters or more than once): %zu, %s"), unwritten.size(), names.c_str())};
|
|
result.message = result.files.empty() ? warning : result.message + ". " + warning;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace scene
|
|
|
|
//---------------------------------------------------------------------------
|