From a3f6db5abf34104504c09a8e254ea3c071eac0e1 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Sun, 29 Oct 2023 13:00:45 +0100 Subject: [PATCH] :envelope: `MSG_EDI_MODIFY_PROJECT_REQUESTED` and UI. This EDI message now hosts all tuneup edits. See enum `ModifyProjectRequestType` in CacheSystem.h. The ActorModifyRequest types INSTALL/REMOVE_ADDONPART_AND_RELOAD were replaced by ModifyProjectRequest types TUNEUP_USE_ADDONPART_SET/RESET. The TopMenubar/Tuning UI now has list of props with checkboxes to toggle them. The toggle immediatelly modifies the tuneup and respawns the actor. All tuneup edit types are handled by `CacheSystem::ModifyProject()`. The code was moved from `GameContext::ModifyActor()`; --- source/main/Application.h | 1 + source/main/GameContext.cpp | 60 +----------- source/main/gui/panels/GUI_TopMenubar.cpp | 93 +++++++++++++------ source/main/main.cpp | 8 ++ source/main/physics/ActorSpawner.cpp | 5 +- source/main/physics/SimData.h | 2 - source/main/resources/CacheSystem.cpp | 82 +++++++++++++++- source/main/resources/CacheSystem.h | 17 ++++ .../tuneup_fileformat/TuneupFileFormat.cpp | 20 ++-- 9 files changed, 191 insertions(+), 97 deletions(-) diff --git a/source/main/Application.h b/source/main/Application.h index 79156272f4..668d2c1931 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -123,6 +123,7 @@ enum MsgType MSG_EDI_RELOAD_BUNDLE_REQUESTED, //!< Payload = RoR::CacheEntryPtr* (owner) MSG_EDI_UNLOAD_BUNDLE_REQUESTED, //!< Payload = RoR::CacheEntryPtr* (owner) MSG_EDI_CREATE_PROJECT_REQUESTED, //!< Payload = RoR::CreateProjectRequest* (owner) + MSG_EDI_MODIFY_PROJECT_REQUESTED, //!< Payload = RoR::UpdateProjectRequest* (owner) }; const char* MsgTypeToString(MsgType type); diff --git a/source/main/GameContext.cpp b/source/main/GameContext.cpp index ea4bd36d92..a415e0143f 100644 --- a/source/main/GameContext.cpp +++ b/source/main/GameContext.cpp @@ -397,56 +397,6 @@ void GameContext::ModifyActor(ActorModifyRequest& rq) // Load our actor again, but only after all actors are deleted. this->ChainMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)srq)); } - else if (rq.amr_type == ActorModifyRequest::Type::INSTALL_ADDONPART_AND_RELOAD) - { - CacheEntryPtr entry = App::GetCacheSystem()->FindEntryByFilename(LT_AllBeam, /*partial=*/false, actor->ar_filename); - if (!entry) - { - Str<500> msg; msg <<"Cannot reload vehicle; file '" << actor->ar_filename << "' not found in ModCache."; - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_ERROR, msg.ToCStr()); - return; - } - - // Make sure the actor has a default .tuneup project assigned. If not, create it. - CacheEntryPtr tuneup_entry = actor->getUsedTuneup(); - if (!tuneup_entry) - { - CreateProjectRequest req; - req.cpr_create_tuneup = true; - req.cpr_source_entry = entry; - req.cpr_name = fmt::format("Tuned {}", actor->getTruckName()); - - tuneup_entry = App::GetCacheSystem()->CreateProject(&req); - } - - // Add the requested addonpart to the TuneupDef document. - tuneup_entry->tuneup_def->use_addonparts.push_back(rq.amr_addonpart->fname); - - // 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) - { - Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().openResource(tuneup_entry->fname, tuneup_entry->resource_group); - RoR::TuneupParser::ExportTuneup(datastream, tuneup_entry->tuneup_def); - } - - // Create spawn request while actor still exists - // Note we don't use `ActorModifyRequest::Type::RELOAD` because we don't need the bundle reloaded. - ActorSpawnRequest* srq = new ActorSpawnRequest; - 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 = tuneup_entry; - srq->asr_cache_entry = entry; - srq->asr_debugview = (int)actor->GetGfxActor()->GetDebugView(); - srq->asr_origin = ActorSpawnRequest::Origin::USER; - - // Remove the actor - this->PushMessage(Message(MSG_SIM_DELETE_ACTOR_REQUESTED, (void*)new ActorPtr(actor))); - - // Load our actor again, but only after it was deleted. - this->ChainMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)srq)); - } } void GameContext::DeleteActor(ActorPtr actor) @@ -735,11 +685,11 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntryPtr entry, std::st case LT_AddonPart: if (m_player_actor) { - ActorModifyRequest* req = new ActorModifyRequest(); - req->amr_actor = m_player_actor->ar_instance_id; - req->amr_addonpart = entry; - req->amr_type = ActorModifyRequest::Type::INSTALL_ADDONPART_AND_RELOAD; - this->PushMessage(Message(MSG_SIM_MODIFY_ACTOR_REQUESTED, req)); + ModifyProjectRequest* req = new ModifyProjectRequest(); + req->mpr_type = ModifyProjectRequestType::TUNEUP_USE_ADDONPART_SET; + req->mpr_subject = entry->fname; + req->mpr_target_actor = m_player_actor; + App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); } break; diff --git a/source/main/gui/panels/GUI_TopMenubar.cpp b/source/main/gui/panels/GUI_TopMenubar.cpp index c3d5b154a6..5ed4fa3c5c 100644 --- a/source/main/gui/panels/GUI_TopMenubar.cpp +++ b/source/main/gui/panels/GUI_TopMenubar.cpp @@ -1458,7 +1458,6 @@ void TopMenubar::Update() else { ImGui::TextDisabled(_LC("TopMenubar", "Used parts:")); - std::string remove_addonpart; for (const std::string& addonpart: tuneup_entry->tuneup_def->use_addonparts) { ImGui::PushID(addonpart.c_str()); @@ -1466,7 +1465,11 @@ void TopMenubar::Update() ImGui::PushStyleColor(ImGuiCol_Text, RED_TEXT); if (ImGui::Button(" X ")) { - remove_addonpart = addonpart; + ModifyProjectRequest* req = new ModifyProjectRequest(); + req->mpr_type = ModifyProjectRequestType::TUNEUP_USE_ADDONPART_RESET; + req->mpr_subject = addonpart; + req->mpr_target_actor = actor; + App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); } ImGui::PopStyleColor(); ImGui::SameLine(); @@ -1474,43 +1477,73 @@ void TopMenubar::Update() ImGui::PopID(); // addonpart.c_str() } + } - if (remove_addonpart != "") + if (ImGui::Button(_LC("TopMenubar", "Select parts"))) + { + CacheEntryPtr actor_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AllBeam, /*partial:*/false, actor->getTruckFileName()); + if (actor_entry && !actor_entry->deleted) { - ActorModifyRequest* req = new ActorModifyRequest(); - req->amr_actor = actor; - req->amr_type = ActorModifyRequest::Type::REMOVE_ADDONPART_AND_RELOAD; - req->amr_addonpart_fname = remove_addonpart; - req->amr_addonpart = App::GetCacheSystem()->FindEntryByFilename(LT_AddonPart, /*partial:*/false, remove_addonpart); - if (req->amr_addonpart) - { - App::GetGameContext()->PushMessage(Message(MSG_SIM_MODIFY_ACTOR_REQUESTED, req)); - } - else - { - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING, - fmt::format(_LC("TopMenubar", "Addon part '{}' not found (likely uninstalled). Removing from the part list."))); - } + Message m(MSG_GUI_OPEN_SELECTOR_REQUESTED); + m.payload = new LoaderType(LT_AddonPart); + m.description = actor_entry->guid; + App::GetGameContext()->PushMessage(m); + } + else + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format(_LC("TopMenubar", "Cannot add parts to '{}' - No valid mod cache entry"), actor->getTruckName())); } } - } - ImGui::Separator(); + ImGui::Separator(); - if (ImGui::Button(_LC("TopMenubar", "Select parts"))) - { - CacheEntryPtr actor_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AllBeam, /*partial:*/false, actor->getTruckFileName()); - if (actor_entry && !actor_entry->deleted) + ImGui::TextDisabled(_LC("TopMenubar", "Default props:")); + // First draw the removed props + for (const std::string& meshname: tuneup_entry->tuneup_def->remove_props) { - Message m(MSG_GUI_OPEN_SELECTOR_REQUESTED); - m.payload = new LoaderType(LT_AddonPart); - m.description = actor_entry->guid; - App::GetGameContext()->PushMessage(m); + ImGui::PushID(meshname.c_str()); + + bool propEnabled = false; + if (ImGui::Checkbox(meshname.c_str(), &propEnabled)) + { + ModifyProjectRequest* req = new ModifyProjectRequest(); + req->mpr_type = ModifyProjectRequestType::TUNEUP_REMOVE_PROP_RESET; + req->mpr_subject = meshname; + req->mpr_target_actor = actor; + App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); + } + + ImGui::PopID(); // meshname } - else + // Then draw existing props (skip aero navlights and dashboard frankenprop) + for (Prop const& p: actor->GetGfxActor()->getProps()) { - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, - fmt::format(_LC("TopMenubar", "Cannot add parts to '{}' - No valid mod cache entry"), actor->getTruckName())); + if (p.pp_beacon_type == 'L' || p.pp_beacon_type == 'R' || p.pp_beacon_type == 'w') + { + continue; // skip special prop - aerial nav light + } + else if (p.pp_wheel_mesh_obj) + { + continue; // skip special prop: dashboard + dirwheel + } + else if (p.pp_mesh_obj->getLoadedMesh()) // Skip broken props + { + std::string meshname = p.pp_mesh_obj->getLoadedMesh()->getName(); + ImGui::PushID(meshname.c_str()); + + bool propEnabled = true; + if (ImGui::Checkbox(meshname.c_str(), &propEnabled)) + { + ModifyProjectRequest* req = new ModifyProjectRequest(); + req->mpr_type = ModifyProjectRequestType::TUNEUP_REMOVE_PROP_SET; + req->mpr_subject = meshname; + req->mpr_target_actor = actor; + App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); + } + + ImGui::PopID(); // meshname + } } } } diff --git a/source/main/main.cpp b/source/main/main.cpp index 916c5af3b5..16b26158bf 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -1023,6 +1023,14 @@ int main(int argc, char *argv[]) break; } + case MSG_EDI_MODIFY_PROJECT_REQUESTED: + { + ModifyProjectRequest* request = static_cast(m.payload); + App::GetCacheSystem()->ModifyProject(request); + delete request; + break; + } + default:; } diff --git a/source/main/physics/ActorSpawner.cpp b/source/main/physics/ActorSpawner.cpp index 4d40d84903..410520405b 100644 --- a/source/main/physics/ActorSpawner.cpp +++ b/source/main/physics/ActorSpawner.cpp @@ -56,6 +56,7 @@ #include "GfxScene.h" #include "Console.h" #include "InputEngine.h" +#include "Language.h" #include "MeshObject.h" #include "PointColDetector.h" #include "Renderdash.h" @@ -129,12 +130,12 @@ void ActorSpawner::ConfigureAddonParts(CacheEntryPtr& tuneup_entry) } else { - this->AddMessage(Message::TYPE_WARNING, fmt::format(_L("Could not load addon part '{}' (file '{}')", addonpart_entry->dname, addonpart_entry->fname))); + this->AddMessage(Message::TYPE_WARNING, fmt::format(_L("Could not load addon part '{}' (file '{}')"), addonpart_entry->dname, addonpart_entry->fname)); } } else { - this->AddMessage(Message::TYPE_WARNING, fmt::format(_L("Requested addon part '{}' is not installed", addonpart_entry->dname, addonpart_entry->fname))); + this->AddMessage(Message::TYPE_WARNING, fmt::format(_L("Requested addon part '{}' is not installed"), addonpart_entry->dname, addonpart_entry->fname)); } } } diff --git a/source/main/physics/SimData.h b/source/main/physics/SimData.h index ad0790ba5b..cb4c9eb9f6 100644 --- a/source/main/physics/SimData.h +++ b/source/main/physics/SimData.h @@ -837,8 +837,6 @@ struct ActorModifyRequest SOFT_RESET, RESTORE_SAVED, WAKE_UP, - INSTALL_ADDONPART_AND_RELOAD, - REMOVE_ADDONPART_AND_RELOAD, }; ActorInstanceID_t amr_actor = ACTORINSTANCEID_INVALID;// not ActorPtr because it's not thread-safe diff --git a/source/main/resources/CacheSystem.cpp b/source/main/resources/CacheSystem.cpp index dd523aca3a..d2af094903 100644 --- a/source/main/resources/CacheSystem.cpp +++ b/source/main/resources/CacheSystem.cpp @@ -25,7 +25,7 @@ #include "CacheSystem.h" -#include +#include "Actor.h" #include "Application.h" #include "SimData.h" #include "ContentManager.h" @@ -46,6 +46,7 @@ #include "TuneupFileFormat.h" #include "Utils.h" +#include #include #include #include @@ -1593,6 +1594,85 @@ 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; + } + + // Make sure the actor has a default .tuneup project assigned. If not, create it. + CacheEntryPtr tuneup_entry = request->mpr_target_actor->getUsedTuneup(); + if (!tuneup_entry) + { + CreateProjectRequest req; + req.cpr_create_tuneup = true; + req.cpr_source_entry = actor_entry; + req.cpr_name = fmt::format("Tuned {}", request->mpr_target_actor->getTruckName()); + + tuneup_entry = App::GetCacheSystem()->CreateProject(&req); // Do it synchronously + } + + switch (request->mpr_type) + { + case ModifyProjectRequestType::TUNEUP_USE_ADDONPART_SET: + // Add the addonpart to the TuneupDef document. + tuneup_entry->tuneup_def->use_addonparts.push_back(request->mpr_subject); + break; + + case ModifyProjectRequestType::TUNEUP_USE_ADDONPART_RESET: + // Erase the addonpart from the TuneupDef document. + RoR::EraseIf(tuneup_entry->tuneup_def->use_addonparts, [request](const std::string& name) { return name == request->mpr_subject; }); + break; + + case ModifyProjectRequestType::TUNEUP_REMOVE_PROP_SET: + // Add the prop to the 'remove_props' in TuneupDef document. + tuneup_entry->tuneup_def->remove_props.insert(request->mpr_subject); + break; + + case ModifyProjectRequestType::TUNEUP_REMOVE_PROP_RESET: + // Erase the prop from 'remove_props' in the TuneupDef document. + tuneup_entry->tuneup_def->remove_props.erase(request->mpr_subject); + 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) + { + Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(tuneup_entry->fname, tuneup_entry->resource_group, /*overwrite:*/true); + Str<200> header; + header + << "// This is a '.tuneup' mod - it's similar to '.skin' mod but deals with '.addonpart' mods.\n" + << "// See https://github.com/RigsOfRods/rigs-of-rods/pull/3096#issuecomment-1783976601\n\n"; + datastream->write(header.GetBuffer(), header.GetLength()); + RoR::TuneupParser::ExportTuneup(datastream, tuneup_entry->tuneup_def); + } + + // Create spawn request while actor still exists + // Note we don't use `ActorModifyRequest::Type::RELOAD` because we don't need the bundle reloaded. + ActorSpawnRequest* srq = new ActorSpawnRequest; + 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_tuneup_entry = tuneup_entry; + srq->asr_cache_entry = actor_entry; + srq->asr_debugview = (int)request->mpr_target_actor->GetGfxActor()->GetDebugView(); + srq->asr_origin = ActorSpawnRequest::Origin::USER; + + // Remove the actor + App::GetGameContext()->PushMessage(Message(MSG_SIM_DELETE_ACTOR_REQUESTED, (void*)new ActorPtr(request->mpr_target_actor))); + + // Load our actor again, but only after it was deleted. + App::GetGameContext()->ChainMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)srq)); +} + 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 a6e3a333f0..32087769c6 100644 --- a/source/main/resources/CacheSystem.h +++ b/source/main/resources/CacheSystem.h @@ -199,6 +199,22 @@ struct CreateProjectRequest bool cpr_create_tuneup; //!< Overrides project type to "tuneup", adds a .tuneup file only. }; +enum class ModifyProjectRequestType +{ + NONE, + TUNEUP_USE_ADDONPART_SET, + TUNEUP_USE_ADDONPART_RESET, + TUNEUP_REMOVE_PROP_SET, + TUNEUP_REMOVE_PROP_RESET, +}; + +struct ModifyProjectRequest +{ + ActorPtr mpr_target_actor; + ModifyProjectRequestType mpr_type = ModifyProjectRequestType::NONE; + std::string mpr_subject; +}; + /// A content database /// MOTIVATION: /// RoR users usually have A LOT of content installed. Traversing it all on every game startup would be a pain. @@ -249,6 +265,7 @@ class CacheSystem 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 6d51557930..07664cde9f 100644 --- a/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp +++ b/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp @@ -118,6 +118,13 @@ void RoR::TuneupParser::ExportTuneup(Ogre::DataStreamPtr& stream, TuneupDefPtr& Str<2000> buf; buf << tuneup->name << "\n"; buf << "{\n"; + buf << "\tpreview = " << tuneup->thumbnail << "\n"; + buf << "\tdescription = " << tuneup->description << "\n"; + buf << "\tauthor_name = " << tuneup->author_name << "\n"; + buf << "\tauthor_id = " << tuneup->author_id << "\n"; + buf << "\tcategory_id = " << (int)tuneup->category_id << "\n"; + buf << "\tguid = " << tuneup->guid << "\n"; + buf << "\n"; for (std::string& addonpart: tuneup->use_addonparts) { buf << "\tuse_addonpart = " << addonpart << "\n"; @@ -126,13 +133,12 @@ void RoR::TuneupParser::ExportTuneup(Ogre::DataStreamPtr& stream, TuneupDefPtr& { buf << "\tremove_prop = " << remove_prop << "\n"; } - buf << "\tpreview = " << tuneup->thumbnail << "\n"; - buf << "\tdescription = " << tuneup->description << "\n"; - buf << "\tauthor_name = " << tuneup->author_name << "\n"; - buf << "\tauthor_id = " << tuneup->author_id << "\n"; - buf << "\tcategory_id = " << (int)tuneup->category_id << "\n"; - buf << "\tguid = " << tuneup->guid << "\n"; buf << "}\n\n"; - stream->write(buf.GetBuffer(), buf.GetLength()); + size_t written = stream->write(buf.GetBuffer(), buf.GetLength()); + if (written < buf.GetLength()) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Error writing file '{}': only written {}/{} bytes", stream->getName(), written, buf.GetLength())); + } }