mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 12:44:52 +02:00
The include bank read every .inc of the scenery directory on the first use, on the main thread: opening the switch panel or selecting a switch stopped the editor until all of them were read, which on a large scenery directory under Windows takes long. The bank now starts looking as the editor starts, in the background, and the main thread only reads the descriptions once the files are read. - the switch drives, turntables and signals are read again from the bank when it changes; the panels show "Looking for the templates..." meanwhile, only the edits which need the templates wait for them - the sweep tool takes the templates from the bank instead of looking through the scenery directory again - a folder linked into one of its own subfolders no longer leads the walk in circles - the log tells how many templates were found and how long it took
979 lines
28 KiB
C++
979 lines
28 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/Logs.h"
|
|
#include "utilities/utilities.h"
|
|
#include "utilities/translation.h"
|
|
#include "editor/editorFormat.hpp"
|
|
|
|
#include <yaml-cpp/yaml.h>
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <future>
|
|
#include <random>
|
|
#include <set>
|
|
#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;
|
|
}
|
|
|
|
// reads template File from the file at specified location. runs in the background as well, so it leaves the messages to
|
|
// the callers
|
|
bool read_file(std::filesystem::path const &Path, std::string const &File, template_text &Text)
|
|
{
|
|
std::ifstream input{Path, std::ios_base::binary};
|
|
if (false == input.is_open())
|
|
{
|
|
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;
|
|
}
|
|
|
|
bool read_template(std::string const &File, template_text &Text, std::string &Error)
|
|
{
|
|
if (false == read_file(Global.asCurrentSceneryPath + File, File, Text))
|
|
{
|
|
Error = format(STR_C("can't open file \"%s\""), File.c_str());
|
|
return false;
|
|
}
|
|
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
|
|
{
|
|
|
|
// Text: the template read before, null if its file couldn't be opened
|
|
include_entry examine(std::string const &File, template_text const *Text)
|
|
{
|
|
include_entry entry;
|
|
entry.file = File;
|
|
entry.statement = std::make_shared<std::string>(editor_includes::directive_mark + File);
|
|
if (Text == nullptr)
|
|
{
|
|
entry.issue = format(STR_C("can't open file \"%s\""), File.c_str());
|
|
return entry;
|
|
}
|
|
auto const &text{*Text};
|
|
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;
|
|
}
|
|
|
|
include_entry examine(std::string const &File)
|
|
{
|
|
template_text text;
|
|
std::string error;
|
|
auto const read{read_template(File, text, error)};
|
|
return examine(File, read ? &text : nullptr);
|
|
}
|
|
|
|
// template file found in the background, for examine() to read its description on the main thread
|
|
struct found_template
|
|
{
|
|
std::string file;
|
|
bool read{false};
|
|
template_text text;
|
|
};
|
|
|
|
// reads every template in the scenery directory, until told to stop
|
|
std::vector<found_template> find_templates(std::filesystem::path const &Root, std::atomic<bool> const &Stop)
|
|
{
|
|
std::vector<found_template> found;
|
|
std::error_code error;
|
|
// a folder linked into one of its own subfolders would lead the walk in circles
|
|
std::set<std::filesystem::path> folders{std::filesystem::canonical(Root, error)};
|
|
// 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 && false == Stop; file.increment(error))
|
|
{
|
|
std::error_code status;
|
|
if (file->is_directory(status))
|
|
{
|
|
if (auto const folder{std::filesystem::canonical(file->path(), status)}; false == static_cast<bool>(status) && false == folders.emplace(folder).second)
|
|
{
|
|
file.disable_recursion_pending();
|
|
}
|
|
continue;
|
|
}
|
|
if (false == file->is_regular_file(status))
|
|
{
|
|
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;
|
|
}
|
|
auto &entry{found.emplace_back()};
|
|
entry.file = name;
|
|
entry.read = read_file(file->path(), name, entry.text);
|
|
if (entry.text.yaml.empty())
|
|
{
|
|
// the rest of the file matters only to the described templates
|
|
entry.text = template_text{};
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct editorIncludeBank::search
|
|
{
|
|
std::atomic<bool> stop{false};
|
|
std::future<std::vector<found_template>> result;
|
|
std::chrono::steady_clock::time_point started{std::chrono::steady_clock::now()};
|
|
|
|
~search()
|
|
{
|
|
// the result waits for the search to end before it goes
|
|
stop = true;
|
|
}
|
|
};
|
|
|
|
editorIncludeBank::~editorIncludeBank() = default;
|
|
|
|
void editorIncludeBank::start()
|
|
{
|
|
if (m_scanned || m_search != nullptr)
|
|
{
|
|
return;
|
|
}
|
|
rescan();
|
|
}
|
|
|
|
void editorIncludeBank::rescan()
|
|
{
|
|
if (m_search != nullptr)
|
|
{
|
|
return;
|
|
}
|
|
m_search = std::make_shared<search>();
|
|
m_search->result = std::async(std::launch::async, find_templates, std::filesystem::path(Global.asCurrentSceneryPath), std::cref(m_search->stop));
|
|
}
|
|
|
|
bool editorIncludeBank::poll()
|
|
{
|
|
if (m_search == nullptr || m_search->result.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
|
|
{
|
|
return false;
|
|
}
|
|
auto found{m_search->result.get()};
|
|
auto const seconds{std::chrono::duration<double>(std::chrono::steady_clock::now() - m_search->started).count()};
|
|
m_search.reset();
|
|
// 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;
|
|
for (auto const &file : found)
|
|
{
|
|
m_entries.emplace_back(examine(file.file, file.read ? &file.text : nullptr));
|
|
if (auto const lookup{statements.find(file.file)}; 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();
|
|
WriteLog(format("Editor: %zu templates found in the scenery directory in %.1f s, %zu of them described for the editor", m_entries.size(), seconds,
|
|
static_cast<std::size_t>(std::count_if(std::begin(m_entries), std::end(m_entries), [](include_entry const &Entry) { return Entry.described; }))));
|
|
return true;
|
|
}
|
|
|
|
void editorIncludeBank::wait()
|
|
{
|
|
start();
|
|
if (m_search != nullptr)
|
|
{
|
|
m_search->result.wait();
|
|
poll();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|