diff --git a/.gitignore b/.gitignore index 5186ceaee..70a6d1c68 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ vcpkg.json # Local Python (setup.bat / CMake) for embedding tools/python314/ +__pycache__/ +*.pyc diff --git a/application/application.cpp b/application/application.cpp index 119cb021e..9b7b82853 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -270,7 +270,11 @@ void eu07_application::DiscordRPCService() } if (currentMode != mode::driver) + { + // the editor and the other modes: nothing to tell, without spinning a core on it + std::this_thread::sleep_for(std::chrono::milliseconds(250)); continue; + } // Discord RPC updater if (simulation::is_ready) @@ -690,19 +694,29 @@ void eu07_application::release_python_lock() void eu07_application::exit() { Global.applicationQuitOrder = true; + // each step of the closing in the log, to tell from it where a closing that hangs stops + auto const closing{std::chrono::steady_clock::now()}; + auto const step = [closing](char const *What) { + WriteLog(std::format("Closing: {} at {} ms", What, std::chrono::duration_cast(std::chrono::steady_clock::now() - closing).count())); + }; + step("the modes"); for (auto &mode : m_modes) mode.reset(); + step("the renderer"); GfxRenderer->Shutdown(); m_network.reset(); // SafeDelete( simulation::Train ); + step("the scenery"); SafeDelete(simulation::Region); + step("the ui and the python workers"); ui_layer::shutdown(); // the python workers detach their GL context on the way out, so they have to be done before the windows they use are destroyed m_taskqueue.exit(); + step("the windows"); for (auto *window : m_windows) { glfwDestroyWindow(window); @@ -713,6 +727,8 @@ void eu07_application::exit() if (!Global.exec_on_exit.empty()) system(Global.exec_on_exit.c_str()); + step("done"); + StopLogService(); auto it = Global.threads.find("LogService"); if (it != Global.threads.end()) { diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index dbdce6068..f8f06ac93 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -21,6 +21,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include "audio/audiorenderer.h" +#include + scenarioloader_mode::scenarioloader_mode() { m_userinterface = std::make_shared(); } @@ -41,11 +43,26 @@ bool scenarioloader_mode::update() { WriteLog("using simulation seed: " + std::to_string(Global.random_seed), logtype::generic); WriteLog("using simulation starting timestamp: " + std::to_string(Global.starting_timestamp), logtype::generic); + // the command line gives the name lowercased, the file may be named in another case, as a scenery made on Windows; + // the scenery is saved back to its own file then + if (auto const cased{FileCased(Global.asCurrentSceneryPath + Global.SceneryFile)}; cased.size() > Global.asCurrentSceneryPath.size()) + Global.SceneryFile = cased.substr(Global.asCurrentSceneryPath.size()); Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" ); WriteLog( "\nLoading scenario \"" + Global.SceneryFile + "\"..." ); timestart = std::chrono::system_clock::now(); - state = simulation::State.deserialize_begin(Global.SceneryFile); + try { + state = simulation::State.deserialize_begin(Global.SceneryFile); + } + // the file it can't read, as the parser and the scenery opening it report it + catch (std::runtime_error const &e) { + ErrorLog(std::format("Bad init: scenario \"{}\" couldn't be opened ({})", Global.SceneryFile, e.what())); + state.reset(); + Global.SceneryFile.clear(); + Application.pop_mode(); + Application.push_mode(eu07_application::mode::launcher); + return true; + } } try { @@ -53,8 +70,13 @@ bool scenarioloader_mode::update() { return true; } catch (invalid_scenery_exception &e) { + // back to the launcher as when the scenario can't be opened, not on to the driver mode with the scenery half loaded ErrorLog( "Bad init: scenario loading failed" ); + state.reset(); + Global.SceneryFile.clear(); Application.pop_mode(); + Application.push_mode(eu07_application::mode::launcher); + return true; } // the loading state holds the scenario file open, which on some systems keeps the file from being replaced // when the scenery is saved in the editor. it'd also get in the way of another load diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 893b84e92..2b1fa1686 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -569,7 +569,7 @@ void ui_layer::render_hierarchy(){ // fitted to the contents when it first appears; no auto resize, which a docked window can't do ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_FirstUseEver); ImGui::Begin((std::string(STR_C("Scene Hierarchy")) + "###scenehierarchy").c_str(), &m_editor_hierarchy); - ImGui::Text("Registered nodes: %zu", scene::Hierarchy.size()); + ImGui::Text(STR_C("Registered nodes: %zu"), scene::Hierarchy.size()); ImGui::BeginChild("hierarchy_list", ImVec2(500, 300), true); for (auto &entry : scene::Hierarchy) @@ -600,7 +600,7 @@ void ui_layer::render_hierarchy(){ if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("UUID: %s\nNode type: %s", uuid.c_str(), typeid(*node).name()); + ImGui::SetTooltip(STR_C("UUID: %s\nNode type: %s"), uuid.c_str(), typeid(*node).name()); } } } @@ -710,14 +710,14 @@ void ui_layer::render_menu_contents() GfxRenderer->Debug_Ui_State(ret); } if(EditorModeFlag){ - ImGui::MenuItem("Hierarchy", nullptr, &m_editor_hierarchy); + ImGui::MenuItem(STR_C("Scene Hierarchy"), nullptr, &m_editor_hierarchy); bool change_history_enabled = editor_mode::change_history(); - if (ImGui::MenuItem("Change History", nullptr, &change_history_enabled)) + if (ImGui::MenuItem(STR_C("Change History"), nullptr, &change_history_enabled)) { editor_mode::set_change_history(change_history_enabled); } bool settings_open = editor_mode::settings_open(); - if (ImGui::MenuItem("Editor Settings", nullptr, &settings_open)) + if (ImGui::MenuItem(STR_C("Editor Settings"), nullptr, &settings_open)) { editor_mode::set_settings_open(settings_open); } diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index d95c962b5..6a57db54b 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -8,6 +8,28 @@ #include "utilities/translation.h" #include +#include + +namespace +{ + +// the skin of the bank of vehicles as it's shared, the one a skin dragged out of the bank points at; none for one gone since +std::shared_ptr shared_skin(ui::skin_set const *Skin) +{ + auto const vehicle{Skin->vehicle.lock()}; + if (vehicle == nullptr) + return {}; + auto const found{std::ranges::find_if(vehicle->matching_skinsets, [Skin](std::shared_ptr const &Set) { return Set.get() == Skin; })}; + return found != vehicle->matching_skinsets.end() ? *found : nullptr; +} + +// the name of a vehicle put in a trainset, with a number of its own +std::string numbered(std::string const &Name) +{ + return std::format("{}_{}", Name, static_cast(LocalRandom(0.0, 10000000.0))); +} + +} // namespace ui::scenerylist_panel::scenerylist_panel(scenery_scanner &scanner) : ui_panel(STR("Scenario list"), false), scanner(scanner), placeholder_mini("textures/mini/other") @@ -23,7 +45,7 @@ bool ui::scenerylist_panel::on_key(int key, int action) if (it == keyboard.keytonamemap.end()) return false; - if (Global.ctrlState && it->second == "delete") + if (Global.ctrlState && it->second == "delete" && selected_trainset != nullptr) purge_selected_trainset(); return true; @@ -117,7 +139,7 @@ void ui::scenerylist_panel::draw_launch_box() { ImGui::NextColumn(); - ImGui::TextWrapped(selected_trainset->description.c_str()); + ImGui::TextWrapped("%s", selected_trainset->description.c_str()); ImGui::Checkbox(STR_C("Trainset turned around"), &selected_trainset->reversed); if (ImGui::IsItemHovered()) @@ -283,6 +305,7 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) draw_droptarget(trainset, position++); ImGui::SameLine(15.0f); + // a vehicle dropped between them goes in once they're all drawn, so they're gone through unchanged for (auto &dyn_desc : trainset.vehicles) { deferred_image *mini = nullptr; @@ -332,6 +355,10 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) ImGui::PopID(); } + if (m_drop) { + m_drop(); + m_drop = nullptr; + } ImGui::NewLine(); ImGui::PopID(); @@ -340,41 +367,47 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) void ui::scenerylist_panel::draw_droptarget(trainset_desc &trainset, int position) { ImGui::Dummy(ImVec2(15, 30)); - if (ImGui::BeginDragDropTarget()) { - const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("vehicle_pure"); - if (payload) { - skin_set *ptr = *reinterpret_cast(payload->Data); - std::shared_ptr skin; - for (auto &s : ptr->vehicle.lock()->matching_skinsets) - if (s.get() == ptr) - skin = s; + if (false == ImGui::BeginDragDropTarget()) + return; + if (auto const *payload{ImGui::AcceptDragDropPayload("vehicle_pure")}) + drop_new_vehicle(trainset, position, *static_cast(payload->Data)); + if (auto const *payload{ImGui::AcceptDragDropPayload("vehicle_set")}) + drop_moved_vehicle(trainset, position, *static_cast(payload->Data)); + ImGui::EndDragDropTarget(); +} - trainset.vehicles.emplace(trainset.vehicles.begin() + position); - dynamic_desc &desc = trainset.vehicles[position]; +// a vehicle of the bank dropped on a gap of a trainset, put in there as a new one of it +void ui::scenerylist_panel::drop_new_vehicle(trainset_desc &trainset, int const position, skin_set const *Skin) +{ + auto skin{shared_skin(Skin)}; + if (skin == nullptr) + return; + m_drop = [&trainset, position, skin] { + auto &desc{*trainset.vehicles.emplace(trainset.vehicles.begin() + position)}; + desc.name = numbered(skin->skin); + desc.vehicle = skin->vehicle.lock(); + desc.skin = skin; + }; +} - desc.name = skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); - desc.vehicle = skin->vehicle.lock(); - desc.skin = skin; +// a vehicle of a trainset dropped on a gap of it or of another one: moved there within it, from another one copied in with a +// name of its own +void ui::scenerylist_panel::drop_moved_vehicle(trainset_desc &trainset, int const position, vehicle_moved const &Moved) +{ + auto copy{Moved.dynamic}; + auto &source{Moved.source}; + auto const from{Moved.position}; + if (&trainset != &source) + copy.name = numbered(copy.skin ? copy.skin->skin : copy.name); + m_drop = [&trainset, &source, from, position, copy] { + auto offset{0}; + if (&trainset == &source) { + trainset.vehicles.erase(trainset.vehicles.begin() + from); + if (from < position) + offset = -1; } - - payload = ImGui::AcceptDragDropPayload("vehicle_set"); - if (payload) { - vehicle_moved *moved = reinterpret_cast(payload->Data); - - dynamic_desc desc_copy = moved->dynamic; - int offset = 0; - if (&trainset == &moved->source) { - trainset.vehicles.erase(trainset.vehicles.begin() + moved->position); - if (moved->position < position) - offset = -1; - } else { - desc_copy.name = desc_copy.skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); - } - - trainset.vehicles.insert(trainset.vehicles.begin() + position + offset, desc_copy); - } - ImGui::EndDragDropTarget(); - } + trainset.vehicles.insert(trainset.vehicles.begin() + position + offset, copy); + }; } void ui::scenerylist_panel::purge_selected_trainset() diff --git a/launcher/scenery_list.h b/launcher/scenery_list.h index 957f1b5ee..0bc91c452 100644 --- a/launcher/scenery_list.h +++ b/launcher/scenery_list.h @@ -54,6 +54,8 @@ private: scenery_desc *selected_scenery = nullptr; trainset_desc *selected_trainset = nullptr; deferred_image placeholder_mini; + // a vehicle dropped on a gap between the vehicles of a trainset: put in there once they're all drawn, not while they are + std::function m_drop; void draw_scenery_list(); void draw_scenery_info(); @@ -67,6 +69,8 @@ private: void open_link(const std::string &link); void draw_trainset(trainset_desc &trainset); void draw_droptarget(trainset_desc &trainset, int position); + void drop_new_vehicle(trainset_desc &trainset, int position, skin_set const *Skin); + void drop_moved_vehicle(trainset_desc &trainset, int position, vehicle_moved const &Moved); void purge_selected_trainset(); }; } // namespace ui diff --git a/launcher/scenery_scanner.cpp b/launcher/scenery_scanner.cpp index bcd18ac90..6e258425e 100644 --- a/launcher/scenery_scanner.cpp +++ b/launcher/scenery_scanner.cpp @@ -2,6 +2,8 @@ #include "scenery_scanner.h" #include "utilities/Logs.h" +#include + scenery_scanner::scenery_scanner(ui::vehicles_bank &bank) : bank(bank) { @@ -10,13 +12,22 @@ scenery_scanner::scenery_scanner(ui::vehicles_bank &bank) void scenery_scanner::scan() { - for (auto &f : std::filesystem::directory_iterator("scenery")) { - std::filesystem::path path(std::filesystem::relative(f.path(), "scenery/")); + std::error_code error; + for (std::filesystem::directory_iterator f{"scenery", std::filesystem::directory_options::skip_permission_denied, error}, end; f != end; f.increment(error)) { + std::filesystem::path path(std::filesystem::relative(f->path(), "scenery/", error)); + std::string name; + try { + name = path.string(); + } + catch (std::system_error const &) { + // a name outside of the code page, which the path can't give as a string on Windows + continue; + } - if (*path.filename().string().begin() == '$') + if (name.empty() || path.filename().string().front() == '$') continue; - if (path.string().ends_with(".scn")) + if (name.ends_with(".scn")) scan_scn(path); } diff --git a/launcher/textures_scanner.cpp b/launcher/textures_scanner.cpp index 524a23ca6..741f978ca 100644 --- a/launcher/textures_scanner.cpp +++ b/launcher/textures_scanner.cpp @@ -4,12 +4,13 @@ void ui::vehicles_bank::scan_textures() { - for (auto &f : std::filesystem::recursive_directory_iterator("dynamic")) { - if (f.path().filename() != "textures.txt") + std::error_code error; + for (std::filesystem::recursive_directory_iterator f{"dynamic", std::filesystem::directory_options::skip_permission_denied, error}, end; f != end; f.increment(error)) { + if (f->path().filename() != "textures.txt") continue; - std::fstream stream(f.path(), std::ios_base::binary | std::ios_base::in); - ctx_path = std::filesystem::relative(f.path().parent_path(), "dynamic/"); + std::fstream stream(f->path(), std::ios_base::binary | std::ios_base::in); + ctx_path = std::filesystem::relative(f->path().parent_path(), "dynamic/", error); std::string line; while (std::getline(stream, line)) diff --git a/model/Texture.cpp b/model/Texture.cpp index 136846a33..4a7b24792 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -706,6 +706,16 @@ void opengl_texture::update_from_memory(size_t width, size_t height, const uint8 if (glGenerateMipmap) glGenerateMipmap(target); glFlush(); + // the content given straight to the gpu can't be read again: released by the garbage sweep, the texture would come back as + // the grey stub + if (type == "internalsrc:") + { + is_static = true; + is_ready = true; + } + // the name may be a new one, and the unit active took the texture past the unit cache + for (auto &unit : units) + unit = -1; } void diff --git a/nodebank_previews_standalone.cpp b/nodebank_previews_standalone.cpp index 39884fd38..d94eb13ed 100644 --- a/nodebank_previews_standalone.cpp +++ b/nodebank_previews_standalone.cpp @@ -290,9 +290,9 @@ int generate_nodebank_previews(int Argc, char *Argv[]) std::thread logservice(LogService); auto const finish = [&](int const Result) { - // give the log service time to write out what's left - std::this_thread::sleep_for(std::chrono::milliseconds(200)); + // the log service writes out what's left before it stops Global.applicationQuitOrder = true; + StopLogService(); logservice.join(); return Result; }; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index d20454c35..a8a1b2139 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -5410,6 +5410,13 @@ void opengl33_renderer::Pick_Node_Callback(std::functionis_busy()) diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 64574420f..0f17ede7f 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -108,6 +108,8 @@ class opengl33_renderer : public gfx_renderer { Pick_Control() const override { return m_pickcontrolitem; } scene::basic_node const * Pick_Node() const override { return m_picksceneryitem; } + void + Forget_Node( scene::basic_node const *Node ) override; glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } bool diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 59152c64d..6313aa55f 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -1888,6 +1888,15 @@ opengl_renderer::Pick_Node_Callback( std::function m_node_pick_requests.emplace_back( Callback ); } +void +opengl_renderer::Forget_Node( scene::basic_node const *Node ) { + + if( m_picksceneryitem == Node ) { + m_picksceneryitem = nullptr; + } + std::ranges::replace_if( m_picksceneryitems, [Node]( scene::basic_node const *Item ) { return Item == Node; }, nullptr ); +} + void opengl_renderer::Render( scene::basic_region *Region ) { diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 78fffa40b..ada032808 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -103,6 +103,8 @@ public: Pick_Control() const override { return m_pickcontrolitem; } scene::basic_node const * Pick_Node() const override { return m_picksceneryitem; } + void + Forget_Node( scene::basic_node const *Node ) override; glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } glm::mat4 diff --git a/rendering/renderer.h b/rendering/renderer.h index 0b9eb60f4..5850e1754 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -90,6 +90,10 @@ public: virtual void Pick_Node_Callback( std::function Callback ) = 0; virtual auto Pick_Control() const -> TSubModel const * = 0; virtual auto Pick_Node() const -> scene::basic_node const * = 0; + // a node deleted from the scene is no longer given as picked + virtual void Forget_Node( scene::basic_node const * /*Node*/ ) { + // a renderer which keeps no node picked has none to forget + } virtual auto Mouse_Position() const -> glm::dvec3 = 0; virtual auto Mouse_Hit() const -> bool { return true; } diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 70f9e3074..ea2177170 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -30,6 +30,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "vehicle/Train.h" #include "application/application.h" +#include "rendering/renderer.h" #include "utilities/Logs.h" #include "vehicle/Driver.h" @@ -561,6 +562,9 @@ std::pair state_manager::rebuild_include(scene::instance_handle const } void state_manager::delete_model(TAnimModel *model) { + if (GfxRenderer) { + GfxRenderer->Forget_Node(model); + } scene::Layers.count(model->layer(), scene::layer_item::model, -1); scene::Layers.forget(model); Region->erase(model); diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 1ff771d1d..664f3b955 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -36,6 +36,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include "editor/editorTerrainStreamer.hpp" +#include namespace simulation { @@ -542,7 +543,14 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } else if( nodedata.type == "track" ) { - auto *path { deserialize_path( Input, Scratchpad, nodedata ) }; + std::unique_ptr made { deserialize_path( Input, Scratchpad, nodedata ) }; + if( made->eType == tt_Unknown ) { + // with no path made it can't be joined nor drawn, and nothing could run on it: it goes with its holder + ErrorLog( std::format( "Bad scenario: track \"{}\" of an unknown type defined in file \"{}\" (line {}) left out", made->name(), Input.Name(), inputline ) ); + return; + } + // from here on the scene owns it + auto *path { made.release() }; // duplicates of named tracks are currently experimentally allowed if( false == simulation::Paths.insert( path ) ) { ErrorLog( "Bad scenario: duplicate track name \"" + path->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); diff --git a/utilities/Logs.cpp b/utilities/Logs.cpp index 38f8062b9..38094d72e 100644 --- a/utilities/Logs.cpp +++ b/utilities/Logs.cpp @@ -73,12 +73,28 @@ std::deque ErrorStack; // lock for log stacks std::mutex logMutex; +namespace +{ +// the log is written through the closing of the simulator, to the end: set once the closing has nothing more to tell +std::atomic &log_stopped() +{ + static std::atomic stopped{false}; + return stopped; +} +} // namespace + +void StopLogService() +{ + log_stopped() = true; +} void LogService() { - while (!Global.applicationQuitOrder) + // one more pass after the stop, so what came just before it gets written too + for (bool last{false}; false == last;) { + last = log_stopped(); { // --- Obsługa InfoStack --- while (!InfoStack.empty()) diff --git a/utilities/Logs.h b/utilities/Logs.h index 93276d651..63727de9c 100644 --- a/utilities/Logs.h +++ b/utilities/Logs.h @@ -23,7 +23,9 @@ enum class logtype : unsigned int { traction = 1 << 9, powergrid = 1 << 10, }; +// writes the log until StopLogService(), and all of it that came before void LogService(); +void StopLogService(); void WriteLog( const char *str, logtype const Type = logtype::generic, bool isError = false ); void Error( const std::string &asMessage, bool box = false ); void Error( const char* &asMessage, bool box = false ); diff --git a/utilities/parser.cpp b/utilities/parser.cpp index b64aee39b..ee5a9ce5b 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -63,6 +63,15 @@ cParser::cParser(std::string const &Stream, buffertype const Type, std::string P // the file is read in one go, tokens are then extracted straight from the memory std::ifstream file(Path, std::ios_base::binary); if (file.fail()) + { + // named in another case than it's on the disk + if (auto const cased{FileCased(Path)}; false == cased.empty()) + { + file.clear(); + file.open(cased, std::ios_base::binary); + } + } + if (file.fail()) { mFailBit = true; } diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index e84307e39..e10cf0d84 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -474,6 +474,26 @@ bool FileExists(std::string const &Filename) return std::filesystem::exists(Filename); } +std::string FileCased(std::filesystem::path const &Filename) +{ + std::error_code error; + if (std::filesystem::exists(Filename, error)) + return Filename.string(); +#ifdef _WIN32 + // the names there are found in any case already: no folder to go through for every file missing, nor names outside of the + // code page to throw on + return {}; +#else + // only the name of the file itself, a folder named in another case isn't found + auto const directory{Filename.has_parent_path() ? Filename.parent_path() : std::filesystem::path{"."}}; + auto const wanted{ToLower(Filename.filename().string())}; + for (std::filesystem::directory_iterator entry{directory, error}, end; entry != end; entry.increment(error)) + if (ToLower(entry->path().filename().string()) == wanted) + return Filename.has_parent_path() ? (directory / entry->path().filename()).string() : entry->path().filename().string(); + return {}; +#endif +} + std::pair FileExists(std::vector const &Names, std::vector const &Extensions) { diff --git a/utilities/utilities.h b/utilities/utilities.h index 5392ba6c5..cc4056cae 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -217,6 +217,9 @@ template bool extract_value(Type_ &Variable, std::string const template <> bool extract_value(bool &Variable, std::string const &Key, std::string const &Input, std::string const &Default); bool FileExists(std::string const &Filename); +// the file as it's named on the disk when only the case of its name differs, as in the sceneries made on Windows; empty when +// there's none. the folders have to be named in their case; on Windows, finding the names in any case itself, the file given or none +std::string FileCased(std::filesystem::path const &Filename); std::pair FileExists(std::vector const &Names, std::vector const &Extensions); diff --git a/world/Track.cpp b/world/Track.cpp index c9c7ae36d..1361ebd95 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -29,6 +29,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "rendering/renderer.h" +#include + // 101206 Ra: trapezoidalne drogi i tory // 110720 Ra: rozprucie zwrotnicy i odcinki izolowane @@ -432,11 +434,15 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } else if (str == "tributary") { - eType = tt_Tributary; + // dopływ: no path of it was ever made, the simulator falling over it as it loaded. a river of its first path now, the + // second one kept and written back + ErrorLog(std::format("Bad track: tributary \"{}\" in file \"{}\" (line {}) is drawn and run on as a river of its first path", m_name, parser->Name(), parser->Line())); + eType = tt_Normal; iCategoryFlag = 4; + m_tributary = true; } else - eType = tt_Unknown; + eType = tt_Unknown; // left out by the loader if (Global.iWriteLogEnabled & 4) WriteLog(str); parser->getTokens(4); @@ -522,6 +528,11 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) break; } } + if( m_tributary ) { + // the second path of a tributary, kept to be written back + m_paths.emplace_back(); + m_paths.back().deserialize( *parser, pOrigin ); + } init_segments( true ); @@ -697,6 +708,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) m_name = m_name.substr(0, i - 1); // usunięcie z nazwy } + if( CurrentSegment() == nullptr ) { + // of an unknown type, no path of it is made + return; + } update_location(); // sleeper transforms are baked later in create_geometry(), once the owning cell has // assigned this track its m_origin (otherwise the local-space matrices would be relative @@ -2336,24 +2351,37 @@ TTrack::deserialize_( std::istream &Input ) { // TODO: implement } +// the keyword the track is written with, by its type and for a plain one by what runs on it +char const * +TTrack::type_keyword() const { + + if( eType == tt_Normal && m_tributary ) { + return "tributary"; + } + if( eType == tt_Normal ) { + switch( iCategoryFlag ) { + case 1: { return "normal"; } + case 2: { return "road"; } + case 4: { return "river"; } + default: { return "none"; } + } + } + switch( eType ) { + case tt_Switch: { return "switch"; } + case tt_Cross: { return "cross"; } + case tt_Table: { return "turn"; } + case tt_Tributary: { return "tributary"; } + default: { return "none"; } + } +} + // export() subclass details, sends basic content of the class in legacy (text) format to provided stream void TTrack::export_as_text_( std::ostream &Output ) const { // header Output << "track "; // type - Output << ( - eType == tt_Normal ? ( - iCategoryFlag == 1 ? "normal" : - iCategoryFlag == 2 ? "road" : - iCategoryFlag == 4 ? "river" : - "none" ) : - eType == tt_Switch ? "switch" : - eType == tt_Cross ? "cross" : - eType == tt_Table ? "turn" : - eType == tt_Tributary ? "tributary" : - "none" ) - << ' '; + Output << type_keyword() << ' '; // basic attributes Output << Length() << ' ' diff --git a/world/Track.h b/world/Track.h index dc6be7412..ba8955032 100644 --- a/world/Track.h +++ b/world/Track.h @@ -188,6 +188,7 @@ public: std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; + bool m_tributary { false }; // loaded as a tributary: a river of its first path, the second one kept to be written back scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; @@ -382,6 +383,8 @@ private: void deserialize_( std::istream &Input ); // export() subclass details, sends basic content of the class in legacy (text) format to provided stream void export_as_text_( std::ostream &Output ) const; + // the keyword the track is written with, by its type and for a plain one by what runs on it + char const *type_keyword() const; // locates specified profile in the profile database, potentially loading it from a file static std::pair fetch_track_rail_profile( std::string const &Profile ); // loads content of specified file and converts it into a vertex array