diff --git a/resources/textures/character-alpha.png b/resources/default_character/character-alpha.png similarity index 100% rename from resources/textures/character-alpha.png rename to resources/default_character/character-alpha.png diff --git a/resources/textures/character.dds b/resources/default_character/character.dds similarity index 100% rename from resources/textures/character.dds rename to resources/default_character/character.dds diff --git a/resources/materials/character.material b/resources/default_character/character.material similarity index 55% rename from resources/materials/character.material rename to resources/default_character/character.material index 8baeae2139..7599e64e9c 100644 --- a/resources/materials/character.material +++ b/resources/default_character/character.material @@ -1,6 +1,6 @@ import * from "managed_mats.material" -material tracks/character: RoR/Managed_Mats/Base +material character: RoR/Managed_Mats/Base { technique BaseTechnique { @@ -12,19 +12,21 @@ material tracks/character: RoR/Managed_Mats/Base texture character.dds } } + + // The multiplayer colorization pass - must be named "ColorChange" pass ColorChange { scene_blend alpha_blend + // The alpha mask texture unit - name doesn't matter. texture_unit { texture character-alpha.png } - texture_unit + // The color texture unit - must be named "PlayerColor" + texture_unit "PlayerColor" { colour_op_ex blend_current_alpha src_manual src_current 0 0 0 } } } -} - - +} \ No newline at end of file diff --git a/resources/meshes/character.mesh b/resources/default_character/character.mesh old mode 100755 new mode 100644 similarity index 100% rename from resources/meshes/character.mesh rename to resources/default_character/character.mesh diff --git a/resources/meshes/character.skeleton b/resources/default_character/character.skeleton old mode 100755 new mode 100644 similarity index 100% rename from resources/meshes/character.skeleton rename to resources/default_character/character.skeleton diff --git a/resources/default_character/default.character b/resources/default_character/default.character new file mode 100644 index 0000000000..65a5ec9e40 --- /dev/null +++ b/resources/default_character/default.character @@ -0,0 +1,200 @@ +; The default character (aka RoRbot) +; ----------------------------------------------------------------------------- +; +; This file format lets you compose 'actions' from one or more +; skeletal animations exported from a 3D modelling tool, +; by responding to SITUATION_ and CONTROL_ flags sent by the game. +; NOTE each 'action' is evaluated separately, there is no either-or relation, +; so you must always blacklist all non-conforming flags to avoid conflicts. +; +; For explanation of individual parameters, +; see file source/main/resources/character_fileformat/CharacterFileFormat.h +; NOTE ON TERMINOLOGY: 'anim' always means skeletal animation, not the 'action'. +; +; For authoritative list of SITUATION_ and CONTROL_ flags, see file RoRnet.h +; +; Tip: in game, use 'Pose Util' UI panel, +; open it from top menubar >> Tools >> Character pose util. +; +; ----------------------------------------------------------------------------- + +; Name displayed in the Selector UI - required +character_name "Default character" + +; required for skinning - use https://guidgenerator.com for your mods. +character_guid "baef1af5-2854-48c3-af05-5087da2d773b" + +; Name of the mesh file - required. +mesh_name "character.mesh" + +; Scaling factor of the mesh - optional, default is 1.0 on all axes. +; NOTE: the 0.02 factor used to be hardcoded because the default character mesh is oversized like that. +mesh_scale 0.02 0.02 0.02 + +begin_action + action_description "driving" + for_situation SITUATION_DRIVING + anim_name "Driving" + playback_time_ratio 0.f + playback_steering_ratio -1.f + anim_continuous false + source_percentual true + anim_neutral_mid true + playback_trim 0.01f +end_action + +begin_action + action_description "swimming" + for_situation SITUATION_IN_DEEP_WATER + for_control CONTROL_MOVE_FORWARD + anim_name "Swim_loop" + playback_h_speed_ratio 1.f + playback_time_ratio 1.f +end_action + +begin_action + action_description "swimming on spot" + for_situation SITUATION_IN_DEEP_WATER + except_control CONTROL_MOVE_FORWARD + anim_name "Spot_swim" + playback_time_ratio 1.f +end_action + +begin_action + action_description "running" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + for_control CONTROL_MOVE_FORWARD + for_control CONTROL_RUN + anim_name "Run" + playback_time_ratio 1.f + playback_h_speed_ratio 1.f +end_action + +begin_action + action_description "walking forward" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + for_control CONTROL_MOVE_FORWARD + except_control CONTROL_RUN + ; when pressing both FORWARD+BACKWARD keys, FORWARD wins in game movement so let it win here also. + except_control CONTROL_MOVE_BACKWARD + anim_name "Walk" + playback_time_ratio 1.f + playback_h_speed_ratio 1.f +end_action + +begin_action + action_description "walking backward" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + for_control CONTROL_MOVE_BACKWARD + ; Do not mix the walk animation into the run animation if user presses both RUN+MOVE_BACKWARDS. + except_control CONTROL_RUN + anim_name "Walk" + playback_time_ratio -1.f + playback_h_speed_ratio 1.f +end_action + +begin_action + action_description "side stepping right" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + for_control CONTROL_SIDESTEP_RIGHT + except_control CONTROL_MOVE_FORWARD + except_control CONTROL_MOVE_BACKWARD + ; Pressing both SIDESTEP+TURN controls makes game combine the movements, so also combine the animations. + ;except_control CONTROL_TURN_RIGHT + ;except_control CONTROL_TURN_LEFT + except_control CONTROL_RUN + anim_name "Side_step" + playback_time_ratio 1.f + anim_autorestart true +end_action + +begin_action + action_description "side stepping left" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + for_control CONTROL_SIDESTEP_LEFT + except_control CONTROL_MOVE_FORWARD + except_control CONTROL_MOVE_BACKWARD + ; Pressing both SIDESTEP+TURN controls makes game combine the movements, so also combine the animations. + ;except_control CONTROL_TURN_RIGHT + ;except_control CONTROL_TURN_LEFT + except_control CONTROL_RUN + anim_name "Side_step" + playback_time_ratio -1.f + anim_autorestart true +end_action + +begin_action + action_description "turning left" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + for_control CONTROL_TURN_LEFT + ; Pressing both SIDESTEP+TURN controls makes game combine the movements, so also combine the animations. + ;except_control CONTROL_SIDESTEP_LEFT + ;except_control CONTROL_SIDESTEP_RIGHT + except_control CONTROL_MOVE_FORWARD + except_control CONTROL_MOVE_BACKWARD + except_control CONTROL_RUN + anim_name "Turn" + playback_time_ratio 1.f + anim_autorestart true +end_action + +begin_action + action_description "turning right" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + for_control CONTROL_TURN_RIGHT + ; Pressing both SIDESTEP+TURN controls makes game combine the movements, so also combine the animations. + ;except_control CONTROL_SIDESTEP_LEFT + ;except_control CONTROL_SIDESTEP_RIGHT + except_control CONTROL_MOVE_FORWARD + except_control CONTROL_MOVE_BACKWARD + except_control CONTROL_RUN + anim_name "Turn" + playback_time_ratio -1.f + anim_autorestart true +end_action + +begin_action + action_description "idle standing" + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + except_situation SITUATION_CUSTOM_MODE_01 + except_control CONTROL_TURN_RIGHT + except_control CONTROL_TURN_LEFT + except_control CONTROL_SIDESTEP_LEFT + except_control CONTROL_SIDESTEP_RIGHT + except_control CONTROL_MOVE_FORWARD + except_control CONTROL_MOVE_BACKWARD + except_control CONTROL_RUN + except_control CONTROL_CUSTOM_01 + anim_name "Idle_sway" + playback_time_ratio 1.f +end_action + +begin_action + action_description "custom action 01 test" + for_control CONTROL_CUSTOM_ACTION_01 + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + except_situation SITUATION_CUSTOM_MODE_01 + anim_name "Spot_swim" + playback_time_ratio 10.0 + anim_continuous false +end_action + +begin_action + action_description "custom mode 01 test" + for_situation SITUATION_CUSTOM_MODE_01 + except_situation SITUATION_IN_DEEP_WATER + except_situation SITUATION_DRIVING + anim_name "Driving" + playback_time_ratio 2.0 +end_action + + 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/source/main/Application.cpp b/source/main/Application.cpp index 828a47f330..c4053da3c8 100644 --- a/source/main/Application.cpp +++ b/source/main/Application.cpp @@ -108,6 +108,8 @@ CVar* sim_no_self_collisions; CVar* sim_gearbox_mode; CVar* sim_soft_reset_mode; CVar* sim_quickload_dialog; +CVar* sim_player_character; +CVar* sim_player_character_skin; // Multiplayer CVar* mp_state; @@ -122,6 +124,8 @@ CVar* mp_server_password; CVar* mp_player_name; CVar* mp_player_token; CVar* mp_api_url; +CVar* mp_override_character; +CVar* mp_override_character_skin; // New remote API CVar* remote_query_url; diff --git a/source/main/Application.h b/source/main/Application.h index 7f10af53fe..a3a163a45b 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -39,7 +39,7 @@ #define ROR_ASSERT(_EXPR) assert(_EXPR) -#define CHARACTER_ANIM_NAME_LEN 10 // Restricted for networking +#define DEFAULT_CHARACTER_FILE "default.character" // Located in file 'resources/default_character.zip' // Legacy macros #define TOSTRING(x) Ogre::StringConverter::toString(x) @@ -251,6 +251,8 @@ enum VisibilityMasks enum LoaderType //!< Operation mode for GUI::MainSelector { LT_None, + LT_Character, // No script alias, invoked from Settings UI. + LT_CharacterMP,// No script alias, invoked from Multiplayer Lobby 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 @@ -303,6 +305,8 @@ 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; +extern CVar* sim_player_character_skin; // Multiplayer extern CVar* mp_state; @@ -317,6 +321,8 @@ extern CVar* mp_server_password; extern CVar* mp_player_name; extern CVar* mp_player_token; extern CVar* mp_api_url; +extern CVar* mp_override_character; // If empty, use `sim_default_character` +extern CVar* mp_override_character_skin; // New remote API extern CVar* remote_query_url; diff --git a/source/main/CMakeLists.txt b/source/main/CMakeLists.txt index b17419a783..f9e83e31fe 100644 --- a/source/main/CMakeLists.txt +++ b/source/main/CMakeLists.txt @@ -33,6 +33,7 @@ set(SOURCE_FILES gfx/DustPool.{h,cpp} gfx/EnvironmentMap.{h,cpp} gfx/GfxActor.{h,cpp} + gfx/GfxCharacter.{h,cpp} gfx/GfxData.h gfx/GfxScene.{h,cpp} gfx/HydraxWater.{h,cpp} @@ -114,6 +115,7 @@ set(SOURCE_FILES gui/imgui/imstb_textedit.h gui/imgui/imstb_truetype.h gui/panels/GUI_AngelScriptExamples.{h,cpp} + gui/panels/GUI_CharacterPoseUtil.{h,cpp} gui/panels/GUI_CollisionsDebug.{h,cpp} gui/panels/GUI_ConsoleView.{h,cpp} gui/panels/GUI_ConsoleWindow.{h,cpp} @@ -191,6 +193,7 @@ set(SOURCE_FILES resources/skin_fileformat/SkinFileFormat.{h,cpp} resources/terrn2_fileformat/Terrn2FileFormat.{h,cpp} resources/tobj_fileformat/TObjFileFormat.{h,cpp} + resources/character_fileformat/CharacterFileFormat.{h,cpp} system/AppCommandLine.cpp system/AppConfig.cpp system/Console.{h,cpp} @@ -341,6 +344,7 @@ target_include_directories(${BINNAME} PRIVATE physics/utils physics/water resources + resources/character_fileformat resources/odef_fileformat/ resources/otc_fileformat/ resources/rig_def_fileformat diff --git a/source/main/ForwardDeclarations.h b/source/main/ForwardDeclarations.h index 64cb8fbd6a..c64e462cc6 100644 --- a/source/main/ForwardDeclarations.h +++ b/source/main/ForwardDeclarations.h @@ -37,6 +37,9 @@ namespace RoR typedef int ScriptUnitId_t; //!< Unique sequentially generated ID of a loaded and running scriptin session. Use `ScriptEngine::getScriptUnit()` static const ScriptUnitId_t SCRIPTUNITID_INVALID = -1; + typedef int CharacterActionID_t; //!< Index to `CharacterDocument::actions`, use `CHARACTERACTIONID_INVALID` as empty value. + static const CharacterActionID_t CHARACTERACTIONID_INVALID = -1; + typedef int PointidID_t; //!< index to `PointColDetector::hit_pointid_list`, use `RoR::POINTIDID_INVALID` as empty value. static const PointidID_t POINTIDID_INVALID = -1; diff --git a/source/main/GameContext.cpp b/source/main/GameContext.cpp index 946e7be90c..ad2263975c 100644 --- a/source/main/GameContext.cpp +++ b/source/main/GameContext.cpp @@ -404,8 +404,6 @@ void GameContext::DeleteActor(ActorPtr actor) } } - App::GetGfxScene()->RemoveGfxActor(actor->GetGfxActor()); - #ifdef USE_SOCKETW if (App::mp_state->getEnum() == MpState::CONNECTED) { @@ -649,9 +647,14 @@ void GameContext::OnLoaderGuiCancel() void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntry* entry, std::string sectionconfig) { - bool spawn_now = false; + bool selection_finished = false; switch (type) { + case LT_Terrain: + m_current_selection.asr_cache_entry = entry; + selection_finished = true; + break; + case LT_Skin: if (entry != &m_dummy_cache_selection) { @@ -665,7 +668,7 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntry* entry, std::stri App::GetGuiManager()->TopMenubar.ai_skin2 = entry->dname; } } - spawn_now = true; + selection_finished = true; break; case LT_Vehicle: @@ -678,6 +681,9 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntry* entry, std::stri case LT_Load: case LT_Extension: case LT_AllBeam: + case LT_Character: + case LT_CharacterMP: + m_current_selector_type = type; m_current_selection.asr_cache_entry = entry; m_current_selection.asr_config = sectionconfig; if (App::GetGuiManager()->TopMenubar.ai_select) @@ -690,7 +696,7 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntry* entry, std::stri } m_current_selection.asr_origin = ActorSpawnRequest::Origin::USER; // Look for extra skins - if (!entry->guid.empty()) + if (entry->guid != "") { CacheQuery skin_query; skin_query.cqy_filter_guid = entry->guid; @@ -708,7 +714,7 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntry* entry, std::stri if (!default_skin_entry) { App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_ACTOR, Console::CONSOLE_SYSTEM_WARNING, - fmt::format(_L("Default skin '{}' for actor '{}' not found!"), entry->default_skin, entry->dname)); + fmt::format(_L("Default skin '{}' for cache entry '{}' not found!"), entry->default_skin, entry->dname)); } if (default_skin_entry && num_skins == 1) { @@ -728,52 +734,101 @@ void GameContext::OnLoaderGuiApply(LoaderType type, CacheEntry* entry, std::stri } else { - spawn_now = true; + selection_finished = true; } } else { - spawn_now = true; + selection_finished = true; } break; default:; } - if (spawn_now) + if (selection_finished) { - if (App::GetGuiManager()->TopMenubar.ai_select) + if (m_current_selection.asr_cache_entry->fext == "terrn2") { - App::GetGuiManager()->TopMenubar.ai_fname = m_current_selection.asr_cache_entry->fname; - App::GetGuiManager()->TopMenubar.ai_dname = m_current_selection.asr_cache_entry->dname; - App::GetGuiManager()->TopMenubar.ai_select = false; - App::GetGuiManager()->TopMenubar.ai_menu = true; + if (App::app_state->getEnum() == AppState::MAIN_MENU) + { + App::GetGameContext()->PushMessage(Message(MSG_SIM_LOAD_TERRN_REQUESTED, m_current_selection.asr_cache_entry->fname)); + } } - else if (App::GetGuiManager()->TopMenubar.ai_select2) + else if (m_current_selection.asr_cache_entry->fext == "character") { - App::GetGuiManager()->TopMenubar.ai_fname2 = m_current_selection.asr_cache_entry->fname; - App::GetGuiManager()->TopMenubar.ai_dname2 = m_current_selection.asr_cache_entry->dname; - App::GetGuiManager()->TopMenubar.ai_select2 = false; - App::GetGuiManager()->TopMenubar.ai_menu = true; + switch (m_current_selector_type) + { + case LT_Character: // Invoked by Settings UI button + App::sim_player_character->setStr(m_current_selection.asr_cache_entry->fname); + if (m_current_selection.asr_skin_entry) + { + App::sim_player_character_skin->setStr(m_current_selection.asr_skin_entry->dname); + } + else + { + App::sim_player_character_skin->setStr(""); + } + break; + + case LT_CharacterMP: // Invoked by MultiplayerSelector UI button + App::mp_override_character->setStr(m_current_selection.asr_cache_entry->fname); + if (m_current_selection.asr_skin_entry) + { + App::mp_override_character_skin->setStr(m_current_selection.asr_skin_entry->dname); + } + else + { + App::mp_override_character_skin->setStr(""); + } + break; + + default:; // uhh, what? + } } else { - ActorSpawnRequest* rq = new ActorSpawnRequest; - *rq = m_current_selection; - this->PushMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)rq)); + if (App::GetGuiManager()->TopMenubar.ai_select) + { + App::GetGuiManager()->TopMenubar.ai_fname = m_current_selection.asr_cache_entry->fname; + App::GetGuiManager()->TopMenubar.ai_dname = m_current_selection.asr_cache_entry->dname; + App::GetGuiManager()->TopMenubar.ai_select = false; + App::GetGuiManager()->TopMenubar.ai_menu = true; + } + else if (App::GetGuiManager()->TopMenubar.ai_select2) + { + App::GetGuiManager()->TopMenubar.ai_fname2 = m_current_selection.asr_cache_entry->fname; + App::GetGuiManager()->TopMenubar.ai_dname2 = m_current_selection.asr_cache_entry->dname; + App::GetGuiManager()->TopMenubar.ai_select2 = false; + App::GetGuiManager()->TopMenubar.ai_menu = true; + } + else + { + ActorSpawnRequest* rq = new ActorSpawnRequest; + *rq = m_current_selection; + this->PushMessage(Message(MSG_SIM_SPAWN_ACTOR_REQUESTED, (void*)rq)); + } } m_current_selection = ActorSpawnRequest(); // Reset + m_current_selector_type = LT_None; } } // -------------------------------- // 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 = m_terrain->getSpawnPos(); float spawn_rot = 0.0f; @@ -821,11 +876,13 @@ void GameContext::CreatePlayerCharacter() { App::GetCameraManager()->UpdateInputEvents(0.02f); } + + return true; } Character* GameContext::GetPlayerCharacter() // Convenience ~ counterpart of `GetPlayerActor()` { - return m_character_factory.GetLocalCharacter(); + return m_character_factory.getLocalCharacter(); } // -------------------------------- diff --git a/source/main/GameContext.h b/source/main/GameContext.h index a801be95e8..3c29efa588 100644 --- a/source/main/GameContext.h +++ b/source/main/GameContext.h @@ -142,7 +142,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; } @@ -194,7 +194,8 @@ class GameContext CacheEntry* m_last_cache_selection = nullptr; //!< Vehicle/load CacheEntry* m_last_skin_selection = nullptr; Ogre::String m_last_section_config; - ActorSpawnRequest m_current_selection; //!< Context of the loader UI + ActorSpawnRequest m_current_selection; //!< Context of the loader UI (may also be Character!) + LoaderType m_current_selector_type = LT_None; CacheEntry m_dummy_cache_selection; // Characters (simplified physics and netcode) diff --git a/source/main/gameplay/Character.cpp b/source/main/gameplay/Character.cpp index 166599230d..fa761ca7ea 100644 --- a/source/main/gameplay/Character.cpp +++ b/source/main/gameplay/Character.cpp @@ -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 2017-2018 Petr Ohlidal + Copyright 2017-2022 Petr Ohlidal For more information, see http://www.rigsofrods.org/ @@ -26,38 +26,39 @@ #include "ActorManager.h" #include "CameraManager.h" #include "Collisions.h" +#include "Console.h" #include "GameContext.h" +#include "GfxCharacter.h" #include "GfxScene.h" #include "InputEngine.h" #include "MovableText.h" #include "Network.h" +#include "RoRnet.h" #include "Terrain.h" #include "Utils.h" #include "Water.h" using namespace Ogre; using namespace RoR; +using namespace RoRnet; -#define LOGSTREAM Ogre::LogManager::getSingleton().stream() - -Character::Character(int source, unsigned int streamid, UTFString player_name, int color_number, bool is_remote) : +Character::Character(CacheEntry* cacheEntry, CacheEntry* skinEntry, int source, unsigned int streamid, UTFString player_name, int color_number, bool is_remote) : m_actor_coupling(nullptr) - , m_can_jump(false) , m_character_rotation(0.0f) , m_character_h_speed(2.0f) , m_character_v_speed(0.0f) , m_color_number(color_number) - , m_anim_time(0.f) - , m_net_last_anim_time(0.f) , m_net_last_update_time(0.f) , m_net_username(player_name) , m_is_remote(is_remote) , m_source_id(source) , m_stream_id(streamid) - , m_gfx_character(nullptr) - , m_driving_anim_length(0.f) - , m_anim_name("Idle_sway") + , m_cache_entry(cacheEntry) + , m_used_skin_entry(skinEntry) { + ROR_ASSERT(cacheEntry && cacheEntry->character_def); + ROR_ASSERT(!skinEntry || skinEntry->skin_def); + static int id_counter = 0; m_instance_name = "Character" + TOSTRING(id_counter); ++id_counter; @@ -70,11 +71,7 @@ Character::Character(int source, unsigned int streamid, UTFString player_name, i Character::~Character() { - if (m_gfx_character != nullptr) - { - App::GetGfxScene()->RemoveGfxCharacter(m_gfx_character); - delete m_gfx_character; - } + } void Character::updateCharacterRotation() @@ -100,20 +97,6 @@ void Character::setRotation(Radian rotation) m_character_rotation = rotation; } -void Character::SetAnimState(std::string mode, float time) -{ - if (m_anim_name != mode) - { - m_anim_name = mode; - m_anim_time = time; - m_net_last_anim_time = 0.0f; - } - else - { - m_anim_time += time; - } -} - float calculate_collision_depth(Vector3 pos) { Vector3 query = pos + 0.3f * Vector3::UNIT_Y; @@ -126,276 +109,273 @@ float calculate_collision_depth(Vector3 pos) return query.y - pos.y; } -void Character::update(float dt) +void toggle_flag(BitMask_t& flags, BitMask_t mask) +{ + if (BITMASK_IS_1(flags, mask)) + BITMASK_SET_0(flags, mask); + else + BITMASK_SET_1(flags, mask); +} + +void Character::updateLocal(float dt) { - if (!m_is_remote && (m_actor_coupling == nullptr) && (App::sim_state->getEnum() != SimState::PAUSED)) + ROR_ASSERT(!isRemote()); + + // Check if movement is enabled + if (BITMASK_IS_1(m_situation_flags, SITUATION_DRIVING) + || (App::sim_state->getEnum() == SimState::PAUSED) + || App::GetCameraManager()->GetCurrentBehavior() == CameraManager::CAMERA_BEHAVIOR_FREE) { - // disable character movement when using the free camera mode or when the menu is opened - // TODO: check for menu being opened - if (App::GetCameraManager()->GetCurrentBehavior() == CameraManager::CAMERA_BEHAVIOR_FREE) - { - return; - } + return; // do not update. + } - Vector3 position = m_character_position; //ASYNCSCENE OLD m_character_scenenode->getPosition(); + bool m_can_jump = false; + m_control_flags = 0; - // gravity force is always on - position.y += m_character_v_speed * dt; - m_character_v_speed += dt * -9.8f; + Vector3 position = m_character_position; - // Trigger script events and handle mesh (ground) collision - Vector3 query = position; - App::GetGameContext()->GetTerrain()->GetCollisions()->collisionCorrect(&query); + // gravity force is always on + position.y += m_character_v_speed * dt; + m_character_v_speed += dt * -9.8f; - // Auto compensate minor height differences - float depth = calculate_collision_depth(position); - if (depth > 0.0f) - { - m_can_jump = true; - m_character_v_speed = std::max(0.0f, m_character_v_speed); - position.y += std::min(depth, 2.0f * dt); - } + // Trigger script events and handle mesh (ground) collision + Vector3 query = position; + App::GetGameContext()->GetTerrain()->GetCollisions()->collisionCorrect(&query); + + // Auto compensate minor height differences + float depth = calculate_collision_depth(position); + if (depth > 0.0f) + { + m_can_jump = true; + m_character_v_speed = std::max(0.0f, m_character_v_speed); + position.y += std::min(depth, 2.0f * dt); + } - // Submesh "collision" + // Submesh "collision" + { + float depth = 0.0f; + for (const ActorPtr& actor : App::GetGameContext()->GetActorManager()->GetActors()) { - float depth = 0.0f; - for (ActorPtr& actor : App::GetGameContext()->GetActorManager()->GetActors()) + if (actor->ar_bounding_box.contains(position)) { - if (actor->ar_bounding_box.contains(position)) + for (int i = 0; i < actor->ar_num_collcabs; i++) { - for (int i = 0; i < actor->ar_num_collcabs; i++) + int tmpv = actor->ar_collcabs[i] * 3; + Vector3 a = actor->ar_nodes[actor->ar_cabs[tmpv + 0]].AbsPosition; + Vector3 b = actor->ar_nodes[actor->ar_cabs[tmpv + 1]].AbsPosition; + Vector3 c = actor->ar_nodes[actor->ar_cabs[tmpv + 2]].AbsPosition; + auto result = Math::intersects(Ray(position, Vector3::UNIT_Y), a, b, c); + if (result.first && result.second < 1.8f) { - int tmpv = actor->ar_collcabs[i] * 3; - Vector3 a = actor->ar_nodes[actor->ar_cabs[tmpv + 0]].AbsPosition; - Vector3 b = actor->ar_nodes[actor->ar_cabs[tmpv + 1]].AbsPosition; - Vector3 c = actor->ar_nodes[actor->ar_cabs[tmpv + 2]].AbsPosition; - auto result = Math::intersects(Ray(position, Vector3::UNIT_Y), a, b, c); - if (result.first && result.second < 1.8f) - { - depth = std::max(depth, result.second); - } + depth = std::max(depth, result.second); } } } - if (depth > 0.0f) - { - m_can_jump = true; - m_character_v_speed = std::max(0.0f, m_character_v_speed); - position.y += std::min(depth, 0.05f); - } } + if (depth > 0.0f) + { + m_can_jump = true; + m_character_v_speed = std::max(0.0f, m_character_v_speed); + position.y += std::min(depth, 0.05f); + } + } - // Obstacle detection - if (position != m_prev_position) + // Obstacle detection + if (position != m_prev_position) + { + const int numstep = 100; + Vector3 diff = position - m_prev_position; + Vector3 base = m_prev_position + Vector3::UNIT_Y * 0.25f; + for (int i = 1; i < numstep; i++) { - const int numstep = 100; - Vector3 diff = position - m_prev_position; - Vector3 base = m_prev_position + Vector3::UNIT_Y * 0.25f; - for (int i = 1; i < numstep; i++) + Vector3 query = base + diff * ((float)i / numstep); + if (App::GetGameContext()->GetTerrain()->GetCollisions()->collisionCorrect(&query, false)) { - Vector3 query = base + diff * ((float)i / numstep); - if (App::GetGameContext()->GetTerrain()->GetCollisions()->collisionCorrect(&query, false)) - { - m_character_v_speed = std::max(0.0f, m_character_v_speed); - position = m_prev_position + diff * ((float)(i - 1) / numstep); - position.y += 0.025f; - break; - } + m_character_v_speed = std::max(0.0f, m_character_v_speed); + position = m_prev_position + diff * ((float)(i - 1) / numstep); + position.y += 0.025f; + break; } } + } - m_prev_position = position; + m_prev_position = position; - // ground contact - float pheight = App::GetGameContext()->GetTerrain()->GetHeightAt(position.x, position.z); + // ground contact + float pheight = App::GetGameContext()->GetTerrain()->GetHeightAt(position.x, position.z); - if (position.y < pheight) - { - position.y = pheight; - m_character_v_speed = 0.0f; - m_can_jump = true; - } + if (position.y < pheight) + { + position.y = pheight; + m_character_v_speed = 0.0f; + m_can_jump = true; + } - // water stuff - bool isswimming = false; - float wheight = -99999; + // water stuff + bool isswimming = false; + float wheight = -99999; - if (App::GetGameContext()->GetTerrain()->getWater()) + if (App::GetGameContext()->GetTerrain()->getWater()) + { + wheight = App::GetGameContext()->GetTerrain()->getWater()->CalcWavesHeight(position); + if (position.y < wheight - 1.8f) { - wheight = App::GetGameContext()->GetTerrain()->getWater()->CalcWavesHeight(position); - if (position.y < wheight - 1.8f) - { - position.y = wheight - 1.8f; - m_character_v_speed = 0.0f; - } + position.y = wheight - 1.8f; + m_character_v_speed = 0.0f; } + } - // 0.1 due to 'jumping' from waves -> not nice looking - if (App::GetGameContext()->GetTerrain()->getWater() && (wheight - pheight > 1.8f) && (position.y + 0.1f <= wheight)) - { - isswimming = true; - } + // 0.1 due to 'jumping' from waves -> not nice looking + if (App::GetGameContext()->GetTerrain()->getWater() && (wheight - pheight > 1.8f) && (position.y + 0.1f <= wheight)) + { + isswimming = true; + } - float tmpJoy = 0.0f; - if (m_can_jump) + float tmpJoy = 0.0f; + if (m_can_jump) + { + if (RoR::App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_JUMP)) { - if (RoR::App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_JUMP)) - { - m_character_v_speed = 2.0f; - m_can_jump = false; - } + m_character_v_speed = 2.0f; + m_can_jump = false; } + } - bool idleanim = true; - float tmpGoForward = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_FORWARD) - + RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_ROT_UP); - float tmpGoBackward = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_BACKWARDS) - + RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_ROT_DOWN); - bool not_walking = (tmpGoForward == 0.f && tmpGoBackward == 0.f); + bool idleanim = true; + float tmpGoForward = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_FORWARD) + + RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_ROT_UP); + float tmpGoBackward = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_BACKWARDS) + + RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_ROT_DOWN); + bool not_walking = (tmpGoForward == 0.f && tmpGoBackward == 0.f); - tmpJoy = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_RIGHT); - if (tmpJoy > 0.0f) + tmpJoy = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_RIGHT); + if (tmpJoy > 0.0f) + { + bool alt_pressed = RoR::App::GetInputEngine()->isKeyDown(OIS::KC_LMENU); + float scale = alt_pressed ? 0.1f : 1.0f; + setRotation(m_character_rotation + dt * 2.0f * scale * Radian(tmpJoy)); + if (!isswimming && not_walking) { - float scale = RoR::App::GetInputEngine()->isKeyDown(OIS::KC_LMENU) ? 0.1f : 1.0f; - setRotation(m_character_rotation + dt * 2.0f * scale * Radian(tmpJoy)); - if (!isswimming && not_walking) - { - this->SetAnimState("Turn", -dt); - idleanim = false; - } + BITMASK_SET_1(m_control_flags, CONTROL_TURN_RIGHT); + idleanim = false; } + } - tmpJoy = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_LEFT); - if (tmpJoy > 0.0f) + tmpJoy = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_LEFT); + if (tmpJoy > 0.0f) + { + bool alt_pressed = RoR::App::GetInputEngine()->isKeyDown(OIS::KC_LMENU); + float scale = alt_pressed ? 0.1f : 1.0f; + setRotation(m_character_rotation - dt * scale * 2.0f * Radian(tmpJoy)); + if (!isswimming && not_walking) { - float scale = RoR::App::GetInputEngine()->isKeyDown(OIS::KC_LMENU) ? 0.1f : 1.0f; - setRotation(m_character_rotation - dt * scale * 2.0f * Radian(tmpJoy)); - if (!isswimming && not_walking) - { - this->SetAnimState("Turn", dt); - idleanim = false; - } + BITMASK_SET_1(m_control_flags, CONTROL_TURN_LEFT); + idleanim = false; } + } - float tmpRun = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_RUN); - float accel = 1.0f; + float tmpRun = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_RUN); + float accel = 1.0f; - tmpJoy = accel = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_SIDESTEP_LEFT); - if (tmpJoy > 0.0f) + tmpJoy = accel = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_SIDESTEP_LEFT); + if (tmpJoy > 0.0f) + { + if (tmpRun > 0.0f) + accel = 3.0f * tmpRun; + // animation missing for that + position += dt * m_character_h_speed * 0.5f * accel * Vector3(cos(m_character_rotation.valueRadians() - Math::HALF_PI), 0.0f, sin(m_character_rotation.valueRadians() - Math::HALF_PI)); + if (!isswimming && not_walking) { - if (tmpRun > 0.0f) - accel = 3.0f * tmpRun; - // animation missing for that - position += dt * m_character_h_speed * 0.5f * accel * Vector3(cos(m_character_rotation.valueRadians() - Math::HALF_PI), 0.0f, sin(m_character_rotation.valueRadians() - Math::HALF_PI)); - if (!isswimming && not_walking) - { - this->SetAnimState("Side_step", -dt); - idleanim = false; - } + BITMASK_SET_1(m_control_flags, CONTROL_SIDESTEP_LEFT); + idleanim = false; } + } - tmpJoy = accel = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_SIDESTEP_RIGHT); - if (tmpJoy > 0.0f) + tmpJoy = accel = RoR::App::GetInputEngine()->getEventValue(EV_CHARACTER_SIDESTEP_RIGHT); + if (tmpJoy > 0.0f) + { + if (tmpRun > 0.0f) + accel = 3.0f * tmpRun; + // animation missing for that + position += dt * m_character_h_speed * 0.5f * accel * Vector3(cos(m_character_rotation.valueRadians() + Math::HALF_PI), 0.0f, sin(m_character_rotation.valueRadians() + Math::HALF_PI)); + if (!isswimming && not_walking) { - if (tmpRun > 0.0f) - accel = 3.0f * tmpRun; - // animation missing for that - position += dt * m_character_h_speed * 0.5f * accel * Vector3(cos(m_character_rotation.valueRadians() + Math::HALF_PI), 0.0f, sin(m_character_rotation.valueRadians() + Math::HALF_PI)); - if (!isswimming && not_walking) - { - this->SetAnimState("Side_step", dt); - idleanim = false; - } + BITMASK_SET_1(m_control_flags, CONTROL_SIDESTEP_RIGHT); + idleanim = false; } + } - tmpJoy = accel = tmpGoForward; - float tmpBack = tmpGoBackward; + tmpJoy = accel = tmpGoForward; + float tmpBack = tmpGoBackward; - tmpJoy = std::min(tmpJoy, 1.0f); - tmpBack = std::min(tmpBack, 1.0f); + tmpJoy = std::min(tmpJoy, 1.0f); + tmpBack = std::min(tmpBack, 1.0f); - if (tmpJoy > 0.0f || tmpRun > 0.0f) - { - if (tmpRun > 0.0f) - accel = 3.0f * tmpRun; + if (tmpJoy > 0.0f || tmpRun > 0.0f) + { + if (tmpRun > 0.0f) + accel = 3.0f * tmpRun; - float time = dt * tmpJoy * m_character_h_speed; + float time = dt * tmpJoy * m_character_h_speed; - if (isswimming) - { - this->SetAnimState("Swim_loop", time); - idleanim = false; - } - else - { - if (tmpRun > 0.0f) - { - this->SetAnimState("Run", time); - idleanim = false; - } - else - { - this->SetAnimState("Walk", time); - idleanim = false; - } - } - position += dt * m_character_h_speed * 1.5f * accel * Vector3(cos(m_character_rotation.valueRadians()), 0.0f, sin(m_character_rotation.valueRadians())); - } - else if (tmpBack > 0.0f) + if (isswimming) { - float time = -dt * m_character_h_speed; - if (isswimming) - { - this->SetAnimState("Spot_swim", time); - idleanim = false; - } - else - { - this->SetAnimState("Walk", time); - idleanim = false; - } - position -= dt * m_character_h_speed * tmpBack * Vector3(cos(m_character_rotation.valueRadians()), 0.0f, sin(m_character_rotation.valueRadians())); + idleanim = false; } - - if (idleanim) + else { - if (isswimming) - { - this->SetAnimState("Spot_swim", dt * 2.0f); - } - else + if (tmpRun > 0.0f) { - this->SetAnimState("Idle_sway", dt * 1.0f); + BITMASK_SET_1(m_control_flags, CONTROL_RUN); } - } - m_character_position = position; - } - else if (m_actor_coupling) // The character occupies a vehicle or machine - { - // Animation - float angle = m_actor_coupling->ar_hydro_dir_wheel_display * -1.0f; // not getSteeringAngle(), but this, as its smoothed - float anim_time_pos = ((angle + 1.0f) * 0.5f) * m_driving_anim_length; - // prevent animation flickering on the borders: - if (anim_time_pos < 0.01f) - { - anim_time_pos = 0.01f; - } - if (anim_time_pos > m_driving_anim_length - 0.01f) - { - anim_time_pos = m_driving_anim_length - 0.01f; + idleanim = false; } - m_anim_name = "Driving"; - m_anim_time = anim_time_pos; - m_net_last_anim_time = 0.0f; + position += dt * m_character_h_speed * 1.5f * accel * Vector3(cos(m_character_rotation.valueRadians()), 0.0f, sin(m_character_rotation.valueRadians())); } - -#ifdef USE_SOCKETW - if ((App::mp_state->getEnum() == MpState::CONNECTED) && !m_is_remote) + else if (tmpBack > 0.0f) { - this->SendStreamData(); + float time = -dt * m_character_h_speed; + idleanim = false; + position -= dt * m_character_h_speed * tmpBack * Vector3(cos(m_character_rotation.valueRadians()), 0.0f, sin(m_character_rotation.valueRadians())); } -#endif // USE_SOCKETW + + if (tmpGoForward != 0.f) + BITMASK_SET_1(m_control_flags, CONTROL_MOVE_FORWARD); + + if (tmpGoBackward != 0.f) + BITMASK_SET_1(m_control_flags, CONTROL_MOVE_BACKWARD); + + if (isswimming) + BITMASK_SET_1(m_situation_flags, RoRnet::SITUATION_IN_DEEP_WATER); + else + BITMASK_SET_0(m_situation_flags, RoRnet::SITUATION_IN_DEEP_WATER); + + m_character_position = position; + + // Custom actions + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_01)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_01); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_02)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_02); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_03)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_03); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_04)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_04); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_05)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_05); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_06)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_06); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_07)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_07); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_08)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_08); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_09)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_09); } + if (App::GetInputEngine()->getEventBoolValue(EV_CHARACTER_CUSTOM_ACTION_10)) { BITMASK_SET_1(m_control_flags, CONTROL_CUSTOM_ACTION_10); } + + // Custom modes (=situations) + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_01)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_01); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_02)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_02); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_03)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_03); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_04)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_04); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_05)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_05); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_06)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_06); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_07)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_07); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_08)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_08); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_09)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_09); } + if (App::GetInputEngine()->getEventBoolValueBounce(EV_CHARACTER_CUSTOM_MODE_10)) { toggle_flag(m_situation_flags, RoRnet::SITUATION_CUSTOM_MODE_10); } } void Character::move(Vector3 offset) @@ -407,19 +387,16 @@ void Character::move(Vector3 offset) void Character::ReportError(const char* detail) { #ifdef USE_SOCKETW - Ogre::UTFString username; + std::string username; RoRnet::UserInfo info; if (!App::GetNetwork()->GetUserInfo(m_source_id, info)) - username = "~~ERROR getting username~~"; + username = "?"; else username = info.username; - char msg_buf[300]; - snprintf(msg_buf, 300, - "[RoR|Networking] ERROR on m_is_remote character (User: '%s', SourceID: %d, StreamID: %d): ", - username.asUTF8_c_str(), m_source_id, m_stream_id); - - LOGSTREAM << msg_buf << detail; + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Error on networked character (User: '{}', SourceID: {}, StreamID: {}): {}", + username, m_source_id, m_stream_id, detail)); #endif } @@ -455,18 +432,16 @@ void Character::SendStreamData() m_net_last_update_time = m_net_timer.getMilliseconds(); - NetCharacterMsgPos msg; + CharacterMsgPos msg; msg.command = CHARACTER_CMD_POSITION; msg.pos_x = m_character_position.x; msg.pos_y = m_character_position.y; msg.pos_z = m_character_position.z; msg.rot_angle = m_character_rotation.valueRadians(); - strncpy(msg.anim_name, m_anim_name.c_str(), CHARACTER_ANIM_NAME_LEN); - msg.anim_time = m_anim_time - m_net_last_anim_time; - - m_net_last_anim_time = m_anim_time; + msg.control_flags = m_control_flags; + msg.situation_flags = m_situation_flags; - App::GetNetwork()->AddPacket(m_stream_id, RoRnet::MSG2_STREAM_DATA_DISCARDABLE, sizeof(NetCharacterMsgPos), (char*)&msg); + App::GetNetwork()->AddPacket(m_stream_id, RoRnet::MSG2_STREAM_DATA_DISCARDABLE, sizeof(CharacterMsgPos), (char*)&msg); #endif // USE_SOCKETW } @@ -475,16 +450,14 @@ void Character::receiveStreamData(unsigned int& type, int& source, unsigned int& #ifdef USE_SOCKETW if (type == RoRnet::MSG2_STREAM_DATA && m_source_id == source && m_stream_id == streamid) { - auto* msg = reinterpret_cast(buffer); + auto* msg = reinterpret_cast(buffer); if (msg->command == CHARACTER_CMD_POSITION) { - auto* pos_msg = reinterpret_cast(buffer); + auto* pos_msg = reinterpret_cast(buffer); this->setPosition(Ogre::Vector3(pos_msg->pos_x, pos_msg->pos_y, pos_msg->pos_z)); this->setRotation(Ogre::Radian(pos_msg->rot_angle)); - if (strnlen(pos_msg->anim_name, CHARACTER_ANIM_NAME_LEN) < CHARACTER_ANIM_NAME_LEN) - { - this->SetAnimState(pos_msg->anim_name, pos_msg->anim_time); - } + m_control_flags = pos_msg->control_flags; + m_situation_flags = pos_msg->situation_flags; } else if (msg->command == CHARACTER_CMD_DETACH) { @@ -495,7 +468,7 @@ void Character::receiveStreamData(unsigned int& type, int& source, unsigned int& } else if (msg->command == CHARACTER_CMD_ATTACH) { - auto* attach_msg = reinterpret_cast(buffer); + auto* attach_msg = reinterpret_cast(buffer); ActorPtr beam = App::GetGameContext()->GetActorManager()->GetActorByNetworkLinks(attach_msg->source_id, attach_msg->stream_id); if (beam != nullptr) { @@ -523,194 +496,124 @@ void Character::receiveStreamData(unsigned int& type, int& source, unsigned int& void Character::SetActorCoupling(bool enabled, ActorPtr actor) { m_actor_coupling = actor; + if (actor) + BITMASK_SET_1(m_situation_flags, SITUATION_DRIVING); + else + BITMASK_SET_0(m_situation_flags, SITUATION_DRIVING); + #ifdef USE_SOCKETW if (App::mp_state->getEnum() == MpState::CONNECTED && !m_is_remote) { if (enabled) { - NetCharacterMsgAttach msg; + CharacterMsgAttach msg; msg.command = CHARACTER_CMD_ATTACH; msg.source_id = m_actor_coupling->ar_net_source_id; msg.stream_id = m_actor_coupling->ar_net_stream_id; - App::GetNetwork()->AddPacket(m_stream_id, RoRnet::MSG2_STREAM_DATA, sizeof(NetCharacterMsgAttach), (char*)&msg); + App::GetNetwork()->AddPacket(m_stream_id, RoRnet::MSG2_STREAM_DATA, sizeof(CharacterMsgAttach), (char*)&msg); } else { - NetCharacterMsgGeneric msg; + CharacterMsgGeneric msg; msg.command = CHARACTER_CMD_DETACH; - App::GetNetwork()->AddPacket(m_stream_id, RoRnet::MSG2_STREAM_DATA, sizeof(NetCharacterMsgGeneric), (char*)&msg); + App::GetNetwork()->AddPacket(m_stream_id, RoRnet::MSG2_STREAM_DATA, sizeof(CharacterMsgGeneric), (char*)&msg); } } #endif // USE_SOCKETW } -// -------------------------------- -// GfxCharacter +ActorPtr Character::GetActorCoupling() { return m_actor_coupling; } -GfxCharacter* Character::SetupGfx() + +const char* Character::ControlFlagToString(BitMask_t action) { - Entity* entity = App::GetGfxScene()->GetSceneManager()->createEntity(m_instance_name + "_mesh", "character.mesh"); - m_driving_anim_length = entity->getAnimationState("Driving")->getLength(); - - // fix disappearing mesh - AxisAlignedBox aabb; - aabb.setInfinite(); - entity->getMesh()->_setBounds(aabb); - - // add entity to the scene node - Ogre::SceneNode* scenenode = App::GetGfxScene()->GetSceneManager()->getRootSceneNode()->createChildSceneNode(); - scenenode->attachObject(entity); - scenenode->setScale(0.02f, 0.02f, 0.02f); - scenenode->setVisible(false); - - // setup colour - MaterialPtr mat1 = MaterialManager::getSingleton().getByName("tracks/character"); - MaterialPtr mat2 = mat1->clone("tracks/" + m_instance_name); - entity->setMaterialName("tracks/" + m_instance_name); - - m_gfx_character = new GfxCharacter(); - m_gfx_character->xc_scenenode = scenenode; - m_gfx_character->xc_character = this; - m_gfx_character->xc_instance_name = m_instance_name; - - return m_gfx_character; + if (BITMASK_IS_1(action, CONTROL_MOVE_FORWARD )) { return "CONTROL_MOVE_FORWARD"; } + if (BITMASK_IS_1(action, CONTROL_MOVE_BACKWARD )) { return "CONTROL_MOVE_BACKWARD"; } + if (BITMASK_IS_1(action, CONTROL_TURN_RIGHT )) { return "CONTROL_TURN_RIGHT"; } + if (BITMASK_IS_1(action, CONTROL_TURN_LEFT )) { return "CONTROL_TURN_LEFT"; } + if (BITMASK_IS_1(action, CONTROL_SIDESTEP_RIGHT)) { return "CONTROL_SIDESTEP_RIGHT"; } + if (BITMASK_IS_1(action, CONTROL_SIDESTEP_LEFT )) { return "CONTROL_SIDESTEP_LEFT"; } + if (BITMASK_IS_1(action, CONTROL_RUN )) { return "CONTROL_RUN"; } + if (BITMASK_IS_1(action, CONTROL_JUMP )) { return "CONTROL_JUMP"; } + if (BITMASK_IS_1(action, CONTROL_SLOW_TURN )) { return "CONTROL_SLOW_TURN"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_01 )) { return "CONTROL_CUSTOM_ACTION_01"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_02 )) { return "CONTROL_CUSTOM_ACTION_02"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_03 )) { return "CONTROL_CUSTOM_ACTION_03"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_04 )) { return "CONTROL_CUSTOM_ACTION_04"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_05 )) { return "CONTROL_CUSTOM_ACTION_05"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_06 )) { return "CONTROL_CUSTOM_ACTION_06"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_07 )) { return "CONTROL_CUSTOM_ACTION_07"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_08 )) { return "CONTROL_CUSTOM_ACTION_08"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_09 )) { return "CONTROL_CUSTOM_ACTION_09"; } + if (BITMASK_IS_1(action, CONTROL_CUSTOM_ACTION_10 )) { return "CONTROL_CUSTOM_ACTION_10"; } + + return "~"; } -RoR::GfxCharacter::~GfxCharacter() +BitMask_t Character::ControlFlagFromString(std::string const& str) { - Entity* ent = static_cast(xc_scenenode->getAttachedObject(0)); - xc_scenenode->detachAllObjects(); - App::GetGfxScene()->GetSceneManager()->destroySceneNode(xc_scenenode); - App::GetGfxScene()->GetSceneManager()->destroyEntity(ent); - MaterialManager::getSingleton().unload("tracks/" + xc_instance_name); + if (str == "CONTROL_MOVE_FORWARD" ) { return CONTROL_MOVE_FORWARD ;} + if (str == "CONTROL_MOVE_BACKWARD" ) { return CONTROL_MOVE_BACKWARD ;} + if (str == "CONTROL_TURN_RIGHT" ) { return CONTROL_TURN_RIGHT ;} + if (str == "CONTROL_TURN_LEFT" ) { return CONTROL_TURN_LEFT ;} + if (str == "CONTROL_SIDESTEP_RIGHT" ) { return CONTROL_SIDESTEP_RIGHT;} + if (str == "CONTROL_SIDESTEP_LEFT" ) { return CONTROL_SIDESTEP_LEFT ;} + if (str == "CONTROL_RUN" ) { return CONTROL_RUN ;} + if (str == "CONTROL_JUMP" ) { return CONTROL_JUMP ;} + if (str == "CONTROL_SLOW_TURN" ) { return CONTROL_SLOW_TURN ;} + if (str == "CONTROL_CUSTOM_ACTION_01" ) { return CONTROL_CUSTOM_ACTION_01;} + if (str == "CONTROL_CUSTOM_ACTION_02" ) { return CONTROL_CUSTOM_ACTION_02;} + if (str == "CONTROL_CUSTOM_ACTION_03" ) { return CONTROL_CUSTOM_ACTION_03;} + if (str == "CONTROL_CUSTOM_ACTION_04" ) { return CONTROL_CUSTOM_ACTION_04;} + if (str == "CONTROL_CUSTOM_ACTION_05" ) { return CONTROL_CUSTOM_ACTION_05;} + if (str == "CONTROL_CUSTOM_ACTION_06" ) { return CONTROL_CUSTOM_ACTION_06;} + if (str == "CONTROL_CUSTOM_ACTION_07" ) { return CONTROL_CUSTOM_ACTION_07;} + if (str == "CONTROL_CUSTOM_ACTION_08" ) { return CONTROL_CUSTOM_ACTION_08;} + if (str == "CONTROL_CUSTOM_ACTION_09" ) { return CONTROL_CUSTOM_ACTION_09;} + if (str == "CONTROL_CUSTOM_ACTION_10" ) { return CONTROL_CUSTOM_ACTION_10;} + return 0; } -void RoR::GfxCharacter::BufferSimulationData() +const char* Character::SituationFlagToString(BitMask_t mask) { - xc_simbuf_prev = xc_simbuf; - - xc_simbuf.simbuf_character_pos = xc_character->getPosition(); - xc_simbuf.simbuf_character_rot = xc_character->getRotation(); - xc_simbuf.simbuf_color_number = xc_character->GetColorNum(); - xc_simbuf.simbuf_net_username = xc_character->GetNetUsername(); - xc_simbuf.simbuf_is_remote = xc_character->GetIsRemote(); - xc_simbuf.simbuf_actor_coupling = xc_character->GetActorCoupling(); - xc_simbuf.simbuf_anim_name = xc_character->GetAnimName(); - xc_simbuf.simbuf_anim_time = xc_character->GetAnimTime(); + if (BITMASK_IS_1(mask, SITUATION_ON_SOLID_GROUND )) { return "SITUATION_ON_SOLID_GROUND"; } + if (BITMASK_IS_1(mask, SITUATION_IN_SHALLOW_WATER)) { return "SITUATION_IN_SHALLOW_WATER"; } + if (BITMASK_IS_1(mask, SITUATION_IN_DEEP_WATER )) { return "SITUATION_IN_DEEP_WATER"; } + if (BITMASK_IS_1(mask, SITUATION_IN_AIR )) { return "SITUATION_IN_AIR"; } + if (BITMASK_IS_1(mask, SITUATION_DRIVING )) { return "SITUATION_DRIVING"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_01 )) { return "SITUATION_CUSTOM_MODE_01"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_02 )) { return "SITUATION_CUSTOM_MODE_02"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_03 )) { return "SITUATION_CUSTOM_MODE_03"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_04 )) { return "SITUATION_CUSTOM_MODE_04"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_05 )) { return "SITUATION_CUSTOM_MODE_05"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_06 )) { return "SITUATION_CUSTOM_MODE_06"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_07 )) { return "SITUATION_CUSTOM_MODE_07"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_08 )) { return "SITUATION_CUSTOM_MODE_08"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_09 )) { return "SITUATION_CUSTOM_MODE_09"; } + if (BITMASK_IS_1(mask, SITUATION_CUSTOM_MODE_10 )) { return "SITUATION_CUSTOM_MODE_10"; } + + return "~"; } -void RoR::GfxCharacter::UpdateCharacterInScene() +BitMask_t Character::SituationFlagFromString(std::string const& str) { - // Actor coupling - if (xc_simbuf.simbuf_actor_coupling != xc_simbuf_prev.simbuf_actor_coupling) - { - if (xc_simbuf.simbuf_actor_coupling != nullptr) - { - // Entering/switching vehicle - xc_scenenode->getAttachedObject(0)->setCastShadows(false); - xc_scenenode->setVisible(xc_simbuf.simbuf_actor_coupling->GetGfxActor()->HasDriverSeatProp()); - } - else if (xc_simbuf_prev.simbuf_actor_coupling != nullptr) - { - // Leaving vehicle - xc_scenenode->getAttachedObject(0)->setCastShadows(true); - xc_scenenode->resetOrientation(); - } - } - - // Position + Orientation - Ogre::Entity* entity = static_cast(xc_scenenode->getAttachedObject(0)); - if (xc_simbuf.simbuf_actor_coupling != nullptr) - { - // We're in vehicle - GfxActor* gfx_actor = xc_simbuf.simbuf_actor_coupling->GetGfxActor(); - - // Update character visibility first - switch (gfx_actor->GetSimDataBuffer().simbuf_actor_state) - { - case ActorState::NETWORKED_HIDDEN: - entity->setVisible(false); - break; - case ActorState::NETWORKED_OK: - entity->setVisible(gfx_actor->HasDriverSeatProp()); - break; - default: - break; // no change. - } - - // If visible, update position - if (entity->isVisible()) - { - Ogre::Vector3 pos; - Ogre::Quaternion rot; - xc_simbuf.simbuf_actor_coupling->GetGfxActor()->CalculateDriverPos(pos, rot); - xc_scenenode->setOrientation(rot); - // hack to position the character right perfect on the default seat (because the mesh has decentered origin) - xc_scenenode->setPosition(pos + (rot * Vector3(0.f, -0.6f, 0.f))); - } - } - else - { - xc_scenenode->resetOrientation(); - xc_scenenode->yaw(-xc_simbuf.simbuf_character_rot); - xc_scenenode->setPosition(xc_simbuf.simbuf_character_pos); - xc_scenenode->setVisible(true); - } - - // Animation - if (xc_simbuf.simbuf_anim_name != xc_simbuf_prev.simbuf_anim_name) - { - // 'Classic' method - enable one anim, exterminate the others ~ only_a_ptr, 06/2018 - AnimationStateIterator it = entity->getAllAnimationStates()->getAnimationStateIterator(); - - while (it.hasMoreElements()) - { - AnimationState* as = it.getNext(); - - if (as->getAnimationName() == xc_simbuf.simbuf_anim_name) - { - as->setEnabled(true); - as->setWeight(1); - as->addTime(xc_simbuf.simbuf_anim_time); - } - else - { - as->setEnabled(false); - as->setWeight(0); - } - } - } - else if (xc_simbuf.simbuf_anim_name != "") // Just do nothing if animation name is empty. May happen during networked play. - { - auto* as_cur = entity->getAnimationState(xc_simbuf.simbuf_anim_name); - as_cur->setTimePosition(xc_simbuf.simbuf_anim_time); - } - - // Multiplayer label -#ifdef USE_SOCKETW - if (App::mp_state->getEnum() == MpState::CONNECTED && !xc_simbuf.simbuf_actor_coupling) - { - // From 'updateCharacterNetworkColor()' - const String materialName = "tracks/" + xc_instance_name; - - MaterialPtr mat = MaterialManager::getSingleton().getByName(materialName); - if (!mat.isNull() && mat->getNumTechniques() > 0 && mat->getTechnique(0)->getNumPasses() > 1 && - mat->getTechnique(0)->getPass(1)->getNumTextureUnitStates() > 1) - { - const auto& state = mat->getTechnique(0)->getPass(1)->getTextureUnitState(1); - Ogre::ColourValue color = App::GetNetwork()->GetPlayerColor(xc_simbuf.simbuf_color_number); - state->setColourOperationEx(LBX_BLEND_CURRENT_ALPHA, LBS_MANUAL, LBS_CURRENT, color); - } - - if ((!xc_simbuf.simbuf_is_remote && !App::mp_hide_own_net_label->getBool()) || - (xc_simbuf.simbuf_is_remote && !App::mp_hide_net_labels->getBool())) - { - float camDist = (xc_scenenode->getPosition() - App::GetCameraManager()->GetCameraNode()->getPosition()).length(); - Ogre::Vector3 scene_pos = xc_scenenode->getPosition(); - scene_pos.y += (1.9f + camDist / 100.0f); - - App::GetGfxScene()->DrawNetLabel(scene_pos, camDist, xc_simbuf.simbuf_net_username, xc_simbuf.simbuf_color_number); - } - } -#endif // USE_SOCKETW + if (str == "SITUATION_ON_SOLID_GROUND") { return SITUATION_ON_SOLID_GROUND; } + if (str == "SITUATION_IN_SHALLOW_WATER") { return SITUATION_IN_SHALLOW_WATER; } + if (str == "SITUATION_IN_DEEP_WATER") { return SITUATION_IN_DEEP_WATER; } + if (str == "SITUATION_IN_AIR") { return SITUATION_IN_AIR; } + if (str == "SITUATION_DRIVING") { return SITUATION_DRIVING; } + if (str == "SITUATION_CUSTOM_MODE_01") { return SITUATION_CUSTOM_MODE_01; } + if (str == "SITUATION_CUSTOM_MODE_02") { return SITUATION_CUSTOM_MODE_02; } + if (str == "SITUATION_CUSTOM_MODE_03") { return SITUATION_CUSTOM_MODE_03; } + if (str == "SITUATION_CUSTOM_MODE_04") { return SITUATION_CUSTOM_MODE_04; } + if (str == "SITUATION_CUSTOM_MODE_05") { return SITUATION_CUSTOM_MODE_05; } + if (str == "SITUATION_CUSTOM_MODE_06") { return SITUATION_CUSTOM_MODE_06; } + if (str == "SITUATION_CUSTOM_MODE_07") { return SITUATION_CUSTOM_MODE_07; } + if (str == "SITUATION_CUSTOM_MODE_08") { return SITUATION_CUSTOM_MODE_08; } + if (str == "SITUATION_CUSTOM_MODE_09") { return SITUATION_CUSTOM_MODE_09; } + if (str == "SITUATION_CUSTOM_MODE_10") { return SITUATION_CUSTOM_MODE_10; } + + return 0; } + +CharacterDocumentPtr Character::getCharacterDocument() { return m_cache_entry->character_def; } \ No newline at end of file diff --git a/source/main/gameplay/Character.h b/source/main/gameplay/Character.h index 21c6071367..c0c78c7b27 100644 --- a/source/main/gameplay/Character.h +++ b/source/main/gameplay/Character.h @@ -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 2017-2018 Petr Ohlidal + Copyright 2017-2022 Petr Ohlidal For more information, see http://www.rigsofrods.org/ @@ -21,8 +21,10 @@ #pragma once -#include "Actor.h" +#include "CharacterFileFormat.h" #include "ForwardDeclarations.h" +#include "RoRnet.h" +#include "SimBuffers.h" #include #include @@ -36,91 +38,92 @@ namespace RoR { /// @addtogroup Character /// @{ +/// +/// Character uses simplified physics and occupies single point in space. +/// Note on animations: +/// This object decides what animations are played and how fast, but doesn't apply it to visual scene. +/// Visual 3D model and animations are loaded and updated by `RoR::GfxCharacter` using data from sim buffers (see file 'SimBuffers.h') class Character { + friend struct GfxCharacter; // visual counterpart. + friend class CharacterFactory; + public: - Character(int source = -1, unsigned int streamid = 0, Ogre::UTFString playerName = "", int color_number = 0, bool is_remote = true); + static const char* ControlFlagToString(BitMask_t single_flag); + static BitMask_t ControlFlagFromString(std::string const& single_flag_str); + + static const char* SituationFlagToString(BitMask_t single_situation_flag); + static BitMask_t SituationFlagFromString(std::string const& single_situation_flag_str); + + Character(CacheEntry* cacheEntry, CacheEntry* skinEntry, int source = -1, unsigned int streamid = 0, Ogre::UTFString playerName = "", int color_number = 0, bool is_remote = true); ~Character(); + + // get info + CharacterDocumentPtr getCharacterDocument(); - int getSourceID() const { return m_source_id; } - bool isRemote() const { return m_is_remote; } - int GetColorNum() const { return m_color_number; } - bool GetIsRemote() const { return m_is_remote; } - Ogre::UTFString const& GetNetUsername() { return m_net_username; } - std::string const & GetAnimName() const { return m_anim_name; } - float GetAnimTime() const { return m_anim_time; } - Ogre::Radian getRotation() const { return m_character_rotation; } - ActorPtr GetActorCoupling() { return m_actor_coupling; } - void setColour(int color) { this->m_color_number = color; } + // get state Ogre::Vector3 getPosition(); + Ogre::Radian getRotation() const { return m_character_rotation; } + ActorPtr GetActorCoupling(); + void setPosition(Ogre::Vector3 position); void setRotation(Ogre::Radian rotation); void move(Ogre::Vector3 offset); - void update(float dt); + void updateLocal(float dt); void updateCharacterRotation(); - void receiveStreamData(unsigned int& type, int& source, unsigned int& streamid, char* buffer); void SetActorCoupling(bool enabled, ActorPtr actor); - GfxCharacter* SetupGfx(); + + // network + void receiveStreamData(unsigned int& type, int& source, unsigned int& streamid, char* buffer); + void SendStreamData(); + int getSourceID() const { return m_source_id; } + bool isRemote() const { return m_is_remote; } + int GetColorNum() const { return m_color_number; } + void setColour(int color) { this->m_color_number = color; } + Ogre::UTFString const& GetNetUsername() { return m_net_username; } + + // get visuals + GfxCharacter* getGfxCharacter() { return m_gfx_character.get(); } private: void ReportError(const char* detail); - void SendStreamData(); void SendStreamSetup(); - void SetAnimState(std::string mode, float time = 0); - ActorPtr m_actor_coupling; //!< The vehicle or machine which the character occupies + // attributes + CacheEntry* m_cache_entry = nullptr; + CacheEntry* m_used_skin_entry = nullptr; + std::string m_instance_name; + + // transforms + Ogre::Vector3 m_character_position; + Ogre::Vector3 m_prev_position; Ogre::Radian m_character_rotation; float m_character_h_speed; float m_character_v_speed; - Ogre::Vector3 m_character_position; - Ogre::Vector3 m_prev_position; - int m_color_number; - int m_stream_id; - int m_source_id; - bool m_can_jump; + + // state + BitMask_t m_control_flags = 0; //!< `RoRnet::ControlFlags` + BitMask_t m_situation_flags = 0; //!< `RoRnet::SituationFlags` + ActorPtr m_actor_coupling; //!< The vehicle or machine which the character occupies + + // network bool m_is_remote; - std::string m_anim_name; - float m_anim_time; - float m_net_last_anim_time; - float m_driving_anim_length; - std::string m_instance_name; + int m_color_number; Ogre::UTFString m_net_username; Ogre::Timer m_net_timer; unsigned long m_net_last_update_time; - GfxCharacter* m_gfx_character; + int m_stream_id = 0; + int m_source_id = 0; // 0=local + + // visuals + std::unique_ptr m_gfx_character; }; /// @} // addtogroup Character /// @} // addtogroup Gameplay -struct GfxCharacter -{ - struct SimBuffer //!< Buffered simulation state for async gfx scene update - { - Ogre::Vector3 simbuf_character_pos; - Ogre::Radian simbuf_character_rot; //!< When on foot - Ogre::UTFString simbuf_net_username; - bool simbuf_is_remote; - int simbuf_color_number; - ActorPtr simbuf_actor_coupling; - std::string simbuf_anim_name; - float simbuf_anim_time; // Intentionally left empty = forces initial update. - }; - - ~GfxCharacter(); - - void BufferSimulationData(); - void UpdateCharacterInScene(); - - Ogre::SceneNode* xc_scenenode; - SimBuffer xc_simbuf; - SimBuffer xc_simbuf_prev; - Character* xc_character; - std::string xc_instance_name; // TODO: Store MaterialPtr-s directly ~only_a_ptr, 05/2018 -}; - } // namespace RoR diff --git a/source/main/gameplay/CharacterFactory.cpp b/source/main/gameplay/CharacterFactory.cpp index 78a2e60665..f97f8a0b9e 100644 --- a/source/main/gameplay/CharacterFactory.cpp +++ b/source/main/gameplay/CharacterFactory.cpp @@ -21,17 +21,28 @@ #include "CharacterFactory.h" +#include "Actor.h" #include "Application.h" +#include "CacheSystem.h" #include "Character.h" +#include "Console.h" +#include "GfxCharacter.h" #include "GfxScene.h" #include "Utils.h" using namespace RoR; +CharacterFactory::CharacterFactory() +{ +} + Character* CharacterFactory::CreateLocalCharacter() { int colourNum = -1; Ogre::UTFString playerName = ""; + // Singleplayer character presets + std::string characterFile = App::sim_player_character->getStr(); + std::string characterSkin = App::sim_player_character_skin->getStr(); #ifdef USE_SOCKETW if (App::mp_state->getEnum() == MpState::CONNECTED) @@ -39,12 +50,58 @@ Character* CharacterFactory::CreateLocalCharacter() RoRnet::UserInfo info = App::GetNetwork()->GetLocalUserData(); colourNum = info.colournum; playerName = tryConvertUTF(info.username); + // Multiplayer character presets + characterFile = info.character_file; + characterSkin = info.character_skin; } #endif // USE_SOCKETW - m_local_character = std::unique_ptr(new Character(-1, 0, playerName, colourNum, false)); - App::GetGfxScene()->RegisterGfxCharacter(m_local_character->SetupGfx()); - return m_local_character.get(); + CacheEntry* cache_entry = App::GetCacheSystem()->FindEntryByFilename(LT_Character, /*partial:*/false, characterFile); + if (!cache_entry) + { + // If this was a custom mod, retry with the builtin + if (characterFile != DEFAULT_CHARACTER_FILE) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Could not find configured character '{}' in mod cache, falling back to '{}'", characterFile, DEFAULT_CHARACTER_FILE)); + App::sim_player_character->setStr(DEFAULT_CHARACTER_FILE); + + cache_entry = App::GetCacheSystem()->FindEntryByFilename(LT_Character, /*partial:*/false, App::sim_player_character->getStr()); + } + } + if (cache_entry) + { + CharacterDocumentPtr document = App::GetCacheSystem()->FetchCharacterDef(cache_entry); // Make sure it exists + if (!document) + { + return nullptr; // Error already reported + } + // The loaded document is now pinned to the CacheEntry + } + else + { + 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; + } + + CacheEntry* skin_entry = this->fetchCharacterSkin(characterSkin, _L("local player")); + Character* ch = new Character(cache_entry, skin_entry, -1, 0, playerName, colourNum, false); + ROR_ASSERT(m_characters.size() == 0); + m_characters.push_back(std::unique_ptr(ch)); + + // GFX setup + try + { + ch->m_gfx_character = std::unique_ptr(new GfxCharacter(ch)); + } + catch (Ogre::Exception& eeh) { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("error creating GfxCharacter, message:{}", eeh.getFullDescription())); + } + + // Done + return ch; } void CharacterFactory::createRemoteInstance(int sourceid, int streamid) @@ -55,22 +112,59 @@ 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); + + CacheEntry* cache_entry = App::GetCacheSystem()->FindEntryByFilename(LT_Character, /*partial:*/false, info.character_file); + if (!cache_entry) + { + // If this was a custom mod, retry with the builtin + if (std::string(info.character_file) != DEFAULT_CHARACTER_FILE) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Could not create custom character character for {} - character '{}' not found in mod cache, falling back to '{}'", info_str, info.character_file, DEFAULT_CHARACTER_FILE)); - Character* ch = new Character(sourceid, streamid, name, colour, true); - App::GetGfxScene()->RegisterGfxCharacter(ch->SetupGfx()); - m_remote_characters.push_back(std::unique_ptr(ch)); + cache_entry = App::GetCacheSystem()->FindEntryByFilename(LT_Character, /*partial:*/false, DEFAULT_CHARACTER_FILE); + } + + 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, info.character_file)); + return; + } + + CharacterDocumentPtr document = App::GetCacheSystem()->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; + } + + CacheEntry* skin_entry = this->fetchCharacterSkin(info.character_skin, info_str); + Character* ch = new Character(cache_entry, skin_entry, sourceid, streamid, name, colour, true); + m_characters.push_back(std::unique_ptr(ch)); + + // GFX setup + try + { + ch->m_gfx_character = std::unique_ptr(new GfxCharacter(ch)); + } + catch (Ogre::Exception& eeh) { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("error creating GfxCharacter, message:{}", eeh.getFullDescription())); + } #endif // USE_SOCKETW } void CharacterFactory::removeStreamSource(int sourceid) { - for (auto it = m_remote_characters.begin(); it != m_remote_characters.end(); it++) + for (auto it = m_characters.begin(); it != m_characters.end(); it++) { if ((*it)->getSourceID() == sourceid) { (*it).reset(); - m_remote_characters.erase(it); + m_characters.erase(it); return; } } @@ -78,19 +172,24 @@ void CharacterFactory::removeStreamSource(int sourceid) void CharacterFactory::Update(float dt) { - m_local_character->update(dt); + // character at [0] is local + m_characters[0]->updateLocal(dt); - for (auto& c : m_remote_characters) +#ifdef USE_SOCKETW + if ((App::mp_state->getEnum() == MpState::CONNECTED)) { - c->update(dt); + m_characters[0]->SendStreamData(); } +#endif // USE_SOCKETW + + } void CharacterFactory::UndoRemoteActorCoupling(ActorPtr actor) { - for (auto& c : m_remote_characters) + for (auto& c : m_characters) { - if (c->GetActorCoupling() == actor) + if (c->isRemote() && c->GetActorCoupling() == actor) { c->SetActorCoupling(false, nullptr); } @@ -99,8 +198,7 @@ void CharacterFactory::UndoRemoteActorCoupling(ActorPtr actor) void CharacterFactory::DeleteAllCharacters() { - m_remote_characters.clear(); // std::unique_ptr<> will do the cleanup... - m_local_character.reset(); // ditto + m_characters.clear(); // std::unique_ptr<> will do the cleanup... } #ifdef USE_SOCKETW @@ -122,11 +220,41 @@ void CharacterFactory::handleStreamData(std::vector packet_b } else { - for (auto& c : m_remote_characters) + for (auto& c : m_characters) { - c->receiveStreamData(packet.header.command, packet.header.source, packet.header.streamid, packet.buffer); + if (c->isRemote()) + c->receiveStreamData(packet.header.command, packet.header.source, packet.header.streamid, packet.buffer); } } } } #endif // USE_SOCKETW + +CacheEntry* CharacterFactory::fetchCharacterSkin(const std::string& skinName, const std::string& errLogPlayer) +{ + if (skinName == "") + return nullptr; + + CacheQuery skinQuery; + skinQuery.cqy_filter_type = LT_Skin; + skinQuery.cqy_search_method = CacheSearchMethod::NAME_FULL; + skinQuery.cqy_search_string = skinName; + if (App::GetCacheSystem()->Query(skinQuery) == 0) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Skin '{}' requested by player player '{}' is not installed, continuing without it.", + skinName, errLogPlayer)); + return nullptr; + } + + if (!App::GetCacheSystem()->FetchSkinDef(skinQuery.cqy_results[0].cqr_entry)) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, + fmt::format("Could not load character skin '{}' for player '{}', continuing without it.", + skinName, errLogPlayer)); + return nullptr; + } + + // The loaded skin is pinned to the CacheEntry now. + return skinQuery.cqy_results[0].cqr_entry; +} diff --git a/source/main/gameplay/CharacterFactory.h b/source/main/gameplay/CharacterFactory.h index 2ddde2794d..82c922baa1 100644 --- a/source/main/gameplay/CharacterFactory.h +++ b/source/main/gameplay/CharacterFactory.h @@ -24,6 +24,7 @@ #include "Application.h" #include "Character.h" +#include "CharacterFileFormat.h" #include "Network.h" #include @@ -39,23 +40,31 @@ namespace RoR { class CharacterFactory { public: - CharacterFactory() {} + CharacterFactory(); Character* CreateLocalCharacter(); - Character* GetLocalCharacter() { return m_local_character.get(); } + void DeleteAllCharacters(); - void UndoRemoteActorCoupling(ActorPtr actor); + void Update(float dt); + + // get + std::vector>& getAllCharacters() { return m_characters; } + Character* getLocalCharacter() { return (m_characters.size() > 0) ? m_characters[0].get() : nullptr; } + + // net #ifdef USE_SOCKETW void handleStreamData(std::vector packet); #endif // USE_SOCKETW + void UndoRemoteActorCoupling(ActorPtr actor); private: - std::unique_ptr m_local_character; - std::vector> m_remote_characters; + std::vector> m_characters; // local character is at [0] void createRemoteInstance(int sourceid, int streamid); void removeStreamSource(int sourceid); + + CacheEntry* fetchCharacterSkin(const std::string& skinName, const std::string& errLogPlayer); }; /// @} // addtogroup Character diff --git a/source/main/gfx/GfxActor.cpp b/source/main/gfx/GfxActor.cpp index ffd272ae71..100ef26529 100644 --- a/source/main/gfx/GfxActor.cpp +++ b/source/main/gfx/GfxActor.cpp @@ -1804,7 +1804,15 @@ void RoR::GfxActor::UpdateSimDataBuffer() bool RoR::GfxActor::IsActorLive() const { - return (m_actor->ar_state < ActorState::LOCAL_SLEEPING); + switch (m_actor->ar_state) + { + case ActorState::LOCAL_SIMULATED: //!< simulated (local) actor + case ActorState::NETWORKED_OK: //!< not simulated (remote) actor + case ActorState::LOCAL_REPLAY: + return true; + default: + return false; + } } void RoR::GfxActor::UpdateCabMesh() diff --git a/source/main/gfx/GfxActor.h b/source/main/gfx/GfxActor.h index 2083a9240e..7431ebd97f 100644 --- a/source/main/gfx/GfxActor.h +++ b/source/main/gfx/GfxActor.h @@ -127,8 +127,6 @@ class GfxActor // Helpers bool IsActorLive() const; //!< Should the visuals be updated for this actor? - bool IsActorInitialized() const { return m_initialized; } //!< Temporary TODO: Remove once the spawn routine is fixed - void InitializeActor() { m_initialized = true; } //!< Temporary TODO: Remove once the spawn routine is fixed void CalculateDriverPos(Ogre::Vector3& out_pos, Ogre::Quaternion& out_rot); int GetActorId() const; int GetActorState() const; @@ -163,7 +161,6 @@ class GfxActor // Game state std::set m_linked_gfx_actors; - bool m_initialized = false; VideoCamState m_vidcam_state = VideoCamState::VCSTATE_ENABLED_ONLINE; DebugViewType m_debug_view = DebugViewType::DEBUGVIEW_NONE; DebugViewType m_last_debug_view = DebugViewType::DEBUGVIEW_SKELETON; // intentional diff --git a/source/main/gfx/GfxCharacter.cpp b/source/main/gfx/GfxCharacter.cpp new file mode 100644 index 0000000000..8ff4ef449f --- /dev/null +++ b/source/main/gfx/GfxCharacter.cpp @@ -0,0 +1,481 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2017-2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#include "GfxCharacter.h" + +#include "Application.h" +#include "Actor.h" +#include "ActorManager.h" +#include "CameraManager.h" +#include "Collisions.h" +#include "Console.h" +#include "GameContext.h" +#include "GfxScene.h" +#include "GUIManager.h" +#include "GUI_CharacterPoseUtil.h" +#include "InputEngine.h" +#include "MovableText.h" +#include "Network.h" +#include "SkinFileFormat.h" +#include "Terrain.h" +#include "Utils.h" +#include "Water.h" + +using namespace Ogre; +using namespace RoR; + +GfxCharacter::GfxCharacter(Character* character) + : xc_character(character) + , xc_instance_name(character->m_instance_name) +{ + ROR_ASSERT(character); + ROR_ASSERT(character->m_cache_entry); + ROR_ASSERT(character->m_cache_entry->character_def); + + // Use the modcache bundle (ZIP) resource group. + // This is equivalent to what `Actor`s do. + xc_custom_resource_group = character->m_cache_entry->resource_group; + + Entity* entity = App::GetGfxScene()->GetSceneManager()->createEntity( + /*entityName:*/ xc_instance_name + "_mesh", + /*meshName:*/ xc_character->m_cache_entry->character_def->mesh_name, + /*groupName:*/ character->m_cache_entry->resource_group); + + // fix disappearing mesh + AxisAlignedBox aabb; + aabb.setInfinite(); + entity->getMesh()->_setBounds(aabb); + + // add entity to the scene node + xc_scenenode = App::GetGfxScene()->GetSceneManager()->getRootSceneNode()->createChildSceneNode(); + xc_scenenode->attachObject(entity); + xc_scenenode->setScale(xc_character->m_cache_entry->character_def->mesh_scale); + xc_scenenode->setVisible(false); + + // setup material - resolve skins (SkinZips) and apply multiplayer color + // NOTE: The mesh (for example Sinbad the Ogre) may use multiple materials + // which means having multiple submeshes in Mesh and subentities in Entity + // (each submesh/subentity can have only 1 material) + for (Ogre::SubEntity* subent: entity->getSubEntities()) + { + const std::string sharedMatName = subent->getSubMesh()->getMaterialName(); + subent->setMaterial(this->FindOrCreateCustomizedMaterial(sharedMatName)); + } + + // setup animation blend + switch (character->getCharacterDocument()->force_animblend) + { + case ForceAnimBlend::CUMULATIVE: entity->getSkeleton()->setBlendMode(ANIMBLEND_CUMULATIVE); break; + case ForceAnimBlend::AVERAGE: entity->getSkeleton()->setBlendMode(ANIMBLEND_AVERAGE); break; + default:; // Keep the preset we loaded from .skeleton file + } + + // setup bone blend masks + for (BoneBlendMaskDef& mask_def : character->getCharacterDocument()->bone_blend_masks) + { + this->SetupBoneBlendMask(mask_def); + } + + // setup diagnostic UI + xc_action_dbg_states.resize(xc_character->m_cache_entry->character_def->actions.size()); + for (CharacterActionDef const& def : xc_character->m_cache_entry->character_def->actions) + { + xc_action_dbg_states[def.action_id] = CharacterActionDbg(); + } +} + +void GfxCharacter::SetupBoneBlendMask(BoneBlendMaskDef const& mask_def) +{ + Entity* entity = static_cast(xc_scenenode->getAttachedObject(0)); + + AnimationState* mask_created = nullptr; + try + { + AnimationState* anim = entity->getAnimationState(mask_def.anim_name); + if (mask_def.bone_weights.size() > 0) + { + anim->createBlendMask(entity->getSkeleton()->getNumBones()); + mask_created = anim; + for (BoneBlendMaskWeightDef const& def : mask_def.bone_weights) + { + Bone* bone = entity->getSkeleton()->getBone(def.bone_name); + anim->setBlendMaskEntry(bone->getHandle(), def.bone_weight); + } + } + } + catch (Exception& eeh) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("error setting up bone blend mask for animation '{}', message:{}", mask_def.anim_name, eeh.getFullDescription())); + if (mask_created) + mask_created->destroyBlendMask(); + } +} + +RoR::GfxCharacter::~GfxCharacter() +{ + Entity* ent = static_cast(xc_scenenode->getAttachedObject(0)); + xc_scenenode->detachAllObjects(); + App::GetGfxScene()->GetSceneManager()->destroySceneNode(xc_scenenode); + App::GetGfxScene()->GetSceneManager()->destroyEntity(ent); + MaterialManager::getSingleton().unload("tracks/" + xc_instance_name); +} + +void RoR::GfxCharacter::BufferSimulationData() +{ + xc_simbuf_prev = xc_simbuf; + + xc_simbuf.simbuf_character_pos = xc_character->getPosition(); + xc_simbuf.simbuf_character_rot = xc_character->getRotation(); + xc_simbuf.simbuf_color_number = xc_character->GetColorNum(); + xc_simbuf.simbuf_net_username = xc_character->GetNetUsername(); + xc_simbuf.simbuf_is_remote = xc_character->isRemote(); + xc_simbuf.simbuf_actor_coupling = xc_character->GetActorCoupling(); + xc_simbuf.simbuf_control_flags = xc_character->m_control_flags; + xc_simbuf.simbuf_situation_flags = xc_character->m_situation_flags; + xc_simbuf.simbuf_character_h_speed = xc_character->m_character_h_speed; +} + +void RoR::GfxCharacter::UpdateCharacterInScene(float dt) +{ + // Actor coupling + if (xc_simbuf.simbuf_actor_coupling != xc_simbuf_prev.simbuf_actor_coupling) + { + if (xc_simbuf.simbuf_actor_coupling != nullptr) + { + // Entering/switching vehicle + xc_scenenode->getAttachedObject(0)->setCastShadows(false); + xc_scenenode->setVisible(xc_simbuf.simbuf_actor_coupling->GetGfxActor()->HasDriverSeatProp()); + } + else if (xc_simbuf_prev.simbuf_actor_coupling != nullptr) + { + // Leaving vehicle + xc_scenenode->getAttachedObject(0)->setCastShadows(true); + xc_scenenode->resetOrientation(); + } + } + + // Position + Orientation + Ogre::Entity* entity = static_cast(xc_scenenode->getAttachedObject(0)); + if (xc_simbuf.simbuf_actor_coupling != nullptr) + { + // We're in vehicle + GfxActor* gfx_actor = xc_simbuf.simbuf_actor_coupling->GetGfxActor(); + + // Update character visibility first + switch (gfx_actor->GetSimDataBuffer().simbuf_actor_state) + { + case ActorState::NETWORKED_HIDDEN: + entity->setVisible(false); + break; + case ActorState::NETWORKED_OK: + entity->setVisible(gfx_actor->HasDriverSeatProp()); + break; + default: + break; // no change. + } + + // If visible, update position + if (entity->isVisible()) + { + Ogre::Vector3 pos; + Ogre::Quaternion rot; + xc_simbuf.simbuf_actor_coupling->GetGfxActor()->CalculateDriverPos(pos, rot); + xc_scenenode->setOrientation(rot); + // hack to position the character right perfect on the default seat (because the mesh has decentered origin) + xc_scenenode->setPosition(pos + (rot * Vector3(0.f, -0.6f, 0.f))); + } + } + else + { + xc_scenenode->resetOrientation(); + xc_scenenode->yaw(-xc_simbuf.simbuf_character_rot); + xc_scenenode->setPosition(xc_simbuf.simbuf_character_pos); + xc_scenenode->setVisible(true); + } + + if (!xc_manual_pose_active) + { + try + { + this->UpdateAnimations(dt); + } + catch (Ogre::Exception& eeh) { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("error updating animations, message:{}", eeh.getFullDescription())); + } + } + + // Multiplayer label +#ifdef USE_SOCKETW + if (App::mp_state->getEnum() == MpState::CONNECTED && !xc_simbuf.simbuf_actor_coupling) + { + if ((!xc_simbuf.simbuf_is_remote && !App::mp_hide_own_net_label->getBool()) || + (xc_simbuf.simbuf_is_remote && !App::mp_hide_net_labels->getBool())) + { + float camDist = (xc_scenenode->getPosition() - App::GetCameraManager()->GetCameraNode()->getPosition()).length(); + Ogre::Vector3 scene_pos = xc_scenenode->getPosition(); + scene_pos.y += (1.9f + camDist / 100.0f); + + App::GetGfxScene()->DrawNetLabel(scene_pos, camDist, xc_simbuf.simbuf_net_username, xc_simbuf.simbuf_color_number); + } + } +#endif // USE_SOCKETW +} + +void GfxCharacter::EvaluateActionDef(CharacterActionDef const& def, float dt) +{ + CharacterActionDbg dbg; + + // Test if applicable. + if ((!BITMASK_IS_1(xc_simbuf.simbuf_situation_flags, def.for_situations)) || // not all situation flags are satisified + (xc_simbuf.simbuf_situation_flags & def.except_situations) || // any of the forbidden situation matches + (!BITMASK_IS_1(xc_simbuf.simbuf_control_flags, def.for_controls)) || // not all action flags are satisfied + (xc_simbuf.simbuf_control_flags & def.except_controls)) // any of the forbidden situation matches + { + dbg.blocking_situations = xc_simbuf.simbuf_situation_flags & def.except_situations; + dbg.blocking_controls = xc_simbuf.simbuf_control_flags & def.except_controls; + dbg.missing_situations = def.for_situations & ~xc_simbuf.simbuf_situation_flags; + dbg.missing_controls = def.for_controls & ~xc_simbuf.simbuf_control_flags; + xc_action_dbg_states[def.action_id] = dbg; + return; + } + + Ogre::Entity* entity = static_cast(xc_scenenode->getAttachedObject(0)); + AnimationState* as = entity->getAnimationState(def.anim_name); + + // Query data sources. + float timepos = 1.f; + if (def.playback_time_ratio != 0.f) + { + timepos *= (def.playback_time_ratio * dt); + dbg.source_dt = dt; + dbg.input_dt = (def.playback_time_ratio * dt); + } + if (def.playback_h_speed_ratio != 0.f) + { + timepos *= (def.playback_h_speed_ratio * xc_simbuf.simbuf_character_h_speed); + dbg.source_hspeed = xc_simbuf.simbuf_character_h_speed; + dbg.input_hspeed = (def.playback_h_speed_ratio * xc_simbuf.simbuf_character_h_speed); + } + if (def.playback_steering_ratio != 0.f && xc_simbuf.simbuf_actor_coupling) + { + timepos *= (def.playback_steering_ratio * xc_simbuf.simbuf_actor_coupling->ar_hydro_dir_wheel_display); + dbg.source_steering = xc_simbuf.simbuf_actor_coupling->ar_hydro_dir_wheel_display; + dbg.input_steering = (def.playback_steering_ratio * xc_simbuf.simbuf_actor_coupling->ar_hydro_dir_wheel_display); + } + + // Transform the anim pos. + if (def.source_percentual) + { + if (def.anim_neutral_mid) + { + timepos = (timepos + 1.0f) * 0.5f; + } + timepos *= as->getLength(); + } + if (def.playback_trim > 0.f) + { + // prevent animation flickering on the borders: + if (timepos < def.playback_trim) + { + timepos = def.playback_trim; + } + if (timepos > as->getLength() - def.playback_trim) + { + timepos = as->getLength() - def.playback_trim; + } + } + if (def.anim_autorestart) + { + // If the animation was just activated, start from 0. + if (!BITMASK_IS_1(xc_simbuf_prev.simbuf_control_flags, def.for_controls)) + { + as->setTimePosition(0.f); + } + } + if (def.anim_continuous) + { + timepos += as->getTimePosition(); + } + + // Update the OGRE object + as->setTimePosition(timepos); + as->setWeight(def.weight); + as->setEnabled(true); + + dbg.active = true; + xc_action_dbg_states[def.action_id] = dbg; +} + +void GfxCharacter::UpdateAnimations(float dt) +{ + // Reset all anims + Ogre::Entity* entity = static_cast(xc_scenenode->getAttachedObject(0)); + AnimationStateSet* stateset = entity->getAllAnimationStates(); + for (auto& state_pair : stateset->getAnimationStates()) + { + AnimationState* as = state_pair.second; + as->setEnabled(false); + as->setWeight(0); + } + + for (CharacterActionDef const& def : xc_character->m_cache_entry->character_def->actions) + { + this->EvaluateActionDef(def, dt); + } +} + +void GfxCharacter::DisableAnim(Ogre::AnimationState* as) +{ + as->setEnabled(false); + as->setWeight(0); +} + +void GfxCharacter::EnableAnim(Ogre::AnimationState* as, float time) +{ + as->setEnabled(true); + as->setWeight(1); + as->setTimePosition(time); // addTime() ? +} + + +Ogre::MaterialPtr GfxCharacter::FindOrCreateCustomizedMaterial(const std::string& mat_lookup_name) +{ + // Spawn helper which resolves skins (SkinZips) and returns unique material + // Derived from `ActorSpawner::FindOrCreateCustomizedMaterial()` + // ------------------------------------------------------------------------ + + // Query .skin material replacements + if (xc_character->m_used_skin_entry != nullptr) + { + std::shared_ptr& skin_def = xc_character->m_used_skin_entry->skin_def; + + auto skin_res = skin_def->replace_materials.find(mat_lookup_name); + if (skin_res != skin_def->replace_materials.end()) + { + // Material substitution found - check if the material exists. + Ogre::MaterialPtr skin_mat = Ogre::MaterialManager::getSingleton().getByName( + skin_res->second, xc_character->m_used_skin_entry->resource_group); + if (!skin_mat.isNull()) + { + // Material exists - clone and return right away - texture substitutions aren't done in this case. + std::string name_buf = fmt::format("{}@{}", skin_mat->getName(), xc_character->m_instance_name); + return skin_mat->clone(name_buf, /*changeGroup=*/true, xc_custom_resource_group); + } + else + { + // Material doesn't exist - log warning and continue. + std::stringstream buf; + buf << "Character: Material '" << skin_res->second << "' from skin '" << xc_character->m_used_skin_entry->dname + << "' not found (filename: '" << xc_character->m_used_skin_entry->fname + << "', resource group: '"<< xc_character->m_used_skin_entry->resource_group + <<"')! Ignoring it..."; + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_WARNING, buf.str()); + } + } + } + + // Skinzip material replacement not found - clone the input material + MaterialPtr sharedMat = MaterialManager::getSingleton().getByName( + /*name:*/ mat_lookup_name, + /*groupName:*/ xc_character->m_cache_entry->resource_group); + std::string name_buf = fmt::format("{}@{}", mat_lookup_name, xc_character->m_instance_name); + MaterialPtr ownMat = sharedMat->clone(name_buf, /*changeGroup=*/true, xc_custom_resource_group); + + // Finally, query .skin texture replacements + if (xc_character->m_used_skin_entry != nullptr) + { + for (auto& technique: ownMat->getTechniques()) + { + for (auto& pass: technique->getPasses()) + { + for (auto& tex_unit: pass->getTextureUnitStates()) + { + const size_t num_frames = tex_unit->getNumFrames(); + for (size_t i = 0; i < num_frames; ++i) + { + const auto end = xc_character->m_used_skin_entry->skin_def->replace_textures.end(); + const auto query = xc_character->m_used_skin_entry->skin_def->replace_textures.find(tex_unit->getFrameTextureName((unsigned int)i)); + if (query != end) + { + // Skin has replacement for this texture + if (xc_character->m_used_skin_entry->resource_group != xc_custom_resource_group) // The skin comes from a SkinZip bundle (different resource group) + { + Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().getByName( + query->second, xc_character->m_used_skin_entry->resource_group); + if (tex.isNull()) + { + // `Ogre::TextureManager` doesn't automatically register all images in resource groups, + // it waits for `Ogre::Resource`s to be created explicitly. + // Normally this is done by `Ogre::MaterialManager` when loading a material. + // In this case we must do it manually + tex = Ogre::TextureManager::getSingleton().create( + query->second, xc_character->m_used_skin_entry->resource_group); + } + tex_unit->_setTexturePtr(tex, i); + } + else // The skin lives in the character bundle (same resource group) + { + tex_unit->setFrameTextureName(query->second, (unsigned int)i); + } + } + } // texture unit frames + } // texture unit states + } // passes + } // techniques + } + + // Apply player color + if (App::mp_state->getEnum() == MpState::CONNECTED) + { + this->ApplyMultiplayerColoring(ownMat, mat_lookup_name); + } + + return ownMat; +} + +void GfxCharacter::ApplyMultiplayerColoring(Ogre::MaterialPtr mat, std::string sharedMatName) +{ + if (!mat.isNull() && mat->getNumTechniques() > 0) // sanity check + { + Ogre::Pass* colorChangePass = mat->getTechnique(0)->getPass("ColorChange"); + if (colorChangePass) + { + Ogre::TextureUnitState* playerColorTexUnit = colorChangePass->getTextureUnitState("PlayerColor"); + if (playerColorTexUnit) + { + playerColorTexUnit->setColourOperationEx(LBX_BLEND_CURRENT_ALPHA, LBS_MANUAL, LBS_CURRENT, + App::GetNetwork()->GetPlayerColor(xc_character->m_color_number)); + } + else + { + std::string msg = fmt::format("Character '{}' material '{}', pass 'ColorChange' doesn't have texture unit named 'PlayerColor', cannot apply player color", xc_character->m_instance_name, sharedMatName); + LOG(msg); + } + } + else + { + std::string msg = fmt::format("Character '{}' material '{}' doesn't have pass named 'ColorChange', cannot apply player color", xc_character->m_instance_name, sharedMatName); + LOG(msg); + } + } +} diff --git a/source/main/gfx/GfxCharacter.h b/source/main/gfx/GfxCharacter.h new file mode 100644 index 0000000000..eef09d4a73 --- /dev/null +++ b/source/main/gfx/GfxCharacter.h @@ -0,0 +1,96 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2017-2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#pragma once + +#include "CharacterFileFormat.h" +#include "ForwardDeclarations.h" +#include "SimBuffers.h" + +#include +#include + +namespace RoR { + +/// @addtogroup Gfx +/// @{ + +/// See `GUI::CharacterPoseUtil` +struct CharacterActionDbg +{ + bool active = false; + + // State diagnostic. + BitMask_t missing_situations = 0; //!< `RoRnet::SituationFlags`; The flags from 'for_situations' mask which are not satisfied. + BitMask_t missing_controls = 0; //!< `RoRnet::ControlFlags`; The flags from 'for_controls' mask which are not satisfied. + BitMask_t blocking_situations = 0; //!< `RoRnet::SituationFlags`; The flags from 'except_situations' mask which block this anim. + BitMask_t blocking_controls = 0; //!< `RoRnet::ControlFlags`; The flags from 'except_controls' mask which block this anim. + + // The raw source data of anim position. + float source_dt = 0.f; + float source_hspeed = 0.f; + float source_steering = 0.f; + + // The transformed inputs to anim position. + float input_dt = 0.f; + float input_hspeed = 0.f; + float input_steering = 0.f; +}; + +/// A visual counterpart to `RoR::Character`. +/// 3D objects are loaded and updated here, but positioning and animations are determined in simulation! +struct GfxCharacter +{ + GfxCharacter(Character* character); + ~GfxCharacter(); + + void BufferSimulationData(); + void UpdateCharacterInScene(float dt); + void DisableAnim(Ogre::AnimationState* anim_state); + void EnableAnim(Ogre::AnimationState* anim_state, float time); + void UpdateAnimations(float dt); + void EvaluateActionDef(CharacterActionDef const& def, float dt); + void SetupBoneBlendMask(BoneBlendMaskDef const& mask_def); + + // Spawn helper which resolves skins (SkinZips) + // Derived from `ActorSpawner::FindOrCreateCustomizedMaterial()` + Ogre::MaterialPtr FindOrCreateCustomizedMaterial(const std::string& mat_lookup_name); + + // Spawn helper + void ApplyMultiplayerColoring(Ogre::MaterialPtr mat, std::string sharedMatName); + + Ogre::SceneNode* xc_scenenode; + CharacterSB xc_simbuf; + CharacterSB xc_simbuf_prev; + Character* xc_character; + std::string xc_instance_name; + std::string xc_custom_resource_group; + + // `GUI::CharacterPoseUtil` context + std::vector xc_action_dbg_states; + bool xc_manual_pose_active = false; + +}; + +/// @} // addtogroup Gfx + +} // namespace RoR + diff --git a/source/main/gfx/GfxScene.cpp b/source/main/gfx/GfxScene.cpp index 9f514cbbb7..7f19af6ce4 100644 --- a/source/main/gfx/GfxScene.cpp +++ b/source/main/gfx/GfxScene.cpp @@ -28,6 +28,7 @@ #include "DustPool.h" #include "HydraxWater.h" #include "GameContext.h" +#include "GfxCharacter.h" #include "GUIManager.h" #include "GUIUtils.h" #include "GUI_DirectionArrow.h" @@ -67,10 +68,6 @@ void GfxScene::ClearScene() } m_dustpools.clear(); - // Delete game elements - m_all_gfx_actors.clear(); - m_all_gfx_characters.clear(); - // Wipe scene manager m_scene_manager->clearScene(); @@ -90,10 +87,13 @@ void GfxScene::Init() void GfxScene::UpdateScene(float dt_sec) { // Actors - start threaded tasks - for (GfxActor* gfx_actor: m_live_gfx_actors) + for (const ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors()) { - gfx_actor->UpdateFlexbodies(); // Push flexbody tasks to threadpool - gfx_actor->UpdateWheelVisuals(); // Push flexwheel tasks to threadpool + if (actor->GetGfxActor()->IsActorLive()) + { + actor->GetGfxActor()->UpdateFlexbodies(); // Push flexbody tasks to threadpool + actor->GetGfxActor()->UpdateWheelVisuals(); // Push flexwheel tasks to threadpool + } } // Var @@ -114,11 +114,11 @@ void GfxScene::UpdateScene(float dt_sec) // Particles if (App::gfx_particles_mode->getInt() == 1) { - for (GfxActor* gfx_actor: m_all_gfx_actors) + for (const ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors()) { - if (!m_simbuf.simbuf_sim_paused && !gfx_actor->GetSimDataBuffer().simbuf_physics_paused) + if (!m_simbuf.simbuf_sim_paused && !actor->GetGfxActor()->GetSimDataBuffer().simbuf_physics_paused) { - gfx_actor->UpdateParticles(m_simbuf.simbuf_sim_speed * dt_sec); + actor->GetGfxActor()->UpdateParticles(m_simbuf.simbuf_sim_speed * dt_sec); } } for (auto itor : m_dustpools) @@ -195,20 +195,21 @@ void GfxScene::UpdateScene(float dt_sec) } // HUD - network labels (always update) - for (GfxActor* gfx_actor: m_all_gfx_actors) + for (const ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors()) { - gfx_actor->UpdateNetLabels(m_simbuf.simbuf_sim_speed * dt_sec); + actor->GetGfxActor()->UpdateNetLabels(m_simbuf.simbuf_sim_speed * dt_sec); } // Player avatars - for (GfxCharacter* a: m_all_gfx_characters) + for (const std::unique_ptr& character: App::GetGameContext()->GetCharacterFactory()->getAllCharacters()) { - a->UpdateCharacterInScene(); + character->getGfxCharacter()->UpdateCharacterInScene(dt_sec); } // Actors - update misc visuals - for (GfxActor* gfx_actor: m_all_gfx_actors) + for (const ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors()) { + GfxActor* gfx_actor = actor->GetGfxActor(); if (gfx_actor->IsActorLive()) { gfx_actor->UpdateRods(); @@ -245,10 +246,13 @@ void GfxScene::UpdateScene(float dt_sec) App::GetGameContext()->GetSceneMouse().UpdateVisuals(); // Actors - finalize threaded tasks - for (GfxActor* gfx_actor: m_live_gfx_actors) + for (const ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors()) { - gfx_actor->FinishWheelUpdates(); - gfx_actor->FinishFlexbodyTasks(); + if (actor->GetGfxActor()->IsActorLive()) + { + actor->GetGfxActor()->FinishFlexbodyTasks(); // Wait for threaded tasks to finish + actor->GetGfxActor()->FinishWheelUpdates(); // Wait for threaded tasks to finish + } } } @@ -273,13 +277,12 @@ DustPool* GfxScene::GetDustPool(const char* name) } } -void GfxScene::RegisterGfxActor(RoR::GfxActor* gfx_actor) -{ - m_all_gfx_actors.push_back(gfx_actor); -} - void GfxScene::BufferSimulationData() { + // Sim data are buffered so that scene+GUI updates could happen in parallel with next physics step. + // See comments on top of 'SimBuffers.h' + // ------------------------------------------------------------------------------------------------ + m_simbuf.simbuf_player_actor = App::GetGameContext()->GetPlayerActor(); m_simbuf.simbuf_character_pos = App::GetGameContext()->GetPlayerCharacter()->getPosition(); m_simbuf.simbuf_sim_paused = App::GetGameContext()->GetActorManager()->IsSimulationPaused(); @@ -296,45 +299,21 @@ void GfxScene::BufferSimulationData() m_simbuf.simbuf_dir_arrow_text = App::GetGameContext()->GetRaceSystem().GetDirArrowText(); m_simbuf.simbuf_dir_arrow_visible = App::GetGameContext()->GetRaceSystem().IsDirArrowVisible(); - m_live_gfx_actors.clear(); - for (GfxActor* a: m_all_gfx_actors) - { - if (a->IsActorLive() || !a->IsActorInitialized()) - { - a->UpdateSimDataBuffer(); - m_live_gfx_actors.push_back(a); - a->InitializeActor(); - } - } - - for (GfxCharacter* a: m_all_gfx_characters) + // Actors + for (const ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors()) { - a->BufferSimulationData(); + if (actor->GetGfxActor()->IsActorLive()) + actor->GetGfxActor()->UpdateSimDataBuffer(); } -} -void GfxScene::RemoveGfxActor(RoR::GfxActor* remove_me) -{ - auto itor = std::remove(m_all_gfx_actors.begin(), m_all_gfx_actors.end(), remove_me); - if (itor != m_all_gfx_actors.end()) + // Characters + for (const std::unique_ptr& character: App::GetGameContext()->GetCharacterFactory()->getAllCharacters()) { - m_all_gfx_actors.erase(itor, m_all_gfx_actors.end()); + character->getGfxCharacter()->BufferSimulationData(); } } -void GfxScene::RegisterGfxCharacter(RoR::GfxCharacter* gfx_character) -{ - m_all_gfx_characters.push_back(gfx_character); -} -void GfxScene::RemoveGfxCharacter(RoR::GfxCharacter* remove_me) -{ - auto itor = std::remove(m_all_gfx_characters.begin(), m_all_gfx_characters.end(), remove_me); - if (itor != m_all_gfx_characters.end()) - { - m_all_gfx_characters.erase(itor, m_all_gfx_characters.end()); - } -} void GfxScene::DrawNetLabel(Ogre::Vector3 scene_pos, float cam_dist, std::string const& nick, int colornum) { diff --git a/source/main/gfx/GfxScene.h b/source/main/gfx/GfxScene.h index fced6a3431..cf94fa625c 100644 --- a/source/main/gfx/GfxScene.h +++ b/source/main/gfx/GfxScene.h @@ -53,25 +53,16 @@ class GfxScene void DrawNetLabel(Ogre::Vector3 pos, float cam_dist, std::string const& nick, int colornum); void UpdateScene(float dt_sec); void ClearScene(); - void RegisterGfxActor(RoR::GfxActor* gfx_actor); - void RemoveGfxActor(RoR::GfxActor* gfx_actor); - void RegisterGfxCharacter(RoR::GfxCharacter* gfx_character); - void RemoveGfxCharacter(RoR::GfxCharacter* gfx_character); void BufferSimulationData(); //!< Run this when simulation is halted GameContextSB& GetSimDataBuffer() { return m_simbuf; } GfxEnvmap& GetEnvMap() { return m_envmap; } RoR::SkidmarkConfig* GetSkidmarkConf () { return &m_skidmark_conf; } Ogre::SceneManager* GetSceneManager() { return m_scene_manager; } - std::vector& GetGfxActors() { return m_all_gfx_actors; } - std::vector& GetGfxCharacters() { return m_all_gfx_characters; } private: std::map m_dustpools; Ogre::SceneManager* m_scene_manager = nullptr; - std::vector m_all_gfx_actors; - std::vector m_live_gfx_actors; - std::vector m_all_gfx_characters; RoR::GfxEnvmap m_envmap; GameContextSB m_simbuf; SkidmarkConfig m_skidmark_conf; diff --git a/source/main/gfx/SimBuffers.h b/source/main/gfx/SimBuffers.h index 2bd5fa46bb..8f49f6eba0 100644 --- a/source/main/gfx/SimBuffers.h +++ b/source/main/gfx/SimBuffers.h @@ -50,6 +50,7 @@ on main thread, but the goal is to do all on sim. thread. GameContext (gamecontext.h) / GameContextSB / GfxScene (gfxscene.h) + Character (character.h) / CharacterSB / GfxCharacter (character.h) Actor (actor.h) / ActorSB / GfxActor (gfxactor.h) node_t (simdata.h) / NodeSB / NodeGfx (gfxdata.h) beam_t (simdata.h) / - / BeamGfx (gfxdata.h) @@ -64,6 +65,24 @@ namespace RoR { +struct CharacterSB +{ + // Transforms + Ogre::Vector3 simbuf_character_pos; + Ogre::Radian simbuf_character_rot; //!< When on foot + float simbuf_character_h_speed; //!< When on foot + + // State + BitMask_t simbuf_control_flags; //!< `RoRnet::ControlFlags` + BitMask_t simbuf_situation_flags; //!< `RoRnet::SituationFlags` + ActorPtr simbuf_actor_coupling; + + // Network + Ogre::UTFString simbuf_net_username; + bool simbuf_is_remote; + int simbuf_color_number; +}; + struct NodeSB { Ogre::Vector3 AbsPosition; // classic name diff --git a/source/main/gui/GUIManager.cpp b/source/main/gui/GUIManager.cpp index 0981f84e66..d95575c9b5 100644 --- a/source/main/gui/GUIManager.cpp +++ b/source/main/gui/GUIManager.cpp @@ -115,6 +115,7 @@ bool GUIManager::AreStaticMenusAllowed() //!< i.e. top menubar / vehicle UI butt return (App::GetCameraManager()->GetCurrentBehavior() != CameraManager::CAMERA_BEHAVIOR_FREE && !this->ConsoleWindow.IsHovered() && !this->GameControls.IsHovered() && + !this->CharacterPoseUtil.IsHovered() && !this->FrictionSettings.IsHovered() && !this->TextureToolWindow.IsHovered() && !this->NodeBeamUtils.IsHovered() && @@ -190,6 +191,11 @@ void GUIManager::DrawSimGuiBuffered(GfxActor* player_gfx_actor) this->FrictionSettings.Draw(); } + if (this->CharacterPoseUtil.IsVisible()) + { + this->CharacterPoseUtil.Draw(); + } + if (this->VehicleDescription.IsVisible()) { this->VehicleDescription.Draw(); diff --git a/source/main/gui/GUIManager.h b/source/main/gui/GUIManager.h index 90f82ae0a7..947c428806 100644 --- a/source/main/gui/GUIManager.h +++ b/source/main/gui/GUIManager.h @@ -32,6 +32,7 @@ #include "GUI_CollisionsDebug.h" #include "GUI_ConsoleWindow.h" #include "GUI_FlexbodyDebug.h" +#include "GUI_CharacterPoseUtil.h" #include "GUI_FrictionSettings.h" #include "GUI_RepositorySelector.h" #include "GUI_GameMainMenu.h" @@ -102,6 +103,7 @@ class GUIManager ~GUIManager(); + GUI::CharacterPoseUtil CharacterPoseUtil; GUI::CollisionsDebug CollisionsDebug; GUI::GameMainMenu GameMainMenu; GUI::GameAbout GameAbout; diff --git a/source/main/gui/panels/GUI_CharacterPoseUtil.cpp b/source/main/gui/panels/GUI_CharacterPoseUtil.cpp new file mode 100644 index 0000000000..32ca501717 --- /dev/null +++ b/source/main/gui/panels/GUI_CharacterPoseUtil.cpp @@ -0,0 +1,384 @@ +/* + This source file is part of Rigs of Rods + Copyright 2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + + +#include "GUI_CharacterPoseUtil.h" + +#include "Application.h" +#include "Actor.h" +#include "GameContext.h" +#include "GfxCharacter.h" +#include "GfxScene.h" +#include "GUIManager.h" +#include "Language.h" + +#include + +using namespace RoR; +using namespace GUI; +using namespace Ogre; + +void CharacterPoseUtil::Draw() +{ + GfxCharacter* gfx_character = this->FetchCharacter(); + if (!gfx_character) + return; // warning already logged + + Entity* ent = static_cast(gfx_character->xc_scenenode->getAttachedObject(0)); + + const int flags = ImGuiWindowFlags_NoCollapse; + bool keep_open = true; + ImGui::Begin(_LC("CharacterPoseUtil", "Character pose utility"), &keep_open, flags); + + ImGui::Text("Character: '%s' (mesh: '%s')", + gfx_character->xc_instance_name.c_str(), + gfx_character->xc_character->getCharacterDocument()->mesh_name.c_str()); + + ImGui::BeginTabBar("CharacterPoseUtilTabs"); + + if (ImGui::BeginTabItem("Anims")) + { + this->DrawSkeletalPanel(ent); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Actions")) + { + this->DrawActionDbgPanel(ent); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + + // Common window epilogue: + + m_is_hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); + App::GetGuiManager()->RequestGuiCaptureKeyboard(m_is_hovered); + + ImGui::End(); + if (!keep_open) + { + this->SetVisible(false); + } +} + +void CharacterPoseUtil::DrawSkeletalPanel(Ogre::Entity* ent) +{ + GfxCharacter* gfx_character = this->FetchCharacter(); + if (!gfx_character) + return; + + ImGui::Checkbox("Manual pose mode", &gfx_character->xc_manual_pose_active); + if (!gfx_character->xc_manual_pose_active) + { + ImGui::TextDisabled("(gray text means 'disabled')"); + } + ImGui::Dummy(ImVec2(350, 1)); // force minimum width + ImGui::Separator(); + + AnimationStateSet* stateset = ent->getAllAnimationStates(); + for (auto& state_pair : stateset->getAnimationStates()) + { + AnimationState* as = state_pair.second; + this->DrawAnimControls(as); + + } +} + +void CharacterPoseUtil::DrawAnimControls(Ogre::AnimationState* anim_state) +{ + GfxCharacter* gfx_character = this->FetchCharacter(); + if (!gfx_character) + return; + + ImGui::PushID(anim_state); + + // anim name line + ImVec4 color = (anim_state->getEnabled()) ? ImGui::GetStyle().Colors[ImGuiCol_Text] : ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]; + const char* uses_boneblendmask_text = anim_state->getBlendMask() ? ", uses bone blend mask!" : ""; + ImGui::TextColored(color, "'%s' (%.2f sec%s)", anim_state->getAnimationName().c_str(), anim_state->getLength(), uses_boneblendmask_text); + if (gfx_character->xc_manual_pose_active) + { + ImGui::SameLine(); + bool enabled = anim_state->getEnabled(); + if (ImGui::Checkbox("Enabled", &enabled)) + { + anim_state->setEnabled(enabled); + anim_state->setWeight(enabled ? 1.f : 0.f); + } + ImGui::SameLine(); + float weight = anim_state->getWeight(); + ImGui::SetNextItemWidth(50.f); + if (ImGui::InputFloat("Weight", &weight)) + { + anim_state->setWeight(weight); + } + } + + // anim progress line + if (gfx_character->xc_manual_pose_active) + { + float timepos = anim_state->getTimePosition(); + if (ImGui::SliderFloat("Time pos", &timepos, 0.f, anim_state->getLength())) + { + anim_state->setTimePosition(timepos); + } + } + else + { + std::string caption = fmt::format("{:.2f} sec", anim_state->getTimePosition()); + ImGui::ProgressBar(anim_state->getTimePosition() / anim_state->getLength(), ImVec2(-1, 0), caption.c_str()); + } + + ImGui::PopID(); // AnimationState* +} + +ImVec4 ForFlagColor(BitMask_t flags, BitMask_t mask, bool active) +{ + GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme(); + ImVec4 normal_text_color = ImGui::GetStyle().Colors[ImGuiCol_Text]; + return (active) + ? theme.success_text_color + : ((BITMASK_IS_1(flags, mask)) ? normal_text_color : theme.warning_text_color); +} + +ImVec4 ExceptFlagColor(BitMask_t flags, BitMask_t mask, bool active) +{ + GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme(); + ImVec4 normal_text_color = ImGui::GetStyle().Colors[ImGuiCol_Text]; + return (active) + ? theme.value_blue_text_color + : ((BITMASK_IS_1(flags, mask)) ? theme.error_text_color : normal_text_color); +} + +void CharacterPoseUtil::DrawActionDbgItemFull(CharacterActionID_t id) +{ + GfxCharacter* gfx_character = this->FetchCharacter(); + if (!gfx_character) + return; + + CharacterActionDbg const& dbg = gfx_character->xc_action_dbg_states[id]; + CharacterActionDef* def = &App::GetGameContext()->GetPlayerCharacter()->getCharacterDocument()->actions[id]; + + + // Draw attributes + ImGui::Text("%s", fmt::format("anim: '{}', continuous: {}, autorestart: {}, neutral_mid: {}", + def->anim_name, def->anim_continuous, def->anim_autorestart, def->anim_neutral_mid).c_str()); + + // Draw the 'for_' flags, the satisfied get colored yellow. If all are satisfied, all get colored green. + ImGui::TextDisabled("For flags:"); + int num_flags = 0; + const int MAX_FLAGS_PER_LINE = 3; + for (int i = 1; i <= 32; i++) + { + BitMask_t testmask = BITMASK(i); + if (BITMASK_IS_1(def->for_situations, testmask)) + { + ImVec4 color = ForFlagColor(dbg.missing_situations, testmask, dbg.active); + if (num_flags > 0 && num_flags % MAX_FLAGS_PER_LINE == 0) + { + ImGui::TextDisabled(" (more):"); + } + ImGui::SameLine(); + ImGui::TextColored(color, "%s", Character::SituationFlagToString(testmask)); + num_flags++; + } + if (BITMASK_IS_1(def->for_controls, testmask)) + { + ImVec4 color = ForFlagColor(dbg.missing_controls, testmask, dbg.active); + if (num_flags > 0 && num_flags % MAX_FLAGS_PER_LINE == 0) + { + ImGui::TextDisabled(" (more):"); + } + ImGui::SameLine(); + ImGui::TextColored(color, "%s", Character::ControlFlagToString(testmask)); + num_flags++; + } + } + + // Draw the 'except_' flags, blocking get colored red. + ImGui::TextDisabled("Except flags:"); + num_flags = 0; + for (int i = 1; i <= 32; i++) + { + BitMask_t testmask = BITMASK(i); + if (BITMASK_IS_1(def->except_situations, testmask)) + { + ImVec4 color = ExceptFlagColor(dbg.blocking_situations, testmask, dbg.active); + if (num_flags > 0 && num_flags % MAX_FLAGS_PER_LINE == 0) + { + ImGui::TextDisabled(" (more):"); + } + ImGui::SameLine(); + ImGui::TextColored(color, "%s", Character::SituationFlagToString(testmask)); + num_flags++; + } + if (BITMASK_IS_1(def->except_controls, testmask)) + { + ImVec4 color = ExceptFlagColor(dbg.blocking_controls, testmask, dbg.active); + if (num_flags > 0 && num_flags % MAX_FLAGS_PER_LINE == 0) + { + ImGui::TextDisabled(" (more):"); + } + ImGui::SameLine(); + ImGui::TextColored(color, "%s", Character::ControlFlagToString(testmask)); + num_flags++; + } + } +} + +void CharacterPoseUtil::DrawActionDbgItemInline(CharacterActionID_t id, Ogre::Entity* ent) +{ + GfxCharacter* gfx_character = this->FetchCharacter(); + if (!gfx_character) + return; + + CharacterActionDbg const& dbg = gfx_character->xc_action_dbg_states[id]; + CharacterActionDef* def = &App::GetGameContext()->GetPlayerCharacter()->getCharacterDocument()->actions[id]; + + AnimationState* as = nullptr; + try + { + as = ent->getAnimationState(def->anim_name); + } + catch (Ogre::ItemIdentityException) + { + ImGui::TextDisabled("ERROR: Animation '%s' does not exist.", def->anim_name.c_str()); + return; + } + GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme(); + + if (dbg.active) + { + ImGui::Text("Playing '%s'", def->anim_name.c_str()); + ImGui::SameLine(); + ImGui::SetNextItemWidth(100); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + std::string caption = fmt::format("{:.2f}/{:.2f} sec", as->getTimePosition(), as->getLength()); + ImGui::ProgressBar(as->getTimePosition() / as->getLength(), ImVec2(-1, 0), caption.c_str()); + ImGui::PopStyleVar(); // ImGuiStyleVar_FramePadding + } + else + { + if (dbg.blocking_situations || dbg.blocking_controls) + { + // Draw the blocking 'except_' flags, colored red. + ImGui::SameLine(); + ImGui::TextDisabled("Blocked by:"); + for (int i = 1; i <= 32; i++) + { + BitMask_t testmask = BITMASK(i); + if (BITMASK_IS_1(dbg.blocking_situations, testmask)) + { + ImGui::SameLine(); + ImGui::TextColored(theme.error_text_color, "%s", Character::SituationFlagToString(testmask)); + } + if (BITMASK_IS_1(dbg.blocking_controls, testmask)) + { + ImGui::SameLine(); + ImGui::TextColored(theme.error_text_color, "%s", Character::ControlFlagToString(testmask)); + } + } + } + else + { + // Draw the 'for_' flags, the satisfied get colored yellow. + ImGui::TextDisabled("Activated by:"); + for (int i = 1; i <= 32; i++) + { + BitMask_t testmask = BITMASK(i); + if (BITMASK_IS_1(def->for_situations, testmask)) + { + ImVec4 color = ForFlagColor(dbg.missing_situations, testmask, false); + ImGui::SameLine(); + ImGui::TextColored(color, "%s", Character::SituationFlagToString(testmask)); + } + if (BITMASK_IS_1(def->for_controls, testmask)) + { + ImVec4 color = ForFlagColor(dbg.missing_controls, testmask, false); + ImGui::SameLine(); + ImGui::TextColored(color, "%s", Character::ControlFlagToString(testmask)); + } + } + } + } +} + +void CharacterPoseUtil::DrawActionDbgPanel(Ogre::Entity* ent) +{ + const float child_height = ImGui::GetWindowHeight() + - ((2.f * ImGui::GetStyle().WindowPadding.y) + (3.f * ImGui::GetItemsLineHeightWithSpacing()) + + ImGui::GetStyle().ItemSpacing.y); + + ImGui::BeginChild("CharacterPoseUi-animDbg-scroll", ImVec2(0.f, child_height), false); + + for (CharacterActionDef const& anim : App::GetGameContext()->GetPlayerCharacter()->getCharacterDocument()->actions) + { + if (ImGui::TreeNode(&anim, "%s", anim.action_description.c_str())) + { + this->DrawActionDbgItemFull(anim.action_id); + ImGui::TreePop(); + } + else + { + ImGui::SameLine(); + this->DrawActionDbgItemInline(anim.action_id, ent); + } + } + + ImGui::EndChild(); +} + +void CharacterPoseUtil::SetVisible(bool v) +{ + GfxCharacter* gfx_character = this->FetchCharacter(); + if (gfx_character) + { + m_is_visible = v; + m_is_hovered = false; + if (!v) + { + gfx_character->xc_manual_pose_active = false; + } + } +} + +GfxCharacter* CharacterPoseUtil::FetchCharacter() +{ + Character* character = App::GetGameContext()->GetPlayerCharacter(); + if (!character) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + "no player character, closing CharacterPoseUtil"); + this->SetVisible(false); + return nullptr; + } + + GfxCharacter* gfx_character = character->getGfxCharacter(); + + if (!gfx_character) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + "no player character visuals, closing CharacterPoseUtil"); + this->SetVisible(false); + return nullptr; + } + + return gfx_character; +} diff --git a/source/main/gui/panels/GUI_CharacterPoseUtil.h b/source/main/gui/panels/GUI_CharacterPoseUtil.h new file mode 100644 index 0000000000..512192fdbd --- /dev/null +++ b/source/main/gui/panels/GUI_CharacterPoseUtil.h @@ -0,0 +1,57 @@ +/* + This source file is part of Rigs of Rods + Copyright 2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#pragma once + +#include "Application.h" +#include "OgreImGui.h" + +#include + +namespace RoR { + +namespace GUI { + +class CharacterPoseUtil +{ +public: + enum class Tab { SKELETAL, GAME }; + + void Draw(); + + void SetVisible(bool visible); + bool IsVisible() const { return m_is_visible; } + bool IsHovered() const { return IsVisible() && m_is_hovered; } + +private: + void DrawAnimControls(Ogre::AnimationState* anim_state); + void DrawActionDbgItemFull(CharacterActionID_t id); + void DrawActionDbgItemInline(CharacterActionID_t id, Ogre::Entity* ent); + void DrawActionDbgPanel(Ogre::Entity* ent); + void DrawSkeletalPanel(Ogre::Entity* ent); + + GfxCharacter* FetchCharacter(); + + bool m_is_visible = false; + bool m_is_hovered = false; + Tab m_selected_tab = Tab::SKELETAL; +}; + +} // namespace GUI +} // namespace RoR diff --git a/source/main/gui/panels/GUI_DirectionArrow.cpp b/source/main/gui/panels/GUI_DirectionArrow.cpp index e66c0f0d89..07de1ed101 100644 --- a/source/main/gui/panels/GUI_DirectionArrow.cpp +++ b/source/main/gui/panels/GUI_DirectionArrow.cpp @@ -49,18 +49,21 @@ void GUI::DirectionArrow::LoadOverlay() void GUI::DirectionArrow::CreateArrow() { - // setup direction arrow - Ogre::Entity* arrow_entity = App::GetGfxScene()->GetSceneManager()->createEntity("arrow2.mesh"); - arrow_entity->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); - - // Add entity to the scene node - m_node = new Ogre::SceneNode(App::GetGfxScene()->GetSceneManager()); - m_node->attachObject(arrow_entity); - m_node->setVisible(false); - m_node->setScale(0.1, 0.1, 0.1); - m_node->setPosition(Ogre::Vector3(-0.6, +0.4, -1)); - m_node->setFixedYawAxis(true, Ogre::Vector3::UNIT_Y); - m_overlay->add3D(m_node); + if (m_overlay) + { + // setup direction arrow + Ogre::Entity* arrow_entity = App::GetGfxScene()->GetSceneManager()->createEntity("arrow2.mesh"); + arrow_entity->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + + // Add entity to the scene node + m_node = new Ogre::SceneNode(App::GetGfxScene()->GetSceneManager()); + m_node->attachObject(arrow_entity); + m_node->setVisible(false); + m_node->setScale(0.1, 0.1, 0.1); + m_node->setPosition(Ogre::Vector3(-0.6, +0.4, -1)); + m_node->setFixedYawAxis(true, Ogre::Vector3::UNIT_Y); + m_overlay->add3D(m_node); + } } void GUI::DirectionArrow::Update(RoR::GfxActor* player_vehicle) diff --git a/source/main/gui/panels/GUI_GameSettings.cpp b/source/main/gui/panels/GUI_GameSettings.cpp index af92a68d1c..a3e0d8691e 100644 --- a/source/main/gui/panels/GUI_GameSettings.cpp +++ b/source/main/gui/panels/GUI_GameSettings.cpp @@ -266,6 +266,9 @@ 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")); + + ImGui::Separator(); + this->DrawPlayerCharacterCfg(); } void GameSettings::DrawAudioSettings() @@ -532,3 +535,25 @@ void GameSettings::SetVisible(bool v) ImTerminateComboboxString(m_combo_items_input_grab); } } + +void GameSettings::DrawPlayerCharacterCfg() +{ + ImGui::TextDisabled("%s:", _LC("GameSettings", "Player character")); + ImGui::SameLine(); + ImGui::Text("%s", App::sim_player_character->getStr().c_str()); + ImGui::SameLine(); + if (App::sim_player_character_skin->getStr() == "") + { + ImGui::TextDisabled("(default skin)"); + } + else + { + ImGui::Text("(Skin: '%s')", App::sim_player_character_skin->getStr().c_str()); + } + ImGui::SameLine(); + if (ImGui::SmallButton(_LC("GameSettings", "Select"))) + { + LoaderType* payload = new LoaderType(LoaderType::LT_Character); + App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_SELECTOR_REQUESTED, (void*)payload)); + } +} diff --git a/source/main/gui/panels/GUI_GameSettings.h b/source/main/gui/panels/GUI_GameSettings.h index 94bf4aa5c0..c15ae1488c 100644 --- a/source/main/gui/panels/GUI_GameSettings.h +++ b/source/main/gui/panels/GUI_GameSettings.h @@ -42,6 +42,9 @@ class GameSettings void DrawControlSettings(); void DrawDiagSettings(); + // helper + void DrawPlayerCharacterCfg(); + // GUI state bool m_is_visible = false; diff --git a/source/main/gui/panels/GUI_MainSelector.cpp b/source/main/gui/panels/GUI_MainSelector.cpp index d1c421c3d0..00c0475935 100644 --- a/source/main/gui/panels/GUI_MainSelector.cpp +++ b/source/main/gui/panels/GUI_MainSelector.cpp @@ -612,24 +612,20 @@ 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) + // Make a copy because `Close()` will reset it. + LoaderType orig_loader_type = m_loader_type; + + // If no config was selected, use the first one. + std::string sectionconfig; + if (sd_entry.sde_entry->sectionconfigs.size() > 0) { - App::GetGameContext()->PushMessage(Message(MSG_SIM_LOAD_TERRN_REQUESTED, sd_entry.sde_entry->fname)); - this->Close(); + sectionconfig = sd_entry.sde_entry->sectionconfigs[m_selected_sectionconfig]; } - else if (App::app_state->getEnum() == AppState::SIMULATION) - { - LoaderType type = m_loader_type; - std::string sectionconfig; - if (sd_entry.sde_entry->sectionconfigs.size() > 0) - { - sectionconfig = sd_entry.sde_entry->sectionconfigs[m_selected_sectionconfig]; - } - this->Close(); - App::GetGameContext()->OnLoaderGuiApply(type, sd_entry.sde_entry, sectionconfig); - } + // Close the UI so that GameContext can reopen it if needed (used for skins) + this->Close(); + + App::GetGameContext()->OnLoaderGuiApply(orig_loader_type, sd_entry.sde_entry, sectionconfig); } // Static helper diff --git a/source/main/gui/panels/GUI_MultiplayerSelector.cpp b/source/main/gui/panels/GUI_MultiplayerSelector.cpp index 77fa3f4878..de45600be5 100644 --- a/source/main/gui/panels/GUI_MultiplayerSelector.cpp +++ b/source/main/gui/panels/GUI_MultiplayerSelector.cpp @@ -194,6 +194,9 @@ void MultiplayerSelector::DrawSetupTab() DrawGCheckbox(App::mp_hide_own_net_label, _LC("MultiplayerSelector", "Hide own net label")); DrawGCheckbox(App::mp_pseudo_collisions, _LC("MultiplayerSelector", "Multiplayer collisions")); + ImGui::Separator(); + this->DrawCharacterOverrideCfg(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + BUTTONS_EXTRA_SPACE); ImGui::Separator(); @@ -430,3 +433,30 @@ void MultiplayerSelector::UpdateServerlist(MpServerInfoVec* data) } } +void MultiplayerSelector::DrawCharacterOverrideCfg() +{ + ImGui::TextDisabled("%s:", _LC("MultiplayerSelector", "Override character")); + ImGui::SameLine(); + ImGui::Text("%s", App::mp_override_character->getStr().c_str()); + ImGui::SameLine(); + if (App::mp_override_character_skin->getStr() == "") + { + ImGui::TextDisabled("(default skin)"); + } + else + { + ImGui::Text("(Skin: '%s')", App::mp_override_character_skin->getStr().c_str()); + } + ImGui::SameLine(); + if (ImGui::SmallButton(_LC("MultiplayerSelector", "Select"))) + { + LoaderType* payload = new LoaderType(LoaderType::LT_CharacterMP); + App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_SELECTOR_REQUESTED, (void*)payload)); + } + ImGui::SameLine(); + if (ImGui::SmallButton(_LC("MultiplayerSelector", "Clear"))) + { + App::mp_override_character->setStr(""); + } +} + diff --git a/source/main/gui/panels/GUI_MultiplayerSelector.h b/source/main/gui/panels/GUI_MultiplayerSelector.h index 86a7ce188b..540cad8419 100644 --- a/source/main/gui/panels/GUI_MultiplayerSelector.h +++ b/source/main/gui/panels/GUI_MultiplayerSelector.h @@ -70,6 +70,7 @@ class MultiplayerSelector void DrawSetupTab(); void DrawDirectTab(); void DrawServerlistTab(); + void DrawCharacterOverrideCfg(); MpServerInfoVec m_serverlist_data; int m_selected_item = -1; diff --git a/source/main/gui/panels/GUI_SurveyMap.cpp b/source/main/gui/panels/GUI_SurveyMap.cpp index bccfc40b7a..ba93c2b3f9 100644 --- a/source/main/gui/panels/GUI_SurveyMap.cpp +++ b/source/main/gui/panels/GUI_SurveyMap.cpp @@ -27,6 +27,7 @@ #include "ContentManager.h" #include "GameContext.h" #include "GfxActor.h" +#include "GfxCharacter.h" #include "GfxScene.h" #include "GUIManager.h" #include "GUI_MainSelector.h" @@ -281,8 +282,13 @@ void SurveyMap::Draw() } // Draw actor icons - for (GfxActor* gfx_actor: App::GetGfxScene()->GetGfxActors()) + for (const ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors()) { + if (actor->ar_state == ActorState::DISPOSED) + continue; + + GfxActor* gfx_actor = actor->GetGfxActor(); + ROR_ASSERT(gfx_actor); const char* type_str = this->getTypeByDriveable(gfx_actor->GetActorDriveable(), gfx_actor->GetActor()); int truckstate = gfx_actor->GetActorState(); Str<100> fileName; @@ -301,9 +307,9 @@ void SurveyMap::Draw() } // Draw character icons - for (GfxCharacter* gfx_character: App::GetGfxScene()->GetGfxCharacters()) + for (const std::unique_ptr& character: App::GetGameContext()->GetCharacterFactory()->getAllCharacters()) { - auto& simbuf = gfx_character->xc_simbuf; + auto& simbuf = character->getGfxCharacter()->xc_simbuf; if (!simbuf.simbuf_actor_coupling) { std::string caption = (App::mp_state->getEnum() == MpState::CONNECTED) ? simbuf.simbuf_net_username : ""; diff --git a/source/main/gui/panels/GUI_TopMenubar.cpp b/source/main/gui/panels/GUI_TopMenubar.cpp index abceb1d4d0..abecb2991c 100644 --- a/source/main/gui/panels/GUI_TopMenubar.cpp +++ b/source/main/gui/panels/GUI_TopMenubar.cpp @@ -642,6 +642,12 @@ void TopMenubar::Update() m_open_menu = TopMenu::TOPMENU_NONE; } + if (ImGui::Button(_LC("TopMenubar", "Character pose util"))) + { + App::GetGuiManager()->CharacterPoseUtil.SetVisible(true); + m_open_menu = TopMenu::TOPMENU_NONE; + } + if (ImGui::Button(_LC("TopMenubar", "Collisions debug"))) { App::GetGuiManager()->CollisionsDebug.SetVisible(true); diff --git a/source/main/main.cpp b/source/main/main.cpp index 8af2c7de1a..e88aaad361 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -568,9 +568,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 @@ -597,8 +597,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) @@ -608,6 +608,8 @@ int main(int argc, char *argv[]) } else { + // Failed to load terrain or character - messagebox is already displayed + if (App::mp_state->getEnum() == MpState::CONNECTED) { App::GetGameContext()->PushMessage(Message(MSG_NET_DISCONNECT_REQUESTED)); @@ -750,7 +752,6 @@ int main(int argc, char *argv[]) { ActorPtr actor = *actor_ptr; actor->ar_state = ActorState::NETWORKED_HIDDEN; // Stop net. updates - App::GetGfxScene()->RemoveGfxActor(actor->GetGfxActor()); // Remove visuals (also stops updating SimBuffer) actor->GetGfxActor()->GetSimDataBuffer().simbuf_actor_state = ActorState::NETWORKED_HIDDEN; // Hack - manually propagate the new state to SimBuffer so Character can reflect it. actor->GetGfxActor()->SetAllMeshesVisible(false); actor->GetGfxActor()->SetCastShadows(false); @@ -771,7 +772,6 @@ int main(int argc, char *argv[]) { ActorPtr actor = *actor_ptr; actor->ar_state = ActorState::NETWORKED_OK; // Resume net. updates - App::GetGfxScene()->RegisterGfxActor(actor->GetGfxActor()); // Restore visuals (also resumes updating SimBuffer) actor->GetGfxActor()->SetAllMeshesVisible(true); actor->GetGfxActor()->SetCastShadows(true); actor->unmuteAllSounds(); // Unmute sounds diff --git a/source/main/network/Network.cpp b/source/main/network/Network.cpp index 25d603bfa8..9f7dde7fb2 100644 --- a/source/main/network/Network.cpp +++ b/source/main/network/Network.cpp @@ -501,6 +501,18 @@ bool Network::ConnectThread() std::string country = App::app_country->getStr().substr(0, 2); strncpy(c.language, (language + std::string("_") + country).c_str(), 5); strcpy(c.sessiontype, "normal"); + // determine character to use + if (App::mp_override_character->getStr() != "") + { + strncpy(c.character_file, App::mp_override_character->getStr().c_str(), RORNET_MAX_CHARACTER_FILE_LEN - 1); + strncpy(c.character_skin, App::mp_override_character_skin->getStr().c_str(), RORNET_MAX_CHARACTER_SKIN_LEN - 1); + } + else + { + strncpy(c.character_file, App::sim_player_character->getStr().c_str(), RORNET_MAX_CHARACTER_FILE_LEN - 1); + strncpy(c.character_skin, App::sim_player_character_skin->getStr().c_str(), RORNET_MAX_CHARACTER_SKIN_LEN - 1); + } + if (!SendNetMessage(MSG2_USER_INFO, 0, sizeof(RoRnet::UserInfo), (char*)&c)) { CouldNotConnect(_L("Establishing network session: error sending user info")); diff --git a/source/main/network/Network.h b/source/main/network/Network.h index 5b88c85260..a41ac02968 100644 --- a/source/main/network/Network.h +++ b/source/main/network/Network.h @@ -55,36 +55,6 @@ struct CurlFailInfo #pragma pack(push, 1) -enum NetCharacterCmd -{ - CHARACTER_CMD_INVALID, - CHARACTER_CMD_POSITION, - CHARACTER_CMD_ATTACH, - CHARACTER_CMD_DETACH -}; - -struct NetCharacterMsgGeneric -{ - int32_t command; -}; - -struct NetCharacterMsgPos -{ - int32_t command; - float pos_x, pos_y, pos_z; - float rot_angle; - float anim_time; - char anim_name[CHARACTER_ANIM_NAME_LEN]; -}; - -struct NetCharacterMsgAttach -{ - int32_t command; - int32_t source_id; - int32_t stream_id; - int32_t position; -}; - struct NetSendPacket { char buffer[RORNET_MAX_MESSAGE_LENGTH]; diff --git a/source/main/network/RoRnet.h b/source/main/network/RoRnet.h index 96e584592d..24d5d4400e 100644 --- a/source/main/network/RoRnet.h +++ b/source/main/network/RoRnet.h @@ -30,9 +30,12 @@ namespace RoRnet { #define RORNET_MAX_PEERS 64 //!< maximum clients connected at the same time #define RORNET_MAX_MESSAGE_LENGTH 8192 //!< maximum size of a RoR message. 8192 bytes = 8 kibibytes #define RORNET_LAN_BROADCAST_PORT 13000 //!< port used to send the broadcast announcement in LAN mode -#define RORNET_MAX_USERNAME_LEN 40 //!< port used to send the broadcast announcement in LAN mode +#define RORNET_MAX_USERNAME_LEN 40 +#define RORNET_MAX_CHARACTER_FILE_LEN 60 +#define RORNET_MAX_CHARACTER_SKIN_LEN 60 -#define RORNET_VERSION "RoRnet_2.44" + +#define RORNET_VERSION "RoRnet_2.45" enum MessageType { @@ -123,6 +126,58 @@ enum Lightmask LIGHTMASK_BLINK_WARN = BITMASK(20), //!< warn blinker on }; +enum ControlFlags +{ + CONTROL_CUSTOM_ACTION_01 = BITMASK(1), + CONTROL_CUSTOM_ACTION_02 = BITMASK(2), + CONTROL_CUSTOM_ACTION_03 = BITMASK(3), + CONTROL_CUSTOM_ACTION_04 = BITMASK(4), + CONTROL_CUSTOM_ACTION_05 = BITMASK(5), + CONTROL_CUSTOM_ACTION_06 = BITMASK(6), + CONTROL_CUSTOM_ACTION_07 = BITMASK(7), + CONTROL_CUSTOM_ACTION_08 = BITMASK(8), + CONTROL_CUSTOM_ACTION_09 = BITMASK(9), + CONTROL_CUSTOM_ACTION_10 = BITMASK(10), + + CONTROL_MOVE_FORWARD = BITMASK(11), + CONTROL_MOVE_BACKWARD = BITMASK(12), + CONTROL_TURN_RIGHT = BITMASK(13), + CONTROL_TURN_LEFT = BITMASK(14), + CONTROL_SIDESTEP_RIGHT = BITMASK(15), + CONTROL_SIDESTEP_LEFT = BITMASK(16), + CONTROL_RUN = BITMASK(17), + CONTROL_JUMP = BITMASK(18), + CONTROL_SLOW_TURN = BITMASK(19), +}; + +enum SituationFlags +{ + SITUATION_CUSTOM_MODE_01 = BITMASK(1), + SITUATION_CUSTOM_MODE_02 = BITMASK(2), + SITUATION_CUSTOM_MODE_03 = BITMASK(3), + SITUATION_CUSTOM_MODE_04 = BITMASK(4), + SITUATION_CUSTOM_MODE_05 = BITMASK(5), + SITUATION_CUSTOM_MODE_06 = BITMASK(6), + SITUATION_CUSTOM_MODE_07 = BITMASK(7), + SITUATION_CUSTOM_MODE_08 = BITMASK(8), + SITUATION_CUSTOM_MODE_09 = BITMASK(9), + SITUATION_CUSTOM_MODE_10 = BITMASK(10), + + SITUATION_ON_SOLID_GROUND = BITMASK(11), + SITUATION_IN_SHALLOW_WATER = BITMASK(12), + SITUATION_IN_DEEP_WATER = BITMASK(13), + SITUATION_IN_AIR = BITMASK(14), + SITUATION_DRIVING = BITMASK(15), +}; + +enum CharacterCmd +{ + CHARACTER_CMD_INVALID, + CHARACTER_CMD_POSITION, + CHARACTER_CMD_ATTACH, + CHARACTER_CMD_DETACH +}; + // -------------------------------- structs ----------------------------------- // Only use datatypes with defined binary sizes (avoid bool, int, wchar_t...) // Prefer alignment to 4 or 2 bytes (put int32/float/etc. fields on top) @@ -180,7 +235,8 @@ struct UserInfo char clientversion[25]; //!< a version number of the client. For example 1 for RoR 0.35 char clientGUID[40]; //!< the clients GUID char sessiontype[10]; //!< the requested session type. For example "normal", "bot", "rcon" - char sessionoptions[128]; //!< reserved for future options + char character_file[RORNET_MAX_CHARACTER_FILE_LEN]; //!< Filename of the chosen character + char character_skin[RORNET_MAX_CHARACTER_SKIN_LEN]; //!< Skin name for the chosen character }; struct VehicleState //!< Formerly `oob_t` @@ -197,6 +253,28 @@ struct VehicleState //!< Formerly `oob_t` BitMask_t lightmask; //!< flagmask: LIGHTMASK_* }; +struct CharacterMsgGeneric +{ + int32_t command; +}; + +struct CharacterMsgPos +{ + int32_t command; + float pos_x, pos_y, pos_z; + float rot_angle; + BitMask_t control_flags; + BitMask_t situation_flags; +}; + +struct CharacterMsgAttach +{ + int32_t command; + int32_t source_id; + int32_t stream_id; + int32_t position; +}; + struct ServerInfo { char protocolversion[20]; //!< protocol version being used diff --git a/source/main/physics/ActorManager.cpp b/source/main/physics/ActorManager.cpp index 2c8c7e7f02..d057341073 100644 --- a/source/main/physics/ActorManager.cpp +++ b/source/main/physics/ActorManager.cpp @@ -266,8 +266,6 @@ ActorPtr ActorManager::CreateNewActor(ActorSpawnRequest rq, RigDef::DocumentPtr actor->GetGfxActor()->FinishFlexbodyTasks(); // Sync tasks from threadpool } - App::GetGfxScene()->RegisterGfxActor(actor->GetGfxActor()); - if (actor->ar_engine) { if (!actor->m_preloaded_with_terrain && App::sim_spawn_running->getBool()) diff --git a/source/main/resources/CacheSystem.cpp b/source/main/resources/CacheSystem.cpp index f11db72cef..1e626906b8 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) @@ -645,6 +645,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); @@ -1068,6 +1073,26 @@ 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); + + for (CharacterAuthorInfo& author: doc->authors) + { + AuthorInfo a; + a.id = author.id; + a.type = author.type; + a.name = author.name; + a.email = author.email; + entry.authors.push_back(a); + } + + entry.dname = doc->character_name; + entry.description = doc->character_description; + entry.guid = doc->character_guid; +} + bool CacheSystem::CheckResourceLoaded(Ogre::String & filename) { Ogre::String group = ""; @@ -1126,6 +1151,15 @@ void CacheSystem::LoadResource(CacheEntry& t) // PagedGeometry is hardcoded to use `Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME` ResourceGroupManager::getSingleton().createResourceGroup(group, /*inGlobalPool=*/true); ResourceGroupManager::getSingleton().addResourceLocation(t.resource_bundle_path, t.resource_bundle_type, group); + App::GetContentManager()->InitManagedMaterials(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); + App::GetContentManager()->InitManagedMaterials(group); } else if (t.fext == "skin") { @@ -1141,8 +1175,8 @@ void CacheSystem::LoadResource(CacheEntry& t) // 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); - App::GetContentManager()->InitManagedMaterials(group); + App::GetContentManager()->AddResourcePack(ContentManager::ResourcePack::TEXTURES, group); App::GetContentManager()->AddResourcePack(ContentManager::ResourcePack::MATERIALS, group); App::GetContentManager()->AddResourcePack(ContentManager::ResourcePack::MESHES, group); @@ -1263,6 +1297,28 @@ std::shared_ptr CacheSystem::FetchSkinDef(CacheEntry* cache_entry) } } +CharacterDocumentPtr CacheSystem::FetchCharacterDef(CacheEntry* cache_entry) +{ + if (!cache_entry->character_def) + { + try + { + App::GetCacheSystem()->LoadResource(*cache_entry); // Load if not already + + 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; +} + size_t CacheSystem::Query(CacheQuery& query) { Ogre::StringUtil::toLowerCase(query.cqy_search_string); @@ -1279,8 +1335,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 || query.cqy_filter_type == LT_CharacterMP); 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") @@ -1323,6 +1381,7 @@ size_t CacheSystem::Query(CacheQuery& query) Str<100> wheels_str; switch (query.cqy_search_method) { + // Partial case-insensitive match in: name, filename, description, author name/email case CacheSearchMethod::FULLTEXT: if (match = this->Match(score, entry.dname, query.cqy_search_string, 0)) { break; } if (match = this->Match(score, entry.fname, query.cqy_search_string, 100)) { break; } @@ -1334,10 +1393,12 @@ size_t CacheSystem::Query(CacheQuery& query) } break; + // Partial case-insensitive match of GUID (intentionally - for search box) case CacheSearchMethod::GUID: match = this->Match(score, entry.guid, query.cqy_search_string, 0); break; + // Partial case-insensitive match of author name/email case CacheSearchMethod::AUTHORS: for (AuthorInfo const& author: entry.authors) { @@ -1346,16 +1407,24 @@ size_t CacheSystem::Query(CacheQuery& query) } break; + // Search by wheel configuration, for example '4x4' case CacheSearchMethod::WHEELS: wheels_str << entry.wheelcount << "x" << entry.propwheelcount; match = this->Match(score, wheels_str.ToCStr(), query.cqy_search_string, 0); break; + // Partial, case-insensitive match in file name case CacheSearchMethod::FILENAME: match = this->Match(score, entry.fname, query.cqy_search_string, 100); break; - default: // CacheSearchMethod::NONE + // Full case-insensitive match in mod name (useful for skins - one .skin file can define multiple skins) + case CacheSearchMethod::NAME_FULL: + match = this->MatchExact(entry.dname, query.cqy_search_string); + break; + + // CacheSearchMethod::NONE -> Show everything + default: match = true; break; }; @@ -1386,6 +1455,12 @@ bool CacheSystem::Match(size_t& out_score, std::string data, std::string const& } } +bool CacheSystem::MatchExact(std::string data, std::string const& query) +{ + Ogre::StringUtil::toLowerCase(data); + return data == query; +} + bool CacheQueryResult::operator<(CacheQueryResult const& other) const { if (cqr_score == other.cqr_score) diff --git a/source/main/resources/CacheSystem.h b/source/main/resources/CacheSystem.h index 3d8098d440..19e398012e 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; @@ -150,19 +152,20 @@ struct CacheQueryResult enum class CacheSearchMethod // Always case-insensitive { - NONE, //!< No searching + NONE, //!< Show everything FULLTEXT, //!< Partial match in: name, filename, description, author name/mail - GUID, //!< Partial match in: guid + GUID, //!< Partial match in: guid (intentionally - for search box) AUTHORS, //!< Partial match in: author name/email WHEELS, //!< Wheel configuration, i.e. 4x4 - FILENAME //!< Partial match in file name + FILENAME, //!< Partial match in file name + NAME_FULL //!< Full match of name (case-insensitive) }; 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; leave empty to disable; not the same as CacheSearchMethod::GUID CacheSearchMethod cqy_search_method = CacheSearchMethod::NONE; std::string cqy_search_string; @@ -183,7 +186,7 @@ enum class CacheValidity /// MOTIVATION: /// RoR users usually have A LOT of content installed. Traversing it all on every game startup would be a pain. /// HOW IT WORKS: -/// For each recognized resource type (vehicle, terrain, skin...) an instance of 'CacheEntry' is created. +/// For each recognized resource type (vehicle, terrain, character, skin...) an instance of 'CacheEntry' is created. /// These entries are persisted in file CACHE_FILE (see above) /// Associated media live in a "resource bundle" (ZIP archive or subdirectory) in content directory (ROR_HOME/mods) and subdirectories. /// If multiple CacheEntries share a bundle, the bundle is loaded only once. Each bundle has dedicated OGRE resource group. @@ -210,6 +213,7 @@ class CacheSystem const CategoryIdNameMap &GetCategories() const { return m_categories; } std::shared_ptr FetchSkinDef(CacheEntry* cache_entry); //!< Loads+parses the .skin file once + CharacterDocumentPtr FetchCharacterDef(CacheEntry* cache_entry); //!< Loads+parses the .character file once CacheEntry *GetEntry(int modid); Ogre::String GetPrettyName(Ogre::String fname); @@ -239,13 +243,15 @@ class CacheSystem 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 void GenerateFileCache(CacheEntry &entry, Ogre::String group); void RemoveFileCache(CacheEntry &entry); - bool Match(size_t& out_score, std::string data, std::string const& query, size_t ); + bool Match(size_t& out_score, std::string data, std::string const& query, size_t); + bool MatchExact(std::string data, const std::string& query); 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 diff --git a/source/main/resources/ContentManager.cpp b/source/main/resources/ContentManager.cpp index 81bccd5a91..a377d9c101 100644 --- a/source/main/resources/ContentManager.cpp +++ b/source/main/resources/ContentManager.cpp @@ -253,6 +253,7 @@ void ContentManager::InitModCache(CacheValidity validity) std::string user = App::sys_user_dir->getStr(); std::string base = App::sys_process_dir->getStr(); std::string objects = PathCombine("resources", "beamobjects.zip"); + std::string character = PathCombine("resources", "default_character.zip"); if (!App::app_extra_mod_path->getStr().empty()) { @@ -265,6 +266,7 @@ void ContentManager::InitModCache(CacheValidity validity) ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(user, "vehicles"), "FileSystem", RGN_CONTENT); ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(base, "content") , "FileSystem", RGN_CONTENT); ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(base, objects) , "Zip" , RGN_CONTENT); + ResourceGroupManager::getSingleton().addResourceLocation(PathCombine(base, character) , "Zip" , RGN_CONTENT); ResourceGroupManager::getSingleton().createResourceGroup(RGN_TEMP, false); if (!App::app_extra_mod_path->getStr().empty()) diff --git a/source/main/resources/character_fileformat/CharacterFileFormat.cpp b/source/main/resources/character_fileformat/CharacterFileFormat.cpp new file mode 100644 index 0000000000..ea70d67bb6 --- /dev/null +++ b/source/main/resources/character_fileformat/CharacterFileFormat.cpp @@ -0,0 +1,268 @@ +/* + This source file is part of Rigs of Rods + Copyright 2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#include "CharacterFileFormat.h" + +#include "Actor.h" +#include "Character.h" +#include "Console.h" +#include "Utils.h" + +using namespace RoR; +using namespace Ogre; + +const int CHA_LINE_BUF_LEN = 4000; + +CharacterDocumentPtr CharacterParser::ProcessOgreStream(Ogre::DataStreamPtr stream) +{ + char raw_line_buf[CHA_LINE_BUF_LEN]; + m_filename = stream->getName(); + m_ctx.action = CharacterActionDef(); + m_def = std::make_shared(); + while (!stream->eof()) + { + stream->readLine(raw_line_buf, CHA_LINE_BUF_LEN); + this->ProcessLine(raw_line_buf); + } + return m_def; +} + +void CharacterParser::ProcessLine(const char* line) +{ + if ((line != nullptr) && (line[0] != 0)) + { + m_cur_line = line; + Ogre::StringUtil::trim(m_cur_line); + this->TokenizeCurrentLine(); + this->ProcessCurrentLine(); + } + m_line_number++; +} + +inline bool StartsWith(std::string const& line, const char* test) +{ + return line.compare(0, strlen(test), test) == 0; +} + +std::string CharacterParser::GetParam(int pos) +{ + if (pos < m_cur_args.size()) + return m_cur_args[pos]; + else + return ""; +} + +static ForceAnimBlend ParseForceAnimBlend(std::string const& line) +{ + if (line == "none") return ForceAnimBlend::NONE; + if (line == "average") return ForceAnimBlend::AVERAGE; + if (line == "cumulative") return ForceAnimBlend::CUMULATIVE; + return ForceAnimBlend::NONE; +} + +void CharacterParser::TokenizeCurrentLine() +{ + // Recognizes quoted strings! + // -------------------------- + m_cur_args.clear(); + m_cur_args.push_back(""); + + bool in_str = false; + for (char c : m_cur_line) + { + if (in_str) + { + if (c == '"') + { + in_str = false; + } + else + { + m_cur_args[m_cur_args.size() - 1] += c; + } + } + else + { + if (c == ' ' || c == '\t') // delimiters + { + if (m_cur_args[m_cur_args.size() - 1] != "") + { + m_cur_args.push_back(""); + } + } + else if (c == '"') + { + in_str = true; + } + else + { + m_cur_args[m_cur_args.size() - 1] += c; + } + } + } +} + +// retval true = continue processing (false = stop) +void CharacterParser::ProcessCurrentLine() +{ + if (!m_ctx.in_action && !m_ctx.in_bone_blend_mask) + { + // Root level + + if (StartsWith(m_cur_line, "character_name")) + { + m_def->character_name = GetParam(1); + } + if (StartsWith(m_cur_line, "character_description")) + { + m_def->character_description = GetParam(1); + } + else if (StartsWith(m_cur_line, "mesh_name")) + { + m_def->mesh_name = GetParam(1); + } + else if (StartsWith(m_cur_line, "character_guid")) + { + m_def->character_guid = GetParam(1); + } + else if (StartsWith(m_cur_line, "mesh_scale")) + { + if (m_cur_args.size() > 1) m_def->mesh_scale.x = Ogre::StringConverter::parseReal(GetParam(1)); + if (m_cur_args.size() > 2) m_def->mesh_scale.y = Ogre::StringConverter::parseReal(GetParam(2)); + if (m_cur_args.size() > 3) m_def->mesh_scale.z = Ogre::StringConverter::parseReal(GetParam(3)); + } + else if (StartsWith(m_cur_line, "force_animblend")) + { + m_def->force_animblend = ParseForceAnimBlend(GetParam(1)); + } + else if (StartsWith(m_cur_line, "begin_action")) + { + m_ctx.in_action = true; + } + else if (StartsWith(m_cur_line, "begin_bone_blend_mask")) + { + m_ctx.in_bone_blend_mask = true; + } + else if (StartsWith(m_cur_line, "author")) + { + CharacterAuthorInfo author; + author.type = GetParam(1); + author.id = Ogre::StringConverter::parseInt(GetParam(2)); + author.name = GetParam(3); + author.email = GetParam(4); + m_def->authors.push_back(author); + } + } + else if (m_ctx.in_action) + { + // In '[begin/end]_action' block. + + if (StartsWith(m_cur_line, "end_action")) + { + m_ctx.action.action_id = static_cast(m_def->actions.size()); + m_def->actions.push_back(m_ctx.action); + m_ctx.action = CharacterActionDef(); + m_ctx.in_action = false; + } + else if (StartsWith(m_cur_line, "anim_name")) + { + m_ctx.action.anim_name = GetParam(1); + } + else if (StartsWith(m_cur_line, "action_description")) + { + m_ctx.action.action_description = GetParam(1); + } + else if (StartsWith(m_cur_line, "for_situation")) + { + BITMASK_SET_1(m_ctx.action.for_situations, Character::SituationFlagFromString(GetParam(1))); + } + else if (StartsWith(m_cur_line, "for_control")) + { + BITMASK_SET_1(m_ctx.action.for_controls, Character::ControlFlagFromString(GetParam(1))); + } + else if (StartsWith(m_cur_line, "except_situation")) + { + BITMASK_SET_1(m_ctx.action.except_situations, Character::SituationFlagFromString(GetParam(1))); + } + else if (StartsWith(m_cur_line, "except_control")) + { + BITMASK_SET_1(m_ctx.action.except_controls, Character::ControlFlagFromString(GetParam(1))); + } + else if (StartsWith(m_cur_line, "playback_time_ratio")) + { + m_ctx.action.playback_time_ratio = Ogre::StringConverter::parseReal(GetParam(1)); + } + else if (StartsWith(m_cur_line, "playback_h_speed_ratio")) + { + m_ctx.action.playback_h_speed_ratio = Ogre::StringConverter::parseReal(GetParam(1)); + } + else if (StartsWith(m_cur_line, "playback_steering_ratio")) + { + m_ctx.action.playback_steering_ratio = Ogre::StringConverter::parseReal(GetParam(1)); + } + else if (StartsWith(m_cur_line, "weight")) + { + m_ctx.action.weight = Ogre::StringConverter::parseReal(GetParam(1)); + } + else if (StartsWith(m_cur_line, "playback_trim")) + { + m_ctx.action.playback_trim = Ogre::StringConverter::parseReal(GetParam(1)); + } + else if (StartsWith(m_cur_line, "anim_continuous")) + { + m_ctx.action.anim_continuous = Ogre::StringConverter::parseBool(GetParam(1)); + } + else if (StartsWith(m_cur_line, "anim_autorestart")) + { + m_ctx.action.anim_autorestart = Ogre::StringConverter::parseBool(GetParam(1)); + } + else if (StartsWith(m_cur_line, "anim_neutral_mid")) + { + m_ctx.action.anim_neutral_mid = Ogre::StringConverter::parseBool(GetParam(1)); + } + else if (StartsWith(m_cur_line, "source_percentual")) + { + m_ctx.action.source_percentual = Ogre::StringConverter::parseBool(GetParam(1)); + } + } + else if (m_ctx.in_bone_blend_mask) + { + // In '[begin/end]_bone_blend_mask' block. + + if (StartsWith(m_cur_line, "end_bone_blend_mask")) + { + m_def->bone_blend_masks.push_back(m_ctx.bone_blend_mask); + m_ctx.bone_blend_mask = BoneBlendMaskDef(); + m_ctx.in_bone_blend_mask = false; + } + else if (StartsWith(m_cur_line, "anim_name")) + { + m_ctx.bone_blend_mask.anim_name = GetParam(1); + } + else if (StartsWith(m_cur_line, "bone_weight")) + { + BoneBlendMaskWeightDef def; + def.bone_name = GetParam(1); + def.bone_weight = Ogre::StringConverter::parseReal(GetParam(2)); + m_ctx.bone_blend_mask.bone_weights.push_back(def); + } + } +} + + diff --git a/source/main/resources/character_fileformat/CharacterFileFormat.h b/source/main/resources/character_fileformat/CharacterFileFormat.h new file mode 100644 index 0000000000..0d6ff2f288 --- /dev/null +++ b/source/main/resources/character_fileformat/CharacterFileFormat.h @@ -0,0 +1,137 @@ +/* + This source file is part of Rigs of Rods + Copyright 2022 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#pragma once + +#include "BitFlags.h" + +#include +#include +#include + +namespace RoR { + +/// @addtogroup Gameplay +/// @{ + +/// @addtogroup Character +/// @{ + +struct BoneBlendMaskWeightDef //!< See `Ogre::AnimationState::setBlendMaskEntry()` +{ + std::string bone_name; + float bone_weight = 0.f; +}; + +struct BoneBlendMaskDef //!< Additional settings for a skeletal animation track exported from 3D modelling tool. +{ + std::string anim_name; //!< Name of the skeletal animation from OGRE's *.skeleton file. + std::vector bone_weights; +}; + +/// Action = one or more skeletal animations playing/blended/masked together to create impression activity. +struct CharacterActionDef +{ + std::string anim_name; //!< Name of the skeletal animation from OGRE's *.skeleton file. + std::string action_description; //!< Gameplay name. + CharacterActionID_t action_id = CHARACTERACTIONID_INVALID; + + // Conditions + BitMask_t for_situations = 0; //!< `RoRnet::SituationFlags`, all must be satisfied. + BitMask_t except_situations = 0; //!< `RoRnet::SituationFlags`, none must be satisfied. + BitMask_t for_controls = 0; //!< `RoRnet::ControlFlags`, all must be satisfied. + BitMask_t except_controls = 0; //!< `RoRnet::ControlFlags`, none must be satisfied. + + // Anim position calculation + float playback_time_ratio = 0.f; //!< How much elapsed time affects animation position. + float playback_h_speed_ratio = 0.f; //!< How much horizontal movement speed affects animation position. + float playback_steering_ratio = 0.f; //!< How much vehicle steering angle affects animation position. + bool anim_continuous = true; //!< Should animation keep advancing and looping, or should it be set to exact position? + bool anim_autorestart = false; //!< Should animation always restart from 0 when activated? + bool anim_neutral_mid = false; //!< Does the anim have the 'neutral' position in it's middle (such as steering left/right) instead of at start? Only effective together with "percentual". + bool source_percentual = false; //!< Is the position source value a percentage of animation length? + float playback_trim = 0.0f; //!< How much to trim the animation position. Useful for i.e. steering animation to avoid flickering. + + // Anim blending weight + float weight = 1.0f; +}; + +enum class ForceAnimBlend //!< Should a specific `Ogre::SkeletonAnimationBlendMode` be forced, or should we keep what the .skeleton file defines? +{ + NONE, //!< Use what's defined in the skeleton, see '' in the XML. + AVERAGE, + CUMULATIVE +}; + +struct CharacterAuthorInfo +{ + int id = -1; + std::string name; + std::string type; + std::string email; +}; + +struct CharacterDocument +{ + std::string character_name; + std::string character_description; + std::string mesh_name; + std::string character_guid; // deal with it + Ogre::Vector3 mesh_scale = Ogre::Vector3(1, 1, 1); + std::vector actions; + std::vector bone_blend_masks; + std::vector authors; + ForceAnimBlend force_animblend = ForceAnimBlend::NONE; //!< Should a specific `Ogre::SkeletonAnimationBlendMode` be forced, or should we keep what the .skeleton file defines? +}; + +typedef std::shared_ptr CharacterDocumentPtr; + +// ----------------------------------------------------------------------------- + +class CharacterParser +{ +public: + + CharacterDocumentPtr ProcessOgreStream(Ogre::DataStreamPtr stream); + +private: + void ProcessLine(const char* line); + void ProcessCurrentLine(); + void TokenizeCurrentLine(); + std::string GetParam(int pos); + + struct CharacterParserContext + { + CharacterActionDef action; + BoneBlendMaskDef bone_blend_mask; + bool in_action = false; + bool in_bone_blend_mask = false; + } m_ctx; //!< Parser context + + CharacterDocumentPtr m_def; + int m_line_number; + std::string m_cur_line; + std::string m_filename; + Ogre::StringVector m_cur_args; // see TokenizeCurrentLine() +}; + +/// @} // addtogroup Character +/// @} // addtogroup Gameplay + +} // namespace RoR \ No newline at end of file diff --git a/source/main/system/CVar.cpp b/source/main/system/CVar.cpp index 2500aebad0..4939d659cd 100644 --- a/source/main/system/CVar.cpp +++ b/source/main/system/CVar.cpp @@ -58,6 +58,8 @@ 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, DEFAULT_CHARACTER_FILE); + App::sim_player_character_skin=this->cVarCreate("sim_player_character_skin","", CVAR_ARCHIVE, ""); 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"); @@ -71,6 +73,8 @@ void Console::cVarSetupBuiltins() App::mp_player_name = this->cVarCreate("mp_player_name", "Nickname", CVAR_ARCHIVE, "Player"); App::mp_player_token = this->cVarCreate("mp_player_token", "User Token", CVAR_ARCHIVE | CVAR_NO_LOG); App::mp_api_url = this->cVarCreate("mp_api_url", "Online API URL", CVAR_ARCHIVE, "http://api.rigsofrods.org"); + App::mp_override_character = this->cVarCreate("mp_override_character", "", CVAR_ARCHIVE, ""); + App::mp_override_character_skin=this->cVarCreate("mp_override_character_skin","", CVAR_ARCHIVE, ""); App::remote_query_url = this->cVarCreate("remote_query_url", "", CVAR_ARCHIVE, "https://v2.api.rigsofrods.org"); App::diag_auto_spawner_report= this->cVarCreate("diag_auto_spawner_report","AutoActorSpawnerReport", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); diff --git a/source/main/utils/InputEngine.cpp b/source/main/utils/InputEngine.cpp index 4da618da5e..5fee86c767 100644 --- a/source/main/utils/InputEngine.cpp +++ b/source/main/utils/InputEngine.cpp @@ -333,6 +333,26 @@ InputEvent eventInfo[] = { {"CHARACTER_SIDESTEP_RIGHT", EV_CHARACTER_SIDESTEP_RIGHT, "Keyboard D", _LC("InputEvent", "sidestep to the right")}, {"CHARACTER_ROT_UP", EV_CHARACTER_ROT_UP, "Keyboard UP", _LC("InputEvent", "rotate view up")}, {"CHARACTER_ROT_DOWN", EV_CHARACTER_ROT_DOWN, "Keyboard DOWN", _LC("InputEvent", "rotate view down")}, + {"CHARACTER_CUSTOM_ACTION_01", EV_CHARACTER_CUSTOM_ACTION_01, "Keyboard EXPL+F1", _LC("InputEvent", "character-specific action slot 1")}, + {"CHARACTER_CUSTOM_ACTION_02", EV_CHARACTER_CUSTOM_ACTION_02, "Keyboard EXPL+F2", _LC("InputEvent", "character-specific action slot 2") }, + {"CHARACTER_CUSTOM_ACTION_03", EV_CHARACTER_CUSTOM_ACTION_03, "Keyboard EXPL+F3", _LC("InputEvent", "character-specific action slot 3") }, + {"CHARACTER_CUSTOM_ACTION_04", EV_CHARACTER_CUSTOM_ACTION_04, "Keyboard EXPL+F4", _LC("InputEvent", "character-specific action slot 4") }, + {"CHARACTER_CUSTOM_ACTION_05", EV_CHARACTER_CUSTOM_ACTION_05, "Keyboard EXPL+F5", _LC("InputEvent", "character-specific action slot 5") }, + {"CHARACTER_CUSTOM_ACTION_06", EV_CHARACTER_CUSTOM_ACTION_06, "Keyboard EXPL+F6", _LC("InputEvent", "character-specific action slot 6") }, + {"CHARACTER_CUSTOM_ACTION_07", EV_CHARACTER_CUSTOM_ACTION_07, "Keyboard EXPL+F7", _LC("InputEvent", "character-specific action slot 7") }, + {"CHARACTER_CUSTOM_ACTION_08", EV_CHARACTER_CUSTOM_ACTION_08, "Keyboard EXPL+F8", _LC("InputEvent", "character-specific action slot 8") }, + {"CHARACTER_CUSTOM_ACTION_09", EV_CHARACTER_CUSTOM_ACTION_09, "Keyboard EXPL+F9", _LC("InputEvent", "character-specific action slot 9") }, + {"CHARACTER_CUSTOM_ACTION_10", EV_CHARACTER_CUSTOM_ACTION_10, "Keyboard EXPL+F10", _LC("InputEvent", "character-specific action slot 10") }, + {"CHARACTER_CUSTOM_MODE_01", EV_CHARACTER_CUSTOM_MODE_01, "Keyboard EXPL+CTRL+1", _LC("InputEvent", "character-specific mode slot 1")}, + {"CHARACTER_CUSTOM_MODE_02", EV_CHARACTER_CUSTOM_MODE_02, "Keyboard EXPL+CTRL+2", _LC("InputEvent", "character-specific mode slot 2") }, + {"CHARACTER_CUSTOM_MODE_03", EV_CHARACTER_CUSTOM_MODE_03, "Keyboard EXPL+CTRL+3", _LC("InputEvent", "character-specific mode slot 3") }, + {"CHARACTER_CUSTOM_MODE_04", EV_CHARACTER_CUSTOM_MODE_04, "Keyboard EXPL+CTRL+4", _LC("InputEvent", "character-specific mode slot 4") }, + {"CHARACTER_CUSTOM_MODE_05", EV_CHARACTER_CUSTOM_MODE_05, "Keyboard EXPL+CTRL+5", _LC("InputEvent", "character-specific mode slot 5") }, + {"CHARACTER_CUSTOM_MODE_06", EV_CHARACTER_CUSTOM_MODE_06, "Keyboard EXPL+CTRL+6", _LC("InputEvent", "character-specific mode slot 6") }, + {"CHARACTER_CUSTOM_MODE_07", EV_CHARACTER_CUSTOM_MODE_07, "Keyboard EXPL+CTRL+7", _LC("InputEvent", "character-specific mode slot 7") }, + {"CHARACTER_CUSTOM_MODE_08", EV_CHARACTER_CUSTOM_MODE_08, "Keyboard EXPL+CTRL+8", _LC("InputEvent", "character-specific mode slot 8") }, + {"CHARACTER_CUSTOM_MODE_09", EV_CHARACTER_CUSTOM_MODE_09, "Keyboard EXPL+CTRL+9", _LC("InputEvent", "character-specific mode slot 9") }, + {"CHARACTER_CUSTOM_MODE_10", EV_CHARACTER_CUSTOM_MODE_10, "Keyboard EXPL+CTRL+0", _LC("InputEvent", "character-specific mode slot 10") }, // Camera {"CAMERA_CHANGE", EV_CAMERA_CHANGE, "Keyboard EXPL+C", _LC("InputEvent", "change camera mode")}, diff --git a/source/main/utils/InputEngine.h b/source/main/utils/InputEngine.h index 0edaf612e2..2ebb1afd8c 100644 --- a/source/main/utils/InputEngine.h +++ b/source/main/utils/InputEngine.h @@ -126,6 +126,8 @@ enum events EV_CAMERA_ZOOM_IN_FAST, //!< zoom camera in faster EV_CAMERA_ZOOM_OUT, //!< zoom camera out EV_CAMERA_ZOOM_OUT_FAST, //!< zoom camera out faster + + // Character EV_CHARACTER_BACKWARDS, //!< step backwards with the character EV_CHARACTER_FORWARD, //!< step forward with the character EV_CHARACTER_JUMP, //!< let the character jump @@ -136,6 +138,30 @@ enum events EV_CHARACTER_RUN, //!< let the character run EV_CHARACTER_SIDESTEP_LEFT, //!< sidestep to the left EV_CHARACTER_SIDESTEP_RIGHT, //!< sidestep to the right + + EV_CHARACTER_CUSTOM_ACTION_01, + EV_CHARACTER_CUSTOM_ACTION_02, + EV_CHARACTER_CUSTOM_ACTION_03, + EV_CHARACTER_CUSTOM_ACTION_04, + EV_CHARACTER_CUSTOM_ACTION_05, + EV_CHARACTER_CUSTOM_ACTION_06, + EV_CHARACTER_CUSTOM_ACTION_07, + EV_CHARACTER_CUSTOM_ACTION_08, + EV_CHARACTER_CUSTOM_ACTION_09, + EV_CHARACTER_CUSTOM_ACTION_10, + + EV_CHARACTER_CUSTOM_MODE_01, + EV_CHARACTER_CUSTOM_MODE_02, + EV_CHARACTER_CUSTOM_MODE_03, + EV_CHARACTER_CUSTOM_MODE_04, + EV_CHARACTER_CUSTOM_MODE_05, + EV_CHARACTER_CUSTOM_MODE_06, + EV_CHARACTER_CUSTOM_MODE_07, + EV_CHARACTER_CUSTOM_MODE_08, + EV_CHARACTER_CUSTOM_MODE_09, + EV_CHARACTER_CUSTOM_MODE_10, + + // Commands EV_COMMANDS_01, //!< Command 1 EV_COMMANDS_02, //!< Command 2 EV_COMMANDS_03, //!< Command 3