Skip to content

Commit

Permalink
🎮 Addon part modding system ✨
Browse files Browse the repository at this point in the history
STATUS: ALPHA! Glitchy, only adding a part works, the remove-button is dummy.

This adds a new mod type "addonpart" (file pattern *.addonpart) which works as a partial truck file - when selected, it adds additional elements to it. Currently supported features are: managedmaterials, props, flexbodies. The point is to automate what users had to do manually before: https://docs.rigsofrods.org/tools-tutorials/addons/

The addon parts receive all usual treatment from modcache: they can have preview images, name + description are always displayed, they can be searched for, the MainSelectorUI can be opened in mode 'LT_AddonPart' to show only addon parts. In addition, the search can be filtered by a vehicle GUID, much like with SkinZips. This is an example addonpart (port of https://forum.rigsofrods.org/resources/heavy-bumper-for-the-chevy-k3500.461/):

```
addonpart_name "Heavy Bumper for the Chevy K3500 (Prop, Black)"
addonpart_description "For use with GMT400 88-98 Chevy Truck Pack"

;addonpart_guid <guid>  ~ multiple GUIDs can be specified
addonpart_guid 057b21c8-cb54-496d-88ce-4855a8d6d43d
; * 1990 Chevy 454ss,  * 1990 Chevy k3500    * 1990 Chevy K3500HD
addonpart_guid 6a53ad83-255d-4d5e-bf14-ca0714e5228d
; * 1990 Chevy k2500 Blazer, -SAS, -Suburban

managedmaterials
    CHeavyBumper		mesh_standard		HeavyBumper.dds		HeavyBumper_s.dds

props
    39, 62, 34,    0.50,    0.32,     -0.5,   0,    180,    180, CHeavyBumper.mesh
```

There's a new menu in TopMenubarUI: "Tuning". It lists already installed addonparts and contains [add parts] button which opens MainSelectorUI in 'LT_AddonPart' mode, with GUID filtering for the current vehicle.

To remember what addonparts user selected, an additional mod type was added: "tuneup" (file pattern *.tuneup) - this works like ".skin for addonparts" and has intentionally similar syntax. There are 2 power tools:
* "use_addonpart <filename>" to install an addonpart on the vehicle (multiple can be used!)
* "remove_prop <meshname>" to strip an existing prop from the vehicle.

Note: An option to replace prop was intentionally not implemented because that would be a duplicate feature with addonparts. It's up to user to remove props if and only if they don't want them together with new parts.

When user uses addonpart with a vehicle for the first time, the game creates a .tuneup file for it  in {Documents\My Games\Rigs of Rods\projects\}. When spawning the vehicle next time, the .tuneup is loaded automatically. In the future the menu will be able to save custom tuneups. Here's an example:

```
Tuned 1990 Chevy 454ss
{
      use_addonpart = CHeavyBumperBlackProp.addonpart
	preview =
	description =
	author_name = ohlidalp
	author_id = -1
	category_id = 8100
	guid = 057b21c8-cb54-496d-88ce-4855a8d6d43d
}
```

Under the hood, applying the addonparts to the vehicle works entirely in system memory, no truck files are modified or written.
* "use_addonpart" works by literally introducing a fake 'section/end_section' to the truck file parsed in memory - see `class RigDef::File::Module` and function `ActorSpawner::ConfigureAddonParts()` for details.
* "remove_prop" works as last-minute check in `ActorSpawner::ProcessProp()`, skipping props which are blacklisted.
  • Loading branch information
ohlidalp committed Feb 4, 2024
1 parent 8610701 commit b1f4d23
Show file tree
Hide file tree
Showing 28 changed files with 1,058 additions and 142 deletions.
6 changes: 4 additions & 2 deletions source/main/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This source file is part of Rigs of Rods
Copyright 2005-2012 Pierre-Michel Ricordel
Copyright 2007-2012 Thomas Fischer
Copyright 2013-2022 Petr Ohlidal
Copyright 2013-2023 Petr Ohlidal
For more information, see http://www.rigsofrods.org/
Expand Down Expand Up @@ -273,7 +273,9 @@ enum LoaderType //!< Operation mode for GUI::MainSelector
LT_Load, // Script "load", ext: load
LT_Extension, // Script "extension", ext: trailer load
LT_Skin, // No script alias, invoked automatically
LT_AllBeam // Invocable from GUI; Script "all", ext: truck car boat airplane train load
LT_AllBeam, // Invocable from GUI; Script "all", ext: truck car boat airplane train load
LT_AddonPart, // No script alias, invoked manually, ext: addonpart
LT_Tuneup, // No script alias, invoked manually, ext: tuneup
};

// ------------------------------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions source/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ set(SOURCE_FILES
physics/water/ScrewProp.{h,cpp}
resources/CacheSystem.{h,cpp}
resources/ContentManager.{h,cpp}
resources/addonpart_fileformat/AddonPartFileFormat.{h,cpp}
resources/otc_fileformat/OTCFileFormat.{h,cpp}
resources/odef_fileformat/ODefFileFormat.{h,cpp}
resources/rig_def_fileformat/RigDef_File.{h,cpp}
Expand All @@ -193,6 +194,7 @@ set(SOURCE_FILES
resources/skin_fileformat/SkinFileFormat.{h,cpp}
resources/terrn2_fileformat/Terrn2FileFormat.{h,cpp}
resources/tobj_fileformat/TObjFileFormat.{h,cpp}
resources/tuneup_fileformat/TuneupFileFormat.{h,cpp}
system/AppCommandLine.cpp
system/AppConfig.cpp
system/Console.{h,cpp}
Expand Down Expand Up @@ -345,12 +347,14 @@ target_include_directories(${BINNAME} PRIVATE
physics/utils
physics/water
resources
resources/addonpart_fileformat
resources/odef_fileformat/
resources/otc_fileformat/
resources/rig_def_fileformat
resources/skin_fileformat
resources/terrn2_fileformat
resources/tobj_fileformat
resources/tuneup_fileformat
system
scripting
scripting/bindings
Expand Down
4 changes: 3 additions & 1 deletion source/main/ForwardDeclarations.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ namespace RoR
struct Terrn2Def;
class Terrn2Parser;
struct Terrn2Telepoint;
class TorqueCurve;
class ThreadPool;
class TorqueCurve;
struct TuneupDef;
class VehicleAI;
class VideoCamera;

Expand Down Expand Up @@ -178,6 +179,7 @@ namespace RoR
typedef RefCountingObjectPtr<SoundScriptInstance> SoundScriptInstancePtr;
typedef RefCountingObjectPtr<SoundScriptTemplate> SoundScriptTemplatePtr;
typedef RefCountingObjectPtr<Terrain> TerrainPtr;
typedef RefCountingObjectPtr<TuneupDef> TuneupDefPtr;
typedef RefCountingObjectPtr<VehicleAI> VehicleAIPtr;

typedef std::vector<ActorPtr> ActorPtrVec;
Expand Down
88 changes: 85 additions & 3 deletions source/main/GameContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#include "SkyXManager.h"
#include "SoundScriptManager.h"
#include "Terrain.h"
#include "TuneupFileFormat.h"
#include "Utils.h"
#include "VehicleAI.h"
#include "GUI_VehicleButtons.h"
Expand Down Expand Up @@ -187,6 +188,16 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq)
m_last_skin_selection = rq.asr_skin_entry;
m_last_section_config = rq.asr_config;

// Check and attach auto-generated .tuneup, if it exists (if not, it will be created on-demand when using top menubar 'Tuning' menu).
CacheQuery query;
query.cqy_filter_type = LT_Tuneup;
query.cqy_filter_category_id = CID_TuneupsAuto;
query.cqy_filter_guid = rq.asr_cache_entry->guid;
if (App::GetCacheSystem()->Query(query) > 0)
{
rq.asr_tuneup_entry = query.cqy_results[0].cqr_entry;
}

if (rq.asr_spawnbox == nullptr)
{
if (m_player_actor != nullptr)
Expand Down Expand Up @@ -220,15 +231,24 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq)
return nullptr; // Error already reported
}

if (rq.asr_skin_entry != nullptr)
if (rq.asr_skin_entry)
{
std::shared_ptr<SkinDef> skin_def = App::GetCacheSystem()->FetchSkinDef(rq.asr_skin_entry); // Make sure it exists
if (skin_def == nullptr)
App::GetCacheSystem()->LoadResource(rq.asr_skin_entry); // Also loads associated .skin file.
if (!rq.asr_skin_entry->skin_def) // Make sure .skin was loaded OK.
{
rq.asr_skin_entry = nullptr; // Error already logged
}
}

if (rq.asr_tuneup_entry)
{
App::GetCacheSystem()->LoadResource(rq.asr_tuneup_entry); // Also loads associated .tuneup file.
if (!rq.asr_tuneup_entry->tuneup_def) // Make sure .tuneup was loaded OK.
{
rq.asr_tuneup_entry = nullptr; // Error already logged
}
}

#ifdef USE_SOCKETW
if (rq.asr_origin != ActorSpawnRequest::Origin::NETWORK)
{
Expand Down Expand Up @@ -366,6 +386,7 @@ void GameContext::ModifyActor(ActorModifyRequest& rq)
srq->asr_rotation = Ogre::Quaternion(Ogre::Degree(270) - Ogre::Radian(actor->getRotation()), Ogre::Vector3::UNIT_Y);
srq->asr_config = actor->getSectionConfig();
srq->asr_skin_entry = actor->getUsedSkin();
srq->asr_tuneup_entry = actor->getUsedTuneup();
srq->asr_cache_entry= entry;
srq->asr_debugview = (int)actor->GetGfxActor()->GetDebugView();
srq->asr_origin = ActorSpawnRequest::Origin::USER;
Expand All @@ -376,6 +397,56 @@ void GameContext::ModifyActor(ActorModifyRequest& rq)
// Load our actor again, but only after all actors are deleted.
this->ChainMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)srq));
}
else if (rq.amr_type == ActorModifyRequest::Type::INSTALL_ADDONPART_AND_RELOAD)
{
CacheEntryPtr entry = App::GetCacheSystem()->FindEntryByFilename(LT_AllBeam, /*partial=*/false, actor->ar_filename);
if (!entry)
{
Str<500> msg; msg <<"Cannot reload vehicle; file '" << actor->ar_filename << "' not found in ModCache.";
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_ERROR, msg.ToCStr());
return;
}

// Make sure the actor has a default .tuneup project assigned. If not, create it.
CacheEntryPtr tuneup_entry = actor->getUsedTuneup();
if (!tuneup_entry)
{
CreateProjectRequest req;
req.cpr_create_tuneup = true;
req.cpr_source_entry = entry;
req.cpr_name = fmt::format("Tuned {}", actor->getTruckName());

tuneup_entry = App::GetCacheSystem()->CreateProject(&req);
}

// Add the requested addonpart to the TuneupDef document.
tuneup_entry->tuneup_def->use_addonparts.push_back(rq.amr_addonpart->fname);

// If this is the auto-generated tuneup, immediatelly update the .tuneup file (user-saved tuneups are only modified on demand).
if (tuneup_entry->categoryid == CID_TuneupsAuto)
{
Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().openResource(tuneup_entry->fname, tuneup_entry->resource_group);
RoR::TuneupParser::ExportTuneup(datastream, tuneup_entry->tuneup_def);
}

// Create spawn request while actor still exists
// Note we don't use `ActorModifyRequest::Type::RELOAD` because we don't need the bundle reloaded.
ActorSpawnRequest* srq = new ActorSpawnRequest;
srq->asr_position = Ogre::Vector3(actor->getPosition().x, actor->getMinHeight(), actor->getPosition().z);
srq->asr_rotation = Ogre::Quaternion(Ogre::Degree(270) - Ogre::Radian(actor->getRotation()), Ogre::Vector3::UNIT_Y);
srq->asr_config = actor->getSectionConfig();
srq->asr_skin_entry = actor->getUsedSkin();
srq->asr_tuneup_entry = tuneup_entry;
srq->asr_cache_entry = entry;
srq->asr_debugview = (int)actor->GetGfxActor()->GetDebugView();
srq->asr_origin = ActorSpawnRequest::Origin::USER;

// Remove the actor
this->PushMessage(Message(MSG_SIM_DELETE_ACTOR_REQUESTED, (void*)new ActorPtr(actor)));

// Load our actor again, but only after it was deleted.
this->ChainMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)srq));
}
}

void GameContext::DeleteActor(ActorPtr actor)
Expand Down Expand Up @@ -661,6 +732,17 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntryPtr entry, std::st
bool spawn_now = false;
switch (type)
{
case LT_AddonPart:
if (m_player_actor)
{
ActorModifyRequest* req = new ActorModifyRequest();
req->amr_actor = m_player_actor->ar_instance_id;
req->amr_addonpart = entry;
req->amr_type = ActorModifyRequest::Type::INSTALL_ADDONPART_AND_RELOAD;
this->PushMessage(Message(MSG_SIM_MODIFY_ACTOR_REQUESTED, req));
}
break;

case LT_Skin:
if (entry != m_dummy_cache_selection)
{
Expand Down
5 changes: 4 additions & 1 deletion source/main/gui/panels/GUI_SurveyMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,10 @@ void SurveyMap::CreateTerrainTextures()
mMapCenterOffset = Ogre::Vector2::ZERO; // Reset, maybe new terrain was loaded
if (mMapTexture)
{
Ogre::TextureManager::getSingleton().unload(mMapTexture->getName(), mMapTexture->getGroup());
if (mMapTexture->getLoadingState() != Ogre::Resource::LoadingState::LOADSTATE_UNLOADED)
{
Ogre::TextureManager::getSingleton().unload(mMapTexture->getName(), mMapTexture->getGroup());
}
Ogre::TextureManager::getSingleton().remove(mMapTexture->getName(), mMapTexture->getGroup());
mMapTexture.setNull();
}
Expand Down
124 changes: 120 additions & 4 deletions source/main/gui/panels/GUI_TopMenubar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "Replay.h"
#include "SkyManager.h"
#include "Terrain.h"
#include "TuneupFileFormat.h"
#include "Water.h"
#include "ScriptEngine.h"
#include "Console.h"
Expand Down Expand Up @@ -129,11 +130,12 @@ void TopMenubar::Update()
std::string settings_title = _LC("TopMenubar", "Settings");
std::string tools_title = _LC("TopMenubar", "Tools");
std::string ai_title = _LC("TopMenubar", "Vehicle AI");
std::string tuning_title = _LC("TopMenubar", "Tuning");

int NUM_BUTTONS = 5;
int NUM_BUTTONS = 6;
if (App::mp_state->getEnum<MpState>() != MpState::CONNECTED)
{
NUM_BUTTONS = 6;
NUM_BUTTONS = 7;
}

float menubar_content_width =
Expand All @@ -143,7 +145,8 @@ void TopMenubar::Update()
ImGui::CalcTextSize(actors_title.c_str()).x +
ImGui::CalcTextSize(savegames_title.c_str()).x +
ImGui::CalcTextSize(settings_title.c_str()).x +
ImGui::CalcTextSize(tools_title.c_str()).x;
ImGui::CalcTextSize(tools_title.c_str()).x +
ImGui::CalcTextSize(tuning_title.c_str()).x;

if (App::mp_state->getEnum<MpState>() != MpState::CONNECTED)
{
Expand Down Expand Up @@ -183,9 +186,20 @@ void TopMenubar::Update()
m_open_menu = TopMenu::TOPMENU_SIM;
}

ImGui::SameLine();

// The 'Tuning' button
ImVec2 tuning_cursor = ImGui::GetCursorPos();
ImGui::Button(tuning_title.c_str());
if ((m_open_menu != TopMenu::TOPMENU_TUNING) && ImGui::IsItemHovered())
{
m_open_menu = TopMenu::TOPMENU_TUNING;
}

ImGui::SameLine();

// The 'AI' button
ImVec2 ai_cursor = ImVec2(0, 0);

if (App::mp_state->getEnum<MpState>() != MpState::CONNECTED)
{
ImGui::SameLine();
Expand Down Expand Up @@ -1407,6 +1421,108 @@ void TopMenubar::Update()
}
break;

case TopMenu::TOPMENU_TUNING:
menu_pos.y = window_pos.y + tuning_cursor.y + MENU_Y_OFFSET;
menu_pos.x = tuning_cursor.x + window_pos.x - ImGui::GetStyle().WindowPadding.x;
ImGui::SetNextWindowPos(menu_pos);
if (ImGui::Begin(_LC("TopMenubar", "Tuning menu"), nullptr, static_cast<ImGuiWindowFlags_>(flags)))
{
const ActorPtr& actor = App::GetGameContext()->GetPlayerActor();
if (!actor)
{
ImGui::PushStyleColor(ImGuiCol_Text, GRAY_HINT_TEXT);
ImGui::Text("%s", _LC("TopMenubar", "You are on foot."));
ImGui::Text("%s", _LC("TopMenubar", "Enter a vehicle to tune it."));
ImGui::PopStyleColor();
}
else
{
CacheEntryPtr& tuneup_entry = actor->getUsedTuneup();
if (!tuneup_entry)
{
ImGui::PushStyleColor(ImGuiCol_Text, GRAY_HINT_TEXT);
ImGui::Text(_LC("TopMenubar", "Not tuned yet."));
ImGui::PopStyleColor();
}
else
{
App::GetCacheSystem()->LoadResource(tuneup_entry);
ROR_ASSERT(tuneup_entry->resource_group != "");
ROR_ASSERT(tuneup_entry->tuneup_def != nullptr);
if (tuneup_entry->tuneup_def->use_addonparts.size() == 0)
{
ImGui::PushStyleColor(ImGuiCol_Text, GRAY_HINT_TEXT);
ImGui::Text(_LC("TopMenubar", "No addon parts."));
ImGui::PopStyleColor();
}
else
{
ImGui::TextDisabled(_LC("TopMenubar", "Used parts:"));
std::string remove_addonpart;
for (const std::string& addonpart: tuneup_entry->tuneup_def->use_addonparts)
{
ImGui::PushID(addonpart.c_str());

ImGui::PushStyleColor(ImGuiCol_Text, RED_TEXT);
if (ImGui::Button(" X "))
{
remove_addonpart = addonpart;
}
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::Text("%s", addonpart.c_str());

ImGui::PopID(); // addonpart.c_str()
}

if (remove_addonpart != "")
{
ActorModifyRequest* req = new ActorModifyRequest();
req->amr_actor = actor;
req->amr_type = ActorModifyRequest::Type::REMOVE_ADDONPART_AND_RELOAD;
req->amr_addonpart_fname = remove_addonpart;
req->amr_addonpart = App::GetCacheSystem()->FindEntryByFilename(LT_AddonPart, /*partial:*/false, remove_addonpart);
if (req->amr_addonpart)
{
App::GetGameContext()->PushMessage(Message(MSG_SIM_MODIFY_ACTOR_REQUESTED, req));
}
else
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING,
fmt::format(_LC("TopMenubar", "Addon part '{}' not found (likely uninstalled). Removing from the part list.")));
}
}
}
}

ImGui::Separator();

if (ImGui::Button(_LC("TopMenubar", "Select parts")))
{
CacheEntryPtr actor_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AllBeam, /*partial:*/false, actor->getTruckFileName());
if (actor_entry && !actor_entry->deleted)
{
Message m(MSG_GUI_OPEN_SELECTOR_REQUESTED);
m.payload = new LoaderType(LT_AddonPart);
m.description = actor_entry->guid;
App::GetGameContext()->PushMessage(m);
}
else
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format(_LC("TopMenubar", "Cannot add parts to '{}' - No valid mod cache entry"), actor->getTruckName()));
}
}
}

m_open_menu_hoverbox_min = menu_pos;
m_open_menu_hoverbox_max.x = menu_pos.x + ImGui::GetWindowWidth();
m_open_menu_hoverbox_max.y = menu_pos.y + ImGui::GetWindowHeight();
App::GetGuiManager()->RequestGuiCaptureKeyboard(ImGui::IsWindowHovered());
ImGui::End();
}
break;

default:
m_open_menu_hoverbox_min = ImVec2(0,0);
m_open_menu_hoverbox_max = ImVec2(0,0);
Expand Down
2 changes: 1 addition & 1 deletion source/main/gui/panels/GUI_TopMenubar.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,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);

enum class TopMenu { TOPMENU_NONE, TOPMENU_SIM, TOPMENU_ACTORS, TOPMENU_SAVEGAMES, TOPMENU_SETTINGS, TOPMENU_TOOLS, TOPMENU_AI };
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 };

TopMenubar(): m_open_menu(), m_daytime(0), m_quickload(false), m_confirm_remove_all(false) {}
Expand Down
Loading

0 comments on commit b1f4d23

Please sign in to comment.