17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 02:49:19 +02:00

feat(editor): layers window, scenery save and include descriptions

This commit is contained in:
2026-10-02 22:40:52 +02:00
parent 73f2f68aa0
commit 3199f3796e
18 changed files with 3045 additions and 123 deletions

View File

@@ -168,6 +168,7 @@ set(SOURCES
"input/editorkeyboardinput.cpp"
"editor/editorSettings.cpp"
"editor/editorModelSets.cpp"
"editor/editorIncludeInfo.cpp"
"editor/editorTerrain.cpp"
"editor/editorTerrainStreamer.cpp"
"editor/editorOrthophoto.cpp"
@@ -179,6 +180,7 @@ set(SOURCES
"application/scenarioloaderuilayer.cpp"
"scene/scenenodegroups.cpp"
"scene/scenelayers.cpp"
"scene/scenelayerssave.cpp"
"simulation/simulationenvironment.cpp"
"simulation/simulationstateserializer.cpp"
"rendering/precipitation.cpp"
@@ -608,6 +610,13 @@ if (WITH_BETTER_RENDERER)
target_link_libraries(${PROJECT_NAME} "betterRenderer")
endif ()
# yaml-cpp, for the descriptions of scenery templates edited in the scenery editor.
# the experimental renderer brings in the same copy on its own, when enabled
if (NOT TARGET yaml-cpp)
add_subdirectory("betterRenderer/thirdparty/yaml-cpp" "${CMAKE_CURRENT_BINARY_DIR}/yaml-cpp" EXCLUDE_FROM_ALL)
endif ()
target_link_libraries(${PROJECT_NAME} yaml-cpp)
# Enable compiling with debug symbols
if(GENERATE_PDB)

View File

@@ -337,6 +337,7 @@ editor_mode::editor_mode() {
// the area fill settings live in the node bank window, in the tab of the fill mode
ui()->set_fill_options([this]() { render_area_fill(); });
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); });
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) {
gather_ground_triangles(Min, Max, true, 50.0f, Out);
@@ -606,6 +607,7 @@ void editor_mode::push_snapshot(scene::basic_node *node, EditorSnapshot::Action
snap.node_name = node->name();
snap.position = node->location();
snap.uuid = node->uuid;
snap.layer = node->layer();
if (auto *model = dynamic_cast<TAnimModel *>(node))
{
@@ -679,11 +681,13 @@ void editor_mode::undo_last()
redoSnap.serialized = snap.serialized;
redoSnap.position = snap.position;
redoSnap.node_ptr = nullptr;
redoSnap.layer = snap.layer;
g_redo.push_back(std::move(redoSnap));
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
if (created)
{
scene::Layers.move(created, snap.layer); // back to the layer it was deleted from, rather than the active one
created->location(snap.position);
created->Angles(snap.rotation);
m_node = created;
@@ -702,6 +706,7 @@ void editor_mode::undo_last()
current.action = snap.action;
current.node_name = snap.node_name;
current.node_ptr = target;
current.layer = target->layer();
current.position = target->location();
if (auto *model = dynamic_cast<TAnimModel *>(target))
{
@@ -758,6 +763,7 @@ void editor_mode::redo_last()
hist.serialized = snap.serialized;
hist.position = snap.position;
hist.uuid = snap.uuid;
hist.layer = snap.layer;
m_history.push_back(std::move(hist));
scene::basic_node *target = simulation::Instances.find(snap.node_name);
@@ -799,6 +805,7 @@ void editor_mode::redo_last()
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
if (created)
{
scene::Layers.move(created, snap.layer);
created->location(snap.position);
created->Angles(snap.rotation);
created->Scale(snap.scale);
@@ -893,7 +900,7 @@ bool editor_mode::update()
update_camera(deltarealtime);
// drop the selection if its layer was hidden or locked in the meantime
if (m_node != nullptr && false == scene::Layers.editable(m_node->layer()))
if (m_node != nullptr && false == scene::Layers.editable(m_node))
{
m_node = nullptr;
m_dragging = false;
@@ -1497,6 +1504,15 @@ void editor_mode::create_chunked_terrain()
}
void editor_mode::save_scene_with_terrain()
{
commit_terrain();
// export scenery; the exported .scm now carries an `editorterrain` directive (streamer is active)
export_scenery();
WriteLog("Editor: saved scene + terrain", logtype::generic);
}
void editor_mode::commit_terrain()
{
// commit authored terrain so the scenery streams it on load. if not already streaming, hand the
// manual grid chunks over to the streamer (same as toggling Stream terrain on)
@@ -1529,10 +1545,37 @@ void editor_mode::save_scene_with_terrain()
}
m_streamer.flush(); // save resident edited chunks to disk
}
// export scenery; the exported .scm now carries an `editorterrain` directive (streamer is active)
export_scenery();
WriteLog("Editor: saved scene + terrain", logtype::generic);
void editor_mode::save()
{
if (scene::Layers.empty())
{
// without the sources tracked during the load there's no telling where the changes belong
ui()->set_status("The scenery wasn't opened for editing. Start the simulator with: -edit <scenery file>", true);
return;
}
// terrain made in the editor is kept in files of its own, the scenery only needs the directive which loads it
std::vector<std::string> rootstatements;
if (m_streamer.active() || false == m_grid_chunks.empty())
{
commit_terrain();
if (false == scene::Layers.terrain_directive())
{
rootstatements.emplace_back(
"editorterrain " + m_streamer.directory() + ' ' + std::to_string(m_streamer.cells()) + ' ' + to_string(m_streamer.cellsize(), 3) + ' '
+ std::to_string(m_streamer.radius()) + " endeditorterrain");
}
}
auto const result = scene::Layers.save(rootstatements);
if (result.success && false == rootstatements.empty())
{
scene::Layers.terrain_directive(true);
}
ui()->set_status(result.message, false == result.success);
WriteLog("Editor: " + result.message, logtype::generic);
}
void editor_mode::export_scenery()
@@ -2196,6 +2239,14 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
return;
}
// save; checked ahead of the camera keys, which have S bound to moving back
if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S)
{
if (is_press(Action))
save();
return;
}
// then internal input handling
if (m_input.keyboard.key(Key, Action))
return;
@@ -2364,9 +2415,13 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
m_node = nullptr;
// nodes of locked layers can't be selected
if (node && false == scene::Layers.editable(node->layer()))
// in a scenery opened for editing some nodes are off limits: the ones in locked layers,
// and the ones whose definitions can't be rewritten on save
if (std::string reason; node && false == scene::Layers.editable(node, &reason))
{
ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be edited: " + reason, true);
node = nullptr;
}
// ignore picks that are beyond allowed placement distance
if (node) {

View File

@@ -96,6 +96,7 @@ class editor_mode : public application_mode
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f, 1.0f, 1.0f};
UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes
scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it
};
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
@@ -160,8 +161,12 @@ class editor_mode : public application_mode
void remove_grid_chunk(int Cx, int Cz);
// handles a click in chunk-edit mode (add a neighbour, or Shift = delete the clicked chunk)
void handle_chunk_edit_click(bool DeleteMode);
// commits authored terrain to disk, enables streaming, and exports the scenery (Ctrl+S)
// commits authored terrain to disk, enables streaming, and exports the scenery
void save_scene_with_terrain();
// commits authored terrain to disk and enables streaming
void commit_terrain();
// writes the changes to the files of scenery opened for editing (Ctrl+S)
void save();
// exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture
void export_scenery();
// raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode

View File

@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
#include "scene/scenenode.h"
#include "scene/scenelayers.h"
#include "rendering/renderer.h"
#include "utilities/translation.h"
editor_ui::editor_ui()
{
@@ -23,6 +24,8 @@ editor_ui::editor_ui()
// bind the panels with ui object. maybe not the best place for this but, eh
add_external_panel(&m_nodebankpanel);
add_external_panel(&m_layerspanel);
add_external_panel(&m_includespanel);
m_nodebankpanel.mode_options = [this](nodebank_panel::edit_mode const Mode) { render_mode_options(Mode); };
m_nodebankpanel.header_sections = [this]() { render_header_sections(); };
@@ -100,98 +103,55 @@ void editor_ui::render_header_sections()
m_itempropertiespanel.render_body();
ImGui::Unindent();
}
if (ImGui::CollapsingHeader("Layers"))
}
void editor_ui::render_menu_contents()
{
if (ImGui::BeginMenu(STR_C("File")))
{
ImGui::Indent();
render_layers();
ImGui::Unindent();
// changes go to the scenery files only if the scenery was loaded with its sources tracked
auto const editsession{false == scene::Layers.empty()};
if (ImGui::MenuItem(STR_C("Save"), "Ctrl+S", false, editsession) && m_save)
{
m_save();
}
if (false == editsession && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
{
ImGui::SetTooltip("%s", STR_C("Start the simulator with -edit <scenery file> to save changes to the scenery files"));
}
if (ImGui::MenuItem(STR_C("Export scenery dump"), "Ctrl+Shift+F11") && m_export)
{
m_export();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", STR_C("Writes the whole scene to _export files next to the scenario, the scenery files are left as they are"));
}
ImGui::EndMenu();
}
ui_layer::render_menu_contents();
if (ImGui::BeginMenu(STR_C("Mode windows")))
{
ImGui::MenuItem(STR_C("Toolset"), nullptr, &m_nodebankpanel.is_open);
ImGui::MenuItem(STR_C("Layers"), nullptr, &m_layerspanel.is_open);
ImGui::MenuItem(STR_C("Include descriptions"), nullptr, &m_includespanel.is_open);
ImGui::EndMenu();
}
}
void editor_ui::render_layers()
void editor_ui::set_file_actions(std::function<void()> Save, std::function<void()> Export)
{
if (scene::Layers.empty())
{
// layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping
ImGui::TextDisabled("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>");
return;
}
m_save = Save;
m_export = std::move(Export);
m_layerspanel.save = std::move(Save);
}
ImGui::TextDisabled("visible, locked, layer file (click: make active)");
std::pair<char const *, scene::layer_item> const itemlabels[] = {{"models", scene::layer_item::model},
{"tracks", scene::layer_item::track},
{"traction", scene::layer_item::traction},
{"power sources", scene::layer_item::powersource},
{"memory cells", scene::layer_item::memcell},
{"event launchers", scene::layer_item::launcher},
{"events", scene::layer_item::event},
{"vehicles", scene::layer_item::vehicle},
{"sounds", scene::layer_item::sound},
{"terrain shapes", scene::layer_item::shape},
{"lines", scene::layer_item::lines}};
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
auto const &layer{scene::Layers.layer(handle)};
auto const isactive{handle == scene::Layers.active()};
ImGui::PushID(static_cast<int>(idx));
// active layer receives new nodes, so it can't be hidden nor locked
if (isactive)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
auto visible{layer.visible};
if (ImGui::Checkbox("##visible", &visible) && false == isactive)
{
scene::Layers.visible(handle, visible);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", isactive ? "Active layer is always visible" : "Show or hide models, tracks and traction of the layer");
}
ImGui::SameLine();
auto locked{layer.locked};
if (ImGui::Checkbox("##locked", &locked) && false == isactive)
{
scene::Layers.locked(handle, locked);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", isactive ? "Active layer can't be locked" : "Nodes of locked layer can't be selected in the viewport");
}
if (isactive)
{
ImGui::PopStyleVar();
}
ImGui::SameLine();
// layers are listed in the order the files were first opened, so indentation alone shows the include tree
auto const label{std::string(2 * scene::Layers.depth(handle), ' ') + Bezogonkow(layer.name) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"};
if (ImGui::Selectable(label.c_str(), isactive))
{
scene::Layers.active(handle);
}
if (ImGui::IsItemHovered())
{
std::string content{isactive ? "Active layer, nodes created in the editor are placed here" : "Click to make this the active layer"};
if (layer.binary)
{
content += "\nThe file wasn't parsed, its content was loaded from binary terrain file";
}
for (auto const &itemlabel : itemlabels)
{
auto const count{layer.items[static_cast<std::size_t>(itemlabel.second)]};
if (count > 0)
{
content += "\n" + std::string{itemlabel.first} + ": " + std::to_string(count);
}
}
ImGui::SetTooltip("%s", content.c_str());
}
ImGui::PopID();
}
void editor_ui::set_status(std::string const &Status, bool const Error)
{
m_layerspanel.status = Status;
m_layerspanel.status_error = Error;
}
void editor_ui::render_rotation_controls()

View File

@@ -53,17 +53,24 @@ class editor_ui : public ui_layer
{
m_gizmooptions = std::move(Renderer);
}
// actions of the file menu, performed by the editor mode
void set_file_actions(std::function<void()> Save, std::function<void()> Export);
// shows outcome of an operation in the layers window
void set_status(std::string const &Status, bool const Error = false);
private:
// methods
void render_mode_options(nodebank_panel::edit_mode const Mode);
void render_header_sections();
// list of scenery layers: visibility, selection lock and choice of the layer receiving new nodes
void render_layers();
void render_menu_contents() override;
// members
itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window
functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs
nodebank_panel m_nodebankpanel{"Toolset", true}; // main editor window: gizmo, node properties, edit modes and the node bank
layers_panel m_layerspanel{"Layers", true};
includes_panel m_includespanel{"Include descriptions", false};
std::function<void()> m_save;
std::function<void()> m_export;
brush_object_list m_brushobjects;
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
model_set_ref m_insertset;

View File

@@ -1076,4 +1076,581 @@ void functions_panel::render_controls()
// ImGui::Checkbox("Get rotation from last object", &rot_from_last);
ImGui::SliderFloat("Rotation value", &rot_value, 0.0f, 360.0f, "%.1f deg");
}
}
}
namespace
{
std::pair<char const *, scene::layer_item> const layer_itemlabels[] = {{"models", scene::layer_item::model},
{"tracks", scene::layer_item::track},
{"traction", scene::layer_item::traction},
{"power sources", scene::layer_item::powersource},
{"memory cells", scene::layer_item::memcell},
{"event launchers", scene::layer_item::launcher},
{"events", scene::layer_item::event},
{"vehicles", scene::layer_item::vehicle},
{"sounds", scene::layer_item::sound},
{"terrain shapes", scene::layer_item::shape},
{"lines", scene::layer_item::lines}};
// what the layer holds, a line per kind of item
std::string layer_content(scene::basic_layer const &Layer)
{
std::string content;
for (auto const &itemlabel : layer_itemlabels)
{
auto const count{Layer.items[static_cast<std::size_t>(itemlabel.second)]};
if (count > 0)
{
content += (content.empty() ? "" : "\n") + std::string{itemlabel.first} + ": " + std::to_string(count);
}
}
return content;
}
std::string layer_name(scene::layer_handle const Layer)
{
return scene::Layers.valid(Layer) ? Bezogonkow(scene::Layers.layer(Layer).name) : "(none)";
}
// button which can be greyed out, with an explanation shown when it's hovered
bool action_button(char const *Label, bool const Enabled, std::string const &Reason = std::string())
{
if (false == Enabled)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
auto const clicked{ImGui::Button(Label) && Enabled};
if (false == Enabled)
{
ImGui::PopStyleVar();
if (false == Reason.empty() && ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", Reason.c_str());
}
}
return clicked;
}
// text field bound to a string
bool input_text(char const *Label, std::string &Value)
{
std::array<char, 256> buffer{};
Value.copy(buffer.data(), buffer.size() - 1);
if (false == ImGui::InputText(Label, buffer.data(), buffer.size()))
{
return false;
}
Value = buffer.data();
return true;
}
ImVec4 const status_errorcolor{1.0f, 0.45f, 0.4f, 1.0f};
} // namespace
void layers_panel::report(std::string const &Status, bool const Error)
{
status = Status;
status_error = Error;
}
void layers_panel::render()
{
if (false == is_open)
{
return;
}
// initially next to the toolset window
ImGui::SetNextWindowPos(ImVec2S(520, 60), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2S(440, 340), ImGuiCond_FirstUseEver);
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
{
if (scene::Layers.empty())
{
// layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping
ImGui::TextDisabled("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>");
}
else
{
if (false == scene::Layers.listed(m_selected))
{
m_selected = null_handle;
}
auto const selected{m_selected != null_handle};
auto const removed{selected && scene::Layers.layer(m_selected).removed};
std::string reason{"Select a layer first"};
if (ImGui::Button("New..."))
{
m_newname[0] = '\0';
m_newparent = (selected && false == removed && false == scene::Layers.layer(m_selected).binary ? m_selected : scene::layer_handle{1});
m_popuperror.clear();
ImGui::OpenPopup("New layer");
}
ImGui::SameLine();
if (removed)
{
if (ImGui::Button("Restore"))
{
scene::Layers.restore(m_selected);
report("Layer \"" + layer_name(m_selected) + "\" restored");
}
}
else if (action_button("Remove...", selected && scene::Layers.can_remove(m_selected, reason), reason))
{
ImGui::OpenPopup("Remove layer");
}
ImGui::SameLine();
// the scenario file is what holds the scenery together, it stays where it is
auto const mergeable{selected && false == removed && (scene::Layers.layer(m_selected).created || false == scene::Layers.layer(m_selected).sites.empty())};
if (action_button("Merge into...", mergeable, selected && false == removed ? "The scenario file can't be merged into another layer" : "Select a layer first"))
{
// the layer which includes the selected one is the most likely target
m_mergetarget = scene::Layers.resolve(scene::Layers.layer(m_selected).parent);
if (false == scene::Layers.can_merge(m_selected, m_mergetarget, reason))
{
m_mergetarget = null_handle;
}
ImGui::OpenPopup("Merge layer");
}
ImGui::SameLine();
if (ImGui::Button("Save") && save)
{
save();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Writes changed nodes and layers to the scenery files (Ctrl+S)");
}
ImGui::Separator();
render_list();
if (false == status.empty())
{
ImGui::PushTextWrapPos(0.0f);
if (status_error)
{
ImGui::TextColored(status_errorcolor, "%s", status.c_str());
}
else
{
ImGui::TextUnformatted(status.c_str());
}
ImGui::PopTextWrapPos();
}
render_popups();
}
}
ImGui::End();
}
void layers_panel::render_list()
{
// the list takes the window except for the room left for the status
ImGui::BeginChild("##layers", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing() * 2.5f));
ImGui::TextDisabled("visible, locked, active, layer file");
for (auto const handle : scene::Layers.tree())
{
// NOTE: copies of the flags, as the calls below can change the layer
auto const &layer{scene::Layers.layer(handle)};
auto const isactive{handle == scene::Layers.active()};
auto const isremoved{layer.removed};
std::string readonly;
auto const iswritable{scene::Layers.writable(handle, &readonly)};
ImGui::PushID(static_cast<int>(handle));
if (isremoved)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
// active layer receives new nodes, so it can't be hidden nor locked. removed layer is out of the picture altogether
auto const fixed{isactive || isremoved};
auto visible{layer.visible};
if (ImGui::Checkbox("##visible", &visible) && false == fixed)
{
scene::Layers.visible(handle, visible);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", isactive ? "Active layer is always visible" : "Show or hide models, tracks and traction of the layer");
}
ImGui::SameLine();
auto locked{layer.locked};
if (ImGui::Checkbox("##locked", &locked) && false == fixed)
{
scene::Layers.locked(handle, locked);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", isactive ? "Active layer can't be locked" : "Nodes of locked layer can't be selected in the viewport");
}
ImGui::SameLine();
if (ImGui::RadioButton("##active", isactive) && false == isactive)
{
if (isremoved || false == iswritable)
{
report("Layer \"" + layer_name(handle) + "\" can't take new nodes" + (isremoved ? "" : ": " + readonly), true);
}
else
{
scene::Layers.active(handle);
}
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Active layer: nodes created in the editor are placed in it");
}
ImGui::SameLine();
auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"};
if (layer.created)
{
label += " [new]";
}
if (isremoved)
{
label += " [removed]";
}
else if (false == iswritable)
{
label += " [read-only]";
}
if (ImGui::Selectable(label.c_str(), handle == m_selected))
{
m_selected = handle;
}
if (ImGui::IsItemHovered())
{
std::string content{layer.created ? "New layer, its file is created on save" : isremoved ? "Dropped from the scenery on save" : "Scenery file"};
if (scene::Layers.valid(layer.parent))
{
content += "\nincluded by: " + layer_name(scene::Layers.resolve(layer.parent));
}
if (false == iswritable)
{
content += "\nread-only: " + readonly;
}
if (auto const items{layer_content(layer)}; false == items.empty())
{
content += "\n" + items;
}
ImGui::SetTooltip("%s", content.c_str());
}
if (isremoved)
{
ImGui::PopStyleVar();
}
ImGui::PopID();
}
ImGui::EndChild();
}
void layers_panel::render_popups()
{
if (ImGui::BeginPopupModal("New layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("File of the new layer, in the scenery directory:");
ImGui::InputTextWithHint("##name", "name.scm", m_newname, IM_ARRAYSIZE(m_newname));
if (ImGui::BeginCombo("Included by", layer_name(m_newparent).c_str()))
{
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
if (false == scene::Layers.listed(handle) || scene::Layers.layer(handle).removed || scene::Layers.layer(handle).binary)
{
continue;
}
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_newparent))
{
m_newparent = handle;
}
}
ImGui::EndCombo();
}
if (false == m_popuperror.empty())
{
ImGui::TextColored(status_errorcolor, "%s", m_popuperror.c_str());
}
if (ImGui::Button("Create"))
{
auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)};
if (created != null_handle)
{
m_selected = created;
report("Layer \"" + layer_name(created) + "\" created and made active. Its file is written on save.");
ImGui::CloseCurrentPopup();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Remove layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
if (false == scene::Layers.listed(m_selected))
{
ImGui::CloseCurrentPopup();
}
else
{
auto const &layer{scene::Layers.layer(m_selected)};
auto included{0};
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
if (handle != m_selected && scene::Layers.listed(handle) && scene::Layers.is_descendant(handle, m_selected))
{
++included;
}
}
ImGui::Text("Remove layer \"%s\" from the scenery?", layer_name(m_selected).c_str());
ImGui::TextUnformatted(layer.created ? "The layer wasn't saved yet, so it leaves no file behind." : "Its include directive is erased on save. The file itself stays on the disk.");
if (included > 0)
{
ImGui::Text("%d layer(s) it includes are removed with it.", included);
}
if (auto const items{layer_content(layer)}; false == items.empty())
{
ImGui::Separator();
ImGui::TextUnformatted("The layer holds:");
ImGui::TextUnformatted(items.c_str());
ImGui::TextDisabled("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells.");
}
if (ImGui::Button("Remove"))
{
auto const name{layer_name(m_selected)};
if (scene::Layers.remove(m_selected))
{
report("Layer \"" + name + "\" removed. It can be restored until the scenery is saved.");
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Merge layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
if (false == scene::Layers.listed(m_selected))
{
ImGui::CloseCurrentPopup();
}
else
{
ImGui::Text("Move content of layer \"%s\" to:", layer_name(m_selected).c_str());
std::string reason;
auto targets{0};
if (ImGui::BeginCombo("##target", layer_name(m_mergetarget).c_str()))
{
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
if (false == scene::Layers.listed(handle) || false == scene::Layers.can_merge(m_selected, handle, reason))
{
continue;
}
++targets;
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_mergetarget))
{
m_mergetarget = handle;
}
}
if (targets == 0)
{
ImGui::TextDisabled("no layer can take it");
}
ImGui::EndCombo();
}
ImGui::TextDisabled("The text of the file is moved on save, the emptied file stays on the disk.\n"
"Merged into the layer which includes it, the content stays where the include was.\n"
"Merged into any other layer it's placed at its end, which changes the load order.");
auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)};
if (action_button("Merge", possible, m_mergetarget != null_handle ? reason : "Pick the target layer"))
{
auto const name{layer_name(m_selected)};
auto const target{m_mergetarget};
if (scene::Layers.merge(m_selected, target))
{
report("Layer \"" + name + "\" merged into \"" + layer_name(target) + "\". The files are changed on save.");
m_selected = target;
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
}
void includes_panel::open(std::string const &File)
{
m_file = File;
m_statuserror = false;
std::string error;
m_loaded = editor_includes::load(File, m_info, error);
if (false == m_loaded)
{
m_status = error;
m_statuserror = true;
return;
}
// every parameter the template uses gets an entry, described or not
auto const count{editor_includes::parameter_count(File)};
for (auto id = 1; id <= count; ++id)
{
m_info.parameter(id);
}
m_status = "The template uses " + std::to_string(count) + " parameter(s)";
}
void includes_panel::render()
{
if (false == is_open)
{
return;
}
ImGui::SetNextWindowPos(ImVec2S(560, 110), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2S(520, 440), ImGuiCond_FirstUseEver);
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
{
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
ImGui::TextWrapped("Describes .inc templates and their parameters. The description is stored in the template file, as //$e comment lines.");
ImGui::PopStyleColor();
// templates the loaded scenery makes use of; known only for scenery opened for editing
auto const &templates{scene::Layers.templates()};
if (false == templates.empty())
{
ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 6.0f), true);
for (auto const &file : templates)
{
if (ImGui::Selectable(Bezogonkow(file).c_str(), file == m_file))
{
open(file);
}
}
ImGui::EndChild();
}
ImGui::PushItemWidth(-ImGui::CalcTextSize(" Open ").x - ImGui::GetStyle().ItemSpacing.x * 2.0f);
auto const entered{ImGui::InputTextWithHint("##file", "path of an .inc file in the scenery directory", m_filename, IM_ARRAYSIZE(m_filename), ImGuiInputTextFlags_EnterReturnsTrue)};
ImGui::PopItemWidth();
ImGui::SameLine();
if ((ImGui::Button("Open") || entered) && m_filename[0] != '\0')
{
std::string file{ToLower(m_filename)};
replace_slashes(file);
open(file);
}
if (m_loaded)
{
ImGui::Separator();
ImGui::Text("%s", Bezogonkow(m_file).c_str());
input_text("Name", m_info.name);
input_text("Category", m_info.category);
input_text("Description", m_info.description);
ImGui::Columns(4, "##parameters", false);
ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x);
ImGui::SetColumnWidth(1, ImGui::CalcTextSize("texture ").x + ImGui::GetFrameHeight() * 2.0f);
ImGui::TextDisabled("param");
ImGui::NextColumn();
ImGui::TextDisabled("role");
ImGui::NextColumn();
ImGui::TextDisabled("label");
ImGui::NextColumn();
ImGui::TextDisabled("default");
ImGui::NextColumn();
for (auto &parameter : m_info.parameters)
{
ImGui::PushID(parameter.id);
ImGui::AlignTextToFramePadding();
ImGui::Text("(p%d)", parameter.id);
ImGui::NextColumn();
ImGui::PushItemWidth(-1.0f);
if (ImGui::BeginCombo("##role", parameter.role.c_str()))
{
for (auto const &role : editor_includes::roles)
{
if (ImGui::Selectable(role.c_str(), role == parameter.role))
{
parameter.role = role;
}
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
ImGui::NextColumn();
ImGui::PushItemWidth(-1.0f);
input_text("##label", parameter.label);
ImGui::PopItemWidth();
ImGui::NextColumn();
ImGui::PushItemWidth(-1.0f);
input_text("##default", parameter.value);
ImGui::PopItemWidth();
ImGui::NextColumn();
ImGui::PopID();
}
ImGui::Columns(1);
if (ImGui::Button("Suggest roles"))
{
editor_includes::suggest(m_file, m_info);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are");
}
ImGui::SameLine();
if (ImGui::Button("Add parameter"))
{
m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1);
}
ImGui::SameLine();
if (ImGui::Button("Save to file"))
{
std::string error;
m_statuserror = false == editor_includes::save(m_file, m_info, error);
m_status = m_statuserror ? error : "Description saved in \"" + m_file + "\"";
}
ImGui::SameLine();
if (ImGui::Button("Reload"))
{
open(m_file);
}
}
if (false == m_status.empty())
{
ImGui::PushTextWrapPos(0.0f);
if (m_statuserror)
{
ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(m_status).c_str());
}
else
{
ImGui::TextUnformatted(Bezogonkow(m_status).c_str());
}
ImGui::PopTextWrapPos();
}
}
ImGui::End();
}

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/uilayer.h"
#include "utilities/Classes.h"
#include "editor/editorIncludeInfo.hpp"
#include <functional>
/*
@@ -148,6 +149,56 @@ class nodebank_panel : public ui_panel
model_set_ref m_setsgroup{model_set_ref::source::nodebank, 0}; // node bank group to append to the edited set
};
// scenery layers of a scenery opened for editing: their visibility, selection lock, the layer receiving new nodes,
// and layer management. the changes are applied to the scenery files on save
class layers_panel : public ui_panel
{
public:
layers_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {}
void render() override;
// saves the scenery; provided by the editor mode
std::function<void()> save;
// outcome of the most recent operation, shown at the bottom of the window
std::string status;
bool status_error{false};
private:
// methods
void render_list();
void render_popups();
void report(std::string const &Status, bool const Error = false);
// members
scene::layer_handle m_selected{null_handle}; // layer the buttons act on
char m_newname[128]{}; // file name of the layer being created
scene::layer_handle m_newparent{null_handle}; // layer which is going to include it
scene::layer_handle m_mergetarget{null_handle};
std::string m_popuperror;
};
// editor of the descriptions of *.inc scenery templates, see editorIncludeInfo.hpp
class includes_panel : public ui_panel
{
public:
includes_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {}
void render() override;
private:
// methods
void open(std::string const &File);
// members
std::string m_file; // template being edited, relative to the scenery directory
char m_filename[256]{}; // name of a template typed in by hand
include_info m_info;
bool m_loaded{false};
std::string m_status;
bool m_statuserror{false};
};
class functions_panel : public ui_panel
{

View File

@@ -0,0 +1,509 @@
/*
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 <yaml-cpp/yaml.h>
#include <filesystem>
#include <sstream>
#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 = "can't open file \"" + File + "\"";
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;
}
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;
}
} // 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)
{
Info = include_info();
template_text text;
if (false == read_template(File, text, Error))
{
return false;
}
try
{
auto const document{YAML::Load(text.yaml)};
if (document.IsNull())
{
return true;
}
if (false == document.IsMap())
{
Error = "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 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 = std::string{"the description is malformed: "} + exception.what();
return false;
}
return true;
}
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 = "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 = std::string{"the existing description is malformed, fix or remove it first: "} + 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 = "can't write file \"" + File + "\"";
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 = "can't replace file \"" + File + "\": " + error.message();
return false;
}
return true;
}
int parameter_count(std::string const &File)
{
template_text text;
std::string error;
if (false == read_template(File, text, error))
{
return 0;
}
// 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;
}
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");
}
}
}
}
} // namespace editor_includes

View File

@@ -0,0 +1,59 @@
/*
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/.
*/
#pragma once
#include <string>
#include <vector>
// description of an *.inc scenery template: what the template is, and what its (pN) parameters mean.
// it's kept in the template file itself, as a YAML document made of the comment lines starting with "//$e", e.g.
// //$e version: 1
// //$e name: Semaphore, 5 lights
// //$e params:
// //$e - {id: 1, role: name, label: Name}
// being a comment it's invisible to the scenery parser and to the older tools
struct include_parameter
{
int id{0}; // number of the (pN) placeholder
std::string role{"free"}; // what the value is; one of editor_includes::roles
std::string label; // name to show to the user
std::string value; // default value
};
struct include_info
{
std::string name;
std::string category;
std::string description;
std::vector<include_parameter> parameters;
// description of specified parameter, created if there's none
include_parameter &parameter(int Id);
};
namespace editor_includes
{
// recognized parameter roles. "free" is a value typed in by the user, with no meaning to the editor;
// the placement roles (pos.*, rot.*) and "name" are the ones the editor can fill in on its own
extern std::vector<std::string> const roles;
// reads description of specified template, located in the scenery directory. a template without the description
// yields an empty one. returns: false if the file can't be read or its description isn't a valid YAML document
bool load(std::string const &File, include_info &Info, std::string &Error);
// stores provided description in specified template. entries of the existing description which aren't
// a part of include_info are preserved. returns: false if the file can't be updated
bool save(std::string const &File, include_info const &Info, std::string &Error);
// highest number of (pN) placeholder used by specified template, 0 if it has none
int parameter_count(std::string const &File);
// sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone
void suggest(std::string const &File, include_info &Info);
} // namespace editor_includes

View File

@@ -14,22 +14,44 @@ http://mozilla.org/MPL/2.0/.
#include "model/AnimModel.h"
#include "world/Track.h"
#include "world/Traction.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include <functional>
namespace scene
{
node_layers Layers;
// converts world space location to the one which produces it when loaded with this context in effect
glm::dvec3 layer_context::to_local(glm::dvec3 Location) const
{
// reverse of the transformation done by the scenery loader: rotation around the vertical axis, then scale, then offset
Location -= offset;
Location /= glm::dvec3{scale};
if (rotation != glm::vec3{0.f})
{
Location = glm::rotateY<double>(Location, -glm::radians(static_cast<double>(rotation.y)));
}
return Location;
}
bool layer_context::matches(layer_context const &Other) const
{
return glm::all(glm::epsilonEqual(offset, Other.offset, 1e-4)) && glm::all(glm::epsilonEqual(rotation, Other.rotation, 1e-4f)) && glm::all(glm::epsilonEqual(scale, Other.scale, 1e-5f));
}
std::size_t basic_layer::item_count() const
{
return std::accumulate(std::begin(items), std::end(items), std::size_t{0});
}
// indicates start of content of specified scenery file. returns: handle to the layer of the file
// indicates start of content of the scenario file. returns: handle to its layer
layer_handle node_layers::open(std::string const &Name)
{
// the same file can be included more than once, all its content goes to a single layer then
auto const lookup{std::find_if(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name; })};
auto const lookup{std::find_if(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })};
auto layerhandle{static_cast<layer_handle>(std::distance(std::begin(m_layers), lookup) + 1)};
if (lookup == std::end(m_layers))
{
@@ -38,6 +60,7 @@ layer_handle node_layers::open(std::string const &Name)
m_layers.emplace_back();
m_layers.back().name = Name;
m_layers.back().parent = handle();
stat(layerhandle);
}
else
{
@@ -45,18 +68,39 @@ layer_handle node_layers::open(std::string const &Name)
layerhandle = handle();
}
}
if (valid(layerhandle) && layerhandle != handle())
{
layer(layerhandle).context_begin = m_context;
}
// NOTE: the stack receives an entry even if the layer wasn't created, to keep it in sync with close() calls
m_stack.emplace_back(layerhandle);
return layerhandle;
}
// indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file
layer_handle node_layers::open(std::string const &Name, include_site Site)
{
Site.parent = handle();
auto const layerhandle{open(Name)};
if (valid(layerhandle) && layerhandle != Site.parent)
{
layer(layerhandle).sites.emplace_back(Site);
}
return layerhandle;
}
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
layer_handle node_layers::close()
{
if (false == m_stack.empty())
{
auto const closing{m_stack.back()};
m_stack.pop_back();
if (valid(closing) && closing != handle())
{
layer(closing).context_end = m_context;
}
}
return handle();
}
@@ -66,22 +110,59 @@ void node_layers::clear()
m_layers.clear();
m_stack.clear();
m_active = null_handle;
m_context = layer_context();
m_sources.clear();
m_erased.clear();
m_templates.clear();
m_terraindirective = false;
}
// follows layer merges. returns: handle of the layer which currently holds content of specified layer
layer_handle node_layers::resolve(layer_handle Layer) const
{
// NOTE: merge() rules out cycles, the counter is only a safeguard
for (auto hops = 0; hops < 64 && valid(Layer) && layer(Layer).merged != null_handle; ++hops)
{
Layer = layer(Layer).merged;
}
return Layer;
}
int node_layers::depth(layer_handle Layer) const
{
auto depth{0};
// NOTE: parent is always created before its children, which rules out cycles
while (valid(Layer) && layer(Layer).parent != null_handle && layer(Layer).parent < Layer)
Layer = resolve(Layer);
while (valid(Layer) && layer(Layer).parent != null_handle && depth < 64)
{
Layer = layer(Layer).parent;
Layer = resolve(layer(Layer).parent);
++depth;
}
return depth;
}
void node_layers::count(layer_handle const Layer, layer_item const Item, int const Change)
std::vector<layer_handle> node_layers::tree() const
{
std::vector<layer_handle> order;
order.reserve(m_layers.size());
// NOTE: the layers are few, plain scans do
std::function<void(layer_handle)> const add = [&](layer_handle const Parent) {
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
if (listed(candidate) && resolve(layer(candidate).parent) == Parent && order.size() < m_layers.size())
{
order.emplace_back(candidate);
add(candidate);
}
}
};
add(null_handle);
return order;
}
void node_layers::count(layer_handle Layer, layer_item const Item, int const Change)
{
Layer = resolve(Layer);
if (false == valid(Layer))
{
return;
@@ -97,9 +178,10 @@ void node_layers::count(layer_handle const Layer, layer_item const Item, int con
}
}
void node_layers::active(layer_handle const Layer)
void node_layers::active(layer_handle Layer)
{
if (false == valid(Layer))
Layer = resolve(Layer);
if (false == valid(Layer) || layer(Layer).removed || false == writable(Layer))
{
return;
}
@@ -110,16 +192,17 @@ void node_layers::active(layer_handle const Layer)
}
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
void node_layers::visible(layer_handle const Layer, bool const Visible)
void node_layers::visible(layer_handle Layer, bool const Visible)
{
Layer = resolve(Layer);
if (false == valid(Layer) || layer(Layer).visible == Visible)
{
return;
}
layer(Layer).visible = Visible;
auto const update_node = [Layer, Visible](basic_node *Node) {
if (Node == nullptr || Node->layer() != Layer)
auto const update_node = [this, Layer, Visible](basic_node *Node) {
if (Node == nullptr || resolve(Node->layer()) != Layer)
{
return;
}
@@ -153,19 +236,101 @@ void node_layers::visible(layer_handle const Layer, bool const Visible)
}
}
void node_layers::locked(layer_handle const Layer, bool const Locked)
void node_layers::locked(layer_handle Layer, bool const Locked)
{
Layer = resolve(Layer);
if (valid(Layer))
{
layer(Layer).locked = Locked;
}
}
// true if nodes of specified layer can be selected and modified in the editor
bool node_layers::editable(layer_handle const Layer) const
// true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't
bool node_layers::writable(layer_handle Layer, std::string *Reason) const
{
Layer = resolve(Layer);
if (false == valid(Layer))
{
return false;
}
auto const &target{layer(Layer)};
char const *reason{nullptr};
if (target.binary)
{
reason = "the file is loaded from binary terrain cache";
}
else if (target.sites.size() > 1)
{
// one definition yields several nodes, there's no telling which of them should be written back
reason = "the file is included more than once";
}
else if (false == target.sites.empty() && target.sites.front().parameters)
{
reason = "the file is included with parameters";
}
if (reason != nullptr && Reason != nullptr)
{
*Reason = reason;
}
return reason == nullptr;
}
// true if nodes of specified layer can be selected and modified in the editor
bool node_layers::editable(layer_handle Layer) const
{
Layer = resolve(Layer);
// nodes without a layer (regular load, or created by the simulation) aren't restricted
return false == valid(Layer) || (layer(Layer).visible && false == layer(Layer).locked);
return false == valid(Layer) || (layer(Layer).visible && false == layer(Layer).locked && false == layer(Layer).removed && writable(Layer));
}
// true if specified node can be selected and modified in the editor. optionally explains why it can't
bool node_layers::editable(basic_node const *Node, std::string *Reason) const
{
if (Node == nullptr)
{
return false;
}
auto const nodelayer{resolve(Node->layer())};
if (false == valid(nodelayer))
{
return true;
}
std::string reason;
if (Node->m_template)
{
reason = "it's a part of an include template (.inc)";
}
else if (false == writable(nodelayer, &reason))
{
// reason was filled by the call
}
else if (layer(nodelayer).locked)
{
reason = "its layer is locked";
}
else if (false == layer(nodelayer).visible || layer(nodelayer).removed)
{
reason = "its layer is hidden";
}
if (false == reason.empty() && Reason != nullptr)
{
*Reason = reason;
}
return reason.empty();
}
// moves specified node to specified layer
void node_layers::move(basic_node *Node, layer_handle Layer)
{
Layer = resolve(Layer);
if (Node == nullptr || false == valid(Layer) || resolve(Node->layer()) == Layer)
{
return;
}
// NOTE: the only nodes the editor creates and moves between layers are model instances
count(Node->layer(), layer_item::model, -1);
Node->layer(Layer);
count(Layer, layer_item::model, 1);
}
std::vector<layer_handle> node_layers::hidden() const
@@ -185,10 +350,323 @@ void node_layers::show_all()
{
for (auto const hiddenlayer : hidden())
{
visible(hiddenlayer, true);
// layers dropped from the scenery stay out of sight
if (false == layer(hiddenlayer).removed)
{
visible(hiddenlayer, true);
}
}
}
// stores origin of a node defined directly in a layer file, to allow rewriting its definition on save
void node_layers::track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles, glm::vec3 const &Scale)
{
if (Node == nullptr || Node->m_template || false == valid(Node->layer()) || false == Span.valid())
{
return;
}
m_sources[Node] = {Node->layer(), Span, m_context, Node->location(), Angles, Scale};
}
// indicates specified node was removed from the scene
void node_layers::forget(basic_node const *Node)
{
auto const lookup{m_sources.find(Node)};
if (lookup == m_sources.end())
{
return;
}
m_erased.emplace_back(lookup->second.layer, lookup->second.span);
m_sources.erase(lookup);
}
bool node_layers::is_descendant(layer_handle Layer, layer_handle const Ancestor) const
{
// NOTE: content of a merged layer, includes of other files among it, belongs to the layer it was merged into
for (auto hops = 0; hops < 64 && valid(Layer); ++hops)
{
Layer = resolve(layer(Layer).parent);
if (Layer == Ancestor)
{
return true;
}
}
return false;
}
std::string node_layers::path(layer_handle const Layer) const
{
return Global.asCurrentSceneryPath + layer(Layer).name;
}
// stores current state of the layer file, to detect later whether it was changed behind our back
void node_layers::stat(layer_handle const Layer)
{
std::error_code error;
auto const filepath{std::filesystem::path(path(Layer))};
auto const filesize{std::filesystem::file_size(filepath, error)};
layer(Layer).filesize = error ? 0 : filesize;
auto const filetime{std::filesystem::last_write_time(filepath, error)};
layer(Layer).filetime = error ? std::filesystem::file_time_type{} : filetime;
}
// creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason
layer_handle node_layers::create(std::string Name, layer_handle Parent, std::string &Reason)
{
Parent = resolve(Parent);
if (false == valid(Parent) || layer(Parent).removed || layer(Parent).binary)
{
Reason = "the parent layer can't include other files";
return null_handle;
}
// file names in the scenery are processed in lower case, with forward slashes
Name = ToLower(Name);
replace_slashes(Name);
Name.erase(0, Name.find_first_not_of(" \t"));
Name.erase(Name.find_last_not_of(" \t") + 1);
if (Name.empty())
{
Reason = "the layer needs a file name";
return null_handle;
}
if (Name.find_first_of(" \t\";:*?<>|") != std::string::npos || Name.front() == '/' || Name.back() == '/' || contains(Name, ".."))
{
Reason = "the file name has to be a plain path inside the scenery directory, without spaces";
return null_handle;
}
if (std::filesystem::path(Name).extension().empty())
{
Name += ".scm";
}
if (Name.ends_with(".inc"))
{
Reason = ".inc files are include templates, not layers";
return null_handle;
}
if (m_layers.size() >= static_cast<std::size_t>(std::numeric_limits<layer_handle>::max()))
{
Reason = "too many layers";
return null_handle;
}
auto const taken{std::any_of(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })};
if (taken || FileExists(Global.asCurrentSceneryPath + Name))
{
Reason = "the file \"" + Name + "\" already exists";
return null_handle;
}
// the include directive goes after the last file included by the parent, so the scenery files stay ahead of
// what may follow them in a scenario, like the vehicles
auto anchor{null_handle};
std::streamoff anchorend{-1};
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const &sibling{m_layers[idx]};
if (sibling.dead || sibling.created || sibling.sites.size() != 1)
{
continue;
}
auto const &site{sibling.sites.front()};
if (site.parent == Parent && false == site.fixed && site.span.end > anchorend)
{
anchor = static_cast<layer_handle>(idx + 1);
anchorend = site.span.end;
}
}
m_layers.emplace_back();
auto const layerhandle{static_cast<layer_handle>(m_layers.size())};
auto &created{m_layers.back()};
created.name = Name;
created.parent = Parent;
created.created = true;
created.editormade = true;
created.anchor = anchor;
// placement the new file is going to be loaded with
created.context_begin = valid(anchor) ? layer(anchor).context_end : layer(Parent).context_end;
created.context_end = created.context_begin;
active(layerhandle);
return layerhandle;
}
bool node_layers::can_remove(layer_handle Layer, std::string &Reason) const
{
if (false == listed(Layer) || layer(Layer).removed)
{
Reason = "no such layer";
return false;
}
auto const &target{layer(Layer)};
if (target.created)
{
return true;
}
if (target.sites.empty())
{
Reason = "the scenario file can't be removed";
return false;
}
if (target.binary)
{
Reason = "the file is loaded from binary terrain cache";
return false;
}
if (target.sites.size() > 1)
{
Reason = "the file is included more than once";
return false;
}
if (target.sites.front().fixed)
{
Reason = "the file is included by an include template (.inc)";
return false;
}
return true;
}
// marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk
bool node_layers::remove(layer_handle Layer)
{
std::string reason;
if (false == can_remove(Layer, reason))
{
return false;
}
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
// layers merged elsewhere keep going, their content is no longer a part of the removed file
if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer)))
{
continue;
}
visible(candidate, false);
layer(candidate).removed = true;
if (m_active == candidate)
{
m_active = null_handle;
}
}
if (m_active == null_handle)
{
// fall back on the scenario file, which is always there
active(1);
}
return true;
}
// takes back remove() which wasn't saved yet
void node_layers::restore(layer_handle Layer)
{
if (false == valid(Layer) || false == layer(Layer).removed || layer(Layer).dead)
{
return;
}
// a layer can't be brought back without the file which includes it
auto const parent{resolve(layer(Layer).parent)};
if (valid(parent) && layer(parent).removed)
{
restore(parent);
}
for (std::size_t idx = 0; idx < m_layers.size(); ++idx)
{
auto const candidate{static_cast<layer_handle>(idx + 1)};
if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer)))
{
continue;
}
layer(candidate).removed = false;
visible(candidate, true);
}
}
bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const
{
if (false == listed(Source) || false == listed(Target) || layer(Source).removed || layer(Target).removed)
{
Reason = "no such layer";
return false;
}
if (Source == Target)
{
Reason = "pick another layer to merge into";
return false;
}
auto const &source{layer(Source)};
auto const &target{layer(Target)};
if (false == writable(Target, &Reason))
{
return false;
}
if (is_descendant(Target, Source))
{
// the merged text would include the file it was put in
Reason = "the target layer is included by the merged one";
return false;
}
if (source.created)
{
// nothing but the nodes made in the editor to move
return true;
}
if (source.sites.empty())
{
Reason = "the scenario file can't be merged into another layer";
return false;
}
if (false == writable(Source, &Reason))
{
return false;
}
if (source.sites.front().fixed)
{
Reason = "the file is included by an include template (.inc)";
return false;
}
if (resolve(source.sites.front().parent) != Target)
{
// the text is moved to the end of another file. its statements have to keep their meaning there,
// and can't leave anything behind for what used to follow it
if (false == source.context_begin.matches(target.context_end) || false == source.context_end.matches(source.context_begin))
{
Reason = "the layers are loaded with different origin, rotation or scale";
return false;
}
}
return true;
}
// moves content of the source layer to the target layer
bool node_layers::merge(layer_handle Source, layer_handle Target)
{
std::string reason;
if (false == can_merge(Source, Target, reason))
{
return false;
}
// start with the same visibility on both sides, as it's kept per node
visible(Source, true);
visible(Target, true);
for (std::size_t idx = 0; idx < layer(Source).items.size(); ++idx)
{
layer(Target).items[idx] += layer(Source).items[idx];
}
layer(Source).merged = Target;
if (m_active == Source)
{
active(Target);
}
return true;
}
// true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes
bool node_layers::pending() const
{
return std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); });
}
} // namespace scene
//---------------------------------------------------------------------------

View File

@@ -11,6 +11,9 @@ http://mozilla.org/MPL/2.0/.
#include "scene/scenenode.h"
#include <filesystem>
#include <set>
namespace scene
{
@@ -31,6 +34,39 @@ enum class layer_item
count_
};
// placement applied to scenery statements by the origin, rotate and scale directives in effect
struct layer_context
{
glm::dvec3 offset{0.0};
glm::vec3 rotation{0.f};
glm::vec3 scale{1.f};
// converts world space location to the one which produces it when loaded with this context in effect
glm::dvec3 to_local(glm::dvec3 Location) const;
bool matches(layer_context const &Other) const;
};
// location of a piece of text in a scenery file, as byte offsets
struct source_span
{
std::streamoff begin{-1};
std::streamoff end{-1};
bool valid() const
{
return begin >= 0 && end > begin;
}
};
// include directive bringing a scenery file into another one
struct include_site
{
layer_handle parent{null_handle}; // layer of the file holding the directive
source_span span; // location of the whole directive in that file
bool parameters{false}; // the file is included with parameters
bool fixed{false}; // the directive can't be rewritten: it's a part of an *.inc template, or its location is unknown
};
// scenery layer: content of a single scenery file (.scn, .scm, .ctr...) other than an .inc node template
struct basic_layer
{
@@ -40,18 +76,51 @@ struct basic_layer
bool locked{false}; // items of locked layer can't be selected in the editor
bool binary{false}; // the file wasn't parsed, its content came from binary terrain file
std::array<std::size_t, static_cast<std::size_t>(layer_item::count_)> items{}; // amount of items of each kind
// source data, used by the scenery save
std::vector<include_site> sites; // include directives bringing the file in; empty for the scenario file
layer_context context_begin; // placement in effect at the start of the file...
layer_context context_end; // ...and right after its end
std::uintmax_t filesize{0}; // state of the file when it was loaded or saved the last time
std::filesystem::file_time_type filetime{};
// pending changes, applied by the scenery save
bool created{false}; // layer was created in the editor, its file doesn't exist yet
bool editormade{false}; // the file was made by the editor during this session, there's no original content to keep a backup of
layer_handle anchor{null_handle}; // for created layer: sibling its include directive goes after
bool removed{false}; // layer is to be dropped from the scenery
layer_handle merged{null_handle}; // layer which took over the content of this one
bool dead{false}; // removal or merge of the layer was saved, it's no longer a part of the scenery
std::size_t item_count() const;
};
// origin of a node defined in a scenery layer file, with the placement it was loaded with
struct node_source
{
layer_handle layer{null_handle}; // layer of the file holding the node definition
source_span span; // location of the definition in that file
layer_context context; // placement in effect at the definition
glm::dvec3 location{0.0};
glm::vec3 angles{0.f};
glm::vec3 scale{1.f};
};
struct save_result
{
bool success{false};
std::vector<std::string> files; // names of saved files
std::string message; // summary, or cause of the failure
};
// holds scenery layers, established during load of scenery opened for editing (-edit)
// NOTE: regular load doesn't create any layers, all nodes are left with null_handle
class node_layers
{
public:
// methods
// indicates start of content of specified scenery file. returns: handle to the layer of the file
// indicates start of content of the scenario file. returns: handle to its layer
layer_handle open(std::string const &Name);
// indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file
layer_handle open(std::string const &Name, include_site Site);
// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty
layer_handle close();
// returns layer of the scenery file being loaded, or null_handle if there's none
@@ -82,32 +151,110 @@ class node_layers
{
return m_layers[Layer - 1];
}
// follows layer merges. returns: handle of the layer which currently holds content of specified layer
layer_handle resolve(layer_handle Layer) const;
// true if the layer is a part of the scenery and should be presented to the user
bool listed(layer_handle const Layer) const
{
return valid(Layer) && false == layer(Layer).dead && layer(Layer).merged == null_handle;
}
// number of layers between specified layer and the scenario file
int depth(layer_handle Layer) const;
// handles of the layers to present to the user, each followed by the layers it includes
std::vector<layer_handle> tree() const;
// adjusts item count of specified layer
void count(layer_handle const Layer, layer_item const Item, int const Change = 1);
void count(layer_handle Layer, layer_item const Item, int const Change = 1);
// layer receiving the items created in the editor, or null_handle if there's none
layer_handle active() const
{
return m_active;
}
// NOTE: active layer is made visible and unlocked
void active(layer_handle const Layer);
void active(layer_handle Layer);
// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it
void visible(layer_handle const Layer, bool const Visible);
void locked(layer_handle const Layer, bool const Locked);
void visible(layer_handle Layer, bool const Visible);
void locked(layer_handle Layer, bool const Locked);
// true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't
bool writable(layer_handle Layer, std::string *Reason = nullptr) const;
// true if nodes of specified layer can be selected and modified in the editor
bool editable(layer_handle const Layer) const;
bool editable(layer_handle Layer) const;
// true if specified node can be selected and modified in the editor. optionally explains why it can't
bool editable(basic_node const *Node, std::string *Reason = nullptr) const;
// moves specified node to specified layer
void move(basic_node *Node, layer_handle Layer);
// handles of layers which are currently hidden
std::vector<layer_handle> hidden() const;
// restores visibility of all layers
void show_all();
// scenery load bookkeeping
// registers *.inc template used by the scenery
void template_used(std::string const &Name)
{
m_templates.emplace(Name);
}
std::set<std::string> const &templates() const
{
return m_templates;
}
// updates placement in effect for the statements which follow
void context(layer_context const &Context)
{
m_context = Context;
}
// stores origin of a node defined directly in a layer file, to allow rewriting its definition on save
void track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles = glm::vec3{0.f}, glm::vec3 const &Scale = glm::vec3{1.f});
// indicates specified node was removed from the scene
void forget(basic_node const *Node);
// notes the scenery already holds the directive setting up terrain made in the editor
void terrain_directive(bool const Present)
{
m_terraindirective = Present;
}
bool terrain_directive() const
{
return m_terraindirective;
}
// layer management. changes are applied to the scenery files by save()
// creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason
layer_handle create(std::string Name, layer_handle Parent, std::string &Reason);
bool can_remove(layer_handle Layer, std::string &Reason) const;
// marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk
bool remove(layer_handle Layer);
// takes back remove() which wasn't saved yet
void restore(layer_handle Layer);
bool can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const;
// moves content of the source layer to the target layer
bool merge(layer_handle Source, layer_handle Target);
// true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes
bool pending() const;
// true if specified layer is included, directly or not, by the other one
bool is_descendant(layer_handle Layer, layer_handle const Ancestor) const;
// writes changes made in the editor to the scenery files: changed, created and deleted model instances and
// memory cells, and layer changes. the files are left intact unless the whole operation can be performed.
// Rootstatements: additional statements to place at the end of the scenario file
save_result save(std::vector<std::string> const &Rootstatements = {});
private:
// types
struct file_patch;
struct composition;
struct save_state;
// methods
std::string path(layer_handle const Layer) const;
void stat(layer_handle const Layer);
bool compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const;
// members
std::vector<basic_layer> m_layers; // layer handle is index in the vector + 1
std::vector<layer_handle> m_stack; // helper, layers of scenery files being loaded
layer_handle m_active{null_handle};
layer_context m_context; // placement in effect at the current point of scenery load
std::unordered_map<basic_node const *, node_source> m_sources; // origins of nodes which can be rewritten on save
std::vector<std::pair<layer_handle, source_span>> m_erased; // definitions of nodes deleted since the load or the last save
std::set<std::string> m_templates; // *.inc templates used by the scenery
bool m_terraindirective{false};
};
extern node_layers Layers;

972
scene/scenelayerssave.cpp Normal file
View File

@@ -0,0 +1,972 @@
/*
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 "simulation/simulation.h"
#include "model/AnimModel.h"
#include "world/MemCell.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
};
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.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, glm::dvec3 const &Location, 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;
}
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
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;
};
std::string text;
std::vector<node_place> nodes;
std::vector<site_place> sites;
// adds another piece of text at the end
void append(composition const &Other)
{
auto const offset{static_cast<std::streamoff>(text.size())};
text += Other.text;
for (auto place : Other.nodes)
{
place.span.begin += offset;
place.span.end += offset;
nodes.emplace_back(place);
}
for (auto place : Other.sites)
{
place.span.begin += offset;
place.span.end += offset;
sites.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::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;
}
for (auto &edit : edits)
{
if (edit.begin < 0)
{
edit.begin = edit.end = basesize;
}
}
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});
}
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)});
}
}
return true;
}
// writes changes made in the editor to the scenery files
save_result node_layers::save(std::vector<std::string> const &Rootstatements)
{
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))};
if (false == (moved || rotated || scaled))
{
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;
}
patched = patch_model(text, placement, error);
}
else
{
patched = patch_memcell(text, source.context.to_local(location), error);
}
if (false == patched)
{
state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\"";
return false;
}
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->m_template)
{
// created in the editor. the definition is placed at the end of the layer file, with the placement in effect there
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_end};
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);
}
}
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));
}
// 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
{
edit.begin = edit.end = -1;
if (false == layer(parent).created)
{
if (false == load(parent))
{
return fail(state.error);
}
auto const &content{state.content[parent]};
if (false == content.empty() && content.back() != '\n')
{
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 ||
(candidate == root && false == Rootstatements.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);
}
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);
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(text.text, eol);
text.append(mergedtext);
}
}
if (auto const lookup{created.find(output)}; lookup != created.end())
{
for (auto const &definition : lookup->second)
{
ensure_newline(text.text, eol);
auto const offset{static_cast<std::streamoff>(text.text.size())};
text.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(definition.second.size())}, true});
text.text += definition.second + eol;
}
}
if (output == root)
{
for (auto const &statement : Rootstatements)
{
ensure_newline(text.text, eol);
text.text += statement + eol;
}
}
}
// 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);
}
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)
{
return fail("can't replace file \"" + layer(output).name + "\": " + error.message());
}
result.files.emplace_back(layer(output).name);
WriteLog("Scenery save: file \"" + layer(output).name + "\" 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_end;
}
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].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;
}
}
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);
}
m_erased.clear();
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() ? ", " : "");
}
}
return result;
}
} // namespace scene
//---------------------------------------------------------------------------

View File

@@ -718,6 +718,7 @@ memory_node::deserialize( cParser &Input, node_data const &Nodedata ) {
basic_node::basic_node( scene::node_data const &Nodedata ) :
m_name( Nodedata.name ),
m_template( Nodedata.is_template ),
m_layer( Nodedata.layer )
{
uuid = UID::random();

View File

@@ -71,6 +71,7 @@ struct node_data {
std::string name;
std::string type;
layer_handle layer { null_handle }; // scenery layer the node is defined in, if any
bool is_template { false }; // the node is defined by an *.inc template rather than directly by a scenery layer file
};
// holds unique piece of geometry, covered with single material
@@ -381,6 +382,7 @@ public:
std::string m_name;
bool m_dirty { false };
bool m_layerhidden { false }; // the node was visible until its scenery layer was hidden
bool m_template { false }; // the node is defined by an *.inc template, which the editor can't rewrite. set only for scenery opened for editing
scene::layer_handle m_layer { null_handle }; // scenery layer this node belongs to, if any
UID uuid;

View File

@@ -495,6 +495,7 @@ TEventLauncher * state_manager::create_eventlauncher(const std::string &src, con
void state_manager::delete_model(TAnimModel *model) {
scene::Layers.count(model->layer(), scene::layer_item::model, -1);
scene::Layers.forget(model);
Region->erase(model);
Instances.purge(model);
}

View File

@@ -437,8 +437,11 @@ void
state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratchpad ) {
auto const inputline = Input.Line(); // cache in case we need to report error
auto const sourcebegin = Input.TokenBegin(); // location of the node definition, for scenery opened for editing
scene::node_data nodedata;
nodedata.layer = scene::Layers.handle();
nodedata.is_template = ( nodedata.layer != null_handle ) && ( false == Input.InLayerFile() );
// common data and node type indicator
Input.getTokens( 4 );
Input
@@ -447,7 +450,6 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
>> nodedata.name
>> nodedata.type;
if( nodedata.name == "none" ) { nodedata.name.clear(); }
nodedata.layer = scene::Layers.handle();
// type-based deserialization. not elegant but it'll do
if( nodedata.type == "dynamic" ) {
@@ -571,6 +573,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
}
scene::Groups.insert( scene::Groups.handle(), instance );
simulation::Region->insert( instance );
scene::Layers.track( instance, { sourcebegin, Input.TokenEnd() }, instance->Angles(), instance->Scale() );
scene::basic_node *hierarchy_node = instance;
if (hierarchy_node)
{ scene::Hierarchy[hierarchy_node->uuid.to_string()] = hierarchy_node;
@@ -626,6 +629,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
}
scene::Groups.insert( scene::Groups.handle(), memorycell );
simulation::Region->insert( memorycell );
scene::Layers.track( memorycell, { sourcebegin, Input.TokenEnd() } );
}
else if( nodedata.type == "eventlauncher" ) {
@@ -685,6 +689,22 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
}
}
namespace {
// passes the placement in effect to the layer bookkeeping of scenery opened for editing
void
sync_layer_context( scene::scratch_data const &Scratchpad ) {
if( true == scene::Layers.empty() ) { return; }
scene::Layers.context( {
( Scratchpad.location.offset.empty() ? glm::dvec3{ 0.0 } : Scratchpad.location.offset.top() ),
Scratchpad.location.rotation,
( Scratchpad.location.scale.empty() ? glm::vec3{ 1.f } : Scratchpad.location.scale.top() ) } );
}
} // namespace
void
state_serializer::deserialize_origin( cParser &Input, scene::scratch_data &Scratchpad ) {
@@ -700,6 +720,7 @@ state_serializer::deserialize_origin( cParser &Input, scene::scratch_data &Scrat
Scratchpad.location.offset.empty() ?
glm::dvec3() :
Scratchpad.location.offset.top() ) );
sync_layer_context( Scratchpad );
}
void
@@ -711,6 +732,7 @@ state_serializer::deserialize_endorigin( cParser &Input, scene::scratch_data &Sc
else {
ErrorLog( "Bad origin: endorigin instruction with empty origin stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" );
}
sync_layer_context( Scratchpad );
}
void
@@ -734,6 +756,7 @@ state_serializer::deserialize_scale( cParser &Input, scene::scratch_data &Scratc
// scales compose component-wise, mirroring how origin offsets compose additively.
glm::vec3 const parent = Scratchpad.location.scale.empty() ? glm::vec3(1.0f) : Scratchpad.location.scale.top();
Scratchpad.location.scale.emplace( factor * parent );
sync_layer_context( Scratchpad );
}
void
@@ -745,6 +768,7 @@ state_serializer::deserialize_endscale( cParser &Input, scene::scratch_data &Scr
else {
ErrorLog( "Bad scale: endscale instruction with empty scale stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" );
}
sync_layer_context( Scratchpad );
}
void
@@ -755,6 +779,7 @@ state_serializer::deserialize_rotate( cParser &Input, scene::scratch_data &Scrat
>> Scratchpad.location.rotation.x
>> Scratchpad.location.rotation.y
>> Scratchpad.location.rotation.z;
sync_layer_context( Scratchpad );
}
void
@@ -860,6 +885,7 @@ state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data
Input.getTokens(4);
Input >> folder >> cells >> cellsize >> radius;
skip_until(Input, "endeditorterrain");
scene::Layers.terrain_directive(true);
if (!folder.empty() && cells > 0 && cellsize > 0.0f)
{

View File

@@ -237,11 +237,13 @@ std::string cParser::readTokenFromStream(bool ToLower, const char *Break)
const auto breakTable = makeBreakTable(Break);
char c = 0;
bool separated = false; // the token was ended by a separator, which isn't a part of it
while (token.empty() && mStream->peek() != EOF) {
while (mStream->peek() != EOF) { // idk why but with mStream->get(c) not all cars are loaded
c = static_cast<char>(mStream->get());
++mPosition;
if (c == '\n') {
++mLine;
}
@@ -249,11 +251,16 @@ std::string cParser::readTokenFromStream(bool ToLower, const char *Break)
const unsigned char uc = static_cast<unsigned char>(c);
if (breakTable[uc]) {
// separator ends token (or continues skipping if token empty)
if (!token.empty())
if (!token.empty()) {
separated = true;
break;
}
continue;
}
if (token.empty()) {
mTokenBegin = mPosition - 1;
}
if (ToLower) c = toLowerChar(c);
token.push_back(c);
@@ -265,6 +272,8 @@ std::string cParser::readTokenFromStream(bool ToLower, const char *Break)
}
}
}
// NOTE: comment glued to the end of a token is counted as a part of it
mTokenEnd = separated ? mPosition - 1 : mPosition;
return token;
}
@@ -275,6 +284,7 @@ void cParser::stripFirstTokenBOM(std::string& token, bool ToLower, const char* B
if (startsWithBOM(token)) {
token.erase(0, 3);
mTokenBegin += 3;
}
// if first "token" was standalone BOM, read the next real token (avoid recursion)
@@ -345,7 +355,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
if (sceneryLayers)
{
// the file is still a part of the scenery, even though its content comes from the binary terrain file
scene::Layers.layer(scene::Layers.open(includefile)).binary = true;
scene::Layers.layer(scene::Layers.open(includefile, include_site())).binary = true;
scene::Layers.close();
}
return;
@@ -373,17 +383,37 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
mIncludeParser->sceneryLayers = true;
// content of an included scenery file forms a layer of its own. *.inc files are node templates
// reused all over the scenery, so their content stays in the layer of the file which includes them
if (false == mIncludeParser->mIncFile && mIncludeParser->mSize > 0)
if (mIncludeParser->mIncFile)
{
scene::Layers.open(includefile);
scene::Layers.template_used(includefile);
}
else if (mIncludeParser->mSize > 0)
{
auto site{include_site()};
site.parameters = false == mIncludeParser->parameters.empty();
scene::Layers.open(includefile, site);
mIncludeParser->mLayerFile = true;
}
}
}
// describes the include directive being processed, for the scenery layer bookkeeping
scene::include_site cParser::include_site() const
{
scene::include_site site;
// the directive can be rewritten on scenery save only if we know where it is, and it sits in a layer file
site.fixed = mIncludeBegin < 0 || mFile.empty() || mIncFile;
if (false == site.fixed)
{
site.span = {mIncludeBegin, mTokenEnd};
}
return site;
}
bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const char* Break) {
// token-mode include: token == "include"
if (expandIncludes && token == "include") {
mIncludeBegin = mTokenBegin;
std::string includefile;
if (allowRandomIncludes)
includefile = deserialize_random_set(*this);
@@ -399,6 +429,7 @@ bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const cha
// line-mode HACK: Break == "\n\r" and line begins with "include"
if (std::strcmp(Break, "\n\r") == 0 && token.compare(0, 7, "include") == 0) {
mIncludeBegin = -1; // the directive is parsed out of a line of text, its exact location isn't known
cParser includeparser(token.substr(7));
std::string includefile;
if (allowRandomIncludes)
@@ -459,6 +490,7 @@ std::string cParser::readQuotes(char const Quote)
bool escaped = false;
while (mStream->get(c))
{ // get all chars until the quote mark
++mPosition;
if (escaped)
{
escaped = false;
@@ -489,6 +521,7 @@ void cParser::skipComment(std::string const &Endmark)
auto const endmarksize = Endmark.size();
while (mStream->get(c))
{
++mPosition;
if (c == '\n')
{
// update line counter
@@ -626,3 +659,18 @@ int cParser::LineMain() const
{
return mIncludeParser ? -1 : mLine;
}
std::streamoff cParser::TokenBegin() const
{
return mIncludeParser ? mIncludeParser->TokenBegin() : mTokenBegin;
}
std::streamoff cParser::TokenEnd() const
{
return mIncludeParser ? mIncludeParser->TokenEnd() : mTokenEnd;
}
bool cParser::InLayerFile() const
{
return mIncludeParser ? mIncludeParser->InLayerFile() : (false == mFile.empty() && false == mIncFile);
}

View File

@@ -15,6 +15,11 @@ http://mozilla.org/MPL/2.0/.
#include <vector>
#include <map>
namespace scene
{
struct include_site;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// cParser -- generic class for parsing text data, either from file or provided string
@@ -94,6 +99,11 @@ class cParser //: public std::stringstream
std::size_t Line() const;
// returns number of currently processed line in main file, -1 if inside include
int LineMain() const;
// location of the most recently read token in its source file, as byte offsets
std::streamoff TokenBegin() const;
std::streamoff TokenEnd() const;
// true if the most recently read token came straight from a scenery file which isn't an *.inc node template
bool InLayerFile() const;
bool expandIncludes = true;
bool allowRandomIncludes = false;
bool skipComments = true;
@@ -101,6 +111,7 @@ class cParser //: public std::stringstream
private:
void startIncludeFromParser(cParser &srcParser, bool ToLower, std::string includefile);
scene::include_site include_site() const;
bool handleIncludeIfPresent(std::string &token, bool ToLower, const char *Break);
// methods:
void readToken(std::string& out, bool ToLower = true, const char *Break = "\n\r\t ;");
@@ -120,6 +131,10 @@ class cParser //: public std::stringstream
std::size_t mLine { 0 }; // currently processed line
bool mIncFile { false }; // the parser is processing an *.inc file
bool mLayerFile { false }; // the parser is processing a file registered as scenery layer
std::streamoff mPosition { 0 }; // amount of bytes read from the stream so far
std::streamoff mTokenBegin { 0 }; // location of the most recently read token
std::streamoff mTokenEnd { 0 };
std::streamoff mIncludeBegin { -1 }; // location of the include directive being processed, -1 if unknown
bool mFirstToken { true }; // processing first token in the current file; helper used when checking for utf bom
typedef std::map<std::string, std::string> commentmap;
commentmap mComments {