Skip to content

Commit

Permalink
Tuning: added 'unwanted' to addons and 'protected' to tuneups.
Browse files Browse the repository at this point in the history
The .addonpart file can now specify "unwanted" props or flexbodies, to simulate swapping of parts. Under the hood, this works by adding the mesh names to "remove" lists, same as if user disabled them from top menu. Example addonpart:

```
  ; Add the stock bumpers to 'tuneup.removed_flexbodies' unless user added it to 'tuneup.protected_flexbodies' first.
  addonpart_unwanted_flexbody "K3500_FBump.mesh"
  addonpart_unwanted_flexbody "454ss_FBump.mesh"�```�
The .tuneup file now contains lists of "protected" props and flexbodies which cannot be removed by the addonpart and remain "removed" or not as the user wishes. This lets user force default meshes to be kept or addonpart meshes to not be added (may be useful if the addonpart specifies multiple meshes). Example of protected entry in .tuneup file:

```
	protected_flexbody = 454ss_FBump.mesh
	protected_flexbody = K3500_FBump.mesh
```
  • Loading branch information
ohlidalp committed Nov 3, 2023
1 parent ace8640 commit a7bdb68
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 57 deletions.
50 changes: 45 additions & 5 deletions source/main/gui/panels/GUI_TopMenubar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1479,13 +1479,13 @@ void TopMenubar::Draw(float dt)

// Delete button (right-aligned)
ImGui::SameLine();
if (tuning_delbtn_cursorx_min < ImGui::GetCursorPosX()) // Make sure button won't draw over item name
tuning_delbtn_cursorx_min = ImGui::GetCursorPosX();
if (tuning_rwidget_cursorx_min < ImGui::GetCursorPosX()) // Make sure button won't draw over item name
tuning_rwidget_cursorx_min = ImGui::GetCursorPosX();
std::string delbtn_text = _LC("Tuning", "Delete");
float delbtn_w = ImGui::CalcTextSize(delbtn_text.c_str()).x + ImGui::GetStyle().FramePadding.x * 2;
float delbtn_cursorx = ImGui::GetWindowContentRegionWidth() - delbtn_w;
if (delbtn_cursorx < tuning_delbtn_cursorx_min)
delbtn_cursorx = tuning_delbtn_cursorx_min;
if (delbtn_cursorx < tuning_rwidget_cursorx_min)
delbtn_cursorx = tuning_rwidget_cursorx_min;
ImGui::SetCursorPosX(delbtn_cursorx);
ImGui::PushStyleColor(ImGuiCol_Button, TUNING_HOLDTOCONFIRM_COLOR);
bool delbtn_pressed = RoR::ImButtonHoldToConfirm(delbtn_text, /*small:*/true,
Expand Down Expand Up @@ -1604,6 +1604,9 @@ void TopMenubar::Draw(float dt)
App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req));
}

this->DrawTuningProtectedChkRightAligned(tuneup_entry->tuneup_def, meshname, tuneup_entry->tuneup_def->isPropProtected(meshname),
ModifyProjectRequestType::TUNEUP_PROTECTED_PROP_SET, ModifyProjectRequestType::TUNEUP_PROTECTED_PROP_RESET);

ImGui::PopID(); // meshname
}
// Then draw existing props (skip aero navlights and dashboard frankenprop)
Expand Down Expand Up @@ -1632,6 +1635,9 @@ void TopMenubar::Draw(float dt)
App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req));
}

this->DrawTuningProtectedChkRightAligned(tuneup_entry->tuneup_def, meshname, tuneup_entry->tuneup_def->isPropProtected(meshname),
ModifyProjectRequestType::TUNEUP_PROTECTED_PROP_SET, ModifyProjectRequestType::TUNEUP_PROTECTED_PROP_RESET);

ImGui::PopID(); // meshname
}
}
Expand Down Expand Up @@ -1659,6 +1665,9 @@ void TopMenubar::Draw(float dt)
App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req));
}

this->DrawTuningProtectedChkRightAligned(tuneup_entry->tuneup_def, meshname, tuneup_entry->tuneup_def->isFlexbodyProtected(meshname),
ModifyProjectRequestType::TUNEUP_PROTECTED_FLEXBODY_SET, ModifyProjectRequestType::TUNEUP_PROTECTED_FLEXBODY_RESET);

ImGui::PopID(); // meshname
}
// Then draw existing flexbodies
Expand All @@ -1677,6 +1686,9 @@ void TopMenubar::Draw(float dt)
App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req));
}

this->DrawTuningProtectedChkRightAligned(tuneup_entry->tuneup_def, meshname, tuneup_entry->tuneup_def->isFlexbodyProtected(meshname),
ModifyProjectRequestType::TUNEUP_PROTECTED_FLEXBODY_SET, ModifyProjectRequestType::TUNEUP_PROTECTED_FLEXBODY_RESET);

ImGui::PopID(); // meshname
}
}
Expand Down Expand Up @@ -2151,8 +2163,36 @@ void TopMenubar::RefreshTuningMenu()
tuning_saves.cqy_results.clear();
App::GetCacheSystem()->Query(tuning_saves);

tuning_delbtn_cursorx_min = 0.f;
tuning_rwidget_cursorx_min = 0.f;
}
tuning_actor = current_actor;
}

void TopMenubar::DrawTuningProtectedChkRightAligned(TuneupDefPtr& tuneup, const std::string& meshname, bool protectchk_value, ModifyProjectRequestType request_type_set, ModifyProjectRequestType request_type_reset)
{
// > resolve the alignment
ImGui::SameLine();
if (tuning_rwidget_cursorx_min < ImGui::GetCursorPosX()) // Make sure button won't draw over item name
tuning_rwidget_cursorx_min = ImGui::GetCursorPosX();
std::string protectchk_text = _LC("Tuning", "Protected");
float protectchk_w = ImGui::CalcTextSize(protectchk_text.c_str()).x + ImGui::GetStyle().FramePadding.x * 2;
float protectchk_cursorx = (ImGui::GetWindowContentRegionWidth() - protectchk_w) - 20.f;
if (protectchk_cursorx < tuning_rwidget_cursorx_min)
protectchk_cursorx = tuning_rwidget_cursorx_min;
ImGui::SetCursorPosX(protectchk_cursorx);

// > set styling and draw
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
bool chk_pressed = ImGui::Checkbox(protectchk_text.c_str(), &protectchk_value);
ImGui::PopStyleVar(1); // ImGuiStyleVar_FramePadding

// > handle user action
if (chk_pressed)
{
ModifyProjectRequest* request = new ModifyProjectRequest();
request->mpr_target_actor = tuning_actor;
request->mpr_subject = meshname;
request->mpr_type = (protectchk_value) ? request_type_set : request_type_reset;
App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, request));
}
}
4 changes: 2 additions & 2 deletions source/main/gui/panels/GUI_TopMenubar.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@ class TopMenubar
const float TUNING_HOLDTOCONFIRM_TIMELIMIT = 1.5f; //!< Delete button must be held for several sec to confirm.
float tuning_holdtoconfirm_time_left = 0.f; //!< Delete button must be held for several sec to confirm.
bool tuning_force_refresh = false;
float tuning_delbtn_cursorx_min = 0.f; //!< Avoid drawing 'Delete' button over saved tuneup names.
float tuning_rwidget_cursorx_min = 0.f; //!< Avoid drawing right-side widgets ('Delete' button or 'Protected' chk) over saved tuneup names.
void RefreshTuningMenu();

private:
void DrawActorListSinglePlayer();
void DrawMpUserToActorList(RoRnet::UserInfo &user); // Multiplayer
void DrawSpecialStateBox(float top_offset);


void DrawTuningProtectedChkRightAligned(TuneupDefPtr& tuneup, const std::string& meshname, bool is_protected, ModifyProjectRequestType request_type_set, ModifyProjectRequestType request_type_reset);

ImVec2 m_open_menu_hoverbox_min;
ImVec2 m_open_menu_hoverbox_max;
Expand Down
4 changes: 2 additions & 2 deletions source/main/physics/ActorSpawner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ void ActorSpawner::ConfigureAddonParts(CacheEntryPtr& tuneup_entry)
CacheEntryPtr addonpart_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AddonPart, /*partial:*/false, addonpart);
if (addonpart_entry)
{
AddonPartParser parser;
auto module = parser.TransformToRigDefModule(addonpart_entry);
AddonPartUtility util;
auto module = util.TransformToRigDefModule(addonpart_entry);
if (module)
{
m_selected_modules.push_back(module);
Expand Down
56 changes: 41 additions & 15 deletions source/main/resources/CacheSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "CacheSystem.h"

#include "Actor.h"
#include "AddonPartFileFormat.h"
#include "Application.h"
#include "SimData.h"
#include "ContentManager.h"
Expand Down Expand Up @@ -1653,39 +1654,50 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request)
CacheEntryPtr tuneup_entry = request->mpr_target_actor->getUsedTuneupEntry();
ROR_ASSERT(tuneup_entry);
ROR_ASSERT(tuneup_entry->tuneup_def);
ROR_ASSERT(tuneup_entry->categoryid == CID_TuneupsAuto); // We never want to modify saved tuneups, only the working (auto-generated) one.

switch (request->mpr_type)
{
case ModifyProjectRequestType::TUNEUP_USE_ADDONPART_SET:
// Add the addonpart to the TuneupDef document.
tuneup_entry->tuneup_def->use_addonparts.insert(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_USE_ADDONPART_RESET:
// Erase the addonpart from the TuneupDef document.
tuneup_entry->tuneup_def->use_addonparts.erase(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_REMOVE_PROP_SET:
// Add the prop to the 'remove_props' in TuneupDef document.
tuneup_entry->tuneup_def->remove_props.insert(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_REMOVE_PROP_RESET:
// Erase the prop from 'remove_props' in the TuneupDef document.
tuneup_entry->tuneup_def->remove_props.erase(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_REMOVE_FLEXBODY_SET:
// Add the flexbody to the 'remove_flexbodies' in TuneupDef document.
tuneup_entry->tuneup_def->remove_flexbodies.insert(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_REMOVE_FLEXBODY_RESET:
// Erase the flexbody from 'remove_flexbodies' in the TuneupDef document.
tuneup_entry->tuneup_def->remove_flexbodies.erase(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_PROTECTED_PROP_SET:
tuneup_entry->tuneup_def->protected_props.insert(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_PROTECTED_PROP_RESET:
tuneup_entry->tuneup_def->protected_props.erase(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_PROTECTED_FLEXBODY_SET:
tuneup_entry->tuneup_def->protected_flexbodies.insert(request->mpr_subject);
break;

case ModifyProjectRequestType::TUNEUP_PROTECTED_FLEXBODY_RESET:
tuneup_entry->tuneup_def->protected_flexbodies.erase(request->mpr_subject);
break;

case ModifyProjectRequestType::PROJECT_LOAD_TUNEUP:
{
// Instead of loading with the saved tuneup directly, keep the autogenerated and sync it with the save.
Expand Down Expand Up @@ -1720,18 +1732,32 @@ void CacheSystem::ModifyProject(ModifyProjectRequest* request)
}


if (tuneup_entry->categoryid == CID_TuneupsAuto)
// resolve unwanted elements
for (const std::string& addonpart_file: tuneup_entry->tuneup_def->use_addonparts)
{
// For auto-generated entries, immediatelly update the .tuneup file (user-saved tuneups are only modified on demand).
Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(tuneup_entry->fname, tuneup_entry->resource_group, /*overwrite:*/true);
Str<200> header;
header
<< "// This is a '.tuneup' mod - it's similar to '.skin' mod but deals with '.addonpart' mods.\n"
<< "// See https://github.com/RigsOfRods/rigs-of-rods/pull/3096#issuecomment-1783976601\n\n";
datastream->write(header.GetBuffer(), header.GetLength());
RoR::TuneupParser::ExportTuneup(datastream, tuneup_entry->tuneup_def);
CacheEntryPtr addonpart_entry = this->FindEntryByFilename(LT_AddonPart, /*partial:*/false, addonpart_file);
if (addonpart_entry)
{
this->LoadResource(addonpart_entry);
AddonPartUtility util;
util.ResolveUnwantedElements(tuneup_entry->tuneup_def, addonpart_entry);
}
else
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING,
fmt::format(_LC("CacheSystem", "Problem evaluating unwanted parts - addonpart '{}' not found in cache"), addonpart_file));
}
}

// Update the .tuneup file (rembember this is the 'working' aka 'autogenerated' tuneup)
Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(tuneup_entry->fname, tuneup_entry->resource_group, /*overwrite:*/true);
Str<200> header;
header
<< "// This is a '.tuneup' mod - it's similar to '.skin' mod but deals with '.addonpart' mods.\n"
<< "// See https://github.com/RigsOfRods/rigs-of-rods/pull/3096#issuecomment-1783976601\n\n";
datastream->write(header.GetBuffer(), header.GetLength());
RoR::TuneupParser::ExportTuneup(datastream, tuneup_entry->tuneup_def);

// Create spawn request while actor still exists
// Note we don't use `ActorModifyRequest::Type::RELOAD` because we don't need the bundle reloaded.
ActorSpawnRequest* srq = new ActorSpawnRequest;
Expand Down
4 changes: 4 additions & 0 deletions source/main/resources/CacheSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ enum class ModifyProjectRequestType
TUNEUP_REMOVE_PROP_RESET, //!< 'subject' is mesh name.
TUNEUP_REMOVE_FLEXBODY_SET, //!< 'subject' is mesh name.
TUNEUP_REMOVE_FLEXBODY_RESET,//!< 'subject' is mesh name.
TUNEUP_PROTECTED_PROP_SET, //!< 'subject' is mesh name.
TUNEUP_PROTECTED_PROP_RESET, //!< 'subject' is mesh name.
TUNEUP_PROTECTED_FLEXBODY_SET, //!< 'subject' is mesh name.
TUNEUP_PROTECTED_FLEXBODY_RESET,//!< 'subject' is mesh name.
PROJECT_LOAD_TUNEUP, //!< 'subject' is tuneup filename. This overwrites the auto-generated tuneup with the save.
PROJECT_RESET_TUNEUP, //!< 'subject' is empty. This resets the auto-generated tuneup to orig. values.
};
Expand Down
77 changes: 67 additions & 10 deletions source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@
#include "Console.h"
#include "GenericFileFormat.h"
#include "RigDef_Parser.h"
#include "TuneupFileFormat.h"

#include <Ogre.h>

using namespace RoR;
using namespace RigDef;

std::shared_ptr<Document::Module> AddonPartParser::TransformToRigDefModule(CacheEntryPtr& entry)
std::shared_ptr<Document::Module> AddonPartUtility::TransformToRigDefModule(CacheEntryPtr& entry)
{
App::GetCacheSystem()->LoadResource(entry);
m_addonpart_entry = entry;
try
{
Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().openResource(entry->fname, entry->resource_group);
Expand All @@ -51,8 +53,14 @@ std::shared_ptr<Document::Module> AddonPartParser::TransformToRigDefModule(Cache

while (!m_context->endOfFile())
{
// Evaluate block
if (m_context->isTokKeyword())
// (ignore 'addonpart_*' directives)
if (m_context->isTokKeyword() && m_context->getTokKeyword().find("addonpart_") != std::string::npos)
{
m_context->seekNextLine();
continue;
}
// Evaluate block
else if (m_context->isTokKeyword())
{
keyword = Parser::IdentifyKeyword(m_context->getTokKeyword());
if (keyword == Keyword::MANAGEDMATERIALS ||
Expand Down Expand Up @@ -82,15 +90,63 @@ std::shared_ptr<Document::Module> AddonPartParser::TransformToRigDefModule(Cache
{
App::GetConsole()->putMessage(
Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING,
fmt::format("Error parsing addonpart file '{}', message: {}",
fmt::format("Could not use addonpart: Error parsing file '{}', message: {}",
entry->fname, e.getFullDescription()));
return nullptr;
}
}

void AddonPartUtility::ResolveUnwantedElements(TuneupDefPtr& tuneup, CacheEntryPtr& addonpart_entry)
{
// Evaluates 'addonpart_unwanted_*' elements, respecting 'protected_*' directives in the tuneup.
// ---------------------------------------------------------------------------------------------
App::GetCacheSystem()->LoadResource(addonpart_entry);
m_addonpart_entry = addonpart_entry;

try
{
Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().openResource(addonpart_entry->fname, addonpart_entry->resource_group);

m_document = new GenericDocument();
BitMask_t options = GenericDocument::OPTION_ALLOW_SLASH_COMMENTS | GenericDocument::OPTION_ALLOW_NAKED_STRINGS;
m_document->loadFromDataStream(datastream, options);
m_context = new GenericDocContext(m_document);

while (!m_context->endOfFile())
{
if (m_context->isTokKeyword(0)
&& m_context->getTokKeyword() == "addonpart_unwanted_prop"
&& m_context->isTokString(1))
{
if (!tuneup->isPropProtected(m_context->getTokString(1)))
tuneup->remove_props.insert(m_context->getTokString(1));
}
else if (m_context->isTokKeyword(0)
&& m_context->getTokKeyword() == "addonpart_unwanted_flexbody"
&& m_context->isTokString(1))
{
if (!tuneup->isFlexbodyProtected(m_context->getTokString(1)))
tuneup->remove_flexbodies.insert(m_context->getTokString(1));
}

m_context->seekNextLine();
}

}
catch (Ogre::Exception& e)
{
App::GetConsole()->putMessage(
Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING,
fmt::format("Addonpart unwanted elements check: Error parsing file '{}', message: {}",
addonpart_entry->fname, e.getFullDescription()));
}
}


// Helpers of `TransformToRigDefModule()`, they expect `m_context` to be in position:
// These expect `m_context` to be in position:

void AddonPartParser::ProcessManagedMaterial()
void AddonPartUtility::ProcessManagedMaterial()
{
ManagedMaterial def;
int n = m_context->countLineArgs();
Expand All @@ -113,7 +169,7 @@ void AddonPartParser::ProcessManagedMaterial()
m_module->managedmaterials.push_back(def);
}

void AddonPartParser::ProcessProp()
void AddonPartUtility::ProcessProp()
{
RigDef::Prop def;
int n = m_context->countLineArgs();
Expand All @@ -122,7 +178,7 @@ void AddonPartParser::ProcessProp()
App::GetConsole()->putMessage(
Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING,
fmt::format("Error parsing addonpart file '{}': 'install_prop' has only {} arguments, expected {}",
m_entry->fname, n, 10));
m_addonpart_entry->fname, n, 10));
return;
}

Expand All @@ -144,15 +200,15 @@ void AddonPartParser::ProcessProp()
m_module->props.push_back(def);
}

void AddonPartParser::ProcessFlexbody()
void AddonPartUtility::ProcessFlexbody()
{
Flexbody def;
int n = m_context->countLineArgs();
if (n < 10)
{
App::GetConsole()->putMessage(
Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING,
fmt::format("Error parsing addonpart file '{}': flexbody has only {} arguments, expected {}", m_entry->fname, n, 10));
fmt::format("Error parsing addonpart file '{}': flexbody has only {} arguments, expected {}", m_addonpart_entry->fname, n, 10));
return;
}

Expand All @@ -177,11 +233,12 @@ void AddonPartParser::ProcessFlexbody()
{
App::GetConsole()->putMessage(
Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING,
fmt::format("Error parsing addonpart file '{}': flexbody is not followed by 'forset'!", m_entry->fname));
fmt::format("Error parsing addonpart file '{}': flexbody is not followed by 'forset'!", m_addonpart_entry->fname));
return;
}

Parser::ProcessForsetLine(def, m_context->getTokString());

m_module->flexbodies.push_back(def);
}

Loading

0 comments on commit a7bdb68

Please sign in to comment.