mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 16:14:52 +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
863 lines
25 KiB
C++
863 lines
25 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 "editor/editorIncludeInfo.hpp"
|
|
|
|
#include "utilities/Globals.h"
|
|
#include "utilities/utilities.h"
|
|
#include "utilities/translation.h"
|
|
#include "editor/editorFormat.hpp"
|
|
|
|
#include <yaml-cpp/yaml.h>
|
|
|
|
#include <filesystem>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <tuple>
|
|
#include <unordered_map>
|
|
|
|
namespace
|
|
{
|
|
|
|
std::string const linemark{"//$e"};
|
|
|
|
// template file split into the description and the rest
|
|
struct template_text
|
|
{
|
|
std::string content; // whole file
|
|
std::string yaml; // description, with the line marks taken off; utf-8
|
|
std::vector<std::pair<std::size_t, std::size_t>> lines; // ranges of the content taken by the description lines, line ends included
|
|
std::string eol{"\r\n"};
|
|
std::size_t start{0}; // location of the first character past the byte order mark
|
|
bool utf8{false}; // the file is utf-8 encoded, rather than windows-1250 like most of the scenery files
|
|
};
|
|
|
|
// true if specified text is valid utf-8. HasExtended: set if the text has any characters outside of ascii
|
|
bool is_utf8(std::string const &Text, bool &HasExtended)
|
|
{
|
|
HasExtended = false;
|
|
for (std::size_t idx = 0; idx < Text.size();)
|
|
{
|
|
auto const lead{static_cast<unsigned char>(Text[idx])};
|
|
if (lead < 0x80)
|
|
{
|
|
++idx;
|
|
continue;
|
|
}
|
|
HasExtended = true;
|
|
auto const length{(lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 0};
|
|
if (length == 0 || idx + length > Text.size())
|
|
{
|
|
return false;
|
|
}
|
|
for (auto offset = 1; offset < length; ++offset)
|
|
{
|
|
if ((static_cast<unsigned char>(Text[idx + offset]) & 0xC0) != 0x80)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
idx += length;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// counterpart of win1250_to_utf8(). characters outside of the supported set are replaced with question marks
|
|
std::string utf8_to_win1250(std::string const &Text)
|
|
{
|
|
// polish letters, as utf-8 sequences; the same set win1250_to_utf8() handles
|
|
static std::unordered_map<std::string, char> const charmap{{"\xC4\x84", '\xA5'}, {"\xC4\x86", '\xC6'}, {"\xC4\x98", '\xCA'}, {"\xC5\x81", '\xA3'}, {"\xC5\x83", '\xD1'}, {"\xC3\x93", '\xD3'},
|
|
{"\xC5\x9A", '\x8C'}, {"\xC5\xB9", '\x8F'}, {"\xC5\xBB", '\xAF'}, {"\xC4\x85", '\xB9'}, {"\xC4\x87", '\xE6'}, {"\xC4\x99", '\xEA'},
|
|
{"\xC5\x82", '\xB3'}, {"\xC5\x84", '\xF1'}, {"\xC3\xB3", '\xF3'}, {"\xC5\x9B", '\x9C'}, {"\xC5\xBA", '\x9F'}, {"\xC5\xBC", '\xBF'}};
|
|
std::string output;
|
|
for (std::size_t idx = 0; idx < Text.size();)
|
|
{
|
|
auto const lead{static_cast<unsigned char>(Text[idx])};
|
|
if (lead < 0x80)
|
|
{
|
|
output += Text[idx++];
|
|
continue;
|
|
}
|
|
auto const length{std::min<std::size_t>((lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 1, Text.size() - idx)};
|
|
auto const lookup{charmap.find(Text.substr(idx, length))};
|
|
output += (lookup != charmap.end() ? lookup->second : '?');
|
|
idx += length;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
bool read_template(std::string const &File, template_text &Text, std::string &Error)
|
|
{
|
|
std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary};
|
|
if (false == input.is_open())
|
|
{
|
|
Error = format(STR_C("can't open file \"%s\""), File.c_str());
|
|
return false;
|
|
}
|
|
auto &content{Text.content};
|
|
content.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
|
|
|
|
auto const bom{content.compare(0, 3, "\xEF\xBB\xBF") == 0};
|
|
Text.start = bom ? 3 : 0;
|
|
bool extended;
|
|
Text.utf8 = bom || (is_utf8(content, extended) && extended);
|
|
if (auto const linebreak{content.find('\n')}; linebreak != std::string::npos)
|
|
{
|
|
Text.eol = (linebreak > 0 && content[linebreak - 1] == '\r') ? "\r\n" : "\n";
|
|
}
|
|
|
|
for (auto linebegin{Text.start}; linebegin < content.size();)
|
|
{
|
|
auto const linebreak{content.find('\n', linebegin)};
|
|
auto const next{linebreak == std::string::npos ? content.size() : linebreak + 1};
|
|
if (content.compare(linebegin, linemark.size(), linemark) == 0)
|
|
{
|
|
auto line{content.substr(linebegin + linemark.size(), next - linebegin - linemark.size())};
|
|
line.erase(line.find_last_not_of("\r\n") + 1);
|
|
// a single space separates the mark from the document; the rest is indentation, which carries meaning
|
|
if (false == line.empty() && line.front() == ' ')
|
|
{
|
|
line.erase(0, 1);
|
|
}
|
|
Text.yaml += line + '\n';
|
|
Text.lines.emplace_back(linebegin, next);
|
|
}
|
|
linebegin = next;
|
|
}
|
|
// a template without a description of its own may have one beside the game data, in editor/templates, where the
|
|
// scripts of tools/editor write it with --sidecar: a check of the game files doesn't take it away
|
|
if (Text.yaml.empty())
|
|
{
|
|
std::ifstream beside{"editor/templates/" + File, std::ios_base::binary};
|
|
if (beside.is_open())
|
|
Text.yaml.assign(std::istreambuf_iterator<char>(beside), std::istreambuf_iterator<char>());
|
|
}
|
|
if (false == is_utf8(Text.yaml, extended))
|
|
{
|
|
Text.yaml = win1250_to_utf8(Text.yaml);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// number of the parameter if specified text holds (pN) placeholder at specified location, 0 otherwise. Length: size of the placeholder
|
|
int placeholder(std::string const &Text, std::size_t const Location, std::size_t &Length)
|
|
{
|
|
if (Text.compare(Location, 2, "(p") != 0)
|
|
{
|
|
return 0;
|
|
}
|
|
auto idx{Location + 2};
|
|
auto id{0};
|
|
while (idx < Text.size() && std::isdigit(static_cast<unsigned char>(Text[idx])) && id < 1000)
|
|
{
|
|
id = id * 10 + (Text[idx] - '0');
|
|
++idx;
|
|
}
|
|
if (idx == Location + 2 || idx >= Text.size() || Text[idx] != ')')
|
|
{
|
|
return 0;
|
|
}
|
|
Length = idx + 1 - Location;
|
|
return id;
|
|
}
|
|
|
|
// number of the parameter if the token is nothing but its placeholder, 0 otherwise
|
|
int exact_placeholder(std::string const &Token)
|
|
{
|
|
std::size_t length{0};
|
|
auto const id{placeholder(Token, 0, length)};
|
|
return length == Token.size() ? id : 0;
|
|
}
|
|
|
|
// number of the first parameter whose placeholder is a part of the token, 0 if there's none
|
|
int first_placeholder(std::string const &Token)
|
|
{
|
|
std::size_t length{0};
|
|
for (auto location{Token.find("(p")}; location != std::string::npos; location = Token.find("(p", location + 1))
|
|
{
|
|
if (auto const id{placeholder(Token, location, length)}; id != 0)
|
|
{
|
|
return id;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// tokens of scenery text, in lower case and with the comments left out
|
|
std::vector<std::string> tokenize(std::string const &Text)
|
|
{
|
|
std::vector<std::string> tokens;
|
|
std::string token;
|
|
auto const flush = [&]() {
|
|
if (false == token.empty())
|
|
{
|
|
tokens.emplace_back(ToLower(token));
|
|
token.clear();
|
|
}
|
|
};
|
|
for (std::size_t idx = 0; idx < Text.size(); ++idx)
|
|
{
|
|
if (Text.compare(idx, 2, "//") == 0)
|
|
{
|
|
flush();
|
|
idx = Text.find('\n', idx);
|
|
if (idx == std::string::npos)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
else if (Text.compare(idx, 2, "/*") == 0)
|
|
{
|
|
flush();
|
|
idx = Text.find("*/", idx + 2);
|
|
if (idx == std::string::npos)
|
|
{
|
|
break;
|
|
}
|
|
++idx;
|
|
}
|
|
else if (Text[idx] == ' ' || Text[idx] == '\t' || Text[idx] == '\n' || Text[idx] == '\r' || Text[idx] == ';')
|
|
{
|
|
flush();
|
|
}
|
|
else
|
|
{
|
|
token += Text[idx];
|
|
}
|
|
}
|
|
flush();
|
|
return tokens;
|
|
}
|
|
|
|
// reads description held by specified template text
|
|
bool parse(template_text const &Text, include_info &Info, std::string &Error)
|
|
{
|
|
Info = include_info();
|
|
auto const &text{Text};
|
|
try
|
|
{
|
|
auto const document{YAML::Load(text.yaml)};
|
|
if (document.IsNull())
|
|
{
|
|
return true;
|
|
}
|
|
if (false == document.IsMap())
|
|
{
|
|
Error = STR_C("the description isn't a list of named values");
|
|
return false;
|
|
}
|
|
Info.name = document["name"].as<std::string>("");
|
|
Info.category = document["category"].as<std::string>("");
|
|
Info.description = document["description"].as<std::string>("");
|
|
if (auto const drive{document["switch"]}; drive.IsDefined() && drive.IsMap())
|
|
{
|
|
Info.switch_hand = drive["hand"].as<std::string>("");
|
|
Info.switch_drive = drive["drive"].as<std::string>("");
|
|
Info.switch_default = drive["default"].as<bool>(false);
|
|
Info.switch_height = drive["height"].as<double>(0.0);
|
|
}
|
|
if (auto const signal{document["signal"]}; signal.IsDefined() && signal.IsMap())
|
|
{
|
|
Info.signal_kind = signal["kind"].as<std::string>("");
|
|
Info.signal_mount = signal["mount"].as<std::string>("");
|
|
Info.signal_lamps = signal["lamps"].as<std::string>("");
|
|
Info.signal_lean = signal["lean"].as<std::string>("");
|
|
Info.signal_read = signal["read"].as<std::string>("");
|
|
}
|
|
if (auto const table{document["turntable"]}; table.IsDefined() && table.IsMap())
|
|
{
|
|
Info.turntable_length = table["length"].as<double>(0.0);
|
|
Info.turntable_angles = table["angles"].as<int>(0);
|
|
Info.turntable_positions = table["positions"].as<int>(0);
|
|
}
|
|
if (auto const parameters{document["params"]}; parameters.IsDefined() && parameters.IsSequence())
|
|
{
|
|
for (auto const &entry : parameters)
|
|
{
|
|
auto const id{entry.IsMap() ? entry["id"].as<int>(0) : 0};
|
|
if (id <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
auto ¶meter{Info.parameter(id)};
|
|
parameter.role = entry["role"].as<std::string>("free");
|
|
parameter.label = entry["label"].as<std::string>("");
|
|
parameter.value = entry["default"].as<std::string>("");
|
|
}
|
|
}
|
|
}
|
|
catch (YAML::Exception const &exception)
|
|
{
|
|
Error = format(STR_C("the description is malformed: %s"), exception.what());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// highest number of (pN) placeholder used by specified template text
|
|
int count_parameters(template_text const &Text)
|
|
{
|
|
// the description lines may mention parameters the template doesn't use
|
|
auto content{Text.content};
|
|
for (auto line{Text.lines.rbegin()}; line != Text.lines.rend(); ++line)
|
|
{
|
|
content.erase(line->first, line->second - line->first);
|
|
}
|
|
auto count{0};
|
|
std::size_t length{0};
|
|
for (auto location{content.find("(p")}; location != std::string::npos; location = content.find("(p", location + 1))
|
|
{
|
|
count = std::max(count, placeholder(content, location, length));
|
|
}
|
|
return count;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
include_parameter &include_info::parameter(int const Id)
|
|
{
|
|
auto lookup{std::find_if(std::begin(parameters), std::end(parameters), [=](include_parameter const &Parameter) { return Parameter.id >= Id; })};
|
|
if (lookup == std::end(parameters) || lookup->id != Id)
|
|
{
|
|
lookup = parameters.emplace(lookup);
|
|
lookup->id = Id;
|
|
}
|
|
return *lookup;
|
|
}
|
|
|
|
namespace editor_includes
|
|
{
|
|
|
|
std::vector<std::string> const roles{"free", "name", "pos.x", "pos.y", "pos.z", "rot.x", "rot.y", "rot.z", "track", "memcell", "event", "model", "texture", "number", "text"};
|
|
|
|
bool load(std::string const &File, include_info &Info, std::string &Error)
|
|
{
|
|
template_text text;
|
|
return read_template(File, text, Error) && parse(text, Info, Error);
|
|
}
|
|
|
|
bool save(std::string const &File, include_info const &Info, std::string &Error)
|
|
{
|
|
template_text text;
|
|
if (false == read_template(File, text, Error))
|
|
{
|
|
return false;
|
|
}
|
|
std::string yaml;
|
|
try
|
|
{
|
|
// start with the existing description, to keep the entries we don't know
|
|
auto document{YAML::Load(text.yaml)};
|
|
if (false == document.IsNull() && false == document.IsMap())
|
|
{
|
|
Error = STR_C("the existing description isn't a list of named values");
|
|
return false;
|
|
}
|
|
if (false == document["version"].IsDefined())
|
|
{
|
|
document["version"] = 1;
|
|
}
|
|
auto const set = [&](char const *Key, std::string const &Value) {
|
|
if (false == Value.empty())
|
|
{
|
|
document[Key] = Value;
|
|
}
|
|
else
|
|
{
|
|
document.remove(Key);
|
|
}
|
|
};
|
|
set("name", Info.name);
|
|
set("category", Info.category);
|
|
set("description", Info.description);
|
|
|
|
YAML::Node parameters{YAML::NodeType::Sequence};
|
|
for (auto const ¶meter : Info.parameters)
|
|
{
|
|
YAML::Node entry{YAML::NodeType::Map};
|
|
if (auto const existing{document["params"]}; existing.IsDefined() && existing.IsSequence())
|
|
{
|
|
for (auto const &candidate : existing)
|
|
{
|
|
if (candidate.IsMap() && candidate["id"].as<int>(0) == parameter.id)
|
|
{
|
|
entry = YAML::Clone(candidate);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
entry["id"] = parameter.id;
|
|
entry["role"] = parameter.role.empty() ? std::string{"free"} : parameter.role;
|
|
if (false == parameter.label.empty())
|
|
{
|
|
entry["label"] = parameter.label;
|
|
}
|
|
else
|
|
{
|
|
entry.remove("label");
|
|
}
|
|
if (false == parameter.value.empty())
|
|
{
|
|
entry["default"] = parameter.value;
|
|
}
|
|
else
|
|
{
|
|
entry.remove("default");
|
|
}
|
|
entry.SetStyle(YAML::EmitterStyle::Flow);
|
|
parameters.push_back(entry);
|
|
}
|
|
if (parameters.size() > 0)
|
|
{
|
|
document["params"] = parameters;
|
|
}
|
|
else
|
|
{
|
|
document.remove("params");
|
|
}
|
|
|
|
YAML::Emitter emitter;
|
|
emitter << document;
|
|
yaml = emitter.c_str();
|
|
}
|
|
catch (YAML::Exception const &exception)
|
|
{
|
|
Error = format(STR_C("the existing description is malformed, fix or remove it first: %s"), exception.what());
|
|
return false;
|
|
}
|
|
if (false == text.utf8)
|
|
{
|
|
yaml = utf8_to_win1250(yaml);
|
|
}
|
|
|
|
std::string block;
|
|
std::istringstream lines{yaml};
|
|
for (std::string line; std::getline(lines, line);)
|
|
{
|
|
block += linemark + (line.empty() ? "" : " " + line) + text.eol;
|
|
}
|
|
// the description replaces the old one, or goes to the top of the file if there was none
|
|
auto content{text.content};
|
|
auto location{text.lines.empty() ? text.start : text.lines.front().first};
|
|
for (auto line{text.lines.rbegin()}; line != text.lines.rend(); ++line)
|
|
{
|
|
content.erase(line->first, line->second - line->first);
|
|
}
|
|
content.insert(location, block);
|
|
|
|
auto const filepath{Global.asCurrentSceneryPath + File};
|
|
std::ofstream output{filepath + ".tmp", std::ios_base::binary | std::ios_base::trunc};
|
|
output << content;
|
|
output.close();
|
|
std::error_code error;
|
|
if (output.fail())
|
|
{
|
|
std::filesystem::remove(filepath + ".tmp", error);
|
|
Error = format(STR_C("can't write file \"%s\""), File.c_str());
|
|
return false;
|
|
}
|
|
// the first save of a file leaves its original content next to it
|
|
if (false == std::filesystem::exists(filepath + ".bak", error))
|
|
{
|
|
std::filesystem::copy_file(filepath, filepath + ".bak", error);
|
|
}
|
|
std::filesystem::rename(filepath + ".tmp", filepath, error);
|
|
if (error)
|
|
{
|
|
Error = format(STR_C("can't replace file \"%s\": %s"), File.c_str(), error.message().c_str());
|
|
std::filesystem::remove(filepath + ".tmp", error);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int parameter_count(std::string const &File)
|
|
{
|
|
template_text text;
|
|
std::string error;
|
|
return read_template(File, text, error) ? count_parameters(text) : 0;
|
|
}
|
|
|
|
void suggest(std::string const &File, include_info &Info)
|
|
{
|
|
template_text text;
|
|
std::string error;
|
|
if (false == read_template(File, text, error))
|
|
{
|
|
return;
|
|
}
|
|
auto const tokens{tokenize(text.content)};
|
|
auto const assign = [&](std::size_t const Index, char const *Role, bool const Exact = true) {
|
|
if (Index >= tokens.size())
|
|
{
|
|
return;
|
|
}
|
|
auto const id{Exact ? exact_placeholder(tokens[Index]) : first_placeholder(tokens[Index])};
|
|
if (id != 0 && Info.parameter(id).role == "free")
|
|
{
|
|
Info.parameter(id).role = Role;
|
|
}
|
|
};
|
|
for (std::size_t idx = 0; idx < tokens.size(); ++idx)
|
|
{
|
|
auto const &token{tokens[idx]};
|
|
if (token == "origin")
|
|
{
|
|
assign(idx + 1, "pos.x");
|
|
assign(idx + 2, "pos.y");
|
|
assign(idx + 3, "pos.z");
|
|
}
|
|
else if (token == "rotate")
|
|
{
|
|
assign(idx + 1, "rot.x");
|
|
assign(idx + 2, "rot.y");
|
|
assign(idx + 3, "rot.z");
|
|
}
|
|
else if (token == "node" && idx + 4 < tokens.size())
|
|
{
|
|
// node <max> <min> <name> <type> <x> <y> <z> ...
|
|
assign(idx + 3, "name", false);
|
|
auto const &type{tokens[idx + 4]};
|
|
if (type == "model" || type == "memcell" || type == "eventlauncher" || type == "sound")
|
|
{
|
|
assign(idx + 5, "pos.x");
|
|
assign(idx + 6, "pos.y");
|
|
assign(idx + 7, "pos.z");
|
|
}
|
|
if (type == "model")
|
|
{
|
|
assign(idx + 8, "rot.y");
|
|
assign(idx + 9, "model");
|
|
assign(idx + 10, "texture");
|
|
}
|
|
if (type == "memcell")
|
|
{
|
|
assign(idx + 11, "track");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string const directive_mark{"include "};
|
|
|
|
bool automatic(std::string const &Role)
|
|
{
|
|
return Role == "name" || Role.starts_with("pos.") || Role.starts_with("rot.");
|
|
}
|
|
|
|
bool complete(include_info const &Info, int const Parameters, std::string *Issue)
|
|
{
|
|
std::string issue;
|
|
if (Info.name.empty())
|
|
{
|
|
issue = STR_C("the template needs a name");
|
|
}
|
|
for (auto id = 1; id <= Parameters && issue.empty(); ++id)
|
|
{
|
|
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
|
|
if (lookup == std::end(Info.parameters) || (false == automatic(lookup->role) && lookup->value.empty()))
|
|
{
|
|
issue = format(STR_C("(p%d) needs a default value, or a role filled in by the editor (name, pos.*, rot.*)"), id);
|
|
}
|
|
}
|
|
if (false == issue.empty() && Issue != nullptr)
|
|
{
|
|
*Issue = issue;
|
|
}
|
|
return issue.empty();
|
|
}
|
|
|
|
std::string number(double const Value)
|
|
{
|
|
std::ostringstream converter;
|
|
converter.imbue(std::locale::classic());
|
|
converter << std::fixed << std::setprecision(3) << Value;
|
|
return converter.str();
|
|
}
|
|
|
|
int parameter_with_role(include_info const &Info, std::string const &Role)
|
|
{
|
|
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [&](include_parameter const &Parameter) { return Parameter.role == Role; })};
|
|
return lookup != std::end(Info.parameters) ? lookup->id : 0;
|
|
}
|
|
|
|
bool parse_directive(std::string const &Directive, std::string &File, std::vector<std::string> &Values)
|
|
{
|
|
// the same rules the scenery parser goes by, except the text is taken as it is written
|
|
std::vector<std::string> tokens;
|
|
std::string token;
|
|
auto const flush = [&]() {
|
|
if (false == token.empty())
|
|
{
|
|
tokens.emplace_back(token);
|
|
token.clear();
|
|
}
|
|
};
|
|
for (std::size_t idx = 0; idx < Directive.size(); ++idx)
|
|
{
|
|
auto const character{Directive[idx]};
|
|
if (character == '\"')
|
|
{
|
|
flush();
|
|
auto const end{Directive.find('\"', idx + 1)};
|
|
if (end == std::string::npos)
|
|
{
|
|
return false;
|
|
}
|
|
tokens.emplace_back(Directive, idx + 1, end - idx - 1);
|
|
idx = end;
|
|
}
|
|
else if (Directive.compare(idx, 2, "//") == 0)
|
|
{
|
|
flush();
|
|
idx = Directive.find('\n', idx);
|
|
if (idx == std::string::npos)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
else if (Directive.compare(idx, 2, "/*") == 0)
|
|
{
|
|
flush();
|
|
idx = Directive.find("*/", idx + 2);
|
|
if (idx == std::string::npos)
|
|
{
|
|
break;
|
|
}
|
|
++idx;
|
|
}
|
|
else if (character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == ';')
|
|
{
|
|
flush();
|
|
}
|
|
else
|
|
{
|
|
token += character;
|
|
}
|
|
}
|
|
flush();
|
|
if (tokens.size() < 3 || ToLower(tokens.front()) != "include" || tokens.back() != "end")
|
|
{
|
|
return false;
|
|
}
|
|
File = tokens[1];
|
|
Values.assign(std::begin(tokens) + 2, std::end(tokens) - 1);
|
|
return true;
|
|
}
|
|
|
|
std::string compose_directive(std::string const &File, std::vector<std::string> const &Values)
|
|
{
|
|
auto const quoted = [](std::string const &Text) { return Text.find_first_of(" \t;") != std::string::npos ? '\"' + Text + '\"' : Text; };
|
|
std::string text{"include " + quoted(File)};
|
|
for (auto const &value : Values)
|
|
{
|
|
// a parameter can't be left out, as the ones after it would take its place
|
|
text += ' ' + (value.empty() ? std::string{"none"} : quoted(value));
|
|
}
|
|
return text + " end";
|
|
}
|
|
|
|
std::string substitute(std::string const &Text, std::vector<std::string> const &Values)
|
|
{
|
|
std::string result;
|
|
std::size_t length{0};
|
|
for (std::size_t idx = 0; idx < Text.size();)
|
|
{
|
|
auto const id{placeholder(Text, idx, length)};
|
|
if (id > 0 && id <= static_cast<int>(Values.size()))
|
|
{
|
|
result += Values[id - 1];
|
|
idx += length;
|
|
}
|
|
else
|
|
{
|
|
result += Text[idx++];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional<float> Yaw, std::string const &Track, glm::dvec2 const &Tilt, std::map<std::string, std::string> const &Values)
|
|
{
|
|
std::vector<std::string> values;
|
|
for (auto id = 1; id <= Parameters; ++id)
|
|
{
|
|
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
|
|
auto const role{lookup != std::end(Info.parameters) ? lookup->role : std::string{"free"}};
|
|
auto value{lookup != std::end(Info.parameters) ? lookup->value : std::string{}};
|
|
if (auto const given{Values.find("#" + std::to_string(id))}; given != Values.end())
|
|
{
|
|
value = given->second;
|
|
}
|
|
else if (auto const given{Values.find(role)}; given != Values.end())
|
|
{
|
|
value = given->second;
|
|
}
|
|
else if (role == "pos.x")
|
|
{
|
|
value = number(Location.x);
|
|
}
|
|
else if (role == "pos.y")
|
|
{
|
|
value = number(Location.y);
|
|
}
|
|
else if (role == "pos.z")
|
|
{
|
|
value = number(Location.z);
|
|
}
|
|
else if (role == "rot.y" && Yaw)
|
|
{
|
|
value = number(*Yaw);
|
|
}
|
|
else if (role == "rot.x" && Tilt.x != 0.0)
|
|
{
|
|
value = number(Tilt.x);
|
|
}
|
|
else if (role == "rot.z" && Tilt.y != 0.0)
|
|
{
|
|
value = number(Tilt.y);
|
|
}
|
|
else if (role.starts_with("rot.") && value.empty())
|
|
{
|
|
value = "0";
|
|
}
|
|
else if (role == "track" && false == Track.empty())
|
|
{
|
|
value = Track;
|
|
}
|
|
else if (role == "name")
|
|
{
|
|
// names have to be unique in the scenery. the default value, or the name of the template, makes them recognizable
|
|
static std::mt19937 engine{std::random_device{}()}; // NOSONAR
|
|
char suffix[16];
|
|
std::snprintf(suffix, sizeof(suffix), "_%08x", static_cast<unsigned int>(engine()));
|
|
value = (value.empty() ? std::filesystem::path(File).stem().string() : value) + suffix;
|
|
}
|
|
values.emplace_back(value);
|
|
}
|
|
return compose_directive(File, values);
|
|
}
|
|
|
|
} // namespace editor_includes
|
|
|
|
editorIncludeBank EditorIncludes;
|
|
|
|
namespace
|
|
{
|
|
|
|
include_entry examine(std::string const &File)
|
|
{
|
|
include_entry entry;
|
|
entry.file = File;
|
|
entry.statement = std::make_shared<std::string>(editor_includes::directive_mark + File);
|
|
template_text text;
|
|
if (false == read_template(File, text, entry.issue))
|
|
{
|
|
return entry;
|
|
}
|
|
entry.described = false == text.yaml.empty();
|
|
if (false == entry.described)
|
|
{
|
|
entry.issue = STR_C("the template has no description");
|
|
return entry;
|
|
}
|
|
include_info info;
|
|
if (false == parse(text, info, entry.issue))
|
|
{
|
|
return entry;
|
|
}
|
|
entry.name = info.name;
|
|
entry.category = info.category;
|
|
entry.complete = editor_includes::complete(info, count_parameters(text), &entry.issue);
|
|
return entry;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void editorIncludeBank::scan()
|
|
{
|
|
// the node bank may hold one of the templates selected, the statements stay in place across the scans
|
|
std::unordered_map<std::string, std::shared_ptr<std::string>> statements;
|
|
for (auto &entry : m_entries)
|
|
{
|
|
statements.emplace(entry.file, entry.statement);
|
|
}
|
|
m_entries.clear();
|
|
m_scanned = true;
|
|
std::error_code error;
|
|
auto const root{std::filesystem::path(Global.asCurrentSceneryPath)};
|
|
// NOTE: on an error the iterator turns into the end one. Folders linked into the scenery folder are a part of it
|
|
auto const options{std::filesystem::directory_options::skip_permission_denied | std::filesystem::directory_options::follow_directory_symlink};
|
|
for (std::filesystem::recursive_directory_iterator file{root, options, error}, end; file != end; file.increment(error))
|
|
{
|
|
if (false == file->is_regular_file(error))
|
|
{
|
|
continue;
|
|
}
|
|
// the scenery refers to its files with lower case names, relative to the scenery directory
|
|
std::string name;
|
|
try
|
|
{
|
|
name = ToLower(file->path().lexically_relative(root).generic_string());
|
|
}
|
|
catch (std::exception const &)
|
|
{
|
|
// the name can't be expressed in the encoding the scenery files use, so nothing can include the file anyway
|
|
continue;
|
|
}
|
|
if (false == name.ends_with(".inc"))
|
|
{
|
|
continue;
|
|
}
|
|
m_entries.emplace_back(examine(name));
|
|
if (auto const lookup{statements.find(name)}; lookup != statements.end())
|
|
{
|
|
m_entries.back().statement = lookup->second;
|
|
}
|
|
}
|
|
std::sort(std::begin(m_entries), std::end(m_entries), [](include_entry const &Left, include_entry const &Right) { return Left.file < Right.file; });
|
|
index();
|
|
}
|
|
|
|
void editorIncludeBank::index()
|
|
{
|
|
m_ready.clear();
|
|
for (std::size_t idx = 0; idx < m_entries.size(); ++idx)
|
|
{
|
|
if (m_entries[idx].complete)
|
|
{
|
|
m_ready.emplace_back(idx);
|
|
}
|
|
}
|
|
std::sort(std::begin(m_ready), std::end(m_ready), [this](std::size_t const Left, std::size_t const Right) {
|
|
auto const &left{m_entries[Left]};
|
|
auto const &right{m_entries[Right]};
|
|
return std::tie(left.category, left.name, left.file) < std::tie(right.category, right.name, right.file);
|
|
});
|
|
++m_revision;
|
|
}
|
|
|
|
void editorIncludeBank::update(std::string const &File)
|
|
{
|
|
auto const lookup{std::find_if(std::begin(m_entries), std::end(m_entries), [&](include_entry const &Entry) { return Entry.file == File; })};
|
|
if (lookup == std::end(m_entries))
|
|
{
|
|
m_entries.insert(std::upper_bound(std::begin(m_entries), std::end(m_entries), File, [](std::string const &Name, include_entry const &Entry) { return Name < Entry.file; }), examine(File));
|
|
}
|
|
else
|
|
{
|
|
// the node bank may hold the template selected, keep its statement
|
|
auto const statement{lookup->statement};
|
|
*lookup = examine(File);
|
|
lookup->statement = statement;
|
|
}
|
|
index();
|
|
}
|