17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 16:14:52 +02:00
Files
maszyna/editor/editorIncludeInfo.cpp
maj00r 949965009b fix(editor): the templates looked for in the background from the start, the panels don't wait for them
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
2026-10-19 16:21:55 +02:00

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 &parameter{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 &parameter : 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();
}