From ace86408589da467fab69c15a46eb3952f5b77ef Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Thu, 2 Nov 2023 14:15:56 +0100 Subject: [PATCH] Tuning: Working save/load/delete of tuneups. --- source/main/GameContext.cpp | 10 +- source/main/gui/GUIUtils.cpp | 35 +++++ source/main/gui/GUIUtils.h | 3 + source/main/gui/panels/GUI_TopMenubar.cpp | 113 ++++++++++------ source/main/gui/panels/GUI_TopMenubar.h | 9 +- source/main/main.cpp | 8 ++ source/main/physics/Actor.cpp | 12 +- source/main/physics/Actor.h | 6 +- source/main/physics/ActorSpawner.cpp | 2 +- source/main/resources/CacheSystem.cpp | 124 +++++++++++++++--- source/main/resources/CacheSystem.h | 23 +++- .../tuneup_fileformat/TuneupFileFormat.cpp | 18 +++ .../tuneup_fileformat/TuneupFileFormat.h | 2 + 13 files changed, 279 insertions(+), 86 deletions(-) diff --git a/source/main/GameContext.cpp b/source/main/GameContext.cpp index 3e722da811..c257a271ad 100644 --- a/source/main/GameContext.cpp +++ b/source/main/GameContext.cpp @@ -236,10 +236,14 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq) LOG(" ===== LOADING VEHICLE: " + rq.asr_filename); - if (rq.asr_cache_entry != nullptr) + if (rq.asr_cache_entry) { rq.asr_filename = rq.asr_cache_entry->fname; } + else + { + rq.asr_cache_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AllBeam, /*partial:*/false, rq.asr_filename); + } RigDef::DocumentPtr def = m_actor_manager.FetchActorDef( rq.asr_filename, rq.asr_origin == ActorSpawnRequest::Origin::TERRN_DEF); @@ -402,8 +406,8 @@ void GameContext::ModifyActor(ActorModifyRequest& rq) srq->asr_position = Ogre::Vector3(actor->getPosition().x, actor->getMinHeight(), actor->getPosition().z); srq->asr_rotation = Ogre::Quaternion(Ogre::Degree(270) - Ogre::Radian(actor->getRotation()), Ogre::Vector3::UNIT_Y); srq->asr_config = actor->getSectionConfig(); - srq->asr_skin_entry = actor->getUsedSkin(); - srq->asr_tuneup_entry = actor->getUsedTuneup(); + srq->asr_skin_entry = actor->getUsedSkinEntry(); + srq->asr_tuneup_entry = actor->getUsedTuneupEntry(); srq->asr_cache_entry= entry; srq->asr_debugview = (int)actor->GetGfxActor()->GetDebugView(); srq->asr_origin = ActorSpawnRequest::Origin::USER; diff --git a/source/main/gui/GUIUtils.cpp b/source/main/gui/GUIUtils.cpp index bf64105ae4..1e6cc96724 100644 --- a/source/main/gui/GUIUtils.cpp +++ b/source/main/gui/GUIUtils.cpp @@ -434,3 +434,38 @@ void RoR::ImDrawModifierKeyHighlighted(OIS::KeyCode key) ImGui::EndChildFrame(); ImGui::PopStyleVar(); // FramePadding } + +bool RoR::ImButtonHoldToConfirm(const std::string& btn_caption, bool small, float time_limit, float& time_left_var, float dt) +{ + + bool btn_pressed = false; + if (small) + btn_pressed = ImGui::SmallButton(btn_caption.c_str()); + else + btn_pressed = ImGui::Button(btn_caption.c_str()); + + if (btn_pressed) + { + time_left_var = time_limit; // Initialize + } + else + { + if (ImGui::IsItemActive() && time_left_var > 0.f) + { + time_left_var -= dt; + if (time_left_var > 0.f) + { + ImGui::BeginTooltip(); + std::string text = _L("Hold to confirm"); + ImGui::TextDisabled(text.c_str()); + ImGui::ProgressBar(time_left_var/time_limit, ImVec2(ImGui::CalcTextSize(text.c_str()).x, 8.f), ""); + ImGui::EndTooltip(); + } + else + { + return true; + } + } + } + return false; +} diff --git a/source/main/gui/GUIUtils.h b/source/main/gui/GUIUtils.h index e3fde7838d..74822b2c9d 100644 --- a/source/main/gui/GUIUtils.h +++ b/source/main/gui/GUIUtils.h @@ -84,4 +84,7 @@ void ImTerminateComboboxString(std::string& target); void ImDrawEventHighlighted(events input_event); void ImDrawModifierKeyHighlighted(OIS::KeyCode key); +// Draws button which must be held for a period to report "clicked" - shows a tooltip with countdown progressbar. +bool ImButtonHoldToConfirm(const std::string& btn_caption, bool small, float time_limit, float& time_left_var, float dt); + } // namespace RoR diff --git a/source/main/gui/panels/GUI_TopMenubar.cpp b/source/main/gui/panels/GUI_TopMenubar.cpp index 8687ac480f..b4f9c11ac0 100644 --- a/source/main/gui/panels/GUI_TopMenubar.cpp +++ b/source/main/gui/panels/GUI_TopMenubar.cpp @@ -1449,7 +1449,7 @@ void TopMenubar::Draw(float dt) else { ROR_ASSERT(tuning_actor->getUsedActorEntry()); - CacheEntryPtr& tuneup_entry = tuning_actor->getUsedTuneup(); + CacheEntryPtr& tuneup_entry = tuning_actor->getUsedTuneupEntry(); ROR_ASSERT(tuneup_entry); // Created by `GameContext::SpawnActor()` ROR_ASSERT(tuneup_entry->resource_group != ""); ROR_ASSERT(tuneup_entry->tuneup_def != nullptr); @@ -1462,86 +1462,108 @@ void TopMenubar::Draw(float dt) ImGui::PushID(tuneup_result.cqr_entry->fname.c_str()); ImGui::Bullet(); + + // Load button (with tuneup name) ImGui::SameLine(); - if (ImGui::SmallButton(_LC("Tuning", "Load"))) + if (ImGui::Button(tuneup_result.cqr_entry->dname.c_str())) { - // Instead of loading with the saved tuneup directly, first overwrite the auto-generated one with the saved one - // and then reload with the autogenerated, so subsequent editing doesn't modify the save until user saves again. - // ------------------------------------------------------------------------------------------------------------- ModifyProjectRequest* req = new ModifyProjectRequest(); req->mpr_type = ModifyProjectRequestType::PROJECT_LOAD_TUNEUP; req->mpr_subject = tuneup_result.cqr_entry->fname; req->mpr_target_actor = tuning_actor; App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); + // Why 'MODIFY_PROJECT_REQUESTED' for loading? + // Instead of loading with the saved tuneup directly, we keep the autogenerated and sync it with the save. + // That way, subsequent editing doesn't modify the save until user saves again. } + + // Delete button (right-aligned) ImGui::SameLine(); - ImGui::Text("%s", tuneup_result.cqr_entry->dname.c_str()); - ImGui::SameLine(); - if (ImGui::SmallButton(_LC("Tuning", "Delete"))) + if (tuning_delbtn_cursorx_min < ImGui::GetCursorPosX()) // Make sure button won't draw over item name + tuning_delbtn_cursorx_min = ImGui::GetCursorPosX(); + std::string delbtn_text = _LC("Tuning", "Delete"); + float delbtn_w = ImGui::CalcTextSize(delbtn_text.c_str()).x + ImGui::GetStyle().FramePadding.x * 2; + float delbtn_cursorx = ImGui::GetWindowContentRegionWidth() - delbtn_w; + if (delbtn_cursorx < tuning_delbtn_cursorx_min) + delbtn_cursorx = tuning_delbtn_cursorx_min; + ImGui::SetCursorPosX(delbtn_cursorx); + ImGui::PushStyleColor(ImGuiCol_Button, TUNING_HOLDTOCONFIRM_COLOR); + bool delbtn_pressed = RoR::ImButtonHoldToConfirm(delbtn_text, /*small:*/true, + TUNING_HOLDTOCONFIRM_TIMELIMIT, /*[by ref]:*/tuning_holdtoconfirm_time_left, dt); + ImGui::PopStyleColor(); //ImGuiCol_Button + if (delbtn_pressed) { - tuning_deletebtn_time_left = TUNING_DELETEBTN_TIMELIMIT; - } - else - { - if (ImGui::IsItemActive() && tuning_deletebtn_time_left > 0.f) - { - tuning_deletebtn_time_left -= dt; - if (tuning_deletebtn_time_left > 0.f) - { - ImGui::BeginTooltip(); - ImGui::ProgressBar(tuning_deletebtn_time_left/TUNING_DELETEBTN_TIMELIMIT); - ImGui::EndTooltip(); - } - else - { - App::GetGameContext()->PushMessage( - Message(MSG_EDI_DELETE_PROJECT_REQUESTED, new CacheEntryPtr(tuneup_result.cqr_entry))); - } - } + App::GetGameContext()->PushMessage(Message(MSG_EDI_DELETE_PROJECT_REQUESTED, new CacheEntryPtr(tuneup_result.cqr_entry))); } } + // WORKING TUNEUP + ImGui::Separator(); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled(_LC("Tuning", "Working tuneup")); if (tuning_savebox_visible) { - ImGui::Separator(); - if (ImGui::SmallButton(_LC("Tuning", "Cancel"))) - { - tuning_savebox_visible = false; - } ImGui::InputText(_LC("Tuning", "Name"), tuning_savebox_buf.GetBuffer(), tuning_savebox_buf.GetCapacity()); if (ImGui::Button(_LC("Tuning","Save"))) { CreateProjectRequest* req = new CreateProjectRequest(); - req->cpr_type = tuning_savebox_overwrite - ? CreateProjectRequestType::SAVE_TUNEUP_OVERWRITE - : CreateProjectRequestType::SAVE_TUNEUP_ONLY_NEW; + req->cpr_overwrite = tuning_savebox_overwrite; + req->cpr_type = CreateProjectRequestType::SAVE_TUNEUP; + req->cpr_source_actor = tuning_actor; req->cpr_source_entry = tuning_actor->getUsedActorEntry(); req->cpr_name = tuning_savebox_buf.ToCStr(); App::GetGameContext()->PushMessage(Message(MSG_EDI_CREATE_PROJECT_REQUESTED, req)); } ImGui::SameLine(); ImGui::Checkbox(_LC("Tuning", "Overwrite"), &tuning_savebox_overwrite); + + // Cancel button (right-aligned) + std::string cancelbtn_text = _LC("Tuning", "Cancel"); + float cancelbtn_w = ImGui::CalcTextSize(cancelbtn_text.c_str()).x + ImGui::GetStyle().FramePadding.x * 2; + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - cancelbtn_w); + if (ImGui::SmallButton(_LC("Tuning", "Cancel"))) + { + tuning_savebox_visible = false; + } + ImGui::Separator(); } else { - if (ImGui::Button(_LC("Tuning", "Save current"))) + ImGui::SameLine(); + if (ImGui::Button(_LC("Tuning", "Save as..."))) { tuning_savebox_visible = true; } - } - ImGui::Separator(); + // Reset button (right-aligned) + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + std::string resetbtn_text = _LC("Tuning", "Reset"); + float delbtn_w = ImGui::CalcTextSize(resetbtn_text.c_str()).x + ImGui::GetStyle().FramePadding.x * 2; + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - delbtn_w); + ImGui::PushStyleColor(ImGuiCol_Button, TUNING_HOLDTOCONFIRM_COLOR); + bool resetbtn_pressed = ImButtonHoldToConfirm(resetbtn_text, /*small:*/false, + TUNING_HOLDTOCONFIRM_TIMELIMIT, /*[by ref]:*/tuning_holdtoconfirm_time_left, dt); + ImGui::PopStyleColor(); //ImGuiCol_Button + if (resetbtn_pressed) + { + ModifyProjectRequest* request = new ModifyProjectRequest(); + request->mpr_target_actor = tuning_actor; + request->mpr_type = ModifyProjectRequestType::PROJECT_RESET_TUNEUP; + App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, request)); + } + } // ADDONPARTS ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver); - std::string addonparts_title = fmt::format(_LC("TopMenubar", "Addon parts ({})##tuningAddons"), tuning_addonparts.cqy_results.size()); + std::string addonparts_title = fmt::format(_LC("TopMenubar", "Addon parts ({})"), tuning_addonparts.cqy_results.size()); if (ImGui::CollapsingHeader(addonparts_title.c_str())) { for (CacheQueryResult& addonpart_result: tuning_addonparts.cqy_results) { - bool used = tuning_actor->getUsedTuneup()->tuneup_def->use_addonparts.count(addonpart_result.cqr_entry->fname) > 0; + bool used = tuning_actor->getUsedTuneupEntry()->tuneup_def->use_addonparts.count(addonpart_result.cqr_entry->fname) > 0; if (ImGui::Checkbox(addonpart_result.cqr_entry->dname.c_str(), &used)) { ModifyProjectRequest* req = new ModifyProjectRequest(); @@ -1564,7 +1586,7 @@ void TopMenubar::Draw(float dt) // Draw props size_t total_props = tuneup_entry->tuneup_def->remove_props.size() + tuning_actor->GetGfxActor()->getProps().size(); - std::string props_title = fmt::format(_LC("TopMenubar", "Props ({})##tuningProps"), total_props); + std::string props_title = fmt::format(_LC("TopMenubar", "Props ({})"), total_props); if (ImGui::CollapsingHeader(props_title.c_str())) { // First draw the removed props @@ -1619,7 +1641,7 @@ void TopMenubar::Draw(float dt) // Ditto for flexbodies size_t total_flexbodies = tuneup_entry->tuneup_def->remove_flexbodies.size() + tuning_actor->GetGfxActor()->GetFlexbodies().size(); - std::string flexbodies_title = fmt::format(_LC("TopMenubar", "Flexbodies ({})##tuningFlexbodies"), total_flexbodies); + std::string flexbodies_title = fmt::format(_LC("TopMenubar", "Flexbodies ({})"), total_flexbodies); if (ImGui::CollapsingHeader(flexbodies_title.c_str())) { // Draw removed flexbodies @@ -2114,8 +2136,10 @@ void TopMenubar::GetPresets() void TopMenubar::RefreshTuningMenu() { const ActorPtr& current_actor = App::GetGameContext()->GetPlayerActor(); - if (current_actor && tuning_actor != current_actor) + if (current_actor && (tuning_actor != current_actor || tuning_force_refresh)) { + ROR_ASSERT(current_actor->getUsedActorEntry()); + tuning_addonparts.cqy_filter_type = LT_AddonPart; tuning_addonparts.cqy_filter_guid = current_actor->getUsedActorEntry()->guid; tuning_addonparts.cqy_results.clear(); @@ -2126,6 +2150,9 @@ void TopMenubar::RefreshTuningMenu() tuning_saves.cqy_filter_category_id = CID_TuneupsUser; // Exclude auto-generated entries tuning_saves.cqy_results.clear(); App::GetCacheSystem()->Query(tuning_saves); + + tuning_delbtn_cursorx_min = 0.f; } tuning_actor = current_actor; } + diff --git a/source/main/gui/panels/GUI_TopMenubar.h b/source/main/gui/panels/GUI_TopMenubar.h index 9089b1837b..b0e1f14b2f 100644 --- a/source/main/gui/panels/GUI_TopMenubar.h +++ b/source/main/gui/panels/GUI_TopMenubar.h @@ -104,8 +104,11 @@ class TopMenubar Str<200> tuning_savebox_buf; //!< Buffer for tuneup name to be saved bool tuning_savebox_visible = false; //!< User pressed 'save active' to open savebox. bool tuning_savebox_overwrite = false; //!< Status of "Overwrite?" checkbox - const float TUNING_DELETEBTN_TIMELIMIT = 2.f; //!< Delete button must be held for several sec to confirm. - float tuning_deletebtn_time_left = 0.f; //!< Delete button must be held for several sec to confirm. + const ImVec4 TUNING_HOLDTOCONFIRM_COLOR = ImVec4(0.25f, 0.15f, 0.2f, 0.5f); + const float TUNING_HOLDTOCONFIRM_TIMELIMIT = 1.5f; //!< Delete button must be held for several sec to confirm. + float tuning_holdtoconfirm_time_left = 0.f; //!< Delete button must be held for several sec to confirm. + bool tuning_force_refresh = false; + float tuning_delbtn_cursorx_min = 0.f; //!< Avoid drawing 'Delete' button over saved tuneup names. void RefreshTuningMenu(); private: @@ -113,6 +116,8 @@ class TopMenubar void DrawMpUserToActorList(RoRnet::UserInfo &user); // Multiplayer void DrawSpecialStateBox(float top_offset); + + ImVec2 m_open_menu_hoverbox_min; ImVec2 m_open_menu_hoverbox_max; TopMenu m_open_menu = TopMenu::TOPMENU_NONE; diff --git a/source/main/main.cpp b/source/main/main.cpp index ce0fa50fc2..8a585be765 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -1032,6 +1032,14 @@ int main(int argc, char *argv[]) break; } + case MSG_EDI_DELETE_PROJECT_REQUESTED: + { + CacheEntryPtr* entry_ptr = static_cast(m.payload); + App::GetCacheSystem()->DeleteProject(*entry_ptr); + delete entry_ptr; + break; + } + default:; } diff --git a/source/main/physics/Actor.cpp b/source/main/physics/Actor.cpp index 31be03edb6..84d0f49a2b 100644 --- a/source/main/physics/Actor.cpp +++ b/source/main/physics/Actor.cpp @@ -4585,20 +4585,12 @@ CacheEntryPtr& Actor::getUsedActorEntry() return m_used_actor_entry; } -CacheEntryPtr& Actor::getUsedSkin() +CacheEntryPtr& Actor::getUsedSkinEntry() { return m_used_skin_entry; } -void Actor::setUsedSkin(CacheEntryPtr& skin) -{ - m_used_skin_entry = skin; -} -CacheEntryPtr& Actor::getUsedTuneup() +CacheEntryPtr& Actor::getUsedTuneupEntry() { return m_used_tuneup_entry; -} -void Actor::setUsedTuneup(CacheEntryPtr& tuneup) -{ - m_used_tuneup_entry = tuneup; } \ No newline at end of file diff --git a/source/main/physics/Actor.h b/source/main/physics/Actor.h index dcfb14ab4b..662cfe0748 100644 --- a/source/main/physics/Actor.h +++ b/source/main/physics/Actor.h @@ -218,10 +218,8 @@ class Actor : public RefCountingObject int getInstanceId() { return ar_instance_id; } // not exported to scripting: CacheEntryPtr& getUsedActorEntry(); //!< The actor entry itself. - CacheEntryPtr& getUsedSkin(); - void setUsedSkin(CacheEntryPtr& skin); - CacheEntryPtr& getUsedTuneup(); - void setUsedTuneup(CacheEntryPtr& tuneup); + CacheEntryPtr& getUsedSkinEntry(); + CacheEntryPtr& getUsedTuneupEntry(); bool isPreloadedWithTerrain() const { return m_preloaded_with_terrain; }; std::vector getAuthors(); std::vector getDescription(); diff --git a/source/main/physics/ActorSpawner.cpp b/source/main/physics/ActorSpawner.cpp index 2de9e2f681..e6812e9c3f 100644 --- a/source/main/physics/ActorSpawner.cpp +++ b/source/main/physics/ActorSpawner.cpp @@ -1559,7 +1559,7 @@ void ActorSpawner::ProcessMinimass(RigDef::Minimass & def) void ActorSpawner::ProcessProp(RigDef::Prop & def) { // Check if removed via .tuneup - CacheEntryPtr& tuneup_entry = m_actor->getUsedTuneup(); + CacheEntryPtr& tuneup_entry = m_actor->getUsedTuneupEntry(); if (tuneup_entry && tuneup_entry->tuneup_def->remove_props.find(def.mesh_name) != tuneup_entry->tuneup_def->remove_props.end()) { LOG(fmt::format("{}: Prop '{}' removed by tuneup '{}'", m_actor->ar_filename, def.mesh_name, tuneup_entry->fname)); diff --git a/source/main/resources/CacheSystem.cpp b/source/main/resources/CacheSystem.cpp index e57cea564b..be7e46264f 100644 --- a/source/main/resources/CacheSystem.cpp +++ b/source/main/resources/CacheSystem.cpp @@ -106,6 +106,16 @@ CacheEntry::~CacheEntry() // Destructs `TuneupDefPtr` which is a `RefCountingObjectPtr<>` so it doesn't compile without `#include "TuneupFileFormat.h"` and thus should not be in header. } +CreateProjectRequest::CreateProjectRequest() +{ + // Constructs `ActorPtr` - doesn't compile without `#include Actor.h` - not pretty if in header (even if auto-generated by C++). +} + +CreateProjectRequest::~CreateProjectRequest() +{ + // Destructs `ActorPtr` - doesn't compile without `#include Actor.h` - not pretty if in header (even if auto-generated by C++). +} + CacheQueryResult::CacheQueryResult(CacheEntryPtr entry, size_t score): cqr_entry(entry), cqr_score(score) @@ -1477,7 +1487,7 @@ CacheEntryPtr CacheSystem::CreateProject(CreateProjectRequest* request) // Create subfolder std::string project_path = PathCombine(App::sys_projects_dir->getStr(), request->cpr_name); - if (FolderExists(project_path)) + if (FolderExists(project_path) && !request->cpr_overwrite) { App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, fmt::format(_LC("CacheSystem", "Project directory '{}' already exists!"), request->cpr_name)); @@ -1498,7 +1508,12 @@ CacheEntryPtr CacheSystem::CreateProject(CreateProjectRequest* request) { project_entry->fext = "tuneup"; // Tell modcache what it is. project_entry->categoryid = CID_TuneupsAuto; // For auto-loading on future spawns of the vehicle. - project_entry->description = request->cpr_description; + project_entry->guid = request->cpr_source_entry->guid; // For lookup of tuneups by vehicle GUID. + } + else if (request->cpr_type == CreateProjectRequestType::SAVE_TUNEUP) + { + project_entry->fext = "tuneup"; // Tell modcache what it is. + project_entry->categoryid = CID_TuneupsUser; // For display in modcache project_entry->guid = request->cpr_source_entry->guid; // For lookup of tuneups by vehicle GUID. } else @@ -1511,20 +1526,34 @@ CacheEntryPtr CacheSystem::CreateProject(CreateProjectRequest* request) project_entry->resource_bundle_path = project_path; // Tell modcache where to load it from. project_entry->fname = fmt::format("{}.{}", request->cpr_name, project_entry->fext); // Compose target mod filename project_entry->dname = request->cpr_name; + project_entry->description = request->cpr_description; project_entry->number = static_cast(m_entries.size() + 1); // Let's number mods from 1 this->LoadResource(project_entry); // This fills `entry.resource_group` - if (request->cpr_type == CreateProjectRequestType::CREATE_TUNEUP) + if (request->cpr_type == CreateProjectRequestType::CREATE_TUNEUP || + request->cpr_type == CreateProjectRequestType::SAVE_TUNEUP) { // Tuneup projects don't contain any media, just the .tuneup file which lists addonparts to use. // Prepare the .tuneup document - TuneupDefPtr tuneup = new TuneupDef(); + TuneupDefPtr tuneup; + if (request->cpr_type == CreateProjectRequestType::SAVE_TUNEUP) + { + ROR_ASSERT(request->cpr_source_actor); + ROR_ASSERT(request->cpr_source_actor->getUsedTuneupEntry()); + tuneup = request->cpr_source_actor->getUsedTuneupEntry()->tuneup_def->clone(); + } + else + { + tuneup = new TuneupDef(); + } tuneup->guid = request->cpr_source_entry->guid; // For lookup of tuneups by vehicle GUID. tuneup->name = request->cpr_name; tuneup->description = request->cpr_description; tuneup->thumbnail = request->cpr_source_entry->filecachename; - tuneup->category_id = CID_TuneupsAuto; + tuneup->category_id = (CacheCategoryId)project_entry->categoryid; + + // Write out the .tuneup file. Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(project_entry->fname, project_entry->resource_group); @@ -1532,6 +1561,13 @@ CacheEntryPtr CacheSystem::CreateProject(CreateProjectRequest* request) // Attach the document to the entry in memory project_entry->tuneup_def = tuneup; + + // In the likely case this was invoked from TopMenubarUI, update it. + if (App::GetGuiManager()->TopMenubar.tuning_savebox_visible) + { + App::GetGuiManager()->TopMenubar.tuning_savebox_visible = false; + App::GetGuiManager()->TopMenubar.tuning_actor = nullptr; // Force refresh + } } else { @@ -1613,16 +1649,10 @@ CacheEntryPtr CacheSystem::CreateProject(CreateProjectRequest* request) void CacheSystem::ModifyProject(ModifyProjectRequest* request) { - CacheEntryPtr actor_entry = this->FindEntryByFilename(LT_AllBeam, /*partial=*/false, request->mpr_target_actor->getTruckFileName()); - if (!actor_entry) - { - Str<500> msg; msg <<"Cannot modify project; actor '" << request->mpr_subject << "' not found in ModCache."; - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_ERROR, msg.ToCStr()); - return; - } - - CacheEntryPtr tuneup_entry = request->mpr_target_actor->getUsedTuneup(); + ROR_ASSERT(request->mpr_target_actor); + CacheEntryPtr tuneup_entry = request->mpr_target_actor->getUsedTuneupEntry(); ROR_ASSERT(tuneup_entry); + ROR_ASSERT(tuneup_entry->tuneup_def); switch (request->mpr_type) { @@ -1656,13 +1686,43 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request) tuneup_entry->tuneup_def->remove_flexbodies.erase(request->mpr_subject); break; + case ModifyProjectRequestType::PROJECT_LOAD_TUNEUP: + { + // Instead of loading with the saved tuneup directly, keep the autogenerated and sync it with the save. + // That way, subsequent editing doesn't modify the save until user saves again. + CacheEntryPtr save_entry = App::GetCacheSystem()->FindEntryByFilename(LT_Tuneup, /*partial:*/false, request->mpr_subject); + if (!save_entry) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format(_LC("CacheSystem", "Error loading tuneup: file '{}', not found in mod cache"), request->mpr_subject)); + return; + } + this->LoadResource(save_entry); + ROR_ASSERT(save_entry->tuneup_def); + + tuneup_entry->tuneup_def->remove_flexbodies.clear(); + tuneup_entry->tuneup_def->remove_flexbodies = save_entry->tuneup_def->remove_flexbodies; + tuneup_entry->tuneup_def->remove_props.clear(); + tuneup_entry->tuneup_def->remove_props = save_entry->tuneup_def->remove_props; + tuneup_entry->tuneup_def->use_addonparts.clear(); + tuneup_entry->tuneup_def->use_addonparts = save_entry->tuneup_def->use_addonparts; + break; + } + + case ModifyProjectRequestType::PROJECT_RESET_TUNEUP: + tuneup_entry->tuneup_def->remove_flexbodies.clear(); + tuneup_entry->tuneup_def->remove_props.clear(); + tuneup_entry->tuneup_def->use_addonparts.clear(); + break; + default: break; } - // If this is the auto-generated tuneup, immediatelly update the .tuneup file (user-saved tuneups are only modified on demand). + if (tuneup_entry->categoryid == CID_TuneupsAuto) { + // For auto-generated entries, immediatelly update the .tuneup file (user-saved tuneups are only modified on demand). Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(tuneup_entry->fname, tuneup_entry->resource_group, /*overwrite:*/true); Str<200> header; header @@ -1678,9 +1738,9 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request) srq->asr_position = Ogre::Vector3(request->mpr_target_actor->getPosition().x, request->mpr_target_actor->getMinHeight(), request->mpr_target_actor->getPosition().z); srq->asr_rotation = Ogre::Quaternion(Ogre::Degree(270) - Ogre::Radian(request->mpr_target_actor->getRotation()), Ogre::Vector3::UNIT_Y); srq->asr_config = request->mpr_target_actor->getSectionConfig(); - srq->asr_skin_entry = request->mpr_target_actor->getUsedSkin(); + srq->asr_skin_entry = request->mpr_target_actor->getUsedSkinEntry(); srq->asr_tuneup_entry = tuneup_entry; - srq->asr_cache_entry = actor_entry; + srq->asr_cache_entry = request->mpr_target_actor->getUsedActorEntry(); srq->asr_debugview = (int)request->mpr_target_actor->GetGfxActor()->GetDebugView(); srq->asr_origin = ActorSpawnRequest::Origin::USER; @@ -1691,6 +1751,36 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request) App::GetGameContext()->ChainMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)srq)); } +void CacheSystem::DeleteProject(CacheEntryPtr& entry) +{ + this->LoadResource(entry); + + // Delete the files, one by one + Ogre::FileInfoListPtr filelist = Ogre::ResourceGroupManager::getSingleton().findResourceFileInfo(entry->resource_group, "*.*"); + for (size_t i = 0; i < filelist->size(); i++) + { + Ogre::FileInfo fileinfo = filelist->at(i); + if (!Ogre::FileSystemLayer::removeFile(fileinfo.path + fileinfo.filename)) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format(_LC("CacheSystem", "Problem deleting project '{}' - could not delete file '{}'"), entry->fname, fileinfo.filename)); + } + } + + // Delete the directory itself + if (!Ogre::FileSystemLayer::removeFile(entry->resource_bundle_path)) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format(_LC("CacheSystem", "Problem deleting project '{}' - could not delete directory '{}'"), entry->fname, entry->resource_bundle_path)); + } + + // Remove the entry + RoR::EraseIf(m_entries, [entry](CacheEntryPtr& e) { return e == entry; }); + + // Force update of Tuning menu in TopMenubarUI. + App::GetGuiManager()->TopMenubar.tuning_actor = nullptr; +} + size_t CacheSystem::Query(CacheQuery& query) { Ogre::StringUtil::toLowerCase(query.cqy_search_string); diff --git a/source/main/resources/CacheSystem.h b/source/main/resources/CacheSystem.h index 49c32f1e33..0a68d7a57c 100644 --- a/source/main/resources/CacheSystem.h +++ b/source/main/resources/CacheSystem.h @@ -196,17 +196,21 @@ enum class CreateProjectRequestType NONE, DEFAULT, //!< Copy files from source mod. Source mod Determines mod file extension. CREATE_TUNEUP, //!< Overrides project type to "tuneup", adds a blank .tuneup file with `CID_TuneupAuto`. - SAVE_TUNEUP_ONLY_NEW, //!< Dumps .tuneup file with `CID_TuneupUser` from source actor, will not overwrite existing. - SAVE_TUNEUP_OVERWRITE, //!< Dumps .tuneup file with `CID_TuneupUser` from source actor. + SAVE_TUNEUP, //!< Dumps .tuneup file with `CID_TuneupUser` from source actor, will not overwrite existing. }; /// Creates subdirectory in 'My Games\Rigs of Rods\projects', pre-populates it with files and adds modcache entry. struct CreateProjectRequest { + CreateProjectRequest(); + ~CreateProjectRequest(); + std::string cpr_name; //!< Directory and also the mod file (without extension). std::string cpr_description; //!< Optional, implemented for tuneups. - CacheEntryPtr cpr_source_entry; //!< The original mod to copy files from. - CreateProjectRequestType cpr_type = CreateProjectRequestType::NONE; + CacheEntryPtr cpr_source_entry; //!< The original mod to copy files from. + ActorPtr cpr_source_actor; //!< Only for type `SAVE_TUNEUP` + CreateProjectRequestType cpr_type = CreateProjectRequestType::NONE; + bool cpr_overwrite; }; enum class ModifyProjectRequestType @@ -219,6 +223,7 @@ enum class ModifyProjectRequestType TUNEUP_REMOVE_FLEXBODY_SET, //!< 'subject' is mesh name. TUNEUP_REMOVE_FLEXBODY_RESET,//!< 'subject' is mesh name. PROJECT_LOAD_TUNEUP, //!< 'subject' is tuneup filename. This overwrites the auto-generated tuneup with the save. + PROJECT_RESET_TUNEUP, //!< 'subject' is empty. This resets the auto-generated tuneup to orig. values. }; struct ModifyProjectRequest @@ -271,14 +276,20 @@ class CacheSystem void UnLoadResource(CacheEntryPtr& t); //!< Unloads associated bundle, destroying all spawned actors. /// @} + /// @name Loading + /// @{ + CacheEntryPtr CreateProject(CreateProjectRequest* request); //!< Creates subdirectory in 'My Games\Rigs of Rods\projects', pre-populates it with files and adds modcache entry. + void ModifyProject(ModifyProjectRequest* request); + void DeleteProject(CacheEntryPtr& entry); + /// @} + const std::vector &GetEntries() const { return m_entries; } const CategoryIdNameMap &GetCategories() const { return m_categories; } Ogre::String GetPrettyName(Ogre::String fname); std::string ActorTypeToName(ActorType driveable); - CacheEntryPtr CreateProject(CreateProjectRequest* request); //!< Creates subdirectory in 'My Games\Rigs of Rods\projects', pre-populates it with files and adds modcache entry. - void ModifyProject(ModifyProjectRequest* request); + private: diff --git a/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp b/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp index 8e66a1d297..c58c7c43da 100644 --- a/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp +++ b/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp @@ -33,6 +33,24 @@ using namespace RoR; +TuneupDefPtr TuneupDef::clone() +{ + TuneupDefPtr ret = new TuneupDef(); + + ret->use_addonparts = this->use_addonparts ; //std::set + ret->remove_props = this->remove_props ; //std::set + ret->remove_flexbodies = this->remove_flexbodies ; //std::set + ret->name = this->name ; //std::string + ret->guid = this->guid ; //std::string + ret->thumbnail = this->thumbnail ; //std::string + ret->description = this->description ; //std::string + ret->author_name = this->author_name ; //std::string + ret->author_id = this->author_id ; //int + ret->category_id = this->category_id ; //CacheCategoryId + + return ret; +} + std::vector RoR::TuneupParser::ParseTuneups(Ogre::DataStreamPtr& stream) { std::vector result; diff --git a/source/main/resources/tuneup_fileformat/TuneupFileFormat.h b/source/main/resources/tuneup_fileformat/TuneupFileFormat.h index 3a94ca4da7..d701311bf0 100644 --- a/source/main/resources/tuneup_fileformat/TuneupFileFormat.h +++ b/source/main/resources/tuneup_fileformat/TuneupFileFormat.h @@ -35,6 +35,8 @@ namespace RoR { struct TuneupDef: public RefCountingObject { + TuneupDefPtr clone(); + std::set use_addonparts; //!< Addonpart filenames std::set remove_props; //!< Mesh names of props to be removed. std::set remove_flexbodies; //!< Mesh names of flexbodies to be removed.