diff --git a/resources/materials/character.material b/resources/materials/character.material deleted file mode 100644 index 8baeae2139..0000000000 --- a/resources/materials/character.material +++ /dev/null @@ -1,30 +0,0 @@ -import * from "managed_mats.material" - -material tracks/character: RoR/Managed_Mats/Base -{ - technique BaseTechnique - { - pass BaseRender - { - depth_bias -20 - texture_unit Diffuse_Map 1 - { - texture character.dds - } - } - pass ColorChange - { - scene_blend alpha_blend - texture_unit - { - texture character-alpha.png - } - texture_unit - { - colour_op_ex blend_current_alpha src_manual src_current 0 0 0 - } - } - } -} - - diff --git a/resources/meshes/character.material b/resources/meshes/character.material deleted file mode 100644 index 49d24af29f..0000000000 --- a/resources/meshes/character.material +++ /dev/null @@ -1,23 +0,0 @@ - -material 1-Default -{ - technique - { - pass - { - ambient 0.588235 0.588235 0.588235 1 - diffuse 0.588235 0.588235 0.588235 1 - specular 0 0 0 1 10 - emissive 0.5 0.5 0.5 1 - - texture_unit - { - texture male_char01_tex.jpg - } - } - - } - -} - - diff --git a/resources/meshes/character.mesh b/resources/meshes/character.mesh deleted file mode 100755 index 014009e801..0000000000 Binary files a/resources/meshes/character.mesh and /dev/null differ diff --git a/resources/meshes/character.skeleton b/resources/meshes/character.skeleton deleted file mode 100755 index ed70792543..0000000000 Binary files a/resources/meshes/character.skeleton and /dev/null differ diff --git a/source/main/Application.cpp b/source/main/Application.cpp index 6623aed6c9..dbb5966ec7 100644 --- a/source/main/Application.cpp +++ b/source/main/Application.cpp @@ -104,6 +104,7 @@ CVar* sim_no_self_collisions; CVar* sim_gearbox_mode; CVar* sim_soft_reset_mode; CVar* sim_quickload_dialog; +CVar* sim_player_character; // Multiplayer CVar* mp_state; diff --git a/source/main/Application.h b/source/main/Application.h index 773c5bdf5b..979fb79817 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -243,6 +243,7 @@ enum VisibilityMasks enum LoaderType //!< Operation mode for GUI::MainSelector { LT_None, + LT_Character, // No script alias, invoked from Settings UI. LT_Terrain, // Invocable from GUI; No script alias, used in main menu LT_Vehicle, // Script "vehicle", ext: truck car LT_Truck, // Script "truck", ext: truck car @@ -294,6 +295,7 @@ extern CVar* sim_no_self_collisions; extern CVar* sim_gearbox_mode; extern CVar* sim_soft_reset_mode; extern CVar* sim_quickload_dialog; +extern CVar* sim_player_character; // Multiplayer extern CVar* mp_state; diff --git a/source/main/GameContext.cpp b/source/main/GameContext.cpp index 0e25e1c4a1..b476c83cd6 100644 --- a/source/main/GameContext.cpp +++ b/source/main/GameContext.cpp @@ -693,10 +693,17 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntry* entry, std::stri // -------------------------------- // Characters -void GameContext::CreatePlayerCharacter() +bool GameContext::CreatePlayerCharacter() { m_character_factory.CreateLocalCharacter(); + if (!this->GetPlayerCharacter()) + { + App::GetGuiManager()->ShowMessageBox(_L("Terrain loading error"), + "Failed to create player character, see console or 'RoR.log' for more info."); + return false; + } + // Adjust character position Ogre::Vector3 spawn_pos = App::GetSimTerrain()->getSpawnPos(); float spawn_rot = 0.0f; @@ -744,6 +751,8 @@ void GameContext::CreatePlayerCharacter() { App::GetCameraManager()->UpdateInputEvents(0.02f); } + + return true; } Character* GameContext::GetPlayerCharacter() // Convenience ~ counterpart of `GetPlayerActor()` diff --git a/source/main/GameContext.h b/source/main/GameContext.h index 81ad22707e..d77a9eef0b 100644 --- a/source/main/GameContext.h +++ b/source/main/GameContext.h @@ -139,7 +139,7 @@ class GameContext /// @name Characters /// @{ - void CreatePlayerCharacter(); //!< Terrain must be loaded + bool CreatePlayerCharacter(); //!< Terrain must be loaded Character* GetPlayerCharacter(); CharacterFactory* GetCharacterFactory() { return &m_character_factory; } diff --git a/source/main/gameplay/CharacterFactory.cpp b/source/main/gameplay/CharacterFactory.cpp index d11c237159..47a614e807 100644 --- a/source/main/gameplay/CharacterFactory.cpp +++ b/source/main/gameplay/CharacterFactory.cpp @@ -22,7 +22,9 @@ #include "CharacterFactory.h" #include "Application.h" +#include "CacheSystem.h" #include "Character.h" +#include "Console.h" #include "GfxScene.h" #include "Utils.h" @@ -32,6 +34,26 @@ CharacterFactory::CharacterFactory() { } +CharacterDocumentPtr CharacterFactory::FetchCharacterDef(CacheEntry* cache_entry) +{ + if (!cache_entry->character_def) + { + try + { + Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().openResource(cache_entry->fname, cache_entry->resource_group); + CharacterParser character_parser; + cache_entry->character_def = character_parser.ProcessOgreStream(datastream); + } + catch (Ogre::Exception& eeh) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("Could not load character, message: {}", eeh.getFullDescription())); + } + } + + return cache_entry->character_def; +} + Character* CharacterFactory::CreateLocalCharacter() { int colourNum = -1; @@ -46,7 +68,21 @@ Character* CharacterFactory::CreateLocalCharacter() } #endif // USE_SOCKETW - m_local_character = std::unique_ptr(new Character(m_character_defs[0], -1, 0, playerName, colourNum, false)); + CacheEntry* cache_entry = App::GetCacheSystem()->FindEntryByFilename(LT_Character, /*partial:*/false, App::sim_player_character->getStr()); + if (!cache_entry) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("Could not find character '{}' in mod cache.", App::sim_player_character->getStr())); + return nullptr; + } + + CharacterDocumentPtr document = this->FetchCharacterDef(cache_entry); + if (!document) + { + return nullptr; // Error already reported + } + + m_local_character = std::unique_ptr(new Character(document, -1, 0, playerName, colourNum, false)); App::GetGfxScene()->RegisterGfxCharacter(m_local_character.get()); return m_local_character.get(); } @@ -59,9 +95,29 @@ void CharacterFactory::createRemoteInstance(int sourceid, int streamid) int colour = info.colournum; Ogre::UTFString name = tryConvertUTF(info.username); - LOG(" new character for " + TOSTRING(sourceid) + ":" + TOSTRING(streamid) + ", colour: " + TOSTRING(colour)); + std::string info_str = fmt::format("player '{}' ({}:{}), colour: {}", info.clientname, sourceid, streamid, colour); + + LOG(" new character for " + info_str); + + std::string filename = App::sim_player_character->getStr(); // TBD: transmit and use the actual character used by the player + + CacheEntry* cache_entry = App::GetCacheSystem()->FindEntryByFilename(LT_Character, /*partial:*/false, filename); + if (!cache_entry) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("Could not create character for {} - character '{}' not found in mod cache.", info_str, filename)); + return; + } + + CharacterDocumentPtr document = this->FetchCharacterDef(cache_entry); + if (!document) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("Could not create character for {} - cannot load file '{}'.", info_str, cache_entry->fname)); + return; + } - Character* ch = new Character(m_character_defs[0], sourceid, streamid, name, colour, true); + Character* ch = new Character(document, sourceid, streamid, name, colour, true); App::GetGfxScene()->RegisterGfxCharacter(ch); m_remote_characters.push_back(std::unique_ptr(ch)); #endif // USE_SOCKETW diff --git a/source/main/gameplay/CharacterFactory.h b/source/main/gameplay/CharacterFactory.h index 17a97df163..405be24d6d 100644 --- a/source/main/gameplay/CharacterFactory.h +++ b/source/main/gameplay/CharacterFactory.h @@ -46,15 +46,13 @@ class CharacterFactory void DeleteAllCharacters(); void UndoRemoteActorCoupling(Actor* actor); void Update(float dt); - void DefineCharacter(CharacterDocumentPtr doc) { m_character_defs.push_back(doc); } + CharacterDocumentPtr FetchCharacterDef(CacheEntry* cache_entry); #ifdef USE_SOCKETW void handleStreamData(std::vector packet); #endif // USE_SOCKETW private: - std::vector m_character_defs; - std::unique_ptr m_local_character; std::vector> m_remote_characters; diff --git a/source/main/gui/panels/GUI_GameSettings.cpp b/source/main/gui/panels/GUI_GameSettings.cpp index e9ba79c283..8239d287bd 100644 --- a/source/main/gui/panels/GUI_GameSettings.cpp +++ b/source/main/gui/panels/GUI_GameSettings.cpp @@ -264,6 +264,17 @@ void GameSettings::DrawGameplaySettings() DrawGCheckbox(App::io_discord_rpc, _LC("GameSettings", "Discord Rich Presence")); DrawGCheckbox(App::sim_quickload_dialog, _LC("GameSettings", "Show confirm. UI dialog for quickload")); + + // Character + ImGui::TextDisabled("%s:", _LC("GameSettings", "Player character")); + ImGui::SameLine(); + ImGui::Text("%s", App::sim_player_character->getStr().c_str()); + ImGui::SameLine(); + if (ImGui::Button(_LC("GameSettings", "Select"))) + { + LoaderType* payload = new LoaderType(LoaderType::LT_Character); + App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_SELECTOR_REQUESTED, (void*)payload)); + } } void GameSettings::DrawAudioSettings() diff --git a/source/main/gui/panels/GUI_MainSelector.cpp b/source/main/gui/panels/GUI_MainSelector.cpp index 9e6acbed9b..a9bcd82b0d 100644 --- a/source/main/gui/panels/GUI_MainSelector.cpp +++ b/source/main/gui/panels/GUI_MainSelector.cpp @@ -582,28 +582,46 @@ void MainSelector::Apply() ROR_ASSERT(m_selected_entry > -1); // Programmer error DisplayEntry& sd_entry = m_display_entries[m_selected_entry]; - if (m_loader_type == LT_Terrain && - App::app_state->getEnum() == AppState::MAIN_MENU) - { - App::GetGameContext()->PushMessage(Message(MSG_SIM_LOAD_TERRN_REQUESTED, sd_entry.sde_entry->fname)); - this->Close(); - } - else if (App::app_state->getEnum() == AppState::SIMULATION) - { - LoaderType type = m_loader_type; - std::string sectionconfig; - if (sd_entry.sde_entry->sectionconfigs.size() > 0) + switch (m_loader_type) + { + case LT_Character: // Invoked by Settings UI button + App::sim_player_character->setStr(sd_entry.sde_entry->fname); + this->Close(); + break; + + case LT_Terrain: // Invoked by Main menu button + if (App::app_state->getEnum() == AppState::MAIN_MENU) { - sectionconfig = sd_entry.sde_entry->sectionconfigs[m_selected_sectionconfig]; - } - this->Close(); + App::GetGameContext()->PushMessage(Message(MSG_SIM_LOAD_TERRN_REQUESTED, sd_entry.sde_entry->fname)); + this->Close(); + } + break; - if (m_loader_type == LT_Skin && sd_entry.sde_entry == &m_dummy_skin) + default: // Vehicle in simulation (Invoked by: top menubar, hotkey, or spawner) + if (App::app_state->getEnum() == AppState::SIMULATION) { - sd_entry.sde_entry = nullptr; - } + // Make a copy because `Close()` will reset it. + LoaderType orig_loader_type = m_loader_type; - App::GetGameContext()->OnLoaderGuiApply(type, sd_entry.sde_entry, sectionconfig); + // If no config was selected, use the first one. + std::string sectionconfig; + if (sd_entry.sde_entry->sectionconfigs.size() > 0) + { + sectionconfig = sd_entry.sde_entry->sectionconfigs[m_selected_sectionconfig]; + } + + // Close the UI so that GameContext can reopen it if needed (used for skins) + this->Close(); + + // If the dummy item was selected, clear the selection. + if (orig_loader_type == LT_Skin && sd_entry.sde_entry == &m_dummy_skin) + { + sd_entry.sde_entry = nullptr; + } + + App::GetGameContext()->OnLoaderGuiApply(orig_loader_type, sd_entry.sde_entry, sectionconfig); + } + break; } } diff --git a/source/main/main.cpp b/source/main/main.cpp index 28ab070ec3..3d2b233881 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -220,20 +220,6 @@ int main(int argc, char *argv[]) App::GetGameContext()->PushMessage(Message(MSG_APP_MODCACHE_LOAD_REQUESTED)); } - // Load classic character - try - { - Ogre::DataStreamPtr stream = Ogre::ResourceGroupManager::getSingleton().openResource("classic.character"); - CharacterParser character_parser; - App::GetGameContext()->GetCharacterFactory()->DefineCharacter( - character_parser.ProcessOgreStream(stream)); - } - catch (Ogre::Exception& eeh) - { - App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, - fmt::format("error loading classic character, message:{}", eeh.getFullDescription())); - } - // Load startup scripts (console, then RoR.cfg) if (App::cli_custom_scripts->getStr() != "") { @@ -534,9 +520,9 @@ int main(int argc, char *argv[]) App::GetGuiManager()->LoadingWindow.SetProgress(5, _L("Loading resources")); App::GetContentManager()->LoadGameplayResources(); - if (App::GetGameContext()->LoadTerrain(m.description)) + if (App::GetGameContext()->LoadTerrain(m.description) + && App::GetGameContext()->CreatePlayerCharacter()) { - App::GetGameContext()->CreatePlayerCharacter(); // Spawn preselected vehicle; commandline has precedence if (App::cli_preset_vehicle->getStr() != "") App::GetGameContext()->SpawnPreselectedActor(App::cli_preset_vehicle->getStr(), App::cli_preset_veh_config->getStr()); // Needs character for position @@ -563,8 +549,8 @@ int main(int argc, char *argv[]) if (App::mp_state->getEnum() == MpState::CONNECTED) { App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_NOTICE, - fmt::format(_LC("ChatBox", "Press {} to start chatting"), - App::GetInputEngine()->getEventCommandTrimmed(EV_COMMON_ENTER_CHATMODE)), "lightbulb.png"); + fmt::format(_LC("ChatBox", "Press {} to start chatting"), + App::GetInputEngine()->getEventCommandTrimmed(EV_COMMON_ENTER_CHATMODE)), "lightbulb.png"); } #endif // USE_SOCKETW if (App::io_outgauge_mode->getInt() > 0) @@ -574,6 +560,13 @@ int main(int argc, char *argv[]) } else { + // Failed to load terrain or character - messagebox is already displayed + if (App::GetSimTerrain()) + { + delete App::GetSimTerrain(); + App::SetSimTerrain(nullptr); + } + if (App::mp_state->getEnum() == MpState::CONNECTED) { App::GetGameContext()->PushMessage(Message(MSG_NET_DISCONNECT_REQUESTED)); diff --git a/source/main/resources/CacheSystem.cpp b/source/main/resources/CacheSystem.cpp index 034bcb41e5..6f9e531eab 100644 --- a/source/main/resources/CacheSystem.cpp +++ b/source/main/resources/CacheSystem.cpp @@ -25,26 +25,25 @@ #include "CacheSystem.h" -#include #include "Application.h" -#include "SimData.h" +#include "CharacterFileFormat.h" #include "ContentManager.h" #include "ErrorUtils.h" +#include "GfxActor.h" +#include "GfxScene.h" #include "GUI_LoadingWindow.h" #include "GUI_GameMainMenu.h" #include "GUIManager.h" -#include "GfxActor.h" -#include "GfxScene.h" #include "Language.h" #include "PlatformUtils.h" #include "RigDef_Parser.h" - +#include "SimData.h" #include "SkinFileFormat.h" #include "Terrain.h" #include "Terrn2FileFormat.h" #include "Utils.h" -#include +#include #include #include #include @@ -111,6 +110,7 @@ CacheSystem::CacheSystem() m_known_extensions.push_back("load"); m_known_extensions.push_back("train"); m_known_extensions.push_back("skin"); + m_known_extensions.push_back("character"); } void CacheSystem::LoadModCache(CacheValidity validity) @@ -642,6 +642,11 @@ void CacheSystem::AddFile(String group, Ogre::FileInfo f, String ext) new_entries.resize(1); FillTerrainDetailInfo(new_entries.back(), ds, f.filename); } + else if (ext == "character") + { + new_entries.resize(1); + FillCharacterDetailInfo(new_entries.back(), ds); + } else if (ext == "skin") { auto new_skins = RoR::SkinParser::ParseSkins(ds); @@ -1059,6 +1064,14 @@ void CacheSystem::FillTerrainDetailInfo(CacheEntry& entry, Ogre::DataStreamPtr d entry.version = def.version; } +void CacheSystem::FillCharacterDetailInfo(CacheEntry& entry, Ogre::DataStreamPtr datastream) +{ + CharacterParser parser; + CharacterDocumentPtr doc = parser.ProcessOgreStream(datastream); + + entry.dname = doc->character_name; +} + bool CacheSystem::CheckResourceLoaded(Ogre::String & filename) { Ogre::String group = ""; @@ -1118,6 +1131,13 @@ void CacheSystem::LoadResource(CacheEntry& t) ResourceGroupManager::getSingleton().createResourceGroup(group, /*inGlobalPool=*/true); ResourceGroupManager::getSingleton().addResourceLocation(t.resource_bundle_path, t.resource_bundle_type, group); } + else if (t.fext == "character") + { + // This is a character mod bundle - use `inGlobalPool=false` to prevent resource name conflicts. + // See bottom 'note' at https://ogrecave.github.io/ogre/api/latest/_resource-_management.html#Resource-Groups + ResourceGroupManager::getSingleton().createResourceGroup(group, /*inGlobalPool=*/false); + ResourceGroupManager::getSingleton().addResourceLocation(t.resource_bundle_path, t.resource_bundle_type, group); + } else if (t.fext == "skin") { // This is a SkinZip bundle - use `inGlobalPool=false` to prevent resource name conflicts. @@ -1270,8 +1290,10 @@ size_t CacheSystem::Query(CacheQuery& query) bool add = false; if (entry.fext == "terrn2") add = (query.cqy_filter_type == LT_Terrain); - if (entry.fext == "skin") + else if (entry.fext == "skin") add = (query.cqy_filter_type == LT_Skin); + else if (entry.fext == "character") + add = (query.cqy_filter_type == LT_Character); else if (entry.fext == "truck") add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Vehicle || query.cqy_filter_type == LT_Truck); else if (entry.fext == "car") diff --git a/source/main/resources/CacheSystem.h b/source/main/resources/CacheSystem.h index 842135a376..41d455756b 100644 --- a/source/main/resources/CacheSystem.h +++ b/source/main/resources/CacheSystem.h @@ -27,6 +27,7 @@ #pragma once #include "Application.h" +#include "CharacterFileFormat.h" #include "Language.h" #include "RigDef_File.h" #include "SimData.h" @@ -84,6 +85,7 @@ class CacheEntry 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 + CharacterDocumentPtr character_def; //!< Cached character definition // following all TRUCK detail information: Ogre::String description; @@ -238,6 +240,7 @@ class CacheSystem : public ZeroedMemoryAllocator void FillTerrainDetailInfo(CacheEntry &entry, Ogre::DataStreamPtr ds, Ogre::String fname); void FillTruckDetailInfo(CacheEntry &entry, Ogre::DataStreamPtr ds, Ogre::String fname, Ogre::String group); + void FillCharacterDetailInfo(CacheEntry& entry, Ogre::DataStreamPtr ds); void GenerateHashFromFilenames(); //!< For quick detection of added/removed content diff --git a/source/main/system/CVar.cpp b/source/main/system/CVar.cpp index 02346b7162..3284134b77 100644 --- a/source/main/system/CVar.cpp +++ b/source/main/system/CVar.cpp @@ -57,6 +57,7 @@ void Console::cVarSetupBuiltins() App::sim_gearbox_mode = this->cVarCreate("sim_gearbox_mode", "GearboxMode", CVAR_ARCHIVE | CVAR_TYPE_INT); App::sim_soft_reset_mode = this->cVarCreate("sim_soft_reset_mode", "", CVAR_TYPE_BOOL, "false"); App::sim_quickload_dialog = this->cVarCreate("sim_quickload_dialog", "", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "true"); + App::sim_player_character = this->cVarCreate("sim_player_character", "", CVAR_ARCHIVE, "classic.character"); App::mp_state = this->cVarCreate("mp_state", "", CVAR_TYPE_INT, "0"/*(int)MpState::DISABLED*/); App::mp_join_on_startup = this->cVarCreate("mp_join_on_startup", "Auto connect", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false");