17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-02 06:59:13 +01:00

fix(editor): guard the places where the editor could crash outright

Found with an address and undefined behaviour sanitizer build, random
monkey sessions on the test sceneries and broken inputs:

- A deleted model left its pointer in the pick list of the renderer, the
  bound node of the panels, the bindings of the infrastructure and the
  selection: the next pick or panel frame read freed memory. The renderer
  forgets it, and one erase_model does the cleaning for all the places
  which deleted a model by hand.
- The plan band of a route read the vertex before the first one in the
  frame a drag was let go.
- A scenery which can't be opened goes back to the launcher with the
  reason in the log, a track of an unknown type or a tributary loads as
  a normal track or a river with a message, and a spline with no finite
  length or step gets a sane number of segments.
- Typed numbers which aren't (NaN, inf, ends 0 m or 1000 km apart, a
  vector of 1e30, a diverging track part of 5 km, a curve threshold of 0,
  a zero history size, radius norms below zero) are refused or clamped,
  where they made the geometry run away or divide by zero.
- Folders which can't be read and file names which aren't valid in the
  locale no longer throw out of the scans of sceneries, textures, sweeps,
  roads and signal plates; the scans skip them.
- The launcher: Ctrl+Del with no trainset selected, a dropped vehicle
  whose skin is gone and a text with a percent sign.
- A terrain mesh made for another resolution of its chunk isn't put into
  the chunk, and a slot is reused only for a mesh of the same size.
- Vehicles on a track changed by the editor are moved over a copy of the
  list they leave.
- The table of the keyboard shortcuts gives its stretched column a
  weight, else ImGui divides 0 by 0 in the first frame of the window.
This commit is contained in:
maj00r
2026-10-21 08:42:29 +02:00
parent 9696b5f795
commit 4512ed4e0b
36 changed files with 311 additions and 111 deletions

View File

@@ -200,6 +200,12 @@ namespace
triangle_grid(std::vector<world_triangle> Triangles, glm::dvec2 const &Min, glm::dvec2 const &Max) : m_triangles(std::move(Triangles)), m_min(Min)
{
glm::dvec2 const extent = glm::max(Max - Min, glm::dvec2(1.0));
if (false == std::isfinite(extent.x) || false == std::isfinite(extent.y))
{
m_triangles.clear();
m_cells.resize(1);
return;
}
// ~8 m cells, coarser for big areas so large terrain triangles don't flood the grid
m_cellsize = std::max(8.0, std::max(extent.x, extent.y) / 128.0);
m_columns = static_cast<int>(std::ceil(extent.x / m_cellsize)) + 1;
@@ -241,11 +247,13 @@ namespace
private:
int column(double const X) const
{
return static_cast<int>(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast<double>(m_columns - 1)));
double const cell = std::floor((X - m_min.x) / m_cellsize);
return cell > 0.0 ? static_cast<int>(std::min(cell, static_cast<double>(m_columns - 1))) : 0;
}
int row(double const Z) const
{
return static_cast<int>(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast<double>(m_rows - 1)));
double const cell = std::floor((Z - m_min.y) / m_cellsize);
return cell > 0.0 ? static_cast<int>(std::min(cell, static_cast<double>(m_rows - 1))) : 0;
}
std::vector<world_triangle> m_triangles;
@@ -862,6 +870,10 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node)
if (!node)
return;
if (ui()->node() == node)
ui()->set_node(nullptr);
infra_forget(node);
for (auto &s : m_history)
{
if (s.node_ptr == node)
@@ -1076,9 +1088,7 @@ void editor_mode::undo_one()
if (auto *model = dynamic_cast<TAnimModel *>(target))
{
nullify_history_pointers(model);
remove_from_hierarchy(model);
simulation::State.delete_model(model);
erase_model(model);
m_node = nullptr;
ui()->set_node(nullptr);
}
@@ -1204,9 +1214,7 @@ void editor_mode::redo_one()
{
if (auto *model = dynamic_cast<TAnimModel *>(target))
{
nullify_history_pointers(model);
remove_from_hierarchy(model);
simulation::State.delete_model(model);
erase_model(model);
m_node = nullptr;
ui()->set_node(nullptr);
}
@@ -3061,7 +3069,10 @@ void editor_mode::set_include_directive(scene::instance_handle const Instance, s
simulation::State.rebuild_include(Instance, retired);
// the pick of the renderer is resolved a few frames after it's requested, and can still deliver one of these
for (auto *model : retired)
{
nullify_history_pointers(model);
m_retired.emplace_back(model, 120);
}
}
void editor_mode::apply_include()
@@ -3332,12 +3343,10 @@ void editor_mode::capture_terrain()
std::string as_text;
model->export_as_text(as_text);
push_snapshot(model, EditorSnapshot::Action::Delete, as_text);
nullify_history_pointers(model);
remove_from_hierarchy(model);
m_node = nullptr;
m_dragging = false;
ui()->set_node(nullptr);
simulation::State.delete_model(model);
erase_model(model);
}
// the ground under a point the cursor landed on. the orthophoto laid over the ground catches the cursor like any
@@ -3453,13 +3462,21 @@ void editor_mode::read_georeference()
std::error_code error;
if (false == std::filesystem::exists(file, error))
{
for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error))
for (std::filesystem::directory_iterator entry{Global.asCurrentSceneryPath, error}, end; entry != end; entry.increment(error))
{
auto name{entry.path().filename().string()};
std::string name;
try
{
name = entry->path().filename().string();
}
catch (std::exception const &)
{
continue;
}
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
if (name == Global.SceneryFile)
{
file = entry.path();
file = entry->path();
break;
}
}
@@ -3696,10 +3713,19 @@ void editor_mode::render_open_scenery_popup()
m_openscenery_list.clear();
m_openscenery_choice.clear();
std::error_code error;
for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error))
for (std::filesystem::directory_iterator entry{Global.asCurrentSceneryPath, error}, end; entry != end; entry.increment(error))
{
auto const name{entry.path().filename().string()};
if (entry.is_regular_file(error) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$')
std::string name;
try
{
name = entry->path().filename().string();
}
catch (std::exception const &)
{
continue;
}
std::error_code status;
if (entry->is_regular_file(status) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$')
m_openscenery_list.emplace_back(name);
}
std::sort(m_openscenery_list.begin(), m_openscenery_list.end(), [&](std::string const &A, std::string const &B) { return lower(A) < lower(B); });
@@ -3807,9 +3833,7 @@ void editor_mode::undo_last_area_fill()
m_node = nullptr;
ui()->set_node(nullptr);
}
nullify_history_pointers(model);
remove_from_hierarchy(model);
simulation::State.delete_model(model);
erase_model(model);
}
m_fill_status = format(STR_C("Removed %zu objects of the last fill"), created.size());
}
@@ -4230,9 +4254,7 @@ void editor_mode::shape_array()
copies.pop_back();
if (model == nullptr)
continue;
nullify_history_pointers(model);
remove_from_hierarchy(model);
simulation::State.delete_model(model);
erase_model(model);
}
std::string definition;
@@ -4317,9 +4339,7 @@ void editor_mode::restore_array_snapshot(EditorSnapshot Snapshot, std::vector<Ed
m_dragging = false;
ui()->set_node(nullptr);
}
nullify_history_pointers(model);
remove_from_hierarchy(model);
simulation::State.delete_model(model);
erase_model(model);
}
else
{
@@ -5488,7 +5508,7 @@ void editor_mode::render_shortcuts_window()
if (false == ImGui::BeginTable(entry.title, 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
continue;
ImGui::TableSetupColumn("##keys", ImGuiTableColumnFlags_WidthFixed, 250.0f * Global.ui_scale);
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch, 1.0f);
for (auto const &item : entry.shortcuts)
{
if (false == matches(item))
@@ -6123,15 +6143,17 @@ void editor_mode::delete_model(TAnimModel *model, bool const Joined)
m_history.back().joined = Joined;
WriteLog(debug, logtype::generic);
// clear history pointers referencing this model before actually deleting it
erase_model(model);
}
void editor_mode::erase_model(TAnimModel *model)
{
nullify_history_pointers(model);
remove_from_hierarchy(model);
if (m_node == model)
{
m_node = nullptr;
m_dragging = false;
ui()->set_node(nullptr);
}
simulation::State.delete_model(model);
}
@@ -6561,6 +6583,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
auto *const previous{m_node};
m_node = nullptr;
select_include(0);
ui()->set_node(additive ? previous : nullptr);
// in a scenery opened for editing selection has its limits. nodes of locked layers are off limits.
// nodes defined by a scenery template can't be rewritten one by one on save, so a click on one
@@ -6722,7 +6745,7 @@ void editor_mode::render_change_history(){
int maxsize = m_max_history_size;
if (ImGui::InputInt(STR_C("Max history size"), &maxsize))
{
m_max_history_size = std::max(0, maxsize);
m_max_history_size = std::max(1, maxsize);
if ((int)m_history.size() > m_max_history_size && m_max_history_size >= 0)
{
auto remove_count = (int)m_history.size() - m_max_history_size;