diff --git a/application/editormode.cpp b/application/editormode.cpp index 3380a950..670eab74 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -534,6 +534,7 @@ editor_mode::editor_mode() { ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); ui()->set_new_scenery([this]() { m_newscenery_asked = true; }); ui()->set_open_scenery([this]() { m_openscenery_asked = true; }); + ui()->set_memcell_focus([this](scene::basic_node *Node) { start_focus(Node); }); ui()->set_edit_menu([this]() { render_edit_menu(); }); ui()->set_tools_menu([this]() { render_track_presets_menu(); }); ui()->set_menu_options([this]() { diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 8591f41a..78ba6414 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -20,6 +20,8 @@ http://mozilla.org/MPL/2.0/. #include "world/Track.h" #include "world/Road.h" #include "world/RoadPoint.h" +#include "world/MemCell.h" +#include "world/Event.h" #include "simulation/simulation.h" #include "imgui/imgui.h" @@ -537,6 +539,51 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin "It opens a few seconds after the last one is gone"); if (ImGui::Checkbox(STR_C("Paint the stop lines"), &State.stoplines)) changed = true; + // what the scenery closes the entry onto the crossing with + char memcell[128]; + copy_text(memcell, sizeof(memcell), State.memcell); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::InputTextWithHint(STR_C("Memory cell"), STR_C("(named after the crossing)"), memcell, sizeof(memcell), ImGuiInputTextFlags_EnterReturnsTrue)) + { + auto name{lower_case(memcell)}; + if (name == "none") + name.clear(); + if (name.find(' ') == std::string::npos && name != State.memcell) + { + State.memcell = name; + changed = true; + } + } + item_tooltip("Events of the scenery close the entry onto the crossing with the first value of this memory cell:\n" + "above 0 the vehicles heading for the crossing are stopped, below 0 it's kept open whatever comes by the rails,\n" + "0 leaves it to the trains. The vehicles past the stops, on the crossing or leaving it, drive on.\n" + "The crossing sets the second value: 1 while the entry is closed, 0 while it's open.\n" + "Left empty, the cell is the one named after the crossing with \":block\" added, made if the scenery doesn't have it"); + if (Point != nullptr) + { + auto *cell{Point->memcell()}; + if (cell == nullptr) + { + ImGui::TextColored(warning, "%s", STR_C("The crossing has no name, so there's no memory cell to tell it by")); + } + else + { + ImGui::Text("\"%s\": %g %g", cell->name().c_str(), cell->Value1(), cell->Value2()); + ImGui::SameLine(); + ImGui::TextUnformatted(Point->closed() ? STR_C("The entry is closed") : STR_C("The entry is open")); + // the value can be tried out right here; the scenery isn't changed by it + int control{cell->Value1() > 0.0 ? 1 : cell->Value1() < 0.0 ? 2 : 0}; + auto const before{control}; + ImGui::RadioButton(STR_C("By the trains"), &control, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Closed"), &control, 1); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Kept open"), &control, 2); + item_tooltip("Sets the first value of the memory cell, the way an event of the scenery would. It isn't saved with the scenery"); + if (control != before) + cell->UpdateValues("", control == 1 ? 1.0 : control == 2 ? -1.0 : 0.0, 0.0, basic_event::flags::value1); + } + } break; } case roadpoint_node::kind_type::spawn: diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index d4d4d60c..6157fa34 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -36,6 +36,7 @@ editor_ui::editor_ui() add_external_panel(&m_nodebankpanel); add_external_panel(&m_layerspanel); add_external_panel(&m_includespanel); + add_external_panel(&m_memcellspanel); // the menu is a part of the layout of the editor, not a pop-up as in the driver mode m_menu_always = true; @@ -985,6 +986,14 @@ void editor_ui::render_menu_contents() ui_layer::render_menu_contents(); + if (ImGui::BeginMenu(STR_C("Debug"))) + { + ImGui::MenuItem(STR_C("Memory cells"), nullptr, &m_memcellspanel.is_open); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Looks up the memory cells of the scenery, shows what's in them and sets it by hand")); + ImGui::EndMenu(); + } + if (m_menuoptions) m_menuoptions(); } diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 49c94ee3..1bfa7e4b 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -108,6 +108,7 @@ class editor_ui : public ui_layer void set_file_actions(std::function Save, std::function Export); void set_new_scenery(std::function New) { m_newscenery = std::move(New); } void set_open_scenery(std::function Open) { m_openscenery = std::move(Open); } + void set_memcell_focus(std::function Focus) { m_memcellspanel.focus = std::move(Focus); } // part of the main window between the docked windows, where the 3d view is seen; for the overlays placed in it static glm::vec2 view_min() { @@ -197,6 +198,7 @@ class editor_ui : public ui_layer nodebank_panel m_nodebankpanel{"Node bank", true}; // templates to insert, paint or fill with layers_panel m_layerspanel{"Layers", true}; includes_panel m_includespanel{"Include database", false}; + memcells_panel m_memcellspanel{"Memory cells", false}; std::function m_save; std::function m_export; std::function m_newscenery; diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index ed41bbf7..312c04e5 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -21,11 +21,13 @@ http://mozilla.org/MPL/2.0/. #include "application/editoruilayer.h" #include "rendering/renderer.h" #include "simulation/simulation.h" +#include "utilities/Logs.h" #include "editor/editorModelSets.hpp" #include "editor/editorFormat.hpp" #include "editor/editorSettings.hpp" #include "imgui/imgui_internal.h" +#include #include #include @@ -2453,3 +2455,197 @@ void includes_panel::render() } ImGui::End(); } + +// memory cells of the scenery +void memcells_panel::render() +{ + if (false == is_open) + { + return; + } + ImGui::SetNextWindowPos(ImVec2S(560, 110), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2S(620, 560), ImGuiCond_FirstUseEver); + auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name}; + if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) + { + render_list(); + render_cell(); + } + ImGui::End(); +} + +void memcells_panel::render_list() +{ + auto const &cells{simulation::Memory.sequence()}; + // the cells are never taken away, only added, so a change of the count is a change of the set + if (m_sorted.size() != static_cast(std::count_if(cells.begin(), cells.end(), [](TMemCell const *Cell) { return Cell != nullptr; }))) + { + m_sorted.clear(); + for (auto *cell : cells) + { + if (cell != nullptr) + { + m_sorted.emplace_back(cell); + } + } + std::stable_sort(m_sorted.begin(), m_sorted.end(), [](TMemCell const *Left, TMemCell const *Right) { return Left->name() < Right->name(); }); + m_listedtime = -1.0; + } + ImGui::SetNextItemWidth(260.0f); + ImGui::InputTextWithHint("##filter", STR_C("Search"), m_filter, sizeof(m_filter)); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("In the texts too"), &m_searchtext); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Log all"))) + { + simulation::Memory.log_all(); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", STR_C("Writes the content of all memory cells to the log")); + } + + std::string filter{m_filter}; + std::transform(filter.begin(), filter.end(), filter.begin(), [](unsigned char Character) { return static_cast(std::tolower(Character)); }); + auto const now{ImGui::GetTime()}; + auto const searchtext{m_searchtext && false == filter.empty()}; + if (filter != m_listedfilter || m_listedtime < 0.0 || (searchtext && now - m_listedtime > 0.5)) + { + m_listedfilter = filter; + m_listedtime = now; + m_listed.clear(); + auto const matches = [&filter](std::string const &Text) { + if (filter.empty()) + { + return true; + } + std::string text{Text}; + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char Character) { return static_cast(std::tolower(Character)); }); + return text.find(filter) != std::string::npos; + }; + for (auto *cell : m_sorted) + { + if (matches(cell->name()) || (searchtext && matches(cell->Text()))) + { + m_listed.emplace_back(cell); + } + } + } + ImGui::TextDisabled(STR_C("Memory cells: %d, shown: %d"), static_cast(m_sorted.size()), static_cast(m_listed.size())); + + auto const listheight{std::max(120.0f, ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() * 7.0f)}; + if (ImGui::BeginTable("##memcells", 5, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(-1.0f, listheight))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Name"), ImGuiTableColumnFlags_WidthStretch, 2.0f); + ImGui::TableSetupColumn(STR_C("Text"), ImGuiTableColumnFlags_WidthStretch, 1.5f); + ImGui::TableSetupColumn(STR_C("Value 1"), ImGuiTableColumnFlags_WidthStretch, 0.7f); + ImGui::TableSetupColumn(STR_C("Value 2"), ImGuiTableColumnFlags_WidthStretch, 0.7f); + ImGui::TableSetupColumn(STR_C("Track"), ImGuiTableColumnFlags_WidthStretch, 1.0f); + ImGui::TableHeadersRow(); + ImGuiListClipper clipper; + clipper.Begin(static_cast(m_listed.size())); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto *cell{m_listed[row]}; + ImGui::PushID(cell); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + auto const name{cell->name().empty() ? std::string{STR_C("(no name)")} : cell->name()}; + if (ImGui::Selectable(name.c_str(), cell == m_selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) + { + select(cell); + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && focus && cell->location() != glm::dvec3{0.0}) + { + focus(cell); + } + } + if (false == cell->is_exportable && ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", STR_C("Made by the simulator, it isn't saved with the scenery")); + } + ImGui::TableNextColumn(); + ImGui::TextUnformatted(cell->Text().c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%g", cell->Value1()); + ImGui::TableNextColumn(); + ImGui::Text("%g", cell->Value2()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(cell->Track != nullptr ? cell->Track->name().c_str() : cell->asTrackName.c_str()); + ImGui::PopID(); + } + } + ImGui::EndTable(); + } +} + +void memcells_panel::select(TMemCell *Cell) +{ + m_selected = Cell; + if (Cell == nullptr) + { + return; + } + auto const &text{Cell->Text()}; + auto const length{std::min(text.size(), sizeof(m_text) - 1)}; + std::memcpy(m_text, text.data(), length); + m_text[length] = 0; + m_value1 = Cell->Value1(); + m_value2 = Cell->Value2(); +} + +void memcells_panel::render_cell() +{ + ImGui::Separator(); + if (m_selected == nullptr) + { + ImGui::TextDisabled("%s", STR_C("Pick a memory cell from the list to set what's in it. A double click shows where it is")); + return; + } + auto *cell{m_selected}; + auto const location{cell->location()}; + ImGui::Text(STR_C("\"%s\": %s %g %g"), cell->name().c_str(), cell->Text().c_str(), cell->Value1(), cell->Value2()); + ImGui::SameLine(); + ImGui::BeginDisabled(false == static_cast(focus) || location == glm::dvec3{0.0}); + if (ImGui::SmallButton(STR_C("Show"))) + { + focus(cell); + } + ImGui::EndDisabled(); + ImGui::TextDisabled(STR_C("At %.1f %.1f %.1f"), location.x, location.y, location.z); + + ImGui::Checkbox("##settext", &m_settext); + ImGui::SameLine(); + ImGui::SetNextItemWidth(260.0f); + ImGui::InputText(STR_C("Text"), m_text, sizeof(m_text)); + ImGui::Checkbox("##setvalue1", &m_setvalue1); + ImGui::SameLine(); + ImGui::SetNextItemWidth(160.0f); + ImGui::InputDouble(STR_C("Value 1"), &m_value1, 1.0, 10.0, "%g"); + ImGui::Checkbox("##setvalue2", &m_setvalue2); + ImGui::SameLine(); + ImGui::SetNextItemWidth(160.0f); + ImGui::InputDouble(STR_C("Value 2"), &m_value2, 1.0, 10.0, "%g"); + ImGui::BeginDisabled(false == (m_settext || m_setvalue1 || m_setvalue2)); + if (ImGui::Button(STR_C("Set"))) + { + auto const mask{(m_settext ? basic_event::flags::text : 0) | (m_setvalue1 ? basic_event::flags::value1 : 0) | (m_setvalue2 ? basic_event::flags::value2 : 0) | + (m_add ? basic_event::flags::mode_add : 0)}; + cell->UpdateValues(m_text, m_value1, m_value2, mask); + WriteLog("Memory cell \"" + cell->name() + "\" set by hand to: " + cell->Text() + " " + std::to_string(cell->Value1()) + " " + std::to_string(cell->Value2())); + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + { + ImGui::SetTooltip("%s", STR_C("Puts the ticked fields in the cell, the way an UpdateValues event does. The scenery files aren't changed")); + } + ImGui::SameLine(); + ImGui::Checkbox(STR_C("Add to the values"), &m_add); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Read again"))) + { + select(cell); + } +} diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 4b00b130..c135601c 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -275,6 +275,40 @@ class includes_panel : public ui_panel bool m_statuserror{false}; }; +// memory cells of the scenery: looked up by name or text, with what's in them, which can be set by hand while the scenery runs +class memcells_panel : public ui_panel +{ + + public: + memcells_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {} + + void render() override; + + // moves the camera to specified node; provided by the editor mode + std::function focus; + + private: + // methods + void render_list(); + void render_cell(); + void select(TMemCell *Cell); + // members + char m_filter[128]{}; + bool m_searchtext{true}; // the filter is looked for in the texts of the cells as well as in their names + std::vector m_sorted; // all cells, in the order of their names + std::vector m_listed; // the ones matching the filter + std::string m_listedfilter; // filter the list was made for + double m_listedtime{-1.0}; // when it was made; the texts change, so it's made again now and then + TMemCell *m_selected{nullptr}; + char m_text[256]{}; // what's going to be put in the selected cell + double m_value1{0.0}; + double m_value2{0.0}; + bool m_settext{true}; + bool m_setvalue1{true}; + bool m_setvalue2{true}; + bool m_add{false}; // the values are added to what the cell holds, the way an event with the add mode does +}; + class functions_panel : public ui_panel { diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 1584e6dd..70aea896 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10290,3 +10290,88 @@ msgstr "Na jakiej długości od końca p1 szyny płynnie przechodzą w szyny tor msgid "How far from the end p2 the rails fade into those of the track joined there, where their textures differ; 0: the joint stays sharp, whatever the track joined there has" msgstr "Na jakiej długości od końca p2 szyny płynnie przechodzą w szyny toru tam dołączonego, gdy ich tekstury są różne; 0: ostra granica na styku, niezależnie od ustawienia toru tam dołączonego" + +# scenery editor: level crossings told by memory cells + +msgid "Memory cell" +msgstr "Komórka pamięci" + +msgid "(named after the crossing)" +msgstr "(nazwana od przejazdu)" + +msgid "Events of the scenery close the entry onto the crossing with the first value of this memory cell:\nabove 0 the vehicles heading for the crossing are stopped, below 0 it's kept open whatever comes by the rails,\n0 leaves it to the trains. The vehicles past the stops, on the crossing or leaving it, drive on.\nThe crossing sets the second value: 1 while the entry is closed, 0 while it's open.\nLeft empty, the cell is the one named after the crossing with \":block\" added, made if the scenery doesn't have it" +msgstr "Eventy scenerii zamykają wjazd na przejazd pierwszą wartością tej komórki pamięci:\npowyżej 0 pojazdy jadące na przejazd są zatrzymywane, poniżej 0 jest on otwarty bez względu na to, co jedzie po torach,\n0 zostawia to pociągom. Pojazdy za miejscami zatrzymania, na przejeździe albo zjeżdżające z niego, jadą dalej.\nPrzejazd ustawia drugą wartość: 1, gdy wjazd jest zamknięty, 0, gdy jest otwarty.\nGdy pole jest puste, jest to komórka nazwana od przejazdu z dodanym \":block\", tworzona, jeśli sceneria jej nie ma" + +msgid "The crossing has no name, so there's no memory cell to tell it by" +msgstr "Przejazd nie ma nazwy, więc nie ma komórki pamięci, którą można nim sterować" + +msgid "The entry is closed" +msgstr "Wjazd zamknięty" + +msgid "The entry is open" +msgstr "Wjazd otwarty" + +msgid "By the trains" +msgstr "Według pociągów" + +msgid "Closed" +msgstr "Zamknięty" + +msgid "Kept open" +msgstr "Otwarty" + +msgid "Sets the first value of the memory cell, the way an event of the scenery would. It isn't saved with the scenery" +msgstr "Ustawia pierwszą wartość komórki pamięci, tak jak zrobiłby to event scenerii. Nie jest zapisywane w scenerii" + +# scenery editor: memory cells window + +msgid "Debug" +msgstr "Debugowanie" + +msgid "Memory cells" +msgstr "Komórki pamięci" + +msgid "Looks up the memory cells of the scenery, shows what's in them and sets it by hand" +msgstr "Wyszukuje komórki pamięci scenerii, pokazuje ich zawartość i pozwala ją ustawić ręcznie" + +msgid "In the texts too" +msgstr "Także w tekstach" + +msgid "Log all" +msgstr "Wszystkie do logu" + +msgid "Writes the content of all memory cells to the log" +msgstr "Zapisuje zawartość wszystkich komórek pamięci do logu" + +msgid "Memory cells: %d, shown: %d" +msgstr "Komórek pamięci: %d, pokazanych: %d" + +msgid "Text" +msgstr "Tekst" + +msgid "Value 1" +msgstr "Wartość 1" + +msgid "Value 2" +msgstr "Wartość 2" + +msgid "Made by the simulator, it isn't saved with the scenery" +msgstr "Utworzona przez symulator, nie jest zapisywana w scenerii" + +msgid "Pick a memory cell from the list to set what's in it. A double click shows where it is" +msgstr "Wybierz komórkę pamięci z listy, aby ustawić jej zawartość. Podwójne kliknięcie pokazuje, gdzie jest" + +msgid "\"%s\": %s %g %g" +msgstr "\"%s\": %s %g %g" + +msgid "At %.1f %.1f %.1f" +msgstr "Położenie %.1f %.1f %.1f" + +msgid "Puts the ticked fields in the cell, the way an UpdateValues event does. The scenery files aren't changed" +msgstr "Wpisuje zaznaczone pola do komórki, tak jak event UpdateValues. Pliki scenerii nie są zmieniane" + +msgid "Add to the values" +msgstr "Dodaj do wartości" + +msgid "Read again" +msgstr "Odczytaj ponownie" diff --git a/world/RoadPoint.cpp b/world/RoadPoint.cpp index 52a7f190..0c44d731 100644 --- a/world/RoadPoint.cpp +++ b/world/RoadPoint.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "world/Track.h" #include "world/Event.h" +#include "world/MemCell.h" #include "world/RoadOrder.h" #include "vehicle/DynObj.h" #include "vehicle/Driver.h" @@ -356,6 +357,14 @@ void roadpoint_node::import(cParser &Input, glm::dvec3 const &Offset) { m_state.stoplines = (Input.getToken() != "no"); } + else if (token == "memcell") + { + m_state.memcell = Input.getToken(); + if (m_state.memcell == "none") + { + m_state.memcell.clear(); + } + } else if (token == "interval") { Input.getTokens(); @@ -488,6 +497,7 @@ void roadpoint_node::bind() { case kind_type::crossing: { + bind_memcell(); bind_crossing(); break; } @@ -574,6 +584,42 @@ void roadpoint_node::bind() } } +// name of the memory cell the crossing is told by +std::string roadpoint_node::memcell_name() const +{ + if (false == m_state.memcell.empty()) + { + return m_state.memcell; + } + return m_name.empty() || m_name == "none" ? std::string{} : m_name + ":block"; +} + +// finds the memory cell the crossing is told by, or makes it +void roadpoint_node::bind_memcell() +{ + auto const name{memcell_name()}; + if (name.empty()) + { + m_memcell = nullptr; + return; + } + if (m_memcell != nullptr && m_memcell->name() == name) + { + return; + } + m_memcell = simulation::Memory.find(name); + if (m_memcell == nullptr) + { + scene::node_data nodedata; + nodedata.name = name; + m_memcell = new TMemCell(nodedata); + // NOTE: it's made anew each time the scenery is loaded, so it isn't saved with it + m_memcell->is_exportable = false; + m_memcell->location(m_state.position); + simulation::Memory.insert(m_memcell); + } +} + // finds the rails of a crossing, and the places the vehicles of each lane are to stop at void roadpoint_node::bind_crossing() { @@ -896,9 +942,12 @@ void roadpoint_node::update_crossing(double const Deltatime) } m_seen.swap(seen); } - if ((m_hold > 0.0) != m_closed) + // the scenery can close the entry, or keep it open, whatever the trains do + auto const control{m_memcell != nullptr ? m_memcell->Value1() : 0.0}; + auto const closed{control > 0.0 || (control == 0.0 && m_hold > 0.0)}; + if (closed != m_closed) { - close(m_hold > 0.0); + close(closed); } } @@ -906,10 +955,15 @@ void roadpoint_node::update_crossing(double const Deltatime) void roadpoint_node::close(bool const Closed) { m_closed = Closed; + // only the vehicles heading for the crossing are stopped, at the stops ahead of it; the ones past them leave it for (std::size_t idx = 0; idx < m_stops.size() && idx < m_events.size(); ++idx) { static_cast(m_events[idx])->m_velocity = (Closed ? 0.0 : -1.0); } + if (m_memcell != nullptr && m_memcell->Value2() != (Closed ? 1.0 : 0.0)) + { + m_memcell->UpdateValues("", 0.0, Closed ? 1.0 : 0.0, basic_event::flags::value2); + } } // puts a waiting vehicle on the road when its time comes @@ -1150,6 +1204,10 @@ void roadpoint_node::export_as_text_(std::ostream &Output) const case kind_type::crossing: { Output << "clearance " << m_state.clearance << ' ' << "warning " << m_state.warning << ' ' << "stoplines " << (m_state.stoplines ? "yes" : "no") << ' '; + if (false == m_state.memcell.empty()) + { + Output << "memcell " << m_state.memcell << ' '; + } break; } case kind_type::spawn: diff --git a/world/RoadPoint.h b/world/RoadPoint.h index 4b390ba6..b0508234 100644 --- a/world/RoadPoint.h +++ b/world/RoadPoint.h @@ -25,19 +25,23 @@ class TDynamicObject; class basic_event; // a place on a road where something happens to the traffic, or is painted for it: -// - a level crossing: vehicles are stopped ahead of the rails for as long as a train is near, +// - a level crossing: vehicles are stopped ahead of the rails for as long as a train is near, or the scenery says so, // - a point where vehicles appear, drawn from a set made ready while the scenery is loaded, // - a point where vehicles are taken away. the ones which came from a point of the previous kind go back to its set, // - a pedestrian crossing: stripes painted across the road. // scenery entries: -// node crossing [clearance ] [warning ] [stoplines ] endcrossing +// node crossing [clearance ] [warning ] [stoplines ] [memcell ] endcrossing // node spawn [interval ] [variation <0..1>] [velocity ] [count ] // [node dynamic [] enddynamic]... endspawn // node despawn [radius ] enddespawn // node crosswalk [length ] endcrosswalk // the vehicles of a spawn point are given the way vehicles are put in a scenery; where they're put and how they're driven // is up to the point, so the path, the offset, the driver and the velocity of these entries don't matter. the listed vehicles -// are what the point draws from to have vehicles of its own on the roads; one which gets stuck is taken off the road +// are what the point draws from to have vehicles of its own on the roads; one which gets stuck is taken off the road. +// a level crossing is told by the scenery through a memory cell: the one named in its entry, or ":block", +// made if the scenery doesn't have it. its first value above zero closes the entry onto the crossing, below zero keeps it open +// whatever comes by the rails, and zero leaves it to the trains. the vehicles past the stops, on the crossing or leaving it, drive on. +// the second value is set by the crossing: 1 while its entry is closed, 0 while it's open class roadpoint_node : public scene::basic_node { @@ -71,6 +75,7 @@ class roadpoint_node : public scene::basic_node float clearance{6.f}; // distance along the road from the point to the places the vehicles stop at float warning{800.f}; // how far along the rails a train coming closes the crossing bool stoplines{true}; // lines are painted where the vehicles stop + std::string memcell; // name of the memory cell the scenery tells the crossing by; empty: the one named after the crossing // point where vehicles appear float interval{20.f}; // seconds from a vehicle to the next float variation{0.5f}; // part of the interval which is drawn at random @@ -135,6 +140,13 @@ class roadpoint_node : public scene::basic_node { return m_rails.size(); } + // memory cell the level crossing is told by, nullptr if there's none + TMemCell *memcell() const + { + return m_memcell; + } + // name of that cell: the one the entry gives, or the one made of the name of the crossing; empty if neither is there + std::string memcell_name() const; // vehicles made for a spawn point, and how many of them wait for their turn std::size_t vehicles() const { @@ -169,6 +181,7 @@ class roadpoint_node : public scene::basic_node void deserialize_(std::istream &Input) override; void export_as_text_(std::ostream &Output) const override; void bind_crossing(); + void bind_memcell(); void update_crossing(double const Deltatime); void update_spawn(double const Deltatime); void update_despawn(); @@ -184,6 +197,7 @@ class roadpoint_node : public scene::basic_node std::map m_seen; // rail vehicles near, with how far each was the last time bool m_closed{false}; double m_hold{0.0}; // time left for the crossing to stay closed after the last train was seen + TMemCell *m_memcell{nullptr}; // what the scenery tells the crossing by double m_scan{0.0}; // time left to the next look at the rails // spawn and despawn std::vector m_lanes; // spawn: the lane the vehicles are put on. despawn: lanes the vehicles are taken from diff --git a/world/TrkFoll.cpp b/world/TrkFoll.cpp index 11f51fd6..0e7ab29a 100644 --- a/world/TrkFoll.cpp +++ b/world/TrkFoll.cpp @@ -22,6 +22,31 @@ http://mozilla.org/MPL/2.0/. #include "vehicle/Driver.h" #include "utilities/Logs.h" +namespace { + +// how far the start of a way across a crossroads can be from the end of the path an axle leaves, to be where the axle gets on +constexpr double cross_reach { 1.0 }; + +// number of the ways across a crossroads; each can be taken either way +int cross_count( TTrack const &Cross ) { + + return Cross.SwitchExtension->iRoads == 4 ? 6 : 3; +} + +// where the way of specified code starts, and the direction it leads in there. the code is the number of the way, 1..6, +// negative if the way is taken against its points +bool cross_start( TTrack const &Cross, int const Code, glm::dvec3 &Point, glm::dvec3 &Direction ) { + + if( Code == 0 || std::abs( Code ) > cross_count( Cross ) ) { return false; } + auto const &segment { Cross.SwitchExtension->Segments[ std::abs( Code ) - 1 ] }; + if( segment == nullptr ) { return false; } + Point = ( Code > 0 ? segment->FastGetPoint_0() : segment->FastGetPoint_1() ); + Direction = ( Code > 0 ? segment->FastGetPoint( 0.05 ) : segment->FastGetPoint( 0.95 ) ) - Point; + return true; +} + +} // namespace + TTrackFollower::~TTrackFollower() { } @@ -30,6 +55,8 @@ bool TTrackFollower::Init(TTrack *pTrack, TDynamicObject *NewOwner, double fDir) { fDirection = fDir; Owner = NewOwner; + // the axle is put on the track, it doesn't come from the one it was on before + pCurrentSegment = nullptr; SetCurrentTrack(pTrack, 0); iEventFlag = 3; // na torze startowym również wykonać eventy 1/2 iEventallFlag = 3; @@ -92,6 +119,9 @@ TTrack * TTrackFollower::SetCurrentTrack(TTrack *pTrack, int end) // kiedy powstaje! iSegment = 1; // doraźna poprawka } + // the way has to start where the axle gets on. the one the driver picked may not: the driver may have looked + // again since the other axle got on, the vehicle may have no driver of its own, or it may be going backwards + iSegment = CrossEntry( *pTrack, iSegment, end ); if ((end ? iSegment : -iSegment) < 0) fDirection = -fDirection; // wtórna zmiana pTrack->SwitchForced(abs(iSegment) - 1, nullptr); // wybór zapamiętanego segmentu @@ -123,6 +153,67 @@ TTrack * TTrackFollower::SetCurrentTrack(TTrack *pTrack, int end) return pCurrentTrack; }; +// picks the way across the crossroads (Cross) for the axle which gets on it by specified end of the path it's on. it's the way +// taken by another axle of the vehicle, or of a vehicle coupled to it, which is on the crossroads already; or the one the driver +// picked (Wish); or, if neither starts where the axle gets on, the one leading the most straight on of the ways which start there +int TTrackFollower::CrossEntry( TTrack const &Cross, int const Wish, int const End ) const { + + if( pCurrentSegment == nullptr || Cross.SwitchExtension == nullptr ) { return Wish; } + + auto const joint { End ? pCurrentSegment->FastGetPoint_1() : pCurrentSegment->FastGetPoint_0() }; + auto const starts_here = [&]( int const Code ) { + glm::dvec3 point, direction; + return cross_start( Cross, Code, point, direction ) && glm::distance( point, joint ) < cross_reach; }; + + if( Owner != nullptr ) { + for( auto const *vehicle : { Owner, Owner->Prev(), Owner->Next() } ) { + if( vehicle == nullptr ) { continue; } + for( auto const *axle : { &vehicle->Axle0, &vehicle->Axle1 } ) { + if( axle == this || axle->pCurrentTrack != &Cross || axle->iSegment == 0 ) { continue; } + if( starts_here( axle->iSegment ) ) { return axle->iSegment; } + // a vehicle going back over the crossroads gets on the way at its other end + if( starts_here( -axle->iSegment ) ) { return -axle->iSegment; } + } + } + } + if( starts_here( Wish ) ) { return Wish; } + + auto const incoming { joint - ( End ? pCurrentSegment->FastGetPoint( 0.95 ) : pCurrentSegment->FastGetPoint( 0.05 ) ) }; + auto nearest { cross_reach }; + for( int way = 1; way <= cross_count( Cross ); ++way ) { + for( auto const code : { way, -way } ) { + glm::dvec3 point, direction; + if( cross_start( Cross, code, point, direction ) ) { + nearest = std::min( nearest, glm::distance( point, joint ) ); + } + } + } + if( nearest >= cross_reach ) { + // no way starts there, the crossroads isn't joined with the path; it's left the way it was + return Wish; + } + auto result { Wish }; + auto straightest { -2.0 }; + for( int way = 1; way <= cross_count( Cross ); ++way ) { + for( auto const code : { way, -way } ) { + glm::dvec3 point, direction; + if( false == cross_start( Cross, code, point, direction ) + || glm::distance( point, joint ) > nearest + 0.05 ) { + continue; + } + auto const straight { + glm::length2( incoming ) > 0.0 && glm::length2( direction ) > 0.0 ? + glm::dot( glm::normalize( incoming ), glm::normalize( direction ) ) : + -1.0 }; + if( straight > straightest ) { + straightest = straight; + result = code; + } + } + } + return result; +} + bool TTrackFollower::Move(double fDistance, bool bPrimary) { // przesuwanie wózka po torach o odległość (fDistance), z wyzwoleniem eventów // bPrimary=true - jest pierwszą osią w pojeździe, czyli generuje eventy i przepisuje pojazd diff --git a/world/TrkFoll.h b/world/TrkFoll.h index a4430490..079aaae4 100644 --- a/world/TrkFoll.h +++ b/world/TrkFoll.h @@ -50,6 +50,7 @@ public: // methods bool ComputatePosition(); // przeliczenie pozycji na torze int JoinedEnd( int const End ) const; + int CrossEntry( TTrack const &Cross, int const Wish, int const End ) const; // members TTrack *pCurrentTrack = nullptr; // na którym torze siê znajduje std::shared_ptr pCurrentSegment; // zwrotnice mog¹ mieæ dwa segmenty