17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 22:39:19 +02:00
Files
maszyna/scene/scenelayerssave.cpp
maj00r d4791f6a29 feat(editor): a model or an include standing by a track bent along it (B, or the Bend along the track section of the toolset): it stays where it stood and follows the track, through the joined tracks as far as it reaches; reference point at its start, origin, middle, end or a point picked on the model, across at its origin, the side facing the track, the far side or its middle, up at its origin, bottom, top or the top of its edge by the track; the blue cross of a bent model dragged for its offset, with Shift for its height and with Ctrl along the track, the values shown at the cursor; a bent model clicked to edit; one step of the undo history; scale of the model kept, parts lit only at night left out; edges of platforms told by the name of the model and by the overhang of their edge over the foot: put 1.725 m from the axis at the height the name gives, turned with the edge to the track when they stood the other way; platforms stand on the plane of the rail heads carried on to their edge (PKP PLK ST-T2), the gauge widening led in from the tracks beyond the ends and from the other path of a switch; gaps between the pieces of a sweep filled with the paths which join them; Platforms and models tab left out of the track editor; laid track eased into the curvature of the one it joins with a transition curve; File > Open scenery... starts the editor again with the scenery picked from a list; the camera saved with the scenery; T and Shift+T turn each turntable to its next and previous position by event launchers written on each save; a turntable track deleted with its template; a scenery with no trainset gets a WMB10 on its first track; vehicle placed where the track is clicked; gauge check takes in the bent models; Polish translation of the editor
2026-10-08 00:51:40 +02:00

1816 lines
56 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 "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 <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 = "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))};
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())))
{
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;
}
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 + "\"";
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);
}
}
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.points[segment_data::point::start] -= Offset;
path.points[segment_data::point::end] -= 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{"(noname)"} : 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("include \"" + *included.file + "\" was placed for different origin, rotation or scale than its layer file receives it with");
}
placed[target].emplace_back(static_cast<instance_handle>(idx + 1));
}
}
// 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())};
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("can't write file \"" + layer(output).name + "\"");
}
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("can't make a backup of file \"" + layer(output).name + "\": " + copyerror.message());
}
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("can't replace file \"" + layer(output).name + "\": " + cause);
}
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
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_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 = "No changes to save.";
}
else
{
result.message = "Saved: ";
for (auto const &file : result.files)
{
result.message += file + (&file != &result.files.back() ? ", " : "");
}
for (auto const &backup : result.backups)
{
result.message += ". Backup before the ground was taken out: " + std::filesystem::path(backup).filename().string();
}
}
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 += ", ...";
}
auto const warning{std::to_string(unwritten.size()) + " changed nodes not saved, their files are included with parameters or more than once: " + names};
WriteLog("Scenery save: " + warning, logtype::generic);
result.message = result.files.empty() ? warning : result.message + ". " + warning;
}
return result;
}
} // namespace scene
//---------------------------------------------------------------------------