diff --git a/source/main/Application.h b/source/main/Application.h index e7cac5ace9..b34aee3962 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -126,6 +126,7 @@ enum MsgType MSG_GUI_SHOW_MESSAGE_BOX_REQUESTED, //!< Payload = MessageBoxConfig* (owner) MSG_GUI_DOWNLOAD_PROGRESS, MSG_GUI_DOWNLOAD_FINISHED, + MSG_GUI_REFRESH_TUNING_MENU_REQUESTED, // Editing MSG_EDI_MODIFY_GROUNDMODEL_REQUESTED, //!< Payload = RoR::ground_model_t* (weak) MSG_EDI_ENTER_TERRN_EDITOR_REQUESTED, diff --git a/source/main/ForwardDeclarations.h b/source/main/ForwardDeclarations.h index 941e9712b3..6341becfd1 100644 --- a/source/main/ForwardDeclarations.h +++ b/source/main/ForwardDeclarations.h @@ -61,6 +61,12 @@ namespace RoR typedef int FlexbodyID_t; //!< Index to `GfxActor::m_flexbodies`, `use RoR::FLEXBODYID_INVALID` as empty value static const FlexbodyID_t FLEXBODYID_INVALID = -1; + typedef int FlareID_t; //!< Index into `Actor::ar_flares`, use `RoR::FLAREID_INVALID` as empty value + static const FlareID_t FLAREID_INVALID = -1; + + typedef int ExhaustID_t; //!< Index into `Actor::exhausts`, use `RoR::EXHAUSTID_INVALID` as empty value + static const ExhaustID_t EXHAUSTID_INVALID = -1; + class Actor; class ActorManager; class ActorSpawner; diff --git a/source/main/GameContext.cpp b/source/main/GameContext.cpp index ed4cb3d44d..3c3cb5dc6d 100644 --- a/source/main/GameContext.cpp +++ b/source/main/GameContext.cpp @@ -243,7 +243,7 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq) } } - if (App::sim_tuning_enabled->getBool()) + if (App::sim_tuning_enabled->getBool() && (App::mp_state->getEnum() != MpState::CONNECTED)) { if (rq.asr_tuneup_entry) { @@ -272,6 +272,7 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq) #endif //SOCKETW ActorPtr fresh_actor = m_actor_manager.CreateNewActor(rq, def); + bool fresh_actor_seat_player = false; // lock slide nodes after spawning the actor? if (def->slide_nodes_connect_instantly) @@ -284,7 +285,7 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq) m_last_spawned_actor = fresh_actor; if (fresh_actor->ar_driveable != NOT_DRIVEABLE) { - this->PushMessage(Message(MSG_SIM_SEAT_PLAYER_REQUESTED, static_cast(new ActorPtr(fresh_actor)))); + fresh_actor_seat_player = true; } if (rq.asr_spawnbox == nullptr) { @@ -298,13 +299,7 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq) fresh_actor->ar_num_nodes > 0 && App::diag_preset_veh_enter->getBool()) { - this->PushMessage(Message(MSG_SIM_SEAT_PLAYER_REQUESTED, static_cast(new ActorPtr(fresh_actor)))); - } - if (fresh_actor->ar_driveable != NOT_DRIVEABLE && - fresh_actor->ar_num_nodes > 0 && - App::cli_preset_veh_enter->getBool()) - { - this->PushMessage(Message(MSG_SIM_SEAT_PLAYER_REQUESTED, static_cast(new ActorPtr(fresh_actor)))); + fresh_actor_seat_player = true; } } else if (rq.asr_origin == ActorSpawnRequest::Origin::TERRN_DEF) @@ -347,10 +342,17 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq) rq.asr_origin != ActorSpawnRequest::Origin::NETWORK && rq.asr_enter) { - this->PushMessage(Message(MSG_SIM_SEAT_PLAYER_REQUESTED, static_cast(new ActorPtr(fresh_actor)))); + fresh_actor_seat_player = true; } } + if (fresh_actor_seat_player) + { + this->PushMessage(Message(MSG_SIM_SEAT_PLAYER_REQUESTED, new ActorPtr(fresh_actor))); + // Loading all addonparts to resolve conflicts is slow and would cause huge UI lag if not forced now. Do it right after player was seated in the actor. + this->ChainMessage(Message(MSG_GUI_REFRESH_TUNING_MENU_REQUESTED)); + } + return fresh_actor; } diff --git a/source/main/gfx/GfxActor.cpp b/source/main/gfx/GfxActor.cpp index a9b389f101..e8514ce0e1 100644 --- a/source/main/gfx/GfxActor.cpp +++ b/source/main/gfx/GfxActor.cpp @@ -3010,10 +3010,9 @@ void RoR::GfxActor::UpdateFlexbodies() for (FlexBody* fb: m_flexbodies) { - // Update visibility - fb->setVisible( - fb->getCameraMode() == CAMERA_MODE_ALWAYS_VISIBLE - || fb->getCameraMode() == m_simbuf.simbuf_cur_cinecam); + // Update visibility (same logic as props) + const bool visible = (fb->fb_camera_mode_active == CAMERA_MODE_ALWAYS_VISIBLE || fb->fb_camera_mode_active == m_simbuf.simbuf_cur_cinecam); + fb->setVisible(visible); // Update visible on background thread if (fb->isVisible()) @@ -3086,6 +3085,12 @@ void RoR::GfxActor::UpdateFlares(float dt_sec, bool is_player) for (int i=0; iar_flares[i]; + + // Skip placeholder flares (removed via Tuning system) + if (!flare.bbs) + { + continue; + } this->SetMaterialFlareOn(i, flare.intensity > 0.3); diff --git a/source/main/gfx/GfxData.h b/source/main/gfx/GfxData.h index e445d9679a..f8272ed927 100644 --- a/source/main/gfx/GfxData.h +++ b/source/main/gfx/GfxData.h @@ -120,9 +120,10 @@ enum class WheelSide: char }; // Dynamic visibility control (value 0 and higher is cinecam index) - common to 'props' and 'flexbodies' -static const int CAMERA_MODE_ALWAYS_HIDDEN = -3; -static const int CAMERA_MODE_ALWAYS_VISIBLE = -2; -static const int CAMERA_MODE_3RDPERSON_ONLY = -1; +typedef int CameraMode_t; +static CameraMode_t CAMERA_MODE_ALWAYS_HIDDEN = -3; +static CameraMode_t CAMERA_MODE_ALWAYS_VISIBLE = -2; +static CameraMode_t CAMERA_MODE_3RDPERSON_ONLY = -1; enum ShifterPropAnim { @@ -171,9 +172,11 @@ struct Prop std::string pp_media[2]; //!< Redundant, for Tuning UI. Media1 = prop mesh name, Media2 = steeringwheel mesh/beaconprop flare mat. std::vector pp_animations; - // Visibility control - int pp_camera_mode_active = CAMERA_MODE_ALWAYS_VISIBLE; //!< Dynamic visibility mode {0 and higher = cinecam index} - int pp_camera_mode_orig = CAMERA_MODE_ALWAYS_VISIBLE; //!< Dynamic visibility mode {0 and higher = cinecam index} + /// @name Visibility control (same as flexbody - see file FlexBody.h) + /// @{ + CameraMode_t pp_camera_mode_active = CAMERA_MODE_ALWAYS_VISIBLE; //!< Dynamic visibility mode {0 and higher = cinecam index} + CameraMode_t pp_camera_mode_orig = CAMERA_MODE_ALWAYS_VISIBLE; //!< Dynamic visibility mode {0 and higher = cinecam index} + /// @} // Special prop - steering wheel MeshObject* pp_wheel_mesh_obj = nullptr; diff --git a/source/main/gui/DashBoardManager.cpp b/source/main/gui/DashBoardManager.cpp index 5f3f8b0803..dc02ee6cd1 100644 --- a/source/main/gui/DashBoardManager.cpp +++ b/source/main/gui/DashBoardManager.cpp @@ -158,6 +158,18 @@ int DashBoardManager::getLinkIDForName(Ogre::String& str) return -1; } +std::string DashBoardManager::getLinkNameForID(DashData id) +{ + if (id > 0 && id < DD_MAX) + { + return data[id].name; + } + else + { + return ""; + } +} + int DashBoardManager::loadDashBoard(Ogre::String filename, bool textureLayer) { diff --git a/source/main/gui/DashBoardManager.h b/source/main/gui/DashBoardManager.h index 1d586c0c9b..8f649f610e 100644 --- a/source/main/gui/DashBoardManager.h +++ b/source/main/gui/DashBoardManager.h @@ -226,6 +226,7 @@ class DashBoardManager int getDataType(size_t key) { return data[key].type; }; int getLinkIDForName(Ogre::String& str); + std::string getLinkNameForID(DashData id); int loadDashBoard(Ogre::String filename, bool textureLayer); diff --git a/source/main/gui/panels/GUI_FlexbodyDebug.cpp b/source/main/gui/panels/GUI_FlexbodyDebug.cpp index ddd42f5595..b58d5f9ca5 100644 --- a/source/main/gui/panels/GUI_FlexbodyDebug.cpp +++ b/source/main/gui/panels/GUI_FlexbodyDebug.cpp @@ -426,6 +426,9 @@ void FlexbodyDebug::DrawDebugView(FlexBody* flexbody, Prop* prop, NodeNum_t node void FlexbodyDebug::UpdateVisibility() { + // Both flexbodies and props use the same dynamic visibility mode, see `CameraMode_t` typedef and constants in file GfxData.h + // --------------------------------------------------------------------------------------------------------------------------- + ActorPtr actor = App::GetGameContext()->GetPlayerActor(); if (!actor) { @@ -442,13 +445,18 @@ void FlexbodyDebug::UpdateVisibility() { prop.pp_camera_mode_active = CAMERA_MODE_ALWAYS_HIDDEN; } + // Override flexbody dynamic visibility mode + for (FlexBody* flexbody: actor->GetGfxActor()->GetFlexbodies()) + { + flexbody->fb_camera_mode_active = CAMERA_MODE_ALWAYS_HIDDEN; + } // Then re-display what we need manually. auto& flexbody_vec = actor->GetGfxActor()->GetFlexbodies(); const int combo_flexbody_selection = m_combo_selection; if (combo_flexbody_selection >= 0 && combo_flexbody_selection < (int)flexbody_vec.size()) { - flexbody_vec[combo_flexbody_selection]->setVisible(true); + flexbody_vec[combo_flexbody_selection]->fb_camera_mode_active = CAMERA_MODE_ALWAYS_VISIBLE; } auto& prop_vec = actor->GetGfxActor()->getProps(); @@ -472,6 +480,11 @@ void FlexbodyDebug::UpdateVisibility() { prop.pp_camera_mode_active = prop.pp_camera_mode_orig; } + // Restore flexbody dynamic visibility mode + for (FlexBody* flexbody: actor->GetGfxActor()->GetFlexbodies()) + { + flexbody->fb_camera_mode_active = flexbody->fb_camera_mode_orig; + } } } diff --git a/source/main/gui/panels/GUI_TopMenubar.cpp b/source/main/gui/panels/GUI_TopMenubar.cpp index 8c1f5de7e3..f99c4a665c 100644 --- a/source/main/gui/panels/GUI_TopMenubar.cpp +++ b/source/main/gui/panels/GUI_TopMenubar.cpp @@ -29,6 +29,7 @@ #include "Actor.h" #include "ActorManager.h" #include "CameraManager.h" +#include "DashBoardManager.h" #include "FlexBody.h" #include "GameContext.h" #include "GfxScene.h" @@ -151,13 +152,13 @@ void TopMenubar::Draw(float dt) ImGui::CalcTextSize(settings_title.c_str()).x + ImGui::CalcTextSize(tools_title.c_str()).x; - if (App::mp_state->getEnum() != MpState::CONNECTED) + if (this->IsMenuEnabled(TopMenu::TOPMENU_AI)) { menubar_num_buttons += 1; menubar_content_width += ImGui::CalcTextSize(ai_title.c_str()).x; } - if (App::sim_tuning_enabled->getBool()) + if (this->IsMenuEnabled(TopMenu::TOPMENU_TUNING)) { menubar_num_buttons += 1; menubar_content_width += ImGui::CalcTextSize(tuning_title.c_str()).x; @@ -202,7 +203,7 @@ void TopMenubar::Draw(float dt) // The 'Tuning' button - only shown if enabled ImVec2 tuning_cursor = ImVec2(0, 0); - if (App::sim_tuning_enabled->getBool()) + if (this->IsMenuEnabled(TopMenu::TOPMENU_TUNING)) { ImGui::SameLine(); tuning_cursor = ImGui::GetCursorPos(); @@ -215,7 +216,7 @@ void TopMenubar::Draw(float dt) // The 'AI' button - only shown in singleplayer ImVec2 ai_cursor = ImVec2(0, 0); - if (App::mp_state->getEnum() != MpState::CONNECTED) + if (this->IsMenuEnabled(TopMenu::TOPMENU_AI)) { ImGui::SameLine(); ai_cursor = ImGui::GetCursorPos(); @@ -1608,12 +1609,19 @@ void TopMenubar::Draw(float dt) std::string addonparts_title = fmt::format(_LC("TopMenubar", "Addon parts ({})"), tuning_addonparts.size()); if (ImGui::CollapsingHeader(addonparts_title.c_str())) { - for (CacheEntryPtr& addonpart_entry: tuning_addonparts) + for (size_t i = 0; i < tuning_addonparts.size(); i++) { - ImGui::PushID(addonpart_entry->fname.c_str()); + const CacheEntryPtr& addonpart_entry = tuning_addonparts[i]; + ImGui::PushID(addonpart_entry->fname.c_str()); + const bool conflict_w_hovered = tuning_hovered_addonpart + && (addonpart_entry != tuning_hovered_addonpart) + && AddonPartUtility::CheckForAddonpartConflict(tuning_hovered_addonpart, addonpart_entry, tuning_conflicts); bool used = TuneupUtil::isAddonPartUsed(tuneup_def, addonpart_entry->fname); - if (ImGui::Checkbox(addonpart_entry->dname.c_str(), &used)) + const ImVec2 checkbox_cursor = ImGui::GetCursorScreenPos(); + if (ImGui::Checkbox(addonpart_entry->dname.c_str(), &used) + && !conflict_w_hovered + && !tuning_addonparts_conflict_w_used[i]) { ModifyProjectRequest* req = new ModifyProjectRequest(); req->mpr_type = (used) @@ -1623,11 +1631,46 @@ void TopMenubar::Draw(float dt) req->mpr_target_actor = tuning_actor; App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); } - // Reload button - ImGui::SameLine(); - ImGui::Dummy(ImVec2(10.f, 1.f)); + // Draw conflict markers + if (tuning_addonparts_conflict_w_used[i]) + { + // Gray-ish X inside the checkbox + const float square_sz = ImGui::GetFrameHeight(); + const ImVec2 min = checkbox_cursor + ImGui::GetStyle().FramePadding*1.4f; + const ImVec2 max = checkbox_cursor + (ImVec2(square_sz, square_sz) - ImGui::GetStyle().FramePadding*1.5f); + const ImColor X_COLOR(0.5f, 0.48f, 0.45f); + ImGui::GetWindowDrawList()->AddLine(min, max, X_COLOR, 4.f); + ImGui::GetWindowDrawList()->AddLine(ImVec2(min.x, max.y), ImVec2(max.x, min.y), X_COLOR, 4.f); + } + if (conflict_w_hovered) + { + // Red unrounded square around the checkbox + const float square_sz = ImGui::GetFrameHeight(); + const ImVec2 min = checkbox_cursor; + const ImVec2 max = checkbox_cursor + ImVec2(square_sz + 0.5f, square_sz); + const ImColor SQ_COLOR(0.7f, 0.1f, 0.f); + ImGui::GetWindowDrawList()->AddRect(min, max, SQ_COLOR, 0.f, ImDrawCornerFlags_None, 3.f); + } + // Record when checkbox is hovered - for drawing conflict markers + if (ImGui::IsItemHovered()) + { + tuning_hovered_addonpart = addonpart_entry; + } + else if (tuning_hovered_addonpart == addonpart_entry) + { + tuning_hovered_addonpart = nullptr; + } + // Reload button (right-aligned) ImGui::SameLine(); - if (ImGui::SmallButton(_LC("Tuning", "Reload"))) + if (tuning_rwidget_cursorx_min < ImGui::GetCursorPosX()) // Make sure button won't draw over save button + tuning_rwidget_cursorx_min = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + std::string reloadbtn_text = _LC("Tuning", "Reload"); + const float reloadbtn_w = ImGui::CalcTextSize(reloadbtn_text.c_str()).x + ImGui::GetStyle().FramePadding.x * 2; + const float reloadbtn_cursorx = std::max(ImGui::GetWindowContentRegionWidth() - reloadbtn_w, tuning_rwidget_cursorx_min); + ImGui::SetCursorPosX(reloadbtn_cursorx); + const bool reloadbtn_pressed = ImGui::SmallButton(reloadbtn_text.c_str()); + if (reloadbtn_pressed) { // Create spawn request while actor still exists // Note we don't use `ActorModifyRequest::Type::RELOAD` because we don't need the bundle reloaded. @@ -1822,6 +1865,117 @@ void TopMenubar::Draw(float dt) ImGui::PopID(); // i } } + + // Draw flares + size_t total_flares = tuning_actor->ar_flares.size(); + std::string flares_title = fmt::format(_LC("Tuning", "Flares ({})"), total_flares); + if (ImGui::CollapsingHeader(flares_title.c_str())) + { + // Draw all flares (those removed by addonparts are also present as placeholders) + for (FlareID_t flareid = 0; flareid < (int)tuning_actor->ar_flares.size(); flareid++) + { + ImGui::PushID(flareid); + ImGui::AlignTextToFramePadding(); + + this->DrawTuningBoxedSubjectIdInline(flareid); + + // Compose flare description string + const FlareType flaretype = tuning_actor->ar_flares[flareid].fl_type; + std::string flarename; + if (flaretype == FlareType::USER) + { + int controlnumber = tuning_actor->ar_flares[flareid].controlnumber + 1; // Convert range 0-9 to 1-10 + flarename = fmt::format("{} {}", (char)flaretype, controlnumber); + } + else if (flaretype == FlareType::DASHBOARD) + { + std::string linkname = tuning_actor->ar_dashboard->getLinkNameForID((DashData)tuning_actor->ar_flares[flareid].dashboard_link); + flarename = fmt::format("{} {}", (char)flaretype, linkname); + } + else + { + flarename = fmt::format("{}", (char)flaretype); + } + + this->DrawTuningForceRemoveControls( + flareid, + flarename, + tuneup_def && tuneup_def->isFlareUnwanted(flareid), + tuneup_def && tuneup_def->isFlareForceRemoved(flareid), + ModifyProjectRequestType::TUNEUP_FORCEREMOVE_FLARE_SET, + ModifyProjectRequestType::TUNEUP_FORCEREMOVE_FLARE_RESET); + + this->DrawTuningProtectedChkRightAligned( + flareid, + tuneup_def && tuneup_def->isFlareProtected(flareid), + ModifyProjectRequestType::TUNEUP_PROTECTED_FLARE_SET, + ModifyProjectRequestType::TUNEUP_PROTECTED_FLARE_RESET); + + ImGui::PopID(); // flareid + } + } + + // Draw exhausts + size_t total_exhausts = tuning_actor->exhausts.size(); + std::string exhausts_title = fmt::format(_LC("Tuning", "Exhausts ({})"), total_exhausts); + if (ImGui::CollapsingHeader(exhausts_title.c_str())) + { + // Draw all exhausts (those removed by addonparts are also present as placeholders) + for (ExhaustID_t exhaustid = 0; exhaustid < (int)tuning_actor->exhausts.size(); exhaustid++) + { + ImGui::PushID(exhaustid); + ImGui::AlignTextToFramePadding(); + + this->DrawTuningBoxedSubjectIdInline(exhaustid); + + this->DrawTuningForceRemoveControls( + exhaustid, + tuning_actor->exhausts[exhaustid].particleSystemName, + tuneup_def && tuneup_def->isExhaustUnwanted(exhaustid), + tuneup_def && tuneup_def->isExhaustForceRemoved(exhaustid), + ModifyProjectRequestType::TUNEUP_FORCEREMOVE_EXHAUST_SET, + ModifyProjectRequestType::TUNEUP_FORCEREMOVE_EXHAUST_RESET); + + this->DrawTuningProtectedChkRightAligned( + exhaustid, + tuneup_def && tuneup_def->isExhaustProtected(exhaustid), + ModifyProjectRequestType::TUNEUP_PROTECTED_EXHAUST_SET, + ModifyProjectRequestType::TUNEUP_PROTECTED_EXHAUST_RESET); + + ImGui::PopID(); // exhaustid + } + } + + // Draw managed materials + size_t total_materials = tuning_actor->ar_managed_materials.size(); + std::string materials_title = fmt::format(_LC("Tuning", "Managed Materials ({})"), total_materials); + if (ImGui::CollapsingHeader(materials_title.c_str())) + { + // Draw all materials (those removed by addonparts are also present as placeholders) + for (auto mm_pair: tuning_actor->ar_managed_materials) + { + const std::string& material_name = mm_pair.first; + ImGui::PushID(material_name.c_str()); + ImGui::AlignTextToFramePadding(); + + this->DrawTuningForceRemoveControls( + TUNING_SUBJECTID_USE_NAME, + material_name, + tuneup_def && tuneup_def->isManagedMatUnwanted(material_name), + tuneup_def && tuneup_def->isManagedMatForceRemoved(material_name), + ModifyProjectRequestType::TUNEUP_FORCEREMOVE_MANAGEDMAT_SET, + ModifyProjectRequestType::TUNEUP_FORCEREMOVE_MANAGEDMAT_RESET); + + this->DrawTuningProtectedChkRightAligned( + TUNING_SUBJECTID_USE_NAME, + tuneup_def && tuneup_def->isManagedMatProtected(material_name), + ModifyProjectRequestType::TUNEUP_PROTECTED_MANAGEDMAT_SET, + ModifyProjectRequestType::TUNEUP_PROTECTED_MANAGEDMAT_RESET, + material_name); + + ImGui::PopID(); // material_name.c_str() + } + } } m_open_menu_hoverbox_min = menu_pos - MENU_HOVERBOX_PADDING; @@ -2296,22 +2450,27 @@ void TopMenubar::RefreshAiPresets() void TopMenubar::RefreshTuningMenu() { - const ActorPtr& current_actor = App::GetGameContext()->GetPlayerActor(); + // Updates/resets the tuning menu for the current vehicle driven by player (if any). + // ------------------------------------------------------------------------------- + if (App::sim_tuning_enabled->getBool() - && current_actor - && (tuning_actor != current_actor || tuning_force_refresh)) + && (App::mp_state->getEnum() != MpState::CONNECTED) + && App::GetGameContext()->GetPlayerActor() + && (tuning_actor != App::GetGameContext()->GetPlayerActor())) { - ROR_ASSERT(current_actor->getUsedActorEntry()); + tuning_actor = App::GetGameContext()->GetPlayerActor(); + ROR_ASSERT(tuning_actor->getUsedActorEntry()); tuning_addonparts.clear(); tuning_saves.resetResults(); // Addonparts matched by GUID - if (current_actor->getUsedActorEntry()->guid != "") + if (tuning_actor->getUsedActorEntry()->guid != "") { CacheQuery query_addonparts; query_addonparts.cqy_filter_type = LT_AddonPart; - query_addonparts.cqy_filter_guid = current_actor->getUsedActorEntry()->guid; + query_addonparts.cqy_filter_guid = tuning_actor->getUsedActorEntry()->guid; + query_addonparts.cqy_filter_target_filename = tuning_actor->getTruckFileName(); // Addonparts without any filenames listed will just pass. App::GetCacheSystem()->Query(query_addonparts); for (CacheQueryResult& res: query_addonparts.cqy_results) { @@ -2320,9 +2479,9 @@ void TopMenubar::RefreshTuningMenu() } // Addonparts force-installed via [browse all] button; watch for duplicates. - if (current_actor->getWorkingTuneupDef()) + if (tuning_actor->getWorkingTuneupDef()) { - for (std::string const& use_addonpart_fname: current_actor->getWorkingTuneupDef()->use_addonparts) + for (std::string const& use_addonpart_fname: tuning_actor->getWorkingTuneupDef()->use_addonparts) { CacheEntryPtr entry = App::GetCacheSystem()->FindEntryByFilename(LT_AddonPart, /*partial:*/false, use_addonpart_fname); if (entry) @@ -2336,23 +2495,55 @@ void TopMenubar::RefreshTuningMenu() } tuning_saves.cqy_filter_type = LT_Tuneup; - tuning_saves.cqy_filter_guid = current_actor->getUsedActorEntry()->guid; + tuning_saves.cqy_filter_guid = tuning_actor->getUsedActorEntry()->guid; + tuning_saves.cqy_filter_target_filename = tuning_actor->getTruckFileName(); tuning_saves.cqy_filter_category_id = CID_Tuneups; // Exclude auto-generated entries tuning_saves.resetResults(); App::GetCacheSystem()->Query(tuning_saves); + // Refresh `tuning_conflicts` database ~ test eligible addonparts each with each once. + tuning_conflicts.clear(); + for (size_t i1 = 0; i1 < tuning_addonparts.size(); i1++) + { + for (size_t i2 = i1; i2 < tuning_addonparts.size(); i2++) + { + if (i1 != i2) + { + AddonPartUtility::RecordAddonpartConflicts(tuning_addonparts[i1], tuning_addonparts[i2], tuning_conflicts); + } + } + } + + // Refresh `tuning_addonparts_conflicting` listing ~ test used addonparts against unused. + tuning_addonparts_conflict_w_used.clear(); + tuning_addonparts_conflict_w_used.resize(tuning_addonparts.size(), false); + if (tuning_actor->getWorkingTuneupDef()) + { + for (const std::string& use_addonpart_fname: tuning_actor->getWorkingTuneupDef()->use_addonparts) + { + CacheEntryPtr use_addonpart_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AddonPart, /*partial:*/false, use_addonpart_fname); + for (size_t i = 0; i < tuning_addonparts.size(); i++) + { + if (tuning_addonparts[i] != use_addonpart_entry) + { + tuning_addonparts_conflict_w_used[i] = tuning_addonparts_conflict_w_used[i] + || AddonPartUtility::CheckForAddonpartConflict(tuning_addonparts[i], use_addonpart_entry, tuning_conflicts); + } + } + } + } + tuning_rwidget_cursorx_min = 0.f; } - else if (!App::sim_tuning_enabled->getBool() || !current_actor) + else if (!App::sim_tuning_enabled->getBool() || !App::GetGameContext()->GetPlayerActor()) { tuning_addonparts.clear(); tuning_saves.resetResults(); tuning_actor = nullptr; } - tuning_actor = current_actor; } -void TopMenubar::DrawTuningProtectedChkRightAligned(int subject_id, bool protectchk_value, ModifyProjectRequestType request_type_set, ModifyProjectRequestType request_type_reset) +void TopMenubar::DrawTuningProtectedChkRightAligned(const int subject_id, bool protectchk_value, ModifyProjectRequestType request_type_set, ModifyProjectRequestType request_type_reset, const std::string& subject /* ="" */) { // > resolve the alignment ImGui::SameLine(); @@ -2375,7 +2566,14 @@ void TopMenubar::DrawTuningProtectedChkRightAligned(int subject_id, bool protect { ModifyProjectRequest* request = new ModifyProjectRequest(); request->mpr_target_actor = tuning_actor; - request->mpr_subject_id = subject_id; + if (subject_id == TUNING_SUBJECTID_USE_NAME) + { + request->mpr_subject = subject; + } + else + { + request->mpr_subject_id = subject_id; + } request->mpr_type = (protectchk_value) ? request_type_set : request_type_reset; App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, request)); } @@ -2425,7 +2623,14 @@ void TopMenubar::DrawTuningForceRemoveControls(const int subject_id, const std:: { ModifyProjectRequest* req = new ModifyProjectRequest(); req->mpr_type = request_type_set; - req->mpr_subject_id = subject_id; + if (subject_id == TUNING_SUBJECTID_USE_NAME) + { + req->mpr_subject = name; + } + else + { + req->mpr_subject_id = subject_id; + } req->mpr_target_actor = tuning_actor; App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); } @@ -2433,9 +2638,29 @@ void TopMenubar::DrawTuningForceRemoveControls(const int subject_id, const std:: { ModifyProjectRequest* req = new ModifyProjectRequest(); req->mpr_type = request_type_reset; - req->mpr_subject_id = subject_id; + if (subject_id == TUNING_SUBJECTID_USE_NAME) + { + req->mpr_subject = name; + } + else + { + req->mpr_subject_id = subject_id; + } req->mpr_target_actor = tuning_actor; App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); } } + +bool TopMenubar::IsMenuEnabled(TopMenu which) +{ + switch (which) + { + case TopMenu::TOPMENU_AI: + return App::mp_state->getEnum() != MpState::CONNECTED; + case TopMenu::TOPMENU_TUNING: + return App::sim_tuning_enabled->getBool() && (App::mp_state->getEnum() != MpState::CONNECTED); + default: + return true; + } +} diff --git a/source/main/gui/panels/GUI_TopMenubar.h b/source/main/gui/panels/GUI_TopMenubar.h index 25279d3d25..d731a7f72c 100644 --- a/source/main/gui/panels/GUI_TopMenubar.h +++ b/source/main/gui/panels/GUI_TopMenubar.h @@ -25,6 +25,7 @@ #pragma once +#include "AddonPartFileFormat.h" #include "CacheSystem.h" #include "RoRnet.h" @@ -53,6 +54,7 @@ class TopMenubar const ImVec4 ORANGE_TEXT = ImVec4(0.9f, 0.6f, 0.0f, 1.f); const ImVec4 RED_TEXT = ImVec4(1.00f, 0.00f, 0.00f, 1.f); const ImVec2 MENU_HOVERBOX_PADDING = ImVec2(25.f, 10.f); + const int TUNING_SUBJECTID_USE_NAME = -2; enum class TopMenu { TOPMENU_NONE, TOPMENU_SIM, TOPMENU_ACTORS, TOPMENU_SAVEGAMES, TOPMENU_SETTINGS, TOPMENU_TOOLS, TOPMENU_AI, TOPMENU_TUNING }; enum class StateBox { STATEBOX_NONE, STATEBOX_REPLAY, STATEBOX_RACE, STATEBOX_LIVE_REPAIR, STATEBOX_QUICK_REPAIR }; @@ -109,25 +111,29 @@ class TopMenubar // Tuning menu ActorPtr tuning_actor; //!< Detecting actor change to update cached values. - std::vector tuning_addonparts; //!< Addonparts of current actor, both matched by GUID and force-installed by user via [browse all] button. + std::vector tuning_addonparts; //!< Addonparts eligible for current actor, both matched by GUID and force-installed by user via [browse all] button. + std::vector tuning_addonparts_conflict_w_used; //!< 1:1 with `tuning_addonparts`; True means the eligible (not used) addonpart conflicts with an used addonpart. + AddonPartConflictVec tuning_conflicts; //!< Conflicts between eligible addonparts tweaking the same element. CacheQuery tuning_saves; //!< Tuneups saved by user, with category ID `RoR::CID_AddonpartUser` 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 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. - bool tuning_force_refresh = false; float tuning_rwidget_cursorx_min = 0.f; //!< Avoid drawing right-side widgets ('Delete' button or 'Protected' chk) over saved tuneup names. + CacheEntryPtr tuning_hovered_addonpart; void RefreshTuningMenu(); private: + bool IsMenuEnabled(TopMenu which); + void DrawActorListSinglePlayer(); void DrawMpUserToActorList(RoRnet::UserInfo &user); // Multiplayer void DrawSpecialStateBox(float top_offset); // Tuning menu helpers void DrawTuningBoxedSubjectIdInline(int subject_id); - void DrawTuningProtectedChkRightAligned(int subject_id, bool is_protected, ModifyProjectRequestType request_type_set, ModifyProjectRequestType request_type_reset); + void DrawTuningProtectedChkRightAligned(const int subject_id, bool is_protected, ModifyProjectRequestType request_type_set, ModifyProjectRequestType request_type_reset, const std::string& subject = ""); void DrawTuningForceRemoveControls(const int subject_id, const std::string& name, const bool is_unwanted, const bool is_force_removed, ModifyProjectRequestType request_type_set, ModifyProjectRequestType request_type_reset); ImVec2 m_open_menu_hoverbox_min; diff --git a/source/main/main.cpp b/source/main/main.cpp index 7637135830..1fff5b53a9 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -911,6 +911,12 @@ int main(int argc, char *argv[]) App::GetGuiManager()->RepositorySelector.DownloadFinished(); break; + case MSG_GUI_REFRESH_TUNING_MENU_REQUESTED: + { + App::GetGuiManager()->TopMenubar.RefreshTuningMenu(); + break; + } + // -- Editing events -- case MSG_EDI_MODIFY_GROUNDMODEL_REQUESTED: @@ -1035,7 +1041,10 @@ int main(int argc, char *argv[]) case MSG_EDI_MODIFY_PROJECT_REQUESTED: { ModifyProjectRequest* request = static_cast(m.payload); - App::GetCacheSystem()->ModifyProject(request); + if (App::mp_state->getEnum() != MpState::CONNECTED) // Do not allow tuning in multiplayer + { + App::GetCacheSystem()->ModifyProject(request); + } delete request; break; } diff --git a/source/main/physics/Actor.cpp b/source/main/physics/Actor.cpp index 26bbd1452c..d57c20ad6e 100644 --- a/source/main/physics/Actor.cpp +++ b/source/main/physics/Actor.cpp @@ -4591,7 +4591,7 @@ void Actor::UpdatePropAnimInputEvents() std::string Actor::getTruckFileResourceGroup() { - return m_gfx_actor->GetResourceGroup(); + return m_used_actor_entry->resource_group; } CacheEntryPtr& Actor::getUsedActorEntry() diff --git a/source/main/physics/ActorSpawner.cpp b/source/main/physics/ActorSpawner.cpp index 9cc1667c31..f5e886e57a 100644 --- a/source/main/physics/ActorSpawner.cpp +++ b/source/main/physics/ActorSpawner.cpp @@ -1282,32 +1282,37 @@ void ActorSpawner::ProcessExhaust(RigDef::Exhaust & def) return; } + const ExhaustID_t exhaust_id = (ExhaustID_t)m_actor->exhausts.size(); exhaust_t exhaust; exhaust.emitterNode = this->GetNodeIndexOrThrow(def.reference_node); exhaust.directionNode = this->GetNodeIndexOrThrow(def.direction_node); std::string template_name = def.particle_name; - if (template_name.empty() || template_name == "default") + if (template_name == "" || template_name == "default") { template_name = "tracks/Smoke"; // defined in `particles/smoke.particle` } + exhaust.particleSystemName = template_name; - std::string name = this->ComposeName(template_name.c_str(), (int)m_actor->exhausts.size()); - exhaust.smoker = this->CreateParticleSystem(name, template_name); - if (exhaust.smoker == nullptr) + if (!TuneupUtil::isExhaustAnyhowRemoved(m_actor->getWorkingTuneupDef(), exhaust_id)) { - std::stringstream msg; - msg << "Failed to create particle system '" << name << "' (template: '" << template_name <<"')"; - AddMessage(Message::TYPE_ERROR, msg.str()); - return; - } + std::string name = this->ComposeName(template_name.c_str(), (int)m_actor->exhausts.size()); + exhaust.smoker = this->CreateParticleSystem(name, template_name); + if (exhaust.smoker == nullptr) + { + std::stringstream msg; + msg << "Failed to create particle system '" << name << "' (template: '" << template_name <<"')"; + AddMessage(Message::TYPE_ERROR, msg.str()); + return; + } - exhaust.smokeNode = m_particles_parent_scenenode->createChildSceneNode(this->ComposeName("exhaust", (int)m_actor->exhausts.size())); - exhaust.smokeNode->attachObject(exhaust.smoker); - exhaust.smokeNode->setPosition(m_actor->ar_nodes[exhaust.emitterNode].AbsPosition); + exhaust.smokeNode = m_particles_parent_scenenode->createChildSceneNode(this->ComposeName("exhaust", (int)m_actor->exhausts.size())); + exhaust.smokeNode->attachObject(exhaust.smoker); + exhaust.smokeNode->setPosition(m_actor->ar_nodes[exhaust.emitterNode].AbsPosition); - m_actor->m_gfx_actor->SetNodeHot(exhaust.emitterNode, true); - m_actor->m_gfx_actor->SetNodeHot(exhaust.directionNode, true); + m_actor->m_gfx_actor->SetNodeHot(exhaust.emitterNode, true); + m_actor->m_gfx_actor->SetNodeHot(exhaust.directionNode, true); + } m_actor->exhausts.push_back(exhaust); } @@ -1546,7 +1551,6 @@ void ActorSpawner::ProcessFlexbody(RigDef::Flexbody& def) try { - std::string mesh_rg = (def._mesh_rg_override != "") ? def._mesh_rg_override : m_actor->getTruckFileResourceGroup(); auto* flexbody = m_flex_factory.CreateFlexBody( (FlexbodyID_t)m_actor->m_gfx_actor->m_flexbodies.size(), this->GetNodeIndexOrThrow(def.reference_node), @@ -1556,13 +1560,15 @@ void ActorSpawner::ProcessFlexbody(RigDef::Flexbody& def) TuneupUtil::getTweakedFlexbodyRotation(m_actor->getWorkingTuneupDef(), flexbody_id, def.rotation), node_indices, TuneupUtil::getTweakedFlexbodyMedia(m_actor->getWorkingTuneupDef(), flexbody_id, 0, def.mesh_name), - TuneupUtil::getTweakedFlexbodyMediaRG(m_actor->getWorkingTuneupDef(), flexbody_id, 0, mesh_rg) + TuneupUtil::getTweakedFlexbodyMediaRG(m_actor->getWorkingTuneupDef(), flexbody_id, 0, this->GetCurrentElementMediaRG()) ); if (flexbody == nullptr) return; // Error already logged - flexbody->setCameraMode(def.camera_settings.mode); + // Dynamic visibility - same as with props + flexbody->fb_camera_mode_orig = def.camera_settings.mode; + flexbody->fb_camera_mode_active = def.camera_settings.mode; m_actor->m_gfx_actor->m_flexbodies.emplace_back(flexbody); } @@ -1640,14 +1646,13 @@ void ActorSpawner::ProcessProp(RigDef::Prop & def) { steering_wheel_offset = def.special_prop_dashboard.offset; } - std::string media1_rg = (def._mesh_rg_override != "") ? def._mesh_rg_override : m_actor->getTruckFileResourceGroup(); prop.pp_wheel_rot_degree = def.special_prop_dashboard.rotation_angle; prop.pp_wheel_scene_node = m_props_parent_scenenode->createChildSceneNode(this->ComposeName("steering wheel @ prop", prop_id)); prop.pp_wheel_pos = steering_wheel_offset; prop.pp_media[1] = TuneupUtil::getTweakedPropMedia(m_actor->getWorkingTuneupDef(), prop_id, 1, def.special_prop_dashboard.mesh_name); prop.pp_wheel_mesh_obj = new MeshObject( prop.pp_media[1], - TuneupUtil::getTweakedPropMediaRG(m_actor->getWorkingTuneupDef(), prop_id, 1, media1_rg), + TuneupUtil::getTweakedPropMediaRG(m_actor->getWorkingTuneupDef(), prop_id, 1, this->GetCurrentElementMediaRG()), this->ComposeName("steering wheel entity @ prop", prop_id), prop.pp_wheel_scene_node ); @@ -1655,12 +1660,11 @@ void ActorSpawner::ProcessProp(RigDef::Prop & def) } /* CREATE THE PROP */ - std::string media0_rg = (def._mesh_rg_override != "") ? def._mesh_rg_override : m_actor->getTruckFileResourceGroup(); prop.pp_scene_node = m_props_parent_scenenode->createChildSceneNode(this->ComposeName("prop", prop_id)); prop.pp_media[0] = TuneupUtil::getTweakedPropMedia(m_actor->getWorkingTuneupDef(), prop_id, 0, def.mesh_name); prop.pp_mesh_obj = new MeshObject(//def.mesh_name, resource_group, instance_name, prop.pp_scene_node); prop.pp_media[0], - TuneupUtil::getTweakedPropMediaRG(m_actor->getWorkingTuneupDef(), prop_id, 0, media0_rg), + TuneupUtil::getTweakedPropMediaRG(m_actor->getWorkingTuneupDef(), prop_id, 0, this->GetCurrentElementMediaRG()), this->ComposeName("prop entity", prop_id), prop.pp_scene_node); @@ -2104,10 +2108,15 @@ void ActorSpawner::ProcessFlare3(RigDef::Flare3 & def) void ActorSpawner::ProcessFlare2(RigDef::Flare2 & def) { + // This processes both 'flares' and 'flares2' (the parser auto-imports 'flares' as `RigDef::Flare2`) + // ------------------------------------------------------------------------------------------------- + if (m_actor->m_flares_mode == GfxFlaresMode::NONE) { return; } int blink_delay = def.blink_delay_milis; float size = def.size; + const FlareID_t flare_id = static_cast(m_actor->ar_flares.size()); + const bool is_placeholder = TuneupUtil::isFlareAnyhowRemoved(m_actor->getWorkingTuneupDef(), flare_id); /* Backwards compatibility */ if (blink_delay == -2) @@ -2185,9 +2194,12 @@ void ActorSpawner::ProcessFlare2(RigDef::Flare2 & def) } /* Visuals */ - flare.snode = m_flares_parent_scenenode->createChildSceneNode(this->ComposeName("flareX", (int)m_actor->ar_flares.size())); - std::string flare_name = this->ComposeName("Flare", static_cast(m_actor->ar_flares.size())); - flare.bbs = App::GetGfxScene()->GetSceneManager()->createBillboardSet(flare_name, 1); + std::string flare_name = this->ComposeName("Flare", flare_id); + if (!is_placeholder) + { + flare.snode = m_flares_parent_scenenode->createChildSceneNode(this->ComposeName("flareX", flare_id)); + flare.bbs = App::GetGfxScene()->GetSceneManager()->createBillboardSet(flare_name, 1); + } // Backwards compatibility: // before 't' (tail light) was introduced in 2022, tail lights were indicated as 'f' (headlight) + custom material. @@ -2230,7 +2242,7 @@ void ActorSpawner::ProcessFlare2(RigDef::Flare2 & def) } } - Ogre::MaterialPtr material = this->FindOrCreateCustomizedMaterial(material_name, m_custom_resource_group); + Ogre::MaterialPtr material = this->FindOrCreateCustomizedMaterial(material_name, this->GetCurrentElementMediaRG()); if (!material.isNull()) { flare.bbs->setMaterial(material); @@ -2240,7 +2252,7 @@ void ActorSpawner::ProcessFlare2(RigDef::Flare2 & def) flare.intensity = 1.f; flare.light = nullptr; - if ((App::gfx_flares_mode->getEnum() >= GfxFlaresMode::CURR_VEHICLE_HEAD_ONLY) && size > 0.001) + if ((App::gfx_flares_mode->getEnum() >= GfxFlaresMode::CURR_VEHICLE_HEAD_ONLY) && size > 0.001 && !is_placeholder) { if (flare.fl_type == FlareType::HEADLIGHT) { @@ -2273,7 +2285,7 @@ void ActorSpawner::ProcessFlare2(RigDef::Flare2 & def) flare.light->setCastShadows(false); } } - if ((App::gfx_flares_mode->getEnum() >= GfxFlaresMode::ALL_VEHICLES_ALL_LIGHTS) && size > 0.001) + if ((App::gfx_flares_mode->getEnum() >= GfxFlaresMode::ALL_VEHICLES_ALL_LIGHTS) && size > 0.001 && !is_placeholder) { if (flare.fl_type == FlareType::TAIL_LIGHT) { @@ -2345,6 +2357,12 @@ Ogre::MaterialPtr ActorSpawner::InstantiateManagedMaterial(const Ogre::String& r void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) { + // This is how textures map between `RigDef::Document` (*.truck etc...) and `RoR::TuneupDef` (*.tuneup): + // def.diffuse_map ~~ tuneup.media[0] + // def.specular_map ~~ tuneup.media[1] + // def.damaged_diffuse_map ~~ tuneup.media[2] + // ========================================================================== + if (m_managed_materials.find(def.name) != m_managed_materials.end()) { this->AddMessage(Message::TYPE_ERROR, "Duplicate managed material name: '" + def.name + "'. Ignoring definition..."); @@ -2352,7 +2370,7 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) } // Check all textures exist - std::string resource_group = (m_current_module->origin_addonpart) ? m_current_module->origin_addonpart->resource_group : m_custom_resource_group; + std::string resource_group = this->GetCurrentElementMediaRG(); if (!Ogre::ResourceGroupManager::getSingleton().resourceExists(resource_group, def.diffuse_map)) { this->AddMessage(Message::TYPE_WARNING, "Skipping managed material, missing texture file: " + def.diffuse_map); @@ -2388,7 +2406,12 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) std::string custom_name = this->ComposeName(def.name); Ogre::MaterialPtr material; - if (def.type == RigDef::ManagedMaterialType::FLEXMESH_STANDARD || def.type == RigDef::ManagedMaterialType::FLEXMESH_TRANSPARENT) + if (TuneupUtil::isManagedMatAnyhowRemoved(m_actor->getWorkingTuneupDef(), def.name)) + { + // Create a placeholder material + material = Ogre::MaterialManager::getSingleton().getByName("tracks/transred")->clone(custom_name, /*changeGroup:*/true, resource_group); + } + else if (def.type == RigDef::ManagedMaterialType::FLEXMESH_STANDARD || def.type == RigDef::ManagedMaterialType::FLEXMESH_TRANSPARENT) { std::string mat_name_base = (def.type == RigDef::ManagedMaterialType::FLEXMESH_STANDARD) @@ -2416,16 +2439,16 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) if (App::gfx_alt_actor_materials->getBool()) { - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Dmg_Diffuse_Map")->setTextureName(def.damaged_diffuse_map); - material->getTechnique("BaseTechnique")->getPass("SpecularMapping1")->getTextureUnitState("SpecularMapping1_Tex")->setTextureName(def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map"), def.name, 0, def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Dmg_Diffuse_Map"), def.name, 2, def.damaged_diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("SpecularMapping1")->getTextureUnitState("SpecularMapping1_Tex"), def.name, 1, def.specular_map); } else { - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Dmg_Diffuse_Map")->setTextureName(def.damaged_diffuse_map); - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Specular_Map")->setTextureName(def.specular_map); - material->getTechnique("BaseTechnique")->getPass("Specular")->getTextureUnitState("Specular_Map")->setTextureName(def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map"), def.name, 0, def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Dmg_Diffuse_Map"), def.name, 2, def.damaged_diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Specular_Map"), def.name, 1, def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("Specular")->getTextureUnitState("Specular_Map"), def.name, 1, def.specular_map); } } else @@ -2436,8 +2459,8 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) { return; } - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Dmg_Diffuse_Map")->setTextureName(def.damaged_diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map"), def.name, 0, def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Dmg_Diffuse_Map"), def.name, 2, def.damaged_diffuse_map); } } else @@ -2461,14 +2484,14 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) if (App::gfx_alt_actor_materials->getBool()) { - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); - material->getTechnique("BaseTechnique")->getPass("SpecularMapping1")->getTextureUnitState("SpecularMapping1_Tex")->setTextureName(def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map"), def.name, 0, def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("SpecularMapping1")->getTextureUnitState("SpecularMapping1_Tex"), def.name, 1, def.specular_map); } else { - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Specular_Map")->setTextureName(def.specular_map); - material->getTechnique("BaseTechnique")->getPass("Specular")->getTextureUnitState("Specular_Map")->setTextureName(def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map") , def.name, 0, def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Specular_Map"), def.name, 1, def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("Specular")->getTextureUnitState("Specular_Map") , def.name, 1, def.specular_map); } } else @@ -2479,7 +2502,7 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) { return; } - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map"), def.name, 0, def.diffuse_map); } } } @@ -2509,14 +2532,14 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) if (App::gfx_alt_actor_materials->getBool()) { - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); - material->getTechnique("BaseTechnique")->getPass("SpecularMapping1")->getTextureUnitState("SpecularMapping1_Tex")->setTextureName(def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map"), def.name, 0, def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("SpecularMapping1")->getTextureUnitState("SpecularMapping1_Tex"), def.name, 1, def.specular_map); } else { - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Specular_Map")->setTextureName(def.specular_map); - material->getTechnique("BaseTechnique")->getPass("Specular")->getTextureUnitState("Specular_Map")->setTextureName(def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map") ,def.name, 0, def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Specular_Map"),def.name, 1, def.specular_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("Specular")->getTextureUnitState("Specular_Map") ,def.name, 1, def.specular_map); } } else @@ -2527,12 +2550,13 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) { return; } - material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map")->setTextureName(def.diffuse_map); + this->AssignManagedMaterialTexture(material->getTechnique("BaseTechnique")->getPass("BaseRender")->getTextureUnitState("Diffuse_Map"), def.name, 0, def.diffuse_map); } } - if (def.type != RigDef::ManagedMaterialType::INVALID) + if (!TuneupUtil::isManagedMatAnyhowRemoved(m_actor->getWorkingTuneupDef(), def.name) + && def.type != RigDef::ManagedMaterialType::INVALID) { if (def.options.double_sided) { @@ -2551,8 +2575,6 @@ void ActorSpawner::ProcessManagedMaterial(RigDef::ManagedMaterial & def) } } - /* Finalize */ - material->compile(); m_managed_materials.insert(std::make_pair(def.name, material)); } @@ -6553,8 +6575,16 @@ Ogre::MaterialPtr ActorSpawner::FindOrCreateCustomizedMaterial(const std::string } lookup_entry.material = orig_mat->clone(this->ComposeName(orig_mat->getName()), true, mat_lookup_rg); + /* + 02:53:47: [RoR|Actor|Error] (Keyword: managedmaterials) Ogre::ItemIdentityException::ItemIdentityException: Texture Pointer is empty. in TextureUnitState::setTexture at C:\Users\Petr\.conan2\p\b\ogre3ff3740bbb9785\b\OgreMain\src\OgreTextureUnitState.cpp (line 271) + 02:55:17: [RoR|ContentManager] Skipping resource with duplicate name: 'pushbar1 (gavrilmv4.truck [Instance ID 1])' (origin: '') + */ + ROR_ASSERT(lookup_entry.material); } + // Register the substitute + m_material_substitutions.insert(std::make_pair(mat_lookup_name, lookup_entry)); + // Finally, query texture replacements - .skin and builtins for (auto& technique: lookup_entry.material->getTechniques()) { @@ -6615,7 +6645,7 @@ Ogre::MaterialPtr ActorSpawner::FindOrCreateCustomizedMaterial(const std::string } // passes } // techniques - m_material_substitutions.insert(std::make_pair(mat_lookup_name, lookup_entry)); // Register the substitute + return lookup_entry.material; } catch (Ogre::Exception& e) @@ -7319,3 +7349,43 @@ void ActorSpawner::CreateMaterialFlare(int flareid, Ogre::MaterialPtr m) m_actor->m_gfx_actor->m_flare_materials.push_back(binding); } + +std::string ActorSpawner::GetCurrentElementMediaRG() +{ + // Media (Textures/Materials/meshes) can be either in AddonPart bundle or the vehicle bundle. + // ========================================================================================= + + if (m_current_module->origin_addonpart) + { + return m_current_module->origin_addonpart->resource_group; + } + else + { + return m_actor->getTruckFileResourceGroup(); + } +} + +void ActorSpawner::AssignManagedMaterialTexture(Ogre::TextureUnitState* tus, const std::string & mm_name, int media_id, const std::string& tex_name) +{ + // Helper for `ProcessManagedMaterial()`, resolves tweaks + // ====================================================== + + try + { + ROR_ASSERT(tus); + if (tus) + { + Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().load( + TuneupUtil::getTweakedManagedMatMedia(m_actor->getWorkingTuneupDef(), mm_name, media_id, tex_name), + TuneupUtil::getTweakedManagedMatMediaRG(m_actor->getWorkingTuneupDef(), mm_name, media_id, this->GetCurrentElementMediaRG())); + + if (tex) + { + tus->setTexture(tex); + } + } + } + catch (...) // Exception is already logged by OGRE + { + } +} diff --git a/source/main/physics/ActorSpawner.h b/source/main/physics/ActorSpawner.h index ebd20f9e0b..15caf4e5a2 100644 --- a/source/main/physics/ActorSpawner.h +++ b/source/main/physics/ActorSpawner.h @@ -388,6 +388,8 @@ class ActorSpawner Ogre::MaterialPtr InstantiateManagedMaterial(Ogre::String const & rg_name, Ogre::String const & source_name, Ogre::String const & clone_name); void CreateCabVisual(); void CreateMaterialFlare(int flare_index, Ogre::MaterialPtr mat); + std::string GetCurrentElementMediaRG(); //!< Where to load media from (the addonpart's bundle or vehicle's bundle?) + void AssignManagedMaterialTexture(Ogre::TextureUnitState* tus, const std::string & mm_name, int media_id, const std::string& tex_name); //!< Helper for `ProcessManagedMaterial()` /// @param rim_ratio Percentual size of the rim. void CreateWheelVisuals( diff --git a/source/main/physics/SimData.h b/source/main/physics/SimData.h index 342806a6d0..8a1542d344 100644 --- a/source/main/physics/SimData.h +++ b/source/main/physics/SimData.h @@ -631,9 +631,9 @@ struct flare_t float offsetx; float offsety; float offsetz; - Ogre::SceneNode *snode; - Ogre::BillboardSet *bbs; - Ogre::Light *light; + Ogre::SceneNode *snode = nullptr; + Ogre::BillboardSet *bbs = nullptr; //!< This remains `nullptr` if removed via `addonpart_unwanted_flare` or Tuning UI. + Ogre::Light *light = nullptr; FlareType fl_type; int controlnumber; //!< Only 'u' type flares, valid values 0-9, maps to EV_TRUCK_LIGHTTOGGLE01 to 10. int dashboard_link; //!< Only 'd' type flares, valid values are DD_* @@ -650,8 +650,9 @@ struct exhaust_t { NodeNum_t emitterNode = NODENUM_INVALID; NodeNum_t directionNode = NODENUM_INVALID; - Ogre::SceneNode *smokeNode; - Ogre::ParticleSystem* smoker; + Ogre::SceneNode *smokeNode = nullptr; + Ogre::ParticleSystem* smoker = nullptr; //!< This remains `nullptr` if removed via `addonpart_unwanted_exhaust` or Tuning UI. + std::string particleSystemName; //!< Name in .particle file ~ for display in Tuning UI. }; diff --git a/source/main/physics/flex/FlexBody.cpp b/source/main/physics/flex/FlexBody.cpp index d52d0ffca9..faf14b7e9d 100644 --- a/source/main/physics/flex/FlexBody.cpp +++ b/source/main/physics/flex/FlexBody.cpp @@ -567,6 +567,7 @@ bool FlexBody::isVisible() const void FlexBody::setVisible(bool visible) { + // Scene node is NULL if disabled via addonpart/tuneup. if (m_scene_node) m_scene_node->setVisible(visible); } diff --git a/source/main/physics/flex/FlexBody.h b/source/main/physics/flex/FlexBody.h index 2e147c750d..31d99e6600 100644 --- a/source/main/physics/flex/FlexBody.h +++ b/source/main/physics/flex/FlexBody.h @@ -25,6 +25,7 @@ #include "Application.h" #include "Locator_t.h" #include "SimData.h" +#include "GfxData.h" #include "RigDef_File.h" #include "Utils.h" @@ -61,6 +62,12 @@ class FlexBody typedef int FlexBodyPlaceholder_t; static const FlexBodyPlaceholder_t TUNING_PLACEHOLDER = -11; + /// @name Visibility control (same as prop - see file GfxData.h) + /// @{ + CameraMode_t fb_camera_mode_active = CAMERA_MODE_ALWAYS_VISIBLE; //!< Dynamic visibility mode {0 and higher = cinecam index} + CameraMode_t fb_camera_mode_orig = CAMERA_MODE_ALWAYS_VISIBLE; //!< Dynamic visibility mode {0 and higher = cinecam index} + /// @} + FlexBody(FlexBodyPlaceholder_t, FlexbodyID_t id, const std::string& orig_meshname); ~FlexBody(); @@ -68,11 +75,6 @@ class FlexBody void updateBlend(); void writeBlend(); - /// Visibility control - /// @param mode {-2 = always, -1 = 3rdPerson only, 0+ = cinecam index} - void setCameraMode(int mode) { m_camera_mode = mode; }; - int getCameraMode() { return m_camera_mode; }; - void computeFlexbody(); //!< Updates mesh deformation; works on CPU using local copy of vertex data. void updateFlexbodyVertexBuffers(); diff --git a/source/main/resources/CacheSystem.cpp b/source/main/resources/CacheSystem.cpp index ce38174ca3..1bc837817f 100644 --- a/source/main/resources/CacheSystem.cpp +++ b/source/main/resources/CacheSystem.cpp @@ -139,6 +139,13 @@ CacheSystem::CacheSystem() m_known_extensions.push_back("addonpart"); m_known_extensions.push_back("tuneup"); m_known_extensions.push_back("assetpack"); + + // register the dirs + m_content_dirs.push_back("mods"); + m_content_dirs.push_back("packs"); + m_content_dirs.push_back("terrains"); + m_content_dirs.push_back("vehicles"); + m_content_dirs.push_back("projects"); } void CacheSystem::LoadModCache(CacheValidity validity) @@ -334,6 +341,15 @@ void CacheSystem::ImportEntryFromJson(rapidjson::Value& j_entry, CacheEntryPtr & { out_entry->addonpart_guids.insert(j_addonguid.GetString()); } + + // Addon part suggested mod filenames + for (rapidjson::Value& j_addonfname: j_entry["addonpart_filenames"].GetArray()) + { + out_entry->addonpart_filenames.insert(j_addonfname.GetString()); + } + + // Tuneup details + out_entry->tuneup_associated_filename = j_entry["tuneup_associated_filename"].GetString(); } CacheValidity CacheSystem::LoadCacheFileJson() @@ -609,6 +625,16 @@ void CacheSystem::ExportEntryToJson(rapidjson::Value& j_entries, rapidjson::Docu } j_entry.AddMember("addonpart_guids", j_addonguids, j_doc.GetAllocator()); + rapidjson::Value j_addonfnames(rapidjson::kArrayType); + for (std::string const & ag: entry->addonpart_filenames) + { + j_addonfnames.PushBack(rapidjson::StringRef(ag.c_str()), j_doc.GetAllocator()); + } + j_entry.AddMember("addonpart_filenames", j_addonfnames, j_doc.GetAllocator()); + + // Tuneup details + j_entry.AddMember("tuneup_associated_filename", rapidjson::StringRef(entry->tuneup_associated_filename.c_str()), j_doc.GetAllocator()); + // Add entry to list j_entries.PushBack(j_entry, j_doc.GetAllocator()); } @@ -900,6 +926,7 @@ void CacheSystem::FillTruckDetailInfo(CacheEntryPtr& entry, Ogre::DataStreamPtr if (def->root_module->guid.size() > 0) { entry->guid = def->root_module->guid[def->root_module->guid.size() - 1].guid; + Ogre::StringUtil::toLowerCase(entry->guid); } entry->fileformatversion = 0; if (def->root_module->fileformatversion.size() > 0) @@ -1142,6 +1169,8 @@ void CacheSystem::FillSkinDetailInfo(CacheEntryPtr &entry, std::shared_ptrdescription = skin_def->description; entry->categoryid = -1; entry->skin_def = skin_def; // Needed to generate preview image + + Ogre::StringUtil::toLowerCase(entry->guid); } void CacheSystem::FillAddonPartDetailInfo(CacheEntryPtr &entry, Ogre::DataStreamPtr ds) @@ -1163,7 +1192,15 @@ void CacheSystem::FillAddonPartDetailInfo(CacheEntryPtr &entry, Ogre::DataStream } else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "addonpart_guid") { - entry->addonpart_guids.insert(ctx->getTokString(1)); + std::string guid = ctx->getTokString(1); + Ogre::StringUtil::toLowerCase(guid); + entry->addonpart_guids.insert(guid); + } + else if (ctx->isTokKeyword() && ctx->getTokKeyword() == "addonpart_filename") + { + std::string fname = ctx->getTokString(1); + Ogre::StringUtil::toLowerCase(fname); + entry->addonpart_filenames.insert(fname); } ctx->seekNextLine(); @@ -1217,6 +1254,10 @@ void CacheSystem::FillTuneupDetailInfo(CacheEntryPtr &entry, TuneupDefPtr& tuneu entry->description = tuneup_def->description; entry->categoryid = tuneup_def->category_id; entry->tuneup_def = tuneup_def; // Needed to generate preview image + entry->tuneup_associated_filename = tuneup_def->filename; + + Ogre::StringUtil::toLowerCase(entry->guid); + Ogre::StringUtil::toLowerCase(entry->tuneup_associated_filename); } void CacheSystem::LoadAssetPack(CacheEntryPtr& target_entry, Ogre::String const & assetpack_filename) @@ -1333,6 +1374,21 @@ void CacheSystem::LoadSupplementaryDocuments(CacheEntryPtr& entry) } } +bool CacheSystem::IsPathContentDirRoot(const std::string& path) const +{ + // Helper for `LoadResource()` because OGRE's 'readOnly' flag, see explanation in `ContentManager::InitModCache()` + // -------------------------------------------------------------------------------------------------------------- + + for (const std::string& cdir: m_content_dirs) + { + if (path == PathCombine(App::sys_user_dir->getStr(), cdir)) + { + return true; + } + } + return false; +} + void CacheSystem::LoadResource(CacheEntryPtr& entry) { if (!entry) @@ -1346,7 +1402,10 @@ void CacheSystem::LoadResource(CacheEntryPtr& entry) } Ogre::String group = CacheSystem::ComposeResourceGroupName(entry); - bool readonly = entry->resource_bundle_type == "Zip"; // Make "FileSystem" (directory) bundles writable. Default is read-only. + + // Make "FileSystem" (directory) bundles writable (Default is read-only), except if it's a root directory. + // See explanation of `readOnly` OGRE flag in `ContentManager::InitModCache()`. + bool readonly = entry->resource_bundle_type == "Zip" || this->IsPathContentDirRoot(entry->resource_bundle_path); bool recursive = false; // Load now. @@ -1616,6 +1675,9 @@ CacheEntryPtr CacheSystem::CreateProject(CreateProjectRequest* request) project_entry->fext = "tuneup"; // Tell modcache what it is. project_entry->categoryid = CID_Tuneups; // For display in modcache project_entry->guid = request->cpr_source_entry->guid; // For lookup of tuneups by vehicle GUID. + Ogre::StringUtil::toLowerCase(project_entry->guid); + project_entry->tuneup_associated_filename = request->cpr_source_entry->fname; // For additional filtering of results (GUID marks a family, not individual mod). + Ogre::StringUtil::toLowerCase(project_entry->tuneup_associated_filename); } else { @@ -1642,6 +1704,7 @@ CacheEntryPtr CacheSystem::CreateProject(CreateProjectRequest* request) TuneupDefPtr tuneup = request->cpr_source_actor->getWorkingTuneupDef()->clone(); tuneup->guid = request->cpr_source_entry->guid; // For lookup of tuneups by vehicle GUID. + tuneup->filename = request->cpr_source_entry->fname; // For additional filtering of results (GUID marks a family, not individual mod). tuneup->name = request->cpr_name; tuneup->description = request->cpr_description; tuneup->thumbnail = request->cpr_source_entry->filecachename; @@ -1771,6 +1834,16 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request) request->mpr_target_actor->getWorkingTuneupDef()->force_remove_props.erase(request->mpr_subject_id); break; + case ModifyProjectRequestType::TUNEUP_FORCEREMOVE_FLARE_SET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->force_remove_flares.insert(request->mpr_subject_id); + break; + + case ModifyProjectRequestType::TUNEUP_FORCEREMOVE_FLARE_RESET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->force_remove_flares.erase(request->mpr_subject_id); + break; + case ModifyProjectRequestType::TUNEUP_FORCEREMOVE_FLEXBODY_SET: request->mpr_target_actor->ensureWorkingTuneupDef(); request->mpr_target_actor->getWorkingTuneupDef()->force_remove_flexbodies.insert(request->mpr_subject_id); @@ -1791,6 +1864,26 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request) request->mpr_target_actor->getWorkingTuneupDef()->force_wheel_sides.erase(request->mpr_subject_id); break; + case ModifyProjectRequestType::TUNEUP_FORCEREMOVE_EXHAUST_SET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->force_remove_exhausts.insert(request->mpr_subject_id); + break; + + case ModifyProjectRequestType::TUNEUP_FORCEREMOVE_EXHAUST_RESET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->force_remove_exhausts.erase(request->mpr_subject_id); + break; + + case ModifyProjectRequestType::TUNEUP_FORCEREMOVE_MANAGEDMAT_SET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->force_remove_managedmats.insert(request->mpr_subject); + break; + + case ModifyProjectRequestType::TUNEUP_FORCEREMOVE_MANAGEDMAT_RESET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->force_remove_managedmats.erase(request->mpr_subject); + break; + case ModifyProjectRequestType::TUNEUP_PROTECTED_PROP_SET: request->mpr_target_actor->ensureWorkingTuneupDef(); request->mpr_target_actor->getWorkingTuneupDef()->protected_props.insert(request->mpr_subject_id); @@ -1821,6 +1914,36 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request) request->mpr_target_actor->getWorkingTuneupDef()->protected_wheels.erase(request->mpr_subject_id); break; + case ModifyProjectRequestType::TUNEUP_PROTECTED_FLARE_SET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->protected_flares.insert(request->mpr_subject_id); + break; + + case ModifyProjectRequestType::TUNEUP_PROTECTED_FLARE_RESET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->protected_flares.erase(request->mpr_subject_id); + break; + + case ModifyProjectRequestType::TUNEUP_PROTECTED_EXHAUST_SET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->protected_exhausts.insert(request->mpr_subject_id); + break; + + case ModifyProjectRequestType::TUNEUP_PROTECTED_EXHAUST_RESET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->protected_exhausts.erase(request->mpr_subject_id); + break; + + case ModifyProjectRequestType::TUNEUP_PROTECTED_MANAGEDMAT_SET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->protected_managedmats.insert(request->mpr_subject); + break; + + case ModifyProjectRequestType::TUNEUP_PROTECTED_MANAGEDMAT_RESET: + request->mpr_target_actor->ensureWorkingTuneupDef(); + request->mpr_target_actor->getWorkingTuneupDef()->protected_managedmats.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. @@ -1917,6 +2040,8 @@ void CacheSystem::DeleteProject(CacheEntryPtr& entry) size_t CacheSystem::Query(CacheQuery& query) { Ogre::StringUtil::toLowerCase(query.cqy_search_string); + Ogre::StringUtil::toLowerCase(query.cqy_filter_guid); + Ogre::StringUtil::toLowerCase(query.cqy_filter_target_filename); std::time_t cur_time = std::time(nullptr); for (CacheEntryPtr& entry: m_entries) { @@ -1931,6 +2056,23 @@ size_t CacheSystem::Query(CacheQuery& query) } } + // Filter by target filename; pass items which have no target filenames listed. + if (query.cqy_filter_target_filename != "") + { + if (entry->fext == "addonpart" + && entry->addonpart_filenames.size() > 0 + && entry->addonpart_filenames.count(query.cqy_filter_target_filename) == 0) + { + continue; + } + else if (entry->fext == "tuneup" + && entry->tuneup_associated_filename != "" + && entry->tuneup_associated_filename != query.cqy_filter_target_filename) + { + continue; + } + } + // Filter by entry type bool add = false; if (entry->fext == "terrn2") @@ -2017,7 +2159,7 @@ size_t CacheSystem::Query(CacheQuery& query) match = this->Match(score, entry->fname, query.cqy_search_string, 100); break; - default: // CacheSearchMethod::NONE + default: // CacheSearchMethod:: match = true; break; }; @@ -2061,3 +2203,4 @@ bool CacheQueryResult::operator<(CacheQueryResult const& other) const return cqr_score < other.cqr_score; } + diff --git a/source/main/resources/CacheSystem.h b/source/main/resources/CacheSystem.h index 7a864b8bed..23e222d320 100644 --- a/source/main/resources/CacheSystem.h +++ b/source/main/resources/CacheSystem.h @@ -39,7 +39,7 @@ #include #define CACHE_FILE "mods.cache" -#define CACHE_FILE_FORMAT 13 +#define CACHE_FILE_FORMAT 14 #define CACHE_FILE_FRESHNESS 86400 // 60*60*24 = one day namespace RoR { @@ -74,8 +74,7 @@ class CacheEntry: public RefCountingObject std::time_t addtimestamp; //!< timestamp when this file was added to the cache Ogre::String uniqueid; //!< file's unique id - Ogre::String guid; //!< global unique id. Type "addonpart" leaves this empty. - std::set addonpart_guids; //!< GUIDs of all vehicles this addonpart is used in. Empty for non-addonpart entries. + Ogre::String guid; //!< global unique id; Type "addonpart" leaves this empty and uses `addonpart_guids`; Always lowercase. int version; //!< file's version std::string resource_bundle_type; //!< Archive type recognized by OGRE resource system: 'FileSystem' or 'Zip' @@ -92,8 +91,16 @@ class CacheEntry: public RefCountingObject RigDef::DocumentPtr actor_def; //!< Cached actor definition (aka truckfile) after first spawn. std::shared_ptr skin_def; //!< Cached skin info, added on first use or during cache rebuild RoR::TuneupDefPtr tuneup_def; //!< Cached tuning info, added on first use or during cache rebuild + RoR::TuneupDefPtr addonpart_data_only; //!< Cached addonpart data (dummy tuneup), only used for evaluating conflicts, see `AddonPartUtility::RecordAddonpartConflicts()` // TBD: Make Terrn2Def a RefcountingObjectPtr<> and cache it here too. + // following all ADDONPART detail information: + std::set addonpart_guids; //!< GUIDs of all vehicles this addonpart is used with. + std::set addonpart_filenames; //!< File names of all vehicles this addonpart is used with. If empty, any filename goes. + + // following all TUNEUP detail information: + std::string tuneup_associated_filename; //!< Value of 'filename' field in the tuneup file; always lowercase. + // following all TRUCK detail information: Ogre::String description; Ogre::String tags; @@ -164,7 +171,7 @@ struct CacheQueryResult enum class CacheSearchMethod // Always case-insensitive { - NONE, //!< No searching + NONE, //!< Ignore the search string and find all FULLTEXT, //!< Partial match in: name, filename, description, author name/mail GUID, //!< Partial match in: guid AUTHORS, //!< Partial match in: author name/email @@ -176,7 +183,8 @@ struct CacheQuery { RoR::LoaderType cqy_filter_type = RoR::LoaderType::LT_None; int cqy_filter_category_id = CacheCategoryId::CID_All; - std::string cqy_filter_guid; //!< Exact match; leave empty to disable + std::string cqy_filter_guid; //!< Exact match (case-insensitive); leave empty to disable + std::string cqy_filter_target_filename; //!< Exact match (case-insensitive); leave empty to disable (currently only used with addonparts) CacheSearchMethod cqy_search_method = CacheSearchMethod::NONE; std::string cqy_search_string; @@ -232,12 +240,24 @@ enum class ModifyProjectRequestType TUNEUP_FORCEREMOVE_FLEXBODY_RESET, //!< 'subject_id' is flexbody ID. TUNEUP_FORCED_WHEEL_SIDE_SET, //!< 'subject_id' is wheel ID, 'value_int' is RoR::WheelSide TUNEUP_FORCED_WHEEL_SIDE_RESET, //!< 'subject_id' is wheel ID. + TUNEUP_FORCEREMOVE_FLARE_SET, //!< 'subject_id' is flare ID. + TUNEUP_FORCEREMOVE_FLARE_RESET, //!< 'subject_id' is flare ID. + TUNEUP_FORCEREMOVE_EXHAUST_SET, //!< 'subject_id' is exhaust ID. + TUNEUP_FORCEREMOVE_EXHAUST_RESET, //!< 'subject_id' is exhaust ID. + TUNEUP_FORCEREMOVE_MANAGEDMAT_SET, //!< 'subject' is managed material name. + TUNEUP_FORCEREMOVE_MANAGEDMAT_RESET,//!< 'subject' is managed material name. TUNEUP_PROTECTED_PROP_SET, //!< 'subject_id' is prop ID. TUNEUP_PROTECTED_PROP_RESET, //!< 'subject_id' is prop ID. TUNEUP_PROTECTED_FLEXBODY_SET, //!< 'subject_id' is flexbody ID. TUNEUP_PROTECTED_FLEXBODY_RESET, //!< 'subject_id' is flexbody ID. TUNEUP_PROTECTED_WHEEL_SET, //!< 'subject_id' is wheel ID. TUNEUP_PROTECTED_WHEEL_RESET, //!< 'subject_id' is wheel ID. + TUNEUP_PROTECTED_FLARE_SET, //!< 'subject_id' is flare ID. + TUNEUP_PROTECTED_FLARE_RESET, //!< 'subject_id' is flare ID. + TUNEUP_PROTECTED_EXHAUST_SET, //!< 'subject_id' is exhaust ID. + TUNEUP_PROTECTED_EXHAUST_RESET, //!< 'subject_id' is exhaust ID. + TUNEUP_PROTECTED_MANAGEDMAT_SET, //!< 'subject' is managed material name. + TUNEUP_PROTECTED_MANAGEDMAT_RESET, //!< 'subject' is managed material 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. }; @@ -311,7 +331,7 @@ class CacheSystem Ogre::String GetPrettyName(Ogre::String fname); std::string ActorTypeToName(ActorType driveable); - + const std::vector& GetContentDirs() const { return m_content_dirs; } private: @@ -361,12 +381,15 @@ class CacheSystem bool Match(size_t& out_score, std::string data, std::string const& query, size_t ); + bool IsPathContentDirRoot(const std::string& path) const; + bool m_loaded = false; std::time_t m_update_time; //!< Ensures that all inserted files share the same timestamp std::string m_filenames_hash_loaded; //!< hash from cachefile, for quick update detection std::string m_filenames_hash_generated; //!< stores hash over the content, for quick update detection std::vector m_entries; std::vector m_known_extensions; //!< the extensions we track in the cache system + std::vector m_content_dirs; //!< the various mod directories we track in the cache system std::set m_resource_paths; //!< A temporary list of existing resource paths std::map m_categories = { // these are the category numbers from the repository. do not modify them! diff --git a/source/main/resources/ContentManager.cpp b/source/main/resources/ContentManager.cpp index 98c04196d9..b47bc259e1 100644 --- a/source/main/resources/ContentManager.cpp +++ b/source/main/resources/ContentManager.cpp @@ -260,9 +260,6 @@ void ContentManager::InitModCache(CacheValidity validity) App::sys_cache_dir->getStr(), "FileSystem", RGN_CACHE, /*recursive=*/false, /*readOnly=*/false); ResourceGroupManager::getSingleton().addResourceLocation( App::sys_thumbnails_dir->getStr(), "FileSystem", RGN_REPO, /*recursive=*/false, /*readOnly=*/false); - std::string user = App::sys_user_dir->getStr(); - std::string base = App::sys_process_dir->getStr(); - std::string objects = PathCombine("resources", "beamobjects.zip"); // Add top-level ZIPs/directories to RGN_CONTENT (non-recursive) @@ -271,13 +268,13 @@ void ContentManager::InitModCache(CacheValidity validity) std::string extra_mod_path = App::app_extra_mod_path->getStr(); ResourceGroupManager::getSingleton().addResourceLocation(extra_mod_path , "FileSystem", RGN_CONTENT); } - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "mods") , "FileSystem", RGN_CONTENT); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "packs") , "FileSystem", RGN_CONTENT); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "terrains"), "FileSystem", RGN_CONTENT); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "vehicles"), "FileSystem", RGN_CONTENT); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "projects"), "FileSystem", RGN_CONTENT); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(base, "content") , "FileSystem", RGN_CONTENT); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(base, objects) , "Zip" , RGN_CONTENT); + for (const std::string& dirname : App::GetCacheSystem()->GetContentDirs()) + { + ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(App::sys_user_dir->getStr(), dirname), "FileSystem", RGN_CONTENT); + } + ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(App::sys_process_dir->getStr(), "content") , "FileSystem", RGN_CONTENT); + std::string objects = PathCombine("resources", "beamobjects.zip"); + ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(App::sys_process_dir->getStr(), objects) , "Zip" , RGN_CONTENT); // Create RGN_TEMP in recursive mode to find all subdirectories. @@ -287,12 +284,11 @@ void ContentManager::InitModCache(CacheValidity validity) std::string extra_mod_path = App::app_extra_mod_path->getStr(); ResourceGroupManager::getSingleton().addResourceLocation(extra_mod_path , "FileSystem", RGN_TEMP, true); } - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "mods") , "FileSystem", RGN_TEMP, true); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "packs") , "FileSystem", RGN_TEMP, true); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "terrains"), "FileSystem", RGN_TEMP, true); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "vehicles"), "FileSystem", RGN_TEMP, true); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "projects"), "FileSystem", RGN_TEMP, true); - ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(base, "content") , "FileSystem", RGN_TEMP, true); + for (const std::string& dirname : App::GetCacheSystem()->GetContentDirs()) + { + ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(App::sys_user_dir->getStr(), dirname), "FileSystem", RGN_TEMP, true); + } + ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(App::sys_process_dir->getStr(), "content") , "FileSystem", RGN_TEMP, true); // Traverse RGN_TEMP and add all subdirectories to RGN_CONTENT. // (TBD: why not just make RGN_CONTENT itself recursive? -- ohlidalp, 10/2023) diff --git a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp index 7590296ded..ff21e48af8 100644 --- a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp +++ b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp @@ -33,7 +33,7 @@ using namespace RoR; using namespace RigDef; -AddonPartUtility::AddonPartUtility() +AddonPartUtility::AddonPartUtility(bool silent_mode) { // Inits `RefCountingObjectPtr<>` (CacheEntryPtr, GenericDocumentPtr) - shouldn't be in header. } @@ -87,7 +87,9 @@ std::shared_ptr AddonPartUtility::TransformToRigDefModule(Cach } else if (keyword == Keyword::MANAGEDMATERIALS || keyword == Keyword::PROPS - || keyword == Keyword::FLEXBODIES) + || keyword == Keyword::FLEXBODIES + || keyword == Keyword::FLARES + || keyword == Keyword::FLARES2) { block = keyword; m_context->seekNextLine(); @@ -104,6 +106,8 @@ std::shared_ptr AddonPartUtility::TransformToRigDefModule(Cach case Keyword::MANAGEDMATERIALS: this->ProcessManagedMaterial(); break; case Keyword::PROPS: this->ProcessProp(); break; case Keyword::FLEXBODIES: this->ProcessFlexbody(); break; + case Keyword::FLARES: this->ProcessFlare(); break; + case Keyword::FLARES2: this->ProcessFlare2(); break; default: break; } } @@ -124,8 +128,8 @@ std::shared_ptr AddonPartUtility::TransformToRigDefModule(Cach void AddonPartUtility::ResolveUnwantedAndTweakedElements(TuneupDefPtr& tuneup, CacheEntryPtr& addonpart_entry) { - // Evaluates 'addonpart_unwanted_*' elements, respecting 'protected_*' directives in the tuneup. - // Also handles 'addonpart_tweak_*' elements, resolving possible conflicts among used parts. + // Evaluates 'addonpart_unwanted_*' directives, respecting 'protected_*' directives in the tuneup. + // Also handles 'addonpart_tweak_*' directives, resolving possible conflicts among used addonparts. // --------------------------------------------------------------------------------------------- App::GetCacheSystem()->LoadResource(addonpart_entry); @@ -149,6 +153,12 @@ void AddonPartUtility::ResolveUnwantedAndTweakedElements(TuneupDefPtr& tuneup, C this->ProcessUnwantedProp(); else if (m_context->getTokKeyword() == "addonpart_unwanted_flexbody" ) this->ProcessUnwantedFlexbody(); + else if (m_context->getTokKeyword() == "addonpart_unwanted_flare" ) + this->ProcessUnwantedFlare(); + else if (m_context->getTokKeyword() == "addonpart_unwanted_exhaust" ) + this->ProcessUnwantedExhaust(); + else if (m_context->getTokKeyword() == "addonpart_unwanted_managedmaterial") + this->ProcessUnwantedManagedMat(); else if (m_context->getTokKeyword() == "addonpart_tweak_wheel") this->ProcessTweakWheel(); else if (m_context->getTokKeyword() == "addonpart_tweak_node") @@ -157,6 +167,8 @@ void AddonPartUtility::ResolveUnwantedAndTweakedElements(TuneupDefPtr& tuneup, C this->ProcessTweakProp(); else if (m_context->getTokKeyword() == "addonpart_tweak_flexbody") this->ProcessTweakFlexbody(); + else if (m_context->getTokKeyword() == "addonpart_tweak_managedmaterial") + this->ProcessTweakManagedMat(); } m_context->seekNextLine(); @@ -179,6 +191,7 @@ void AddonPartUtility::ResetUnwantedAndTweakedElements(TuneupDefPtr& tuneup) // Unwanted tuneup->unwanted_flexbodies.clear(); tuneup->unwanted_props.clear(); + tuneup->unwanted_flares.clear(); // Tweaked tuneup->node_tweaks.clear(); @@ -256,7 +269,6 @@ void AddonPartUtility::ProcessProp() def.rotation.z = m_context->getTokFloat(8); def.mesh_name = m_context->getTokString(9); - def._mesh_rg_override = m_addonpart_entry->resource_group; m_module->props.push_back(def); } @@ -287,7 +299,6 @@ void AddonPartUtility::ProcessFlexbody() def.rotation.z = m_context->getTokFloat(8); def.mesh_name = m_context->getTokString(9); - def._mesh_rg_override = m_addonpart_entry->resource_group; m_context->seekNextLine(); @@ -314,6 +325,83 @@ void AddonPartUtility::ProcessFlexbody() m_module->flexbodies.push_back(def); } +void AddonPartUtility::ProcessFlare() +{ + int n = m_context->countLineArgs(); + if (n < 5) + { + App::GetConsole()->putMessage( + Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Error parsing addonpart file '{}': flare has only {} arguments, expected {}", m_addonpart_entry->fname, n, 5)); + return; + } + + Flare2 def; // We auto-import 'flares' as 'flares2', leaving the `offset.z` at 0. + int importflags = Node::Ref::REGULAR_STATE_IS_VALID | Node::Ref::REGULAR_STATE_IS_NUMBERED; + def.reference_node = Node::Ref("", (unsigned int)m_context->getTokFloat(0), importflags, 0); + def.node_axis_x = Node::Ref("", (unsigned int)m_context->getTokFloat(1), importflags, 0); + def.node_axis_y = Node::Ref("", (unsigned int)m_context->getTokFloat(2), importflags, 0); + def.offset.x = m_context->getTokFloat(3); + def.offset.y = m_context->getTokFloat(4); + + if (n > 5) def.type = (FlareType)m_context->getTokString(5)[0]; + + if (n > 6) + { + switch (def.type) + { + case FlareType::USER: def.control_number = (int)m_context->getTokFloat(6); break; + case FlareType::DASHBOARD: def.dashboard_link = m_context->getTokString(6); break; + default: break; + } + } + + if (n > 7) { def.blink_delay_milis = (int)m_context->getTokFloat(7); } + if (n > 8) { def.size = m_context->getTokFloat(8); } + if (n > 9) { def.material_name = m_context->getTokString(9); } + + m_module->flares2.push_back(def); +} + +void AddonPartUtility::ProcessFlare2() +{ + int n = m_context->countLineArgs(); + if (n < 6) + { + App::GetConsole()->putMessage( + Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Error parsing addonpart file '{}': flare2 has only {} arguments, expected {}", m_addonpart_entry->fname, n, 6)); + return; + } + + Flare2 def; + int importflags = Node::Ref::REGULAR_STATE_IS_VALID | Node::Ref::REGULAR_STATE_IS_NUMBERED; + def.reference_node = Node::Ref("", (unsigned int)m_context->getTokFloat(0), importflags, 0); + def.node_axis_x = Node::Ref("", (unsigned int)m_context->getTokFloat(1), importflags, 0); + def.node_axis_y = Node::Ref("", (unsigned int)m_context->getTokFloat(2), importflags, 0); + def.offset.x = m_context->getTokFloat(3); + def.offset.y = m_context->getTokFloat(4); + def.offset.z = m_context->getTokFloat(5); // <-- Specific to 'flares2' (the only difference) + + if (n > 6) def.type = (FlareType)m_context->getTokString(6)[0]; + + if (n > 7) + { + switch (def.type) + { + case FlareType::USER: def.control_number = (int)m_context->getTokFloat(7); break; + case FlareType::DASHBOARD: def.dashboard_link = m_context->getTokString(7); break; + default: break; + } + } + + if (n > 8) { def.blink_delay_milis = (int)m_context->getTokFloat(8); } + if (n > 9) { def.size = m_context->getTokFloat(9); } + if (n > 10) { def.material_name = m_context->getTokString(10); } + + m_module->flares2.push_back(def); +} + // Helpers of `ResolveUnwantedAndTweakedElements()`, they expect `m_context` to be in position: void AddonPartUtility::ProcessUnwantedProp() @@ -325,18 +413,18 @@ void AddonPartUtility::ProcessUnwantedProp() if (!m_tuneup->isPropProtected((PropID_t)m_context->getTokFloat(1))) { m_tuneup->unwanted_props.insert((PropID_t)m_context->getTokFloat(1)); - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': marking prop '{}' as UNWANTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking prop '{}' as UNWANTED", m_addonpart_entry->fname, m_context->getTokKeyword(), (int)m_context->getTokFloat(1))); } else { - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': skipping prop '{}' because it's marked PROTECTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping prop '{}' because it's marked PROTECTED", m_addonpart_entry->fname, m_context->getTokKeyword(), m_context->getTokString(1))); } } else { - LOG(fmt::format("[RoR|Addonpart] WARNING: file '{}', element '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); } } @@ -349,18 +437,91 @@ void AddonPartUtility::ProcessUnwantedFlexbody() if (!m_tuneup->isFlexbodyProtected((FlexbodyID_t)m_context->getTokFloat(1))) { m_tuneup->unwanted_flexbodies.insert((FlexbodyID_t)m_context->getTokFloat(1)); - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': marking flexbody '{}' as UNWANTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking flexbody '{}' as UNWANTED", m_addonpart_entry->fname, m_context->getTokKeyword(), (int)m_context->getTokFloat(1))); } else { - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': skipping flexbody '{}' because it's marked PROTECTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping flexbody '{}' because it's marked PROTECTED", m_addonpart_entry->fname, m_context->getTokKeyword(), m_context->getTokString(1))); } } else { - LOG(fmt::format("[RoR|Addonpart] WARNING: file '{}', element '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + } +} + +void AddonPartUtility::ProcessUnwantedFlare() +{ + ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_flare"); // also asserts !EOF and TokenType::KEYWORD + + if (m_context->isTokFloat(1)) + { + if (!m_tuneup->isFlareProtected((FlareID_t)m_context->getTokFloat(1))) + { + m_tuneup->unwanted_flexbodies.insert((FlareID_t)m_context->getTokFloat(1)); + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking flare '{}' as UNWANTED", + m_addonpart_entry->fname, m_context->getTokKeyword(), (int)m_context->getTokFloat(1))); + } + else + { + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping flare '{}' because it's marked PROTECTED", + m_addonpart_entry->fname, m_context->getTokKeyword(), m_context->getTokString(1))); + } + } + else + { + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + } +} + +void AddonPartUtility::ProcessUnwantedExhaust() +{ + ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_exhaust"); // also asserts !EOF and TokenType::KEYWORD + + if (m_context->isTokFloat(1)) + { + if (!m_tuneup->isExhaustProtected((ExhaustID_t)m_context->getTokFloat(1))) + { + m_tuneup->unwanted_exhausts.insert((ExhaustID_t)m_context->getTokFloat(1)); + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking exhaust '{}' as UNWANTED", + m_addonpart_entry->fname, m_context->getTokKeyword(), (int)m_context->getTokFloat(1))); + } + else + { + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping exhaust '{}' because it's marked PROTECTED", + m_addonpart_entry->fname, m_context->getTokKeyword(), m_context->getTokString(1))); + } + } + else + { + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + } +} + +void AddonPartUtility::ProcessUnwantedManagedMat() +{ + ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_managedmaterial"); // also asserts !EOF and TokenType::KEYWORD + + if (m_context->isTokString(1)) + { + std::string mat_name = m_context->getTokString(1); + if (!m_tuneup->isManagedMatProtected(mat_name)) + { + m_tuneup->unwanted_managedmats.insert(mat_name); + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking managedmaterial '{}' as UNWANTED", + m_addonpart_entry->fname, m_context->getTokKeyword(), mat_name)); + } + else + { + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping managedmaterial '{}' because it's marked PROTECTED", + m_addonpart_entry->fname, m_context->getTokKeyword(), mat_name)); + } + } + else + { + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); } } @@ -386,29 +547,29 @@ void AddonPartUtility::ProcessTweakWheel() if (!stop && m_context->isTokFloat(6)) { data.twt_rim_radius = m_context->getTokFloat(6); } else { stop=true; } m_tuneup->wheel_tweaks.insert(std::make_pair(wheel_id, data)); - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': tweaking wheel {}" + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Sheduling tweak for wheel '{}'" " with params {{ media1={}, media2={}, side={}, tire_radius={}, rim_radius={} }}", m_addonpart_entry->fname, m_context->getTokKeyword(), wheel_id, data.twt_media[0], data.twt_media[1], (char)data.twt_side, data.twt_tire_radius, data.twt_rim_radius)); } else if (m_tuneup->wheel_tweaks[wheel_id].twt_origin != m_addonpart_entry->fname) { - m_tuneup->wheel_tweaks.erase(wheel_id); - - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': Conflict of tweaks at wheel '{}', addon parts '{}' and '{}'", + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for wheel '{}' due to conflict with '{}'", m_addonpart_entry->fname, m_context->getTokKeyword(), wheel_id, - m_tuneup->wheel_tweaks[wheel_id].twt_origin, m_addonpart_entry->fname)); + m_tuneup->wheel_tweaks[wheel_id].twt_origin)); + + m_tuneup->wheel_tweaks.erase(wheel_id); } } else { - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': skipping wheel '{}' because it's marked PROTECTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping wheel '{}' because it's marked PROTECTED", m_addonpart_entry->fname, m_context->getTokKeyword(), (int)m_context->getTokFloat(1))); } } else { - LOG(fmt::format("[RoR|Addonpart] WARNING: file '{}', element '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); } } @@ -431,29 +592,29 @@ void AddonPartUtility::ProcessTweakNode() data.tnt_pos.z = m_context->getTokFloat(4); m_tuneup->node_tweaks.insert(std::make_pair(nodenum, data)); - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': tweaking node {}" + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for node '{}'" " with params {{ x={}, y={}, z={} }}", m_addonpart_entry->fname, m_context->getTokKeyword(), nodenum, data.tnt_pos.x, data.tnt_pos.y, data.tnt_pos.z)); } else if (m_tuneup->node_tweaks[nodenum].tnt_origin != m_addonpart_entry->fname) { - m_tuneup->node_tweaks.erase(nodenum); - - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': Conflict of tweaks at node '{}', addon parts '{}' and '{}'", + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for node '{}' due to conflict with '{}'", m_addonpart_entry->fname, m_context->getTokKeyword(), nodenum, - m_tuneup->node_tweaks[nodenum].tnt_origin, m_addonpart_entry->fname)); + m_tuneup->node_tweaks[nodenum].tnt_origin)); + + m_tuneup->node_tweaks.erase(nodenum); } } else { - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': skipping node '{}' because it's marked PROTECTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping node '{}' because it's marked PROTECTED", m_addonpart_entry->fname, m_context->getTokKeyword(), nodenum)); } } else { - LOG(fmt::format("[RoR|Addonpart] WARNING: file '{}', element '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); } } @@ -484,7 +645,7 @@ void AddonPartUtility::ProcessTweakFlexbody() data.tft_media = m_context->getTokString(8); m_tuneup->flexbody_tweaks.insert(std::make_pair(flexbody_id, data)); - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': tweaking flexbody {}" + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for flexbody '{}'" " with params {{ offsetX={}, offsetY={}, offsetZ={}, rotX={}, rotY={}, rotZ={}, media={} }}", m_addonpart_entry->fname, m_context->getTokKeyword(), flexbody_id, data.tft_offset.x, data.tft_offset.y, data.tft_offset.z, @@ -492,22 +653,22 @@ void AddonPartUtility::ProcessTweakFlexbody() } else if (m_tuneup->flexbody_tweaks[flexbody_id].tft_origin != m_addonpart_entry->fname) { - m_tuneup->flexbody_tweaks.erase(flexbody_id); - - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': Conflict of tweaks at flexbody '{}', addon parts '{}' and '{}'", + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for flexbody '{}' due to conflict with '{}'", m_addonpart_entry->fname, m_context->getTokKeyword(), flexbody_id, - m_tuneup->flexbody_tweaks[flexbody_id].tft_origin, m_addonpart_entry->fname)); + m_tuneup->flexbody_tweaks[flexbody_id].tft_origin)); + + m_tuneup->flexbody_tweaks.erase(flexbody_id); } } else { - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': skipping flexbody '{}' because it's marked PROTECTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping flexbody '{}' because it's marked PROTECTED", m_addonpart_entry->fname, m_context->getTokKeyword(), (int)m_context->getTokFloat(1))); } } else { - LOG(fmt::format("[RoR|Addonpart] WARNING: file '{}', element '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); } } @@ -522,7 +683,7 @@ void AddonPartUtility::ProcessTweakProp() m_context->isTokString(8)) // media { const int prop_id = (int)m_context->getTokFloat(1); - if (!m_tuneup->isFlexbodyProtected(prop_id)) + if (!m_tuneup->isPropProtected(prop_id)) { if (m_tuneup->prop_tweaks.find(prop_id) == m_tuneup->prop_tweaks.end()) { @@ -540,7 +701,7 @@ void AddonPartUtility::ProcessTweakProp() if (m_context->isTokString(9)) data.tpt_media[1] = m_context->getTokString(9); // <== Optional Media2 is specific for prop m_tuneup->prop_tweaks.insert(std::make_pair(prop_id, data)); - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': tweaking prop {}" + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for prop '{}'" " with params {{ media1={}, offsetX={}, offsetY={}, offsetZ={}, rotX={}, rotY={}, rotZ={}, media2={} }}", m_addonpart_entry->fname, m_context->getTokKeyword(), prop_id, data.tpt_media[0], data.tpt_offset.x, data.tpt_offset.y, data.tpt_offset.z, @@ -549,21 +710,161 @@ void AddonPartUtility::ProcessTweakProp() } else if (m_tuneup->prop_tweaks[prop_id].tpt_origin != m_addonpart_entry->fname) { - m_tuneup->prop_tweaks.erase(prop_id); - - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': Conflict of tweaks at prop '{}', addon parts '{}' and '{}'", + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for prop '{}' due to conflict with '{}'", m_addonpart_entry->fname, m_context->getTokKeyword(), prop_id, - m_tuneup->prop_tweaks[prop_id].tpt_origin, m_addonpart_entry->fname)); + m_tuneup->prop_tweaks[prop_id].tpt_origin)); + + m_tuneup->prop_tweaks.erase(prop_id); } } else { - LOG(fmt::format("[RoR|Addonpart] INFO: file '{}', element '{}': skipping prop '{}' because it's marked PROTECTED", + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping prop '{}' because it's marked PROTECTED", m_addonpart_entry->fname, m_context->getTokKeyword(), (int)m_context->getTokFloat(1))); } } else { - LOG(fmt::format("[RoR|Addonpart] WARNING: file '{}', element '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); + } +} + +void AddonPartUtility::ProcessTweakManagedMat() +{ + ROR_ASSERT(m_context->getTokKeyword() == "addonpart_tweak_managedmaterial"); // also asserts !EOF and TokenType::KEYWORD + + if (m_context->isTokString(1) && m_context->isTokString(2)) + { + const std::string& mat_name = m_context->getTokString(1); + if (!m_tuneup->isManagedMatProtected(mat_name)) + { + if (m_tuneup->managedmat_tweaks.find(mat_name) == m_tuneup->managedmat_tweaks.end()) + { + TuneupManagedMatTweak data; + bool stop=false; + data.tmt_origin = m_addonpart_entry->fname; + data.tmt_name = mat_name; + data.tmt_type = m_context->getTokString(2); + if (!stop && m_context->isTokString(3)) { data.tmt_media[0] = m_context->getTokString(3); } else {stop=true;} + if (!stop && m_context->isTokString(4)) { data.tmt_media[1] = m_context->getTokString(4); } else {stop=true;} + if (!stop && m_context->isTokString(5)) { data.tmt_media[2] = m_context->getTokString(5); } else {stop=true;} + m_tuneup->managedmat_tweaks.insert(std::make_pair(mat_name, data)); + + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for managed material '{}'" + " with params {{ type={}, media1={}, media2={}, media3={} }}", + m_addonpart_entry->fname, m_context->getTokKeyword(), mat_name, data.tmt_type, data.tmt_media[0], data.tmt_media[1], data.tmt_media[2])); + } + else if (m_tuneup->managedmat_tweaks[mat_name].tmt_origin != m_addonpart_entry->fname) + { + this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for managed material '{}' due to conflict with '{}'", + m_addonpart_entry->fname, m_context->getTokKeyword(), mat_name, + m_tuneup->managedmat_tweaks[mat_name].tmt_origin)); + + m_tuneup->managedmat_tweaks.erase(mat_name); + } + } + else + { + this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping managed material '{}' because it's marked PROTECTED", + m_addonpart_entry->fname, m_context->getTokKeyword(), mat_name)); + } + } +} + +void AddonPartUtility::RecordAddonpartConflicts(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts) +{ + LOG(fmt::format("[RoR|Addonpart] -- Performing `RecordAddonpartConflicts()` between '{}' and '{}' ~ this involves generating dummy tuneups (hence messages below) --", addonpart1->fname, addonpart2->fname)); + + // Make sure both addonparts are loaded and cached. + App::GetCacheSystem()->LoadResource(addonpart1); + if (!addonpart1->addonpart_data_only) + { + addonpart1->addonpart_data_only = new TuneupDef(); + AddonPartUtility util(/*silent mode:*/true); + util.ResolveUnwantedAndTweakedElements(addonpart1->addonpart_data_only, addonpart1); + } + + App::GetCacheSystem()->LoadResource(addonpart2); + if (!addonpart2->addonpart_data_only) + { + addonpart2->addonpart_data_only = new TuneupDef(); + AddonPartUtility util(/*silent mode:*/true); + util.ResolveUnwantedAndTweakedElements(addonpart2->addonpart_data_only, addonpart2); + } + + // NODE TWEAKS: + for (auto& i_pair: addonpart1->addonpart_data_only->node_tweaks) + { + NodeNum_t suspect = i_pair.second.tnt_nodenum; + TuneupNodeTweak* offender = nullptr; + if (TuneupUtil::isNodeTweaked(addonpart2->addonpart_data_only, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_node", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - node {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + // WHEEL TWEAKS: + for (auto& i_pair: addonpart1->addonpart_data_only->wheel_tweaks) + { + WheelID_t suspect = i_pair.second.twt_wheel_id; + TuneupWheelTweak* offender = nullptr; + if (TuneupUtil::isWheelTweaked(addonpart2->addonpart_data_only, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_wheel", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - wheel {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + // PROP TWEAKS: + for (auto& i_pair:addonpart1->addonpart_data_only->prop_tweaks) + { + PropID_t suspect = i_pair.second.tpt_prop_id; + TuneupPropTweak* offender = nullptr; + if (TuneupUtil::isPropTweaked(addonpart2->addonpart_data_only, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_prop", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - prop {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + // FLEXBODY TWEAKS: + for (auto& i_pair: addonpart1->addonpart_data_only->flexbody_tweaks) + { + FlexbodyID_t suspect = i_pair.second.tft_flexbody_id; + TuneupFlexbodyTweak* offender = nullptr; + if (TuneupUtil::isFlexbodyTweaked(addonpart2->addonpart_data_only, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_flexbody", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - flexbody {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + LOG(fmt::format("[RoR|Addonpart] -- Done with `RecordAddonpartConflicts()` between '{}' and '{}' --", addonpart1->fname, addonpart2->fname)); +} + +bool AddonPartUtility::CheckForAddonpartConflict(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts) +{ + if (!addonpart1 || !addonpart2) + { + return false; + } + + for (AddonPartConflict& conflict: conflicts) + { + if ((conflict.atc_addonpart1 == addonpart1 && conflict.atc_addonpart2 == addonpart2) || + (conflict.atc_addonpart1 == addonpart2 && conflict.atc_addonpart2 == addonpart1)) + { + return true; + } + } + return false; +} + +void AddonPartUtility::Log(const std::string& text) +{ + if (!m_silent_mode) + { + LOG(text); } } diff --git a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h index 95184d81f7..86e9b31c69 100644 --- a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h +++ b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h @@ -35,11 +35,21 @@ namespace RoR { +struct AddonPartConflict //!< Conflict between two addonparts tweaking the same element +{ + CacheEntryPtr atc_addonpart1; + CacheEntryPtr atc_addonpart2; + std::string atc_keyword; + int atc_element_id = -1; +}; + +typedef std::vector AddonPartConflictVec; + /// NOTE: Modcache processes this format directly using `RoR::GenericDocument`, see `RoR::CacheSystem::FillAddonPartDetailInfo()` class AddonPartUtility { public: - AddonPartUtility(); + AddonPartUtility(bool silent_mode = false); ~AddonPartUtility(); /// transforms the addonpart to `RigDef::File::Module` (fake 'section/end_section') used for spawning. @@ -52,20 +62,32 @@ class AddonPartUtility static void ResetUnwantedAndTweakedElements(TuneupDefPtr& tuneup); + static void RecordAddonpartConflicts(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts); + + static bool CheckForAddonpartConflict(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts); + private: // Helpers of `TransformToRigDefModule()`, they expect `m_context` to be in position: void ProcessManagedMaterial(); void ProcessDirectiveSetManagedMaterialsOptions(); void ProcessProp(); void ProcessFlexbody(); + void ProcessFlare(); + void ProcessFlare2(); + + // Helpers of `ResolveUnwantedAndTweakedElements()`, they expect `m_context` to be in position: void ProcessTweakWheel(); void ProcessTweakNode(); void ProcessTweakFlexbody(); void ProcessTweakProp(); - - // Helpers of `ResolveUnwantedAndTweakedElements()`, they expect `m_context` to be in position: + void ProcessTweakManagedMat(); void ProcessUnwantedProp(); void ProcessUnwantedFlexbody(); + void ProcessUnwantedFlare(); + void ProcessUnwantedExhaust(); + void ProcessUnwantedManagedMat(); + + void Log(const std::string& text); // Shared state: GenericDocumentPtr m_document; @@ -76,6 +98,7 @@ class AddonPartUtility RigDef::ManagedMaterialsOptions m_managedmaterials_options; // ResolveUnwantedAndTweakedElements() state: TuneupDefPtr m_tuneup; + bool m_silent_mode; }; }; // namespace RoR diff --git a/source/main/resources/rig_def_fileformat/RigDef_File.h b/source/main/resources/rig_def_fileformat/RigDef_File.h index d35a28c4bf..952423ef47 100644 --- a/source/main/resources/rig_def_fileformat/RigDef_File.h +++ b/source/main/resources/rig_def_fileformat/RigDef_File.h @@ -894,7 +894,6 @@ struct Flexbody Ogre::Vector3 offset = Ogre::Vector3::ZERO; Ogre::Vector3 rotation = Ogre::Vector3::ZERO; Ogre::String mesh_name; - Ogre::String _mesh_rg_override; //!< Needed for addonparts std::list animations; std::vector node_list_to_import; std::vector node_list; @@ -1104,7 +1103,6 @@ struct Prop Ogre::Vector3 offset = Ogre::Vector3::ZERO; Ogre::Vector3 rotation = Ogre::Vector3::ZERO; Ogre::String mesh_name; - Ogre::String _mesh_rg_override; //!< Needed for addonparts std::list animations; CameraSettings camera_settings; SpecialProp special = SpecialProp::NONE; diff --git a/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp b/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp index a787e7e351..525257ce8c 100644 --- a/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp +++ b/source/main/resources/tuneup_fileformat/TuneupFileFormat.cpp @@ -47,6 +47,7 @@ TuneupDefPtr TuneupDef::clone() ret->author_name = this->author_name ; //std::string ret->author_id = this->author_id ; //int ret->category_id = this->category_id ; //CacheCategoryId + ret->filename = this->filename ; //std::string // addonparts ret->use_addonparts = this->use_addonparts ; @@ -445,6 +446,97 @@ bool RoR::TuneupUtil::isFlexbodyTweaked(TuneupDefPtr& tuneup_def, FlexbodyID_t f return false; } +// > flare +bool RoR::TuneupUtil::isFlareAnyhowRemoved(TuneupDefPtr& tuneup_def, FlareID_t flare_id) +{ + return tuneup_def + && (tuneup_def->isFlareUnwanted(flare_id) || tuneup_def->isFlareForceRemoved(flare_id)); +} + +// > exhaust +bool RoR::TuneupUtil::isExhaustAnyhowRemoved(TuneupDefPtr& tuneup_def, ExhaustID_t exhaust_id) +{ + return tuneup_def + && (tuneup_def->isExhaustUnwanted(exhaust_id) || tuneup_def->isExhaustForceRemoved(exhaust_id)); +} + +// > managedmaterials +bool RoR::TuneupUtil::isManagedMatAnyhowRemoved(TuneupDefPtr& tuneup_def, const std::string& material_name) +{ + return tuneup_def + && (tuneup_def->isManagedMatUnwanted(material_name) || tuneup_def->isManagedMatForceRemoved(material_name)); +} + +bool RoR::TuneupUtil::isManagedMatTweaked(TuneupDefPtr& tuneup_def, const std::string& material_name, TuneupManagedMatTweak*& out_tweak) +{ + if (tuneup_def) + { + auto itor = tuneup_def->managedmat_tweaks.find(material_name); + if (itor != tuneup_def->managedmat_tweaks.end()) + { + out_tweak = &itor->second; + return true; + } + } + + return false; +} + +std::string RoR::TuneupUtil::getTweakedManagedMatType(TuneupDefPtr& tuneup_def, const std::string& material_name, const std::string& orig_val) +{ + TuneupManagedMatTweak* tweak = nullptr; + if (TuneupUtil::isManagedMatTweaked(tuneup_def, material_name, tweak)) + { + ROR_ASSERT(tweak); + return (tweak->tmt_type != "") ? tweak->tmt_type : orig_val; + } + else + { + return orig_val; + } +} + +std::string RoR::TuneupUtil::getTweakedManagedMatMedia(TuneupDefPtr& tuneup_def, const std::string& material_name, int media_idx, const std::string& orig_val) +{ + TuneupManagedMatTweak* tweak = nullptr; + if (TuneupUtil::isManagedMatTweaked(tuneup_def, material_name, tweak)) + { + ROR_ASSERT(tweak); + ROR_ASSERT(tweak->tmt_media.size() > media_idx); + return (tweak->tmt_media[media_idx] != "") ? tweak->tmt_media[media_idx] : orig_val; + } + else + { + return orig_val; + } +} + +std::string RoR::TuneupUtil::getTweakedManagedMatMediaRG(TuneupDefPtr& tuneup_def, const std::string& material_name, int media_idx, const std::string& orig_val) +{ + TuneupManagedMatTweak* tweak = nullptr; + if (TuneupUtil::isManagedMatTweaked(tuneup_def, material_name, tweak)) + { + ROR_ASSERT(tweak); + ROR_ASSERT(tweak->tmt_media.size() > media_idx); + if (tweak->tmt_media[media_idx] != "") + { + // Find the tweak addonpart + CacheEntryPtr addonpart_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AddonPart, /*partial:*/false, tweak->tmt_origin); + if (addonpart_entry) + { + return addonpart_entry->resource_group; + } + else + { + LOG(fmt::format("[RoR|Tuneup] WARN managedmaterial '{}': Addonpart '{}' not found in modcache!", material_name, tweak->tmt_origin)); + return orig_val; + } + } + } + + return orig_val; +} + bool RoR::TuneupUtil::isAddonPartUsed(TuneupDefPtr& tuneup_entry, const std::string& filename) { return tuneup_entry @@ -528,6 +620,7 @@ void RoR::TuneupUtil::ParseTuneupAttribute(const std::string& line, TuneupDefPtr if (attrib == "category_id" && params.size() == 2) { tuneup_def->category_id = (CacheCategoryId)PARSEINT(params[1]); return; } if (attrib == "guid" && params.size() >= 2) { tuneup_def->guid = params[1]; Ogre::StringUtil::trim(tuneup_def->guid); Ogre::StringUtil::toLowerCase(tuneup_def->guid); return; } if (attrib == "name" && params.size() >= 2) { tuneup_def->name = params[1]; Ogre::StringUtil::trim(tuneup_def->name); return; } + if (attrib == "filename" && params.size() >= 2) { tuneup_def->filename = params[1]; Ogre::StringUtil::trim(tuneup_def->filename); return; } // Addonparts and extracted data if (attrib == "use_addonpart" && params.size() == 2) { tuneup_def->use_addonparts.insert(params[1]); return; } @@ -555,6 +648,7 @@ void RoR::TuneupUtil::ExportTuneup(Ogre::DataStreamPtr& stream, TuneupDefPtr& tu buf << "\tauthor_id = " << tuneup->author_id << "\n"; buf << "\tcategory_id = " << (int)tuneup->category_id << "\n"; buf << "\tguid = " << tuneup->guid << "\n"; + buf << "\tfilename = " << tuneup->filename << "\n"; buf << "\n"; // Addonparts and extracted data: diff --git a/source/main/resources/tuneup_fileformat/TuneupFileFormat.h b/source/main/resources/tuneup_fileformat/TuneupFileFormat.h index 092ad50394..e0e027950e 100644 --- a/source/main/resources/tuneup_fileformat/TuneupFileFormat.h +++ b/source/main/resources/tuneup_fileformat/TuneupFileFormat.h @@ -79,12 +79,24 @@ struct TuneupFlexbodyTweak //!< Data of 'addonpart_tweak_flexbody std::string tft_origin; //!< Addonpart filename }; +struct TuneupManagedMatTweak //!< Data of 'addonpart_tweak_managedmaterial []' +{ + std::string tmt_name; //!< Arg#1, required + std::string tmt_type; //!< Arg#2, required + std::array tmt_media; //!< Arg#3, required, Arg#4, optional, Arg#5, optional + std::string tmt_origin; //!< Addonpart filename +}; + +/// Dual purpose: +/// 1. representing a .tuneup file, see `CacheEntry::tuneup_def` (the obvious use) +/// 2. holding addonpart data for conflict resolution, see `CacheEntry::addonpart_data_only` (an additional hack) struct TuneupDef: public RefCountingObject { /// @name General info /// @{ std::string name; - std::string guid; + std::string guid; //!< target vehicle GUID + std::string filename; //!< target vehicle filename std::string thumbnail; std::string description; std::string author_name; @@ -100,8 +112,12 @@ struct TuneupDef: public RefCountingObject std::map wheel_tweaks; //!< Mesh name and radius overrides via 'addonpart_tweak_wheel' std::map prop_tweaks; //!< Mesh name(s), offset and rotation overrides via 'addonpart_tweak_prop' std::map flexbody_tweaks; //!< Mesh name, offset and rotation overrides via 'addonpart_tweak_flexbody' + std::map managedmat_tweaks; //!< Managed material overrides via 'addonpart_tweak_managedmaterial' std::set unwanted_props; //!< 'addonpart_unwanted_prop' directives. std::set unwanted_flexbodies; //!< 'addonpart_unwanted_flexbody' directives. + std::set unwanted_flares; //!< 'addonpart_unwanted_flare' directives. + std::set unwanted_exhausts; //!< 'addonpart_unwanted_exhaust' directives. + std::set unwanted_managedmats; //!< 'addonpart_unwanted_managedmaterial' directives. /// @} /// @name UI-controlled forced changes (override addonparts) @@ -109,14 +125,20 @@ struct TuneupDef: public RefCountingObject std::set force_remove_props; //!< UI overrides std::set force_remove_flexbodies; //!< UI overrides std::map force_wheel_sides; //!< UI overrides + std::set force_remove_flares; //!< User unticked an UI checkbox in Tuning menu, section Flares. + std::set force_remove_exhausts; //!< User unticked an UI checkbox in Tuning menu, section Exhausts. + std::set force_remove_managedmats;//!< User unticked an UI checkbox in Tuning menu, section Managed Materials. /// @} /// @name UI-controlled protection from addonpart tweaks /// @{ std::set protected_nodes; //!< Nodes that cannot be altered via 'addonpart_tweak_node' std::set protected_wheels; //!< Wheels that cannot be altered via 'addonpart_tweak_wheel' - std::set protected_props; //!< Props which cannot be altered via 'addonpart_tweak_prop' or 'addonpart_remove_prop' directive. - std::set protected_flexbodies; //!< Flexbodies which cannot be removed via 'addonpart_tweak_flexbody' or 'addonpart_remove_flexbody' directive. + std::set protected_props; //!< Props which cannot be altered via 'addonpart_tweak_prop' or 'addonpart_unwanted_prop' directive. + std::set protected_flexbodies; //!< Flexbodies which cannot be altered via 'addonpart_tweak_flexbody' or 'addonpart_unwanted_flexbody' directive. + std::set protected_flares; //!< Flares which cannot be altered via 'addonpart_unwanted_flare' directive. + std::set protected_exhausts; //!< Exhausts which cannot be altered via 'addonpart_unwanted_exhaust' directive. + std::set protected_managedmats; //!< Managed materials which cannot be altered via 'addonpart_tweak_managedmaterial' directive. /// @} TuneupDefPtr clone(); @@ -124,16 +146,22 @@ struct TuneupDef: public RefCountingObject /// @name Protection helpers /// @{ - bool isPropProtected(PropID_t propid) { return protected_props.find(propid) != protected_props.end(); } - bool isFlexbodyProtected(FlexbodyID_t flexbodyid) { return protected_flexbodies.find(flexbodyid) != protected_flexbodies.end(); } - bool isWheelProtected(int wheelid) const { return protected_wheels.find(wheelid) != protected_wheels.end(); } + bool isPropProtected(PropID_t propid) const { return protected_props.find(propid) != protected_props.end(); } + bool isFlexbodyProtected(FlexbodyID_t flexbodyid) const { return protected_flexbodies.find(flexbodyid) != protected_flexbodies.end(); } + bool isWheelProtected(WheelID_t wheelid) const { return protected_wheels.find(wheelid) != protected_wheels.end(); } bool isNodeProtected(NodeNum_t nodenum) const { return protected_nodes.find(nodenum) != protected_nodes.end(); } + bool isFlareProtected(FlareID_t flareid) const { return protected_flares.find(flareid) != protected_flares.end(); } + bool isExhaustProtected(ExhaustID_t exhaustid) const { return protected_exhausts.find(exhaustid) != protected_exhausts.end(); } + bool isManagedMatProtected(const std::string& matname) const { return protected_managedmats.find(matname) != protected_managedmats.end(); } /// @} /// @name Unwanted-state helpers /// @{ bool isPropUnwanted(PropID_t propid) { return unwanted_props.find(propid) != unwanted_props.end(); } bool isFlexbodyUnwanted(FlexbodyID_t flexbodyid) { return unwanted_flexbodies.find(flexbodyid) != unwanted_flexbodies.end(); } + bool isFlareUnwanted(FlareID_t flareid) { return unwanted_flares.find(flareid) != unwanted_flares.end(); } + bool isExhaustUnwanted(ExhaustID_t exhaustid) { return unwanted_exhausts.find(exhaustid) != unwanted_exhausts.end(); } + bool isManagedMatUnwanted(const std::string& matname) { return unwanted_managedmats.find(matname) != unwanted_managedmats.end(); } /// @} /// @name Forced-state helpers @@ -141,6 +169,9 @@ struct TuneupDef: public RefCountingObject bool isPropForceRemoved(PropID_t propid) { return force_remove_props.find(propid) != force_remove_props.end(); } bool isFlexbodyForceRemoved(FlexbodyID_t flexbodyid) { return force_remove_flexbodies.find(flexbodyid) != force_remove_flexbodies.end(); } bool isWheelSideForced(WheelID_t wheelid, WheelSide& out_val) const; + bool isFlareForceRemoved(FlareID_t flareid) { return force_remove_flares.find(flareid) != force_remove_flares.end(); } + bool isExhaustForceRemoved(ExhaustID_t exhaustid) { return force_remove_exhausts.find(exhaustid) != force_remove_exhausts.end(); } + bool isManagedMatForceRemoved(const std::string& matname) { return force_remove_managedmats.find(matname) != force_remove_managedmats.end(); } /// @} }; @@ -192,6 +223,25 @@ class TuneupUtil static bool isFlexbodyTweaked(TuneupDefPtr& tuneup_entry, FlexbodyID_t flexbody_id, TuneupFlexbodyTweak*& out_tweak); /// @} + /// @name Flare helpers + /// @{ + static bool isFlareAnyhowRemoved(TuneupDefPtr& tuneup_def, FlareID_t flare_id); + /// @} + + /// @name Exhaust helpers + /// @{ + static bool isExhaustAnyhowRemoved(TuneupDefPtr& tuneup_def, ExhaustID_t exhaust_id); + /// @} + + /// @name Managed material helpers + /// @{ + static bool isManagedMatAnyhowRemoved(TuneupDefPtr& tuneup_def, const std::string& matname); + static std::string getTweakedManagedMatType(TuneupDefPtr& tuneup_def, const std::string& matname, const std::string& orig_val); + static std::string getTweakedManagedMatMedia(TuneupDefPtr& tuneup_def, const std::string& matname, int media_idx, const std::string& orig_val); + static std::string getTweakedManagedMatMediaRG(TuneupDefPtr& tuneup_def, const std::string& matname, int media_idx, const std::string& orig_val); + static bool isManagedMatTweaked(TuneupDefPtr& tuneup_def, const std::string& matname, TuneupManagedMatTweak*& out_tweak); + /// @} + private: static void ParseTuneupAttribute(const std::string& line, TuneupDefPtr& tuneup_def); diff --git a/source/main/utils/GenericFileFormat.cpp b/source/main/utils/GenericFileFormat.cpp index 0e816be27f..898e959ad8 100644 --- a/source/main/utils/GenericFileFormat.cpp +++ b/source/main/utils/GenericFileFormat.cpp @@ -69,6 +69,7 @@ struct DocumentParser void ProcessChar(const char c); void ProcessEOF(); + void ProcessSeparatorWithinBool(); void BeginToken(const char c); void UpdateComment(const char c); @@ -580,6 +581,28 @@ void DocumentParser::UpdateNumber(const char c) } } +void DocumentParser::ProcessSeparatorWithinBool() +{ + this->DiscontinueBool(); + switch (partial_tok_type) + { + case PartialToken::KEYWORD: + this->FlushStringishToken(TokenType::KEYWORD); + break; + case PartialToken::STRING_NAKED: + this->FlushStringishToken(TokenType::STRING); + break; + default: + // Discard token + tok.push_back('\0'); + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("{}, line {}, pos {}: discarding incomplete boolean token '{}'", datastream->getName(), line_num, line_pos, tok.data())); + tok.clear(); + partial_tok_type = PartialToken::NONE; + break; + } +} + void DocumentParser::UpdateBool(const char c) { switch (c) @@ -590,22 +613,12 @@ void DocumentParser::UpdateBool(const char c) case ' ': case ',': case '\t': - // Discard token - tok.push_back('\0'); - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, - fmt::format("{}, line {}, pos {}: discarding incomplete boolean token '{}'", datastream->getName(), line_num, line_pos, tok.data())); - tok.clear(); - partial_tok_type = PartialToken::NONE; + this->ProcessSeparatorWithinBool(); line_pos++; break; case '\n': - // Discard token - tok.push_back('\0'); - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, - fmt::format("{}, line {}, pos {}: discarding incomplete boolean token '{}'", datastream->getName(), line_num, line_pos, tok.data())); - tok.clear(); - partial_tok_type = PartialToken::NONE; + this->ProcessSeparatorWithinBool(); // Break line doc.tokens.push_back({ TokenType::LINEBREAK, 0.f }); line_num++; @@ -615,16 +628,11 @@ void DocumentParser::UpdateBool(const char c) case ':': if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_COLON) { - // Discard token - tok.push_back('\0'); - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, - fmt::format("{}, line {}, pos {}: discarding incomplete boolean token '{}'", datastream->getName(), line_num, line_pos, tok.data())); - tok.clear(); - partial_tok_type = PartialToken::NONE; + this->ProcessSeparatorWithinBool(); } else { - partial_tok_type = PartialToken::GARBAGE; + this->DiscontinueBool(); tok.push_back(c); } line_pos++; @@ -633,16 +641,11 @@ void DocumentParser::UpdateBool(const char c) case '=': if (options & GenericDocument::OPTION_ALLOW_SEPARATOR_EQUALS) { - // Discard token - tok.push_back('\0'); - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, - fmt::format("{}, line {}, pos {}: discarding incomplete boolean token '{}'", datastream->getName(), line_num, line_pos, tok.data())); - tok.clear(); - partial_tok_type = PartialToken::NONE; + this->ProcessSeparatorWithinBool(); } else { - partial_tok_type = PartialToken::GARBAGE; + this->DiscontinueBool(); tok.push_back(c); } line_pos++;